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 license@arduino.cc.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of a method returning it's output (or string message) just by returning an error. But this can be fine.

At least, add a small comment to the function, explaining why we are checking the GPT and why this is the way we use to know if the board supports preserving user's data or not.

Suggested change
func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) error {
// Checks the board GPT table and counts the number of partitions, this tells if the board supports preserving or not user's data.
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
}