diff --git a/Tools/Controller/cmd/controller/runtime_configuration.go b/Tools/Controller/cmd/controller/runtime_configuration.go index 250a8941..b493ad14 100644 --- a/Tools/Controller/cmd/controller/runtime_configuration.go +++ b/Tools/Controller/cmd/controller/runtime_configuration.go @@ -294,8 +294,9 @@ func commandOptions(store *appconfig.Store, fallbackProject string) control.Comm ArduinoConfig: config.Programming.ToolchainConfig, Avrdude: config.Programming.Avrdude, AvrdudeConf: config.Programming.AvrdudeConf, - Programmer: configuredProgrammer(config), - HostConfig: store.Current, + AvrdudeBackend: config.Programming.AvrdudeBackend, AvrdudeAdapter: config.Programming.AvrdudeAdapter, + Programmer: configuredProgrammer(config), + HostConfig: store.Current, BuzzerRuntime: func(settings native.Settings, haveBoard bool) appconfig.BuzzerRuntimeStatus { state := store.BuzzerRuntimeState() var backend, executable, backendError string @@ -352,14 +353,15 @@ func commandOptions(store *appconfig.Store, fallbackProject string) control.Comm ArduinoConfig: current.Programming.ToolchainConfig, Avrdude: current.Programming.Avrdude, AvrdudeConf: current.Programming.AvrdudeConf, - Programmer: configuredProgrammer(current), - HostConfig: store.Current, - BuzzerRuntime: options.BuzzerRuntime, - UpdateHostConfig: options.UpdateHostConfig, - Macros: options.Macros, - InitializeBoard: options.InitializeBoard, - BlankBoard: options.BlankBoard, - USBaspDriver: options.USBaspDriver, + AvrdudeBackend: current.Programming.AvrdudeBackend, AvrdudeAdapter: current.Programming.AvrdudeAdapter, + Programmer: configuredProgrammer(current), + HostConfig: store.Current, + BuzzerRuntime: options.BuzzerRuntime, + UpdateHostConfig: options.UpdateHostConfig, + Macros: options.Macros, + InitializeBoard: options.InitializeBoard, + BlankBoard: options.BlankBoard, + USBaspDriver: options.USBaspDriver, } } return options @@ -517,10 +519,11 @@ func apiOptions( ToolchainConfig: config.Programming.ToolchainConfig, Avrdude: config.Programming.Avrdude, AvrdudeConf: config.Programming.AvrdudeConf, - Programmer: configuredProgrammer(config), - MotionDoorPolicy: config.Safety.MotionDoorPolicy, - RF: config.RF, - OSActions: config.OSActions, + AvrdudeBackend: config.Programming.AvrdudeBackend, AvrdudeAdapter: config.Programming.AvrdudeAdapter, + Programmer: configuredProgrammer(config), + MotionDoorPolicy: config.Safety.MotionDoorPolicy, + RF: config.RF, + OSActions: config.OSActions, LCDPresentation: controllerapi.LCDPresentationOptions{ Enabled: config.UI.LCDServiceEnabled, Debounce: time.Duration(config.UI.LCDPromptDebounceMS) * time.Millisecond, diff --git a/Tools/Controller/controller.go b/Tools/Controller/controller.go index cf1a7272..47166de3 100644 --- a/Tools/Controller/controller.go +++ b/Tools/Controller/controller.go @@ -258,6 +258,8 @@ type Options struct { ToolchainConfig string `json:"toolchain_config,omitempty"` Avrdude string `json:"avrdude,omitempty"` AvrdudeConf string `json:"avrdude_conf,omitempty"` + AvrdudeBackend string `json:"avrdude_backend,omitempty"` + AvrdudeAdapter string `json:"avrdude_adapter,omitempty"` Programmer string `json:"programmer,omitempty"` Macros []Macro `json:"macros,omitempty"` StripEffects []StripEffect `json:"strip_effects,omitempty"` @@ -717,10 +719,11 @@ func New(options Options) *Client { ArduinoConfig: options.ToolchainConfig, Avrdude: options.Avrdude, AvrdudeConf: options.AvrdudeConf, - Programmer: options.Programmer, - HostConfig: client.currentHostConfig, - UpdateHostConfig: client.updateHostConfig, - Outputs: client.outputs, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, + Programmer: options.Programmer, + HostConfig: client.currentHostConfig, + UpdateHostConfig: client.updateHostConfig, + Outputs: client.outputs, } client.engine = control.NewCommandEngine(runtime, control.CommandOptions{ ProjectPath: options.ProjectPath, @@ -732,11 +735,12 @@ func New(options Options) *Client { ArduinoConfig: options.ToolchainConfig, Avrdude: options.Avrdude, AvrdudeConf: options.AvrdudeConf, - Programmer: options.Programmer, - HostConfig: client.currentHostConfig, - UpdateHostConfig: client.updateHostConfig, - Resolve: client.currentCommandOptions, - Outputs: client.outputs, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, + Programmer: options.Programmer, + HostConfig: client.currentHostConfig, + UpdateHostConfig: client.updateHostConfig, + Resolve: client.currentCommandOptions, + Outputs: client.outputs, }) automationContext, cancelAutomations := context.WithCancel(context.Background()) go func() { @@ -826,8 +830,9 @@ func (client *Client) ApplyHostOptions(options Options) bool { FirmwareFeatures: firmwareFeatures, FirmwareFeaturesError: firmwareFeaturesErr, ArduinoCLI: options.ToolchainCLI, Avrdude: options.Avrdude, - ArduinoConfig: options.ToolchainConfig, - AvrdudeConf: options.AvrdudeConf, + ArduinoConfig: options.ToolchainConfig, + AvrdudeConf: options.AvrdudeConf, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, Programmer: options.Programmer, HostConfig: client.currentHostConfig, UpdateHostConfig: client.updateHostConfig, diff --git a/Tools/Controller/controller_test.go b/Tools/Controller/controller_test.go index fb81e357..00754016 100644 --- a/Tools/Controller/controller_test.go +++ b/Tools/Controller/controller_test.go @@ -94,15 +94,15 @@ func TestPublicOptionsExposeCanonicalFirmwareFeatureStatus(t *testing.T) { } func TestToolchainConfigurationSurvivesPublicFacadeUpdates(t *testing.T) { - client := New(Options{ToolchainCLI: "cli", ToolchainConfig: "managed.yaml", Avrdude: "avr", AvrdudeConf: "avr.conf"}) + client := New(Options{ToolchainCLI: "cli", ToolchainConfig: "managed.yaml", Avrdude: "avr", AvrdudeConf: "avr.conf", AvrdudeBackend: "cli", AvrdudeAdapter: "adapter"}) defer client.Shutdown() options := client.currentCommandOptions() - if options.ArduinoConfig != "managed.yaml" || options.ArduinoCLI != "cli" || options.Avrdude != "avr" || options.AvrdudeConf != "avr.conf" { + if options.ArduinoConfig != "managed.yaml" || options.ArduinoCLI != "cli" || options.Avrdude != "avr" || options.AvrdudeConf != "avr.conf" || options.AvrdudeBackend != "cli" || options.AvrdudeAdapter != "adapter" { t.Fatalf("initial programming paths lost: %#v", options) } - client.ApplyHostOptions(Options{ToolchainCLI: "updated-cli", ToolchainConfig: "updated.yaml", Avrdude: "updated-avr", AvrdudeConf: "updated.conf"}) + client.ApplyHostOptions(Options{ToolchainCLI: "updated-cli", ToolchainConfig: "updated.yaml", Avrdude: "updated-avr", AvrdudeConf: "updated.conf", AvrdudeBackend: "library", AvrdudeAdapter: "updated-adapter"}) options = client.currentCommandOptions() - if options.ArduinoConfig != "updated.yaml" || options.ArduinoCLI != "updated-cli" || options.Avrdude != "updated-avr" || options.AvrdudeConf != "updated.conf" { + if options.ArduinoConfig != "updated.yaml" || options.ArduinoCLI != "updated-cli" || options.Avrdude != "updated-avr" || options.AvrdudeConf != "updated.conf" || options.AvrdudeBackend != "library" || options.AvrdudeAdapter != "updated-adapter" { t.Fatalf("updated programming paths lost: %#v", options) } } diff --git a/Tools/Controller/host/config.go b/Tools/Controller/host/config.go index b32743b1..490119b6 100644 --- a/Tools/Controller/host/config.go +++ b/Tools/Controller/host/config.go @@ -39,13 +39,14 @@ func controllerOptionsFromConfig( ToolchainConfig: config.Programming.ToolchainConfig, Avrdude: config.Programming.Avrdude, AvrdudeConf: config.Programming.AvrdudeConf, - Programmer: config.Programming.Programmer, - Macros: publicMacros(config.Macros), - Melodies: append([]controller.Melody(nil), config.Melodies...), - StatusEffects: append([]controller.StatusLEDEffect(nil), config.StatusEffects...), - Scripts: cloneStrings(config.Scripts), - Automations: publicAutomations(config.Automations), - MotionDoorPolicy: config.Safety.MotionDoorPolicy, + AvrdudeBackend: config.Programming.AvrdudeBackend, AvrdudeAdapter: config.Programming.AvrdudeAdapter, + Programmer: config.Programming.Programmer, + Macros: publicMacros(config.Macros), + Melodies: append([]controller.Melody(nil), config.Melodies...), + StatusEffects: append([]controller.StatusLEDEffect(nil), config.StatusEffects...), + Scripts: cloneStrings(config.Scripts), + Automations: publicAutomations(config.Automations), + MotionDoorPolicy: config.Safety.MotionDoorPolicy, LCDPresentation: controller.LCDPresentationOptions{ Enabled: config.UI.LCDServiceEnabled, Debounce: time.Duration(config.UI.LCDPromptDebounceMS) * time.Millisecond, diff --git a/Tools/Controller/host/config_test.go b/Tools/Controller/host/config_test.go index 9d76727a..e4986f07 100644 --- a/Tools/Controller/host/config_test.go +++ b/Tools/Controller/host/config_test.go @@ -11,8 +11,10 @@ func TestControllerConfigRetainsToolchainConfiguration(t *testing.T) { config.Programming.ToolchainConfig = "selected-data.yaml" config.Programming.Avrdude = "avr" config.Programming.AvrdudeConf = "avr.conf" + config.Programming.AvrdudeBackend = "cli" + config.Programming.AvrdudeAdapter = "adapter" got, err := controllerOptionsFromConfig(config, "config.json") - if err != nil || got.ToolchainConfig != "selected-data.yaml" || got.ToolchainCLI != "cli" || got.Avrdude != "avr" || got.AvrdudeConf != "avr.conf" { + if err != nil || got.ToolchainConfig != "selected-data.yaml" || got.ToolchainCLI != "cli" || got.Avrdude != "avr" || got.AvrdudeConf != "avr.conf" || got.AvrdudeBackend != "cli" || got.AvrdudeAdapter != "adapter" { t.Fatalf("configured paths lost: %#v %v", got, err) } } diff --git a/Tools/Controller/internal/control/commands.go b/Tools/Controller/internal/control/commands.go index 9181350e..8303d1dd 100644 --- a/Tools/Controller/internal/control/commands.go +++ b/Tools/Controller/internal/control/commands.go @@ -50,6 +50,8 @@ type CommandOptions struct { ArduinoConfig string Avrdude string AvrdudeConf string + AvrdudeBackend string + AvrdudeAdapter string Programmer string HostConfig func() appconfig.Config BuzzerRuntime func(native.Settings, bool) appconfig.BuzzerRuntimeStatus @@ -4298,6 +4300,7 @@ func programCommandRun( Method: method, Operation: operation, FQBN: options.FQBN, ArduinoCLI: options.ArduinoCLI, ArduinoConfig: options.ArduinoConfig, Avrdude: options.Avrdude, AvrdudeConf: options.AvrdudeConf, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, Programmer: options.Programmer, USBaspAutoSlow: true, } @@ -4720,7 +4723,8 @@ func safeFlashCommand( Method: method, Port: port, Operation: programmer.OperationBackup, FQBN: options.FQBN, Programmer: options.Programmer, ArduinoCLI: options.ArduinoCLI, ArduinoConfig: options.ArduinoConfig, Avrdude: options.Avrdude, - AvrdudeConf: options.AvrdudeConf, + AvrdudeConf: options.AvrdudeConf, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, USBaspAutoSlow: true, ApplicationHash: snapshot.Hello.BuildHash, ApplicationIdentitySchema: snapshot.Hello.IdentitySchema, @@ -5019,6 +5023,7 @@ func recoverProgrammingCommand( HexPath: firmwarePath, FQBN: options.FQBN, Programmer: options.Programmer, ArduinoCLI: options.ArduinoCLI, ArduinoConfig: options.ArduinoConfig, Avrdude: options.Avrdude, AvrdudeConf: options.AvrdudeConf, + AvrdudeBackend: options.AvrdudeBackend, AvrdudeAdapter: options.AvrdudeAdapter, USBaspAutoSlow: true, } verifyErr := programmer.VerifyFlashReadbackWithRunner( diff --git a/Tools/Controller/internal/control/programming_lifecycle.go b/Tools/Controller/internal/control/programming_lifecycle.go index 761498ef..23360164 100644 --- a/Tools/Controller/internal/control/programming_lifecycle.go +++ b/Tools/Controller/internal/control/programming_lifecycle.go @@ -1174,7 +1174,11 @@ func findFailedProgrammingSession( return session, err } targetSHA256 = strings.ToLower(strings.TrimSpace(targetSHA256)) - matching := session.HostResult == "failed" && session.SafeStateApplied && + // A verified write can still lose the connection while restoring settings. + // Recovery performs fresh flash readback before attempting restoration again. + recoverable := session.HostResult == "failed" || + (session.HostResult == "succeeded" && session.Phase == "restore-failed-safe") + matching := recoverable && session.SafeStateApplied && strings.EqualFold(session.TargetFirmwareSHA256, targetSHA256) if !matching { return nil, fmt.Errorf( diff --git a/Tools/Controller/internal/control/programming_lifecycle_test.go b/Tools/Controller/internal/control/programming_lifecycle_test.go index 67f93b01..42b3d72d 100644 --- a/Tools/Controller/internal/control/programming_lifecycle_test.go +++ b/Tools/Controller/internal/control/programming_lifecycle_test.go @@ -880,6 +880,36 @@ func TestFindRetryableProgrammingSessionUsesNewestMarkerBeforeOlderConflicts(t * } } +func TestProgrammingRecoveryAcceptsVerifiedWriteWithFailedRestore(t *testing.T) { + paths, firmware := programmingLifecycleFixture(t) + document, err := programmer.LoadIntelHex(firmware) + if err != nil { + t.Fatal(err) + } + identity := programmingIdentity(connectedProgrammingSnapshot(0).Port) + session := &ProgrammingSession{ + Format: programmingMarkerFormat, PreparedAt: time.Now().UTC(), + Device: identity, TargetFirmwareSHA256: document.SourceSHA256, + SafeStateApplied: true, Phase: "restore-failed-safe", HostResult: "succeeded", + } + if _, err := persistProgrammingMarker(paths, session); err != nil { + t.Fatal(err) + } + if got, err := findFailedProgrammingSession(paths, identity, document.SourceSHA256); err != nil || got == nil { + t.Fatalf("verified write with failed restore must permit fresh-readback recovery: %v", err) + } + if _, err := findFailedProgrammingSession(paths, identity, strings.Repeat("0", 64)); err == nil { + t.Fatal("recovery accepted a different image") + } + session.Phase = "restored" + if _, err := persistProgrammingMarker(paths, session); err != nil { + t.Fatal(err) + } + if _, err := findFailedProgrammingSession(paths, identity, document.SourceSHA256); err == nil { + t.Fatal("completed success was treated as failed restoration") + } +} + func TestProgrammingLifecycleFailedProgrammerResultRetainsLatchAndMarker(t *testing.T) { paths, firmware := programmingLifecycleFixture(t) device := &fakeProgrammingDevice{