@@ -21,6 +21,7 @@ func RegisterPico(r *dispatch.Router) {
2121 r .Register ("pico.info" , picoInfo )
2222 r .Register ("pico.bootsel" , picoBootsel )
2323 r .Register ("pico.flash" , picoFlash )
24+ r .Register ("pico.verify" , picoVerify )
2425 r .Register ("pico.save" , picoSave )
2526 r .Register ("pico.reset" , picoReset )
2627}
@@ -42,6 +43,47 @@ func runPicotool(ctx context.Context, args ...string) (stdout, stderr string, er
4243 return out .String (), errb .String (), runErr
4344}
4445
46+ // picoStringArg pulls a string off the JSON args map and trims it.
47+ // Missing keys, wrong types, and whitespace-only values all collapse
48+ // to the empty string so callers can do a single `if s == ""` check.
49+ func picoStringArg (args map [string ]json.RawMessage , key string ) string {
50+ v , ok := args [key ]
51+ if ! ok {
52+ return ""
53+ }
54+ var s string
55+ _ = json .Unmarshal (v , & s )
56+ return strings .TrimSpace (s )
57+ }
58+
59+ // withSerial appends `--id <ser>` to a picotool arg slice when a
60+ // serial number was supplied. Pins a command to a specific board
61+ // when multiple Picos are connected to the same host.
62+ func withSerial (args []string , serial string ) []string {
63+ if serial == "" {
64+ return args
65+ }
66+ return append (args , "--id" , serial )
67+ }
68+
69+ // picoFamilies are the picotool `--family` identifiers we accept on
70+ // load/verify. Validated up front so a typo doesn't reach picotool.
71+ var picoFamilies = map [string ]struct {}{
72+ "rp2040" : {},
73+ "rp2350-arm-s" : {},
74+ "rp2350-arm-ns" : {},
75+ "rp2350-riscv" : {},
76+ "absolute" : {},
77+ "data" : {},
78+ }
79+
80+ // picoCPUs are the architectures picotool can switch to on RP2350
81+ // via `reboot -c`. RP2040 has only one core arch and will reject it.
82+ var picoCPUs = map [string ]struct {}{
83+ "arm" : {},
84+ "riscv" : {},
85+ }
86+
4587func picoList (ctx context.Context , _ map [string ]json.RawMessage ) (interface {}, error ) {
4688 out , errb , err := runPicotool (ctx , "info" , "-a" )
4789 if err != nil {
@@ -60,26 +102,37 @@ func picoList(ctx context.Context, _ map[string]json.RawMessage) (interface{}, e
60102}
61103
62104func picoInfo (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
63- cmdArgs := []string {"info" , "-a" , "-m" , "-d" , "-l" }
64- if v , ok := args ["serial" ]; ok {
65- var s string
66- _ = json .Unmarshal (v , & s )
67- if s != "" {
68- cmdArgs = append (cmdArgs , "--id" , s )
69- }
70- }
105+ cmdArgs := withSerial ([]string {"info" , "-a" , "-m" , "-d" , "-l" }, picoStringArg (args , "serial" ))
71106 out , errb , err := runPicotool (ctx , cmdArgs ... )
72107 if err != nil {
73108 return map [string ]interface {}{"ok" : false , "reason" : err .Error (), "stderr" : strings .TrimSpace (errb )}, nil
74109 }
75110 return map [string ]interface {}{"ok" : true , "raw" : out }, nil
76111}
77112
78- func picoBootsel (ctx context.Context , _ map [string ]json.RawMessage ) (interface {}, error ) {
113+ func picoBootselArgs (args map [string ]json.RawMessage ) ([]string , error ) {
114+ cmdArgs := []string {"reboot" , "-f" , "-u" }
115+ if cpu := picoStringArg (args , "cpu" ); cpu != "" {
116+ if _ , ok := picoCPUs [cpu ]; ! ok {
117+ return nil , fmt .Errorf ("pico.bootsel: cpu must be 'arm' or 'riscv'" )
118+ }
119+ cmdArgs = append (cmdArgs , "-c" , cpu )
120+ }
121+ if part := picoStringArg (args , "partition" ); part != "" {
122+ cmdArgs = append (cmdArgs , "-g" , part )
123+ }
124+ return withSerial (cmdArgs , picoStringArg (args , "serial" )), nil
125+ }
126+
127+ func picoBootsel (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
128+ cmdArgs , err := picoBootselArgs (args )
129+ if err != nil {
130+ return nil , err
131+ }
79132 if err := requireRiskConsent (ctx , "pico.bootsel" , "Reboots an attached Pico into BOOTSEL mode. Connected software may lose its current device connection." ); err != nil {
80133 return nil , err
81134 }
82- out , errb , err := runPicotool (ctx , "reboot" , "-f" , "-u" )
135+ out , errb , err := runPicotool (ctx , cmdArgs ... )
83136 if err != nil {
84137 return map [string ]interface {}{
85138 "ok" : false ,
@@ -91,47 +144,113 @@ func picoBootsel(ctx context.Context, _ map[string]json.RawMessage) (interface{}
91144 return map [string ]interface {}{"ok" : true , "raw" : out }, nil
92145}
93146
94- func picoFlash (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
95- var uf2 string
96- if v , ok := args ["uf2_path" ]; ok {
97- _ = json .Unmarshal (v , & uf2 )
98- }
147+ func picoFlashArgs (args map [string ]json.RawMessage ) (cmdArgs []string , uf2 string , err error ) {
148+ uf2 = picoStringArg (args , "uf2_path" )
99149 if uf2 == "" {
100- return nil , fmt .Errorf ("pico.flash: 'uf2_path' is required" )
150+ return nil , "" , fmt .Errorf ("pico.flash: 'uf2_path' is required" )
151+ }
152+ cmdArgs = []string {"load" , "-fx" }
153+ if family := picoStringArg (args , "family" ); family != "" {
154+ if _ , ok := picoFamilies [family ]; ! ok {
155+ return nil , "" , fmt .Errorf ("pico.flash: unknown family %q" , family )
156+ }
157+ cmdArgs = append (cmdArgs , "--family" , family )
158+ }
159+ cmdArgs = append (cmdArgs , uf2 )
160+ return withSerial (cmdArgs , picoStringArg (args , "serial" )), uf2 , nil
161+ }
162+
163+ func picoFlash (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
164+ cmdArgs , uf2 , err := picoFlashArgs (args )
165+ if err != nil {
166+ return nil , err
101167 }
102168 if err := requireRiskConsent (ctx , "pico.flash" , "Flashes firmware to an attached Pico from a UF2 file. Bad firmware can make the device stop working until it is reflashed." ); err != nil {
103169 return nil , err
104170 }
105- out , errb , err := runPicotool (ctx , "load" , "-fx" , uf2 )
171+ out , errb , err := runPicotool (ctx , cmdArgs ... )
106172 if err != nil {
107173 return map [string ]interface {}{"ok" : false , "reason" : err .Error (), "stderr" : strings .TrimSpace (errb ), "stdout" : strings .TrimSpace (out )}, nil
108174 }
109175 return map [string ]interface {}{"ok" : true , "uf2" : uf2 , "raw" : out }, nil
110176}
111177
112- func picoSave (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
113- var out string
114- if v , ok := args ["out_path" ]; ok {
115- _ = json .Unmarshal (v , & out )
178+ func picoVerifyArgs (args map [string ]json.RawMessage ) (cmdArgs []string , path string , err error ) {
179+ path = picoStringArg (args , "file_path" )
180+ if path == "" {
181+ return nil , "" , fmt .Errorf ("pico.verify: 'file_path' is required" )
182+ }
183+ cmdArgs = []string {"verify" , "-f" }
184+ if family := picoStringArg (args , "family" ); family != "" {
185+ if _ , ok := picoFamilies [family ]; ! ok {
186+ return nil , "" , fmt .Errorf ("pico.verify: unknown family %q" , family )
187+ }
188+ cmdArgs = append (cmdArgs , "--family" , family )
116189 }
190+ cmdArgs = append (cmdArgs , path )
191+ return withSerial (cmdArgs , picoStringArg (args , "serial" )), path , nil
192+ }
193+
194+ // picoVerify reads back flash from the device and compares it byte
195+ // for byte to a UF2/ELF/BIN on disk. Read-only on the device, so no
196+ // risk-consent gate -- the host is not changed by a verify.
197+ func picoVerify (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
198+ cmdArgs , path , err := picoVerifyArgs (args )
199+ if err != nil {
200+ return nil , err
201+ }
202+ out , errb , runErr := runPicotool (ctx , cmdArgs ... )
203+ if runErr != nil {
204+ return map [string ]interface {}{
205+ "ok" : false ,
206+ "reason" : runErr .Error (),
207+ "stderr" : strings .TrimSpace (errb ),
208+ "stdout" : strings .TrimSpace (out ),
209+ "file_path" : path ,
210+ }, nil
211+ }
212+ return map [string ]interface {}{"ok" : true , "file_path" : path , "raw" : out }, nil
213+ }
214+
215+ func picoSave (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
216+ out := picoStringArg (args , "out_path" )
117217 if out == "" {
118218 return nil , fmt .Errorf ("pico.save: 'out_path' is required" )
119219 }
120220 if err := requireRiskConsent (ctx , "pico.save" , "Writes a Pico flash dump to a file on this computer. Existing files may be replaced by picotool behavior." ); err != nil {
121221 return nil , err
122222 }
123- stdout , errb , err := runPicotool (ctx , "save" , "-a" , out )
223+ cmdArgs := withSerial ([]string {"save" , "-a" , out }, picoStringArg (args , "serial" ))
224+ stdout , errb , err := runPicotool (ctx , cmdArgs ... )
124225 if err != nil {
125226 return map [string ]interface {}{"ok" : false , "reason" : err .Error (), "stderr" : strings .TrimSpace (errb ), "stdout" : strings .TrimSpace (stdout )}, nil
126227 }
127228 return map [string ]interface {}{"ok" : true , "out_path" : out , "raw" : stdout }, nil
128229}
129230
130- func picoReset (ctx context.Context , _ map [string ]json.RawMessage ) (interface {}, error ) {
231+ func picoResetArgs (args map [string ]json.RawMessage ) ([]string , error ) {
232+ cmdArgs := []string {"reboot" }
233+ if cpu := picoStringArg (args , "cpu" ); cpu != "" {
234+ if _ , ok := picoCPUs [cpu ]; ! ok {
235+ return nil , fmt .Errorf ("pico.reset: cpu must be 'arm' or 'riscv'" )
236+ }
237+ cmdArgs = append (cmdArgs , "-c" , cpu )
238+ }
239+ if part := picoStringArg (args , "partition" ); part != "" {
240+ cmdArgs = append (cmdArgs , "-g" , part )
241+ }
242+ return withSerial (cmdArgs , picoStringArg (args , "serial" )), nil
243+ }
244+
245+ func picoReset (ctx context.Context , args map [string ]json.RawMessage ) (interface {}, error ) {
246+ cmdArgs , err := picoResetArgs (args )
247+ if err != nil {
248+ return nil , err
249+ }
131250 if err := requireRiskConsent (ctx , "pico.reset" , "Reboots an attached Pico. Connected software may lose its current device connection." ); err != nil {
132251 return nil , err
133252 }
134- out , errb , err := runPicotool (ctx , "reboot" )
253+ out , errb , err := runPicotool (ctx , cmdArgs ... )
135254 if err != nil {
136255 return map [string ]interface {}{"ok" : false , "reason" : err .Error (), "stderr" : strings .TrimSpace (errb ), "stdout" : strings .TrimSpace (out )}, nil
137256 }
0 commit comments