Skip to content
12 changes: 6 additions & 6 deletions cmd/arduino-flasher-cli/flash/flash.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
)

func NewFlashCmd() *cobra.Command {
var forceYes bool
var forceYes, preserveUser bool
var tempDir string
appCmd := &cobra.Command{
Use: "flash",
Expand Down Expand Up @@ -70,12 +70,12 @@ NOTE: On Windows, required drivers are automatically installed with elevated pri
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
checkDriversInstalled()
runFlashCommand(cmd.Context(), args, forceYes, tempDir)
runFlashCommand(cmd.Context(), args, forceYes, preserveUser, tempDir)
},
}
appCmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "Automatically confirm all prompts")
appCmd.Flags().StringVar(&tempDir, "temp-dir", "", "Path to the directory in which the image will be downloaded and extracted")
// TODO: add --clean-install flag or something similar to distinguish between keeping and purging the /home directory
appCmd.Flags().BoolVar(&preserveUser, "preserve-user", false, "Preserve user partition")

return appCmd
}
Expand All @@ -91,13 +91,13 @@ func checkDriversInstalled() {
}
}

func runFlashCommand(ctx context.Context, args []string, forceYes bool, tempDir string) {
func runFlashCommand(ctx context.Context, args []string, forceYes bool, preserveUser bool, tempDir string) {
imagePath, err := paths.New(args[0]).Abs()
if err != nil {
feedback.Fatal(i18n.Tr("could not find image absolute path: %v", err), feedback.ErrBadArgument)
}

if !forceYes {
if !forceYes && !preserveUser {
feedback.Print("\nWARNING: flashing a new Linux image on the board will erase any existing data you have on it.")
feedback.Printf("Do you want to proceed and flash %s on the board? (yes/no)", args[0])

Expand All @@ -113,7 +113,7 @@ func runFlashCommand(ctx context.Context, args []string, forceYes bool, tempDir
}
}

err = updater.Flash(ctx, imagePath, args[0], forceYes, tempDir)
err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir)
if err != nil {
feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument)
}
Expand Down
23 changes: 23 additions & 0 deletions internal/updater/artifacts/artifacts_read_xml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is part of arduino-flasher-cli.
//
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-flasher-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package artifacts

import (
_ "embed"
)

//go:embed read.xml
var ReadXML []byte
4 changes: 4 additions & 0 deletions internal/updater/artifacts/read.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" ?>
<data>
<read SECTOR_SIZE_IN_BYTES="512" filename="dump.bin" physical_partition_number="0" num_partition_sectors="20" start_sector="1"/>
</data>
94 changes: 89 additions & 5 deletions internal/updater/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ package updater

import (
"context"
"encoding/hex"
"fmt"
"runtime"
"strconv"
"strings"

"github.com/arduino/go-paths-helper"
"github.com/shirou/gopsutil/v4/disk"
Expand All @@ -31,8 +34,9 @@ import (
const GiB = uint64(1024 * 1024 * 1024)
const DownloadDiskSpace = uint64(12)
const ExtractDiskSpace = uint64(10)
const yesPrompt = "yes"

func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, tempDir string) error {
func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string) error {
if !imagePath.Exist() {
temp, err := SetTempDir("download-", tempDir)
if err != nil {
Expand Down Expand Up @@ -86,10 +90,10 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes
imagePath = tempContent[0]
}

return FlashBoard(ctx, imagePath.String(), version)
return FlashBoard(ctx, imagePath.String(), version, preserveUser)
}

func FlashBoard(ctx context.Context, downloadedImagePath string, version string) error {
func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool) error {
var flashDir *paths.Path
for _, entry := range []string{"flash", "flash_UnoQ"} {
if p := paths.New(downloadedImagePath, entry); p.Exist() {
Expand Down Expand Up @@ -125,9 +129,40 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string)
if err != nil {
return err
}
// TODO: add logic to preserve the user partition

rawProgram := "rawprogram0.xml"
if preserveUser {
if errT := checkBoardGPTTable(ctx, qdlPath, flashDir); errT == nil && flashDir.Join("rawprogram0.nouser.xml").Exist() {
rawProgram = "rawprogram0.nouser.xml"
} else {
res, err := func(target string) (bool, error) {
warnStr := "Linux image " + target + " does not support user partition preservation"
if errT != nil {
warnStr = errT.Error()
}
feedback.Printf("\nWARNING: %s.", warnStr)
feedback.Printf("Do you want to proceed and flash %s on the board, erasing any existing data you have on it? (yes/no)", target)

var yesInput string
_, err := fmt.Scanf("%s\n", &yesInput)
if err != nil {
return false, err
}
yes := strings.ToLower(yesInput) == yesPrompt || strings.ToLower(yesInput) == "y"
return yes, nil
}(version)
if err != nil {
return err
}
if !res {
return fmt.Errorf("flashing not confirmed by user, exiting")
}
}

}

feedback.Print(i18n.Tr("Flashing with qdl"))
cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", "rawprogram0.xml", "patch0.xml")
cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", rawProgram, "patch0.xml")
if err != nil {
return err
}
Expand All @@ -141,3 +176,52 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string)

return nil
}

func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) error {
dumpBinPath := qdlPath.Parent().Join("dump.bin")
readXMLPath := qdlPath.Parent().Join("read.xml")
err := readXMLPath.WriteFile(artifacts.ReadXML)
if err != nil {
return err
}
cmd, err := paths.NewProcess(nil, qdlPath.String(), "--storage", "emmc", flashDir.Join("prog_firehose_ddr.elf").String(), readXMLPath.String())
if err != nil {
return err
}
cmd.SetDir(qdlPath.Parent().String())
if err := cmd.RunWithinContext(ctx); err != nil {
return err
}
if !dumpBinPath.Exist() {
return fmt.Errorf("it was not possible to access the current Debian image GPT table")
}
dump, err := dumpBinPath.ReadFile()
if err != nil {
return err
}
strDump := hex.Dump(dump)

strDumpSlice := strings.Split(strDump, "\n")
// the max number of partitions is stored at entry 0x50
maxPartitions, err := strconv.ParseInt(strings.Split(strDumpSlice[5], " ")[2], 16, 16)
if err != nil {
return err
}

numPartitions := 0
// starting from entry 0x200, there is a new partition every 0x80 bytes
// TODO: check if the size of each partition is 80h or just assume it?
for i := 32; numPartitions < int(maxPartitions); i += 8 {
// partitions are made of non-zero bytes, if all 0s then there are no more entries
if strings.Contains(strDumpSlice[i], "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00") {
break
}
numPartitions++
}

if numPartitions == 73 && maxPartitions == 76 {
return fmt.Errorf("the current Debian image (R0) does not support user partition preservation")
}

return nil
}