From 3fdc66998f532e0bdadd16f39ddd9ef51e349d42 Mon Sep 17 00:00:00 2001 From: lucarin91 Date: Wed, 17 Dec 2025 13:25:37 +0100 Subject: [PATCH 1/3] wip --- cmd/arduino-flasher-cli/flash/flash.go | 2 +- internal/helper/helper.go | 34 +++ internal/updater/flasher.go | 119 +++++++++- rpc/cc/arduino/flasher/v1/common.pb.go | 45 ++-- rpc/cc/arduino/flasher/v1/common.proto | 130 +++++------ service/service_flash.go | 286 +++++++++++++------------ 6 files changed, 390 insertions(+), 226 deletions(-) create mode 100644 internal/helper/helper.go diff --git a/cmd/arduino-flasher-cli/flash/flash.go b/cmd/arduino-flasher-cli/flash/flash.go index d7f5862..bd3487b 100644 --- a/cmd/arduino-flasher-cli/flash/flash.go +++ b/cmd/arduino-flasher-cli/flash/flash.go @@ -113,7 +113,7 @@ func runFlashCommand(ctx context.Context, args []string, forceYes bool, preserve } } - err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir) + err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir, nil) if err != nil { feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument) } diff --git a/internal/helper/helper.go b/internal/helper/helper.go new file mode 100644 index 0000000..a18de4a --- /dev/null +++ b/internal/helper/helper.go @@ -0,0 +1,34 @@ +package helper + +import ( + "bytes" +) + +// CallbackWriter is a custom writer that processes each line calling the callback. +type CallbackWriter struct { + callback func(line string) + buffer []byte +} + +// NewCallbackWriter creates a new CallbackWriter. +func NewCallbackWriter(process func(line string)) *CallbackWriter { + return &CallbackWriter{ + callback: process, + buffer: make([]byte, 0, 1024), + } +} + +// Write implements the io.Writer interface. +func (p *CallbackWriter) Write(data []byte) (int, error) { + p.buffer = append(p.buffer, data...) + for { + idx := bytes.IndexByte(p.buffer, '\n') + if idx == -1 { + break + } + line := p.buffer[:idx] // Do not include \n + p.buffer = p.buffer[idx+1:] + p.callback(string(line)) + } + return len(data), nil +} diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index 94a621f..99bdab5 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -16,9 +16,12 @@ package updater import ( + "bufio" "context" "encoding/hex" "fmt" + "log/slog" + "regexp" "runtime" "strconv" "strings" @@ -28,6 +31,7 @@ import ( "github.com/arduino/arduino-flasher-cli/cmd/feedback" "github.com/arduino/arduino-flasher-cli/cmd/i18n" + "github.com/arduino/arduino-flasher-cli/internal/helper" "github.com/arduino/arduino-flasher-cli/internal/updater/artifacts" ) @@ -36,7 +40,7 @@ const DownloadDiskSpace = uint64(12) const ExtractDiskSpace = uint64(10) const yesPrompt = "yes" -func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string) error { +func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string, callback FlahsCallback) error { if !imagePath.Exist() { temp, err := SetTempDir("download-", tempDir) if err != nil { @@ -90,10 +94,26 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes imagePath = tempContent[0] } - return FlashBoard(ctx, imagePath.String(), version, preserveUser) + return FlashBoard(ctx, imagePath.String(), version, preserveUser, nil) } -func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool) error { +type TypeEvent int + +const ( + EventWaiting TypeEvent = 3 + EventFlashed TypeEvent = 4 +) + +type FlashEvent struct { + Type TypeEvent + Progress int + MaxProgress int + Log string +} + +type FlahsCallback func(FlashEvent) + +func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool, callback FlahsCallback) error { var flashDir *paths.Path for _, entry := range []string{"flash", "flash_UnoQ"} { if p := paths.New(downloadedImagePath, entry); p.Exist() { @@ -161,6 +181,11 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, } + totalPartitions, err := getTotalPartition(flashDir.Join(rawProgram)) + if err != nil { + return err + } + feedback.Print(i18n.Tr("Flashing with qdl")) cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", rawProgram, "patch0.xml") if err != nil { @@ -168,8 +193,36 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, } // Setting the directory is needed because rawprogram0.xml contains relative file paths cmd.SetDir(flashDir.String()) - cmd.RedirectStderrTo(stdout) - cmd.RedirectStdoutTo(stdout) + + w := stdout + if callback != nil { + progress := 0 + w = helper.NewCallbackWriter(func(line string) { + parsedLine, err := parseQdlLogLine(line) + if err != nil { + slog.Warn("could not parse qdl log line", "error", err, "line", line) + return + } + + switch parsedLine.Op { + case Waiting: + callback(FlashEvent{ + Type: EventWaiting, + Log: line, + }) + case Flasherd: + progress++ + callback(FlashEvent{ + Type: EventFlashed, + Log: line, + Progress: progress, + MaxProgress: totalPartitions, + }) + } + }) + } + cmd.RedirectStderrTo(w) + cmd.RedirectStdoutTo(w) if err := cmd.RunWithinContext(ctx); err != nil { return err } @@ -226,3 +279,59 @@ func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) erro return nil } + +type Op int + +const ( + Waiting Op = 1 + Flasherd Op = 2 +) + +var qdlProgressRegex = regexp.MustCompile(`(\w)\s+(:?(".*?")\s+(\w+)(?:\s+at\s+(\d+kB/s))?)?`) + +type QDLLogLine struct { + Op Op + Log string +} + +func parseQdlLogLine(line string) (QDLLogLine, error) { + matches := qdlProgressRegex.FindStringSubmatch(line) + if matches == nil { + return QDLLogLine{}, fmt.Errorf("line %q does not match progress format", line) + } + slog.Debug("parsed qdl log line", "full", matches[0], "matches", matches) + + if strings.HasPrefix(matches[1], "Waiting for") || strings.HasPrefix(matches[1], "waiting for") { + return QDLLogLine{ + Op: Waiting, + Log: line, + }, nil + } + + if strings.HasPrefix(matches[1], "Flashed") { + return QDLLogLine{ + Op: Flasherd, + Log: line, + }, nil + } + + return QDLLogLine{}, fmt.Errorf("line %q does not match known operations", line) +} + +func getTotalPartition(path *paths.Path) (int, error) { + f, err := path.Open() + if err != nil { + return 0, err + } + + r := bufio.NewScanner(f) + var total int + for r.Scan() { + c := strings.Count(r.Text(), " Date: Wed, 17 Dec 2025 13:39:52 +0100 Subject: [PATCH 2/3] simplify parsing --- internal/updater/flasher.go | 14 +++----------- service/service_flash.go | 2 ++ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index 99bdab5..fe94f3e 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -21,7 +21,6 @@ import ( "encoding/hex" "fmt" "log/slog" - "regexp" "runtime" "strconv" "strings" @@ -287,28 +286,21 @@ const ( Flasherd Op = 2 ) -var qdlProgressRegex = regexp.MustCompile(`(\w)\s+(:?(".*?")\s+(\w+)(?:\s+at\s+(\d+kB/s))?)?`) - type QDLLogLine struct { Op Op Log string } func parseQdlLogLine(line string) (QDLLogLine, error) { - matches := qdlProgressRegex.FindStringSubmatch(line) - if matches == nil { - return QDLLogLine{}, fmt.Errorf("line %q does not match progress format", line) - } - slog.Debug("parsed qdl log line", "full", matches[0], "matches", matches) - - if strings.HasPrefix(matches[1], "Waiting for") || strings.HasPrefix(matches[1], "waiting for") { + line = strings.ToLower(line) + if strings.HasPrefix(line, "waiting for") { return QDLLogLine{ Op: Waiting, Log: line, }, nil } - if strings.HasPrefix(matches[1], "Flashed") { + if strings.HasPrefix(line, "flashed") { return QDLLogLine{ Op: Flasherd, Log: line, diff --git a/service/service_flash.go b/service/service_flash.go index a880657..a629eee 100644 --- a/service/service_flash.go +++ b/service/service_flash.go @@ -64,6 +64,8 @@ func (s *flasherServerImpl) Flash(req *flasher.FlashRequest, stream flasher.Flas Name: msg.GetName(), Message: msg.GetMessage(), Completed: msg.GetCompleted(), + Progress: msg.GetProgress(), + Total: msg.GetTotal(), }, }, }) From b4db7391d3975d9d626dfabeef3b7b23ec5e36e9 Mon Sep 17 00:00:00 2001 From: lucarin91 Date: Wed, 17 Dec 2025 16:27:13 +0100 Subject: [PATCH 3/3] improve parsing --- internal/updater/flasher.go | 89 ++++++------------------------- internal/updater/parseqdl.go | 61 +++++++++++++++++++++ internal/updater/parseqdl_test.go | 72 +++++++++++++++++++++++++ service/service_flash.go | 20 ++++--- 4 files changed, 164 insertions(+), 78 deletions(-) create mode 100644 internal/updater/parseqdl.go create mode 100644 internal/updater/parseqdl_test.go diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index fe94f3e..f4c5edb 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -16,11 +16,9 @@ package updater import ( - "bufio" "context" "encoding/hex" "fmt" - "log/slog" "runtime" "strconv" "strings" @@ -96,18 +94,18 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes return FlashBoard(ctx, imagePath.String(), version, preserveUser, nil) } -type TypeEvent int +type EventType int const ( - EventWaiting TypeEvent = 3 - EventFlashed TypeEvent = 4 + EventLog EventType = iota + EventProgress ) type FlashEvent struct { - Type TypeEvent - Progress int - MaxProgress int - Log string + Type EventType + Log string + Progress int + Total int } type FlahsCallback func(FlashEvent) @@ -197,25 +195,21 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, if callback != nil { progress := 0 w = helper.NewCallbackWriter(func(line string) { - parsedLine, err := parseQdlLogLine(line) - if err != nil { - slog.Warn("could not parse qdl log line", "error", err, "line", line) - return - } + parsedLine := parseQdlLogLine(line) switch parsedLine.Op { - case Waiting: + case Flashed: + progress++ callback(FlashEvent{ - Type: EventWaiting, - Log: line, + Type: EventProgress, + Log: line, + Progress: progress, + Total: totalPartitions, }) - case Flasherd: - progress++ + default: callback(FlashEvent{ - Type: EventFlashed, - Log: line, - Progress: progress, - MaxProgress: totalPartitions, + Type: EventLog, + Log: line, }) } }) @@ -278,52 +272,3 @@ func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) erro return nil } - -type Op int - -const ( - Waiting Op = 1 - Flasherd Op = 2 -) - -type QDLLogLine struct { - Op Op - Log string -} - -func parseQdlLogLine(line string) (QDLLogLine, error) { - line = strings.ToLower(line) - if strings.HasPrefix(line, "waiting for") { - return QDLLogLine{ - Op: Waiting, - Log: line, - }, nil - } - - if strings.HasPrefix(line, "flashed") { - return QDLLogLine{ - Op: Flasherd, - Log: line, - }, nil - } - - return QDLLogLine{}, fmt.Errorf("line %q does not match known operations", line) -} - -func getTotalPartition(path *paths.Path) (int, error) { - f, err := path.Open() - if err != nil { - return 0, err - } - - r := bufio.NewScanner(f) - var total int - for r.Scan() { - c := strings.Count(r.Text(), "