@@ -21,16 +21,19 @@ import (
21
21
"io"
22
22
"path/filepath"
23
23
"strings"
24
+ "time"
24
25
25
26
"github.com/arduino/arduino-cli/arduino"
26
27
"github.com/arduino/arduino-cli/arduino/cores"
27
28
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
29
+ "github.com/arduino/arduino-cli/arduino/discovery"
28
30
"github.com/arduino/arduino-cli/arduino/globals"
29
31
"github.com/arduino/arduino-cli/arduino/serialutils"
30
32
"github.com/arduino/arduino-cli/arduino/sketch"
31
33
"github.com/arduino/arduino-cli/commands"
32
34
"github.com/arduino/arduino-cli/executils"
33
35
"github.com/arduino/arduino-cli/i18n"
36
+ f "github.com/arduino/arduino-cli/internal/algorithms"
34
37
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
35
38
paths "github.com/arduino/go-paths-helper"
36
39
properties "github.com/arduino/go-properties-orderedmap"
@@ -123,24 +126,24 @@ func getUserFields(toolID string, platformRelease *cores.PlatformRelease) []*rpc
123
126
}
124
127
125
128
// Upload FIXMEDOC
126
- func Upload (ctx context.Context , req * rpc.UploadRequest , outStream io.Writer , errStream io.Writer ) error {
129
+ func Upload (ctx context.Context , req * rpc.UploadRequest , outStream io.Writer , errStream io.Writer ) ( * rpc. UploadResult , error ) {
127
130
logrus .Tracef ("Upload %s on %s started" , req .GetSketchPath (), req .GetFqbn ())
128
131
129
132
// TODO: make a generic function to extract sketch from request
130
133
// and remove duplication in commands/compile.go
131
134
sketchPath := paths .New (req .GetSketchPath ())
132
135
sk , err := sketch .New (sketchPath )
133
136
if err != nil && req .GetImportDir () == "" && req .GetImportFile () == "" {
134
- return & arduino.CantOpenSketchError {Cause : err }
137
+ return nil , & arduino.CantOpenSketchError {Cause : err }
135
138
}
136
139
137
- pme , release := commands .GetPackageManagerExplorer (req )
140
+ pme , pmeRelease := commands .GetPackageManagerExplorer (req )
138
141
if pme == nil {
139
- return & arduino.InvalidInstanceError {}
142
+ return nil , & arduino.InvalidInstanceError {}
140
143
}
141
- defer release ()
144
+ defer pmeRelease ()
142
145
143
- if err := runProgramAction (
146
+ updatedPort , err := runProgramAction (
144
147
pme ,
145
148
sk ,
146
149
req .GetImportFile (),
@@ -155,11 +158,14 @@ func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, er
155
158
errStream ,
156
159
req .GetDryRun (),
157
160
req .GetUserFields (),
158
- ); err != nil {
159
- return err
161
+ )
162
+ if err != nil {
163
+ return nil , err
160
164
}
161
165
162
- return nil
166
+ return & rpc.UploadResult {
167
+ UpdatedUploadPort : updatedPort ,
168
+ }, nil
163
169
}
164
170
165
171
// UsingProgrammer FIXMEDOC
@@ -169,7 +175,7 @@ func UsingProgrammer(ctx context.Context, req *rpc.UploadUsingProgrammerRequest,
169
175
if req .GetProgrammer () == "" {
170
176
return & arduino.MissingProgrammerError {}
171
177
}
172
- err := Upload (ctx , & rpc.UploadRequest {
178
+ _ , err := Upload (ctx , & rpc.UploadRequest {
173
179
Instance : req .GetInstance (),
174
180
SketchPath : req .GetSketchPath (),
175
181
ImportFile : req .GetImportFile (),
@@ -186,36 +192,38 @@ func UsingProgrammer(ctx context.Context, req *rpc.UploadUsingProgrammerRequest,
186
192
187
193
func runProgramAction (pme * packagemanager.Explorer ,
188
194
sk * sketch.Sketch ,
189
- importFile , importDir , fqbnIn string , port * rpc.Port ,
195
+ importFile , importDir , fqbnIn string , userPort * rpc.Port ,
190
196
programmerID string ,
191
197
verbose , verify , burnBootloader bool ,
192
198
outStream , errStream io.Writer ,
193
- dryRun bool , userFields map [string ]string ) error {
194
-
195
- if burnBootloader && programmerID == "" {
196
- return & arduino.MissingProgrammerError {}
197
- }
199
+ dryRun bool , userFields map [string ]string ,
200
+ ) (* rpc.Port , error ) {
201
+ port := discovery .PortFromRPCPort (userPort )
198
202
if port == nil || (port .Address == "" && port .Protocol == "" ) {
199
203
// For no-port uploads use "default" protocol
200
- port = & rpc .Port {Protocol : "default" }
204
+ port = & discovery .Port {Protocol : "default" }
201
205
}
202
206
logrus .WithField ("port" , port ).Tracef ("Upload port" )
203
207
208
+ if burnBootloader && programmerID == "" {
209
+ return nil , & arduino.MissingProgrammerError {}
210
+ }
211
+
204
212
fqbn , err := cores .ParseFQBN (fqbnIn )
205
213
if err != nil {
206
- return & arduino.InvalidFQBNError {Cause : err }
214
+ return nil , & arduino.InvalidFQBNError {Cause : err }
207
215
}
208
216
logrus .WithField ("fqbn" , fqbn ).Tracef ("Detected FQBN" )
209
217
210
218
// Find target board and board properties
211
219
_ , boardPlatform , board , boardProperties , buildPlatform , err := pme .ResolveFQBN (fqbn )
212
220
if boardPlatform == nil {
213
- return & arduino.PlatformNotFoundError {
221
+ return nil , & arduino.PlatformNotFoundError {
214
222
Platform : fmt .Sprintf ("%s:%s" , fqbn .Package , fqbn .PlatformArch ),
215
223
Cause : err ,
216
224
}
217
225
} else if err != nil {
218
- return & arduino.UnknownFQBNError {Cause : err }
226
+ return nil , & arduino.UnknownFQBNError {Cause : err }
219
227
}
220
228
logrus .
221
229
WithField ("boardPlatform" , boardPlatform ).
@@ -232,7 +240,7 @@ func runProgramAction(pme *packagemanager.Explorer,
232
240
programmer = buildPlatform .Programmers [programmerID ]
233
241
}
234
242
if programmer == nil {
235
- return & arduino.ProgrammerNotFoundError {Programmer : programmerID }
243
+ return nil , & arduino.ProgrammerNotFoundError {Programmer : programmerID }
236
244
}
237
245
}
238
246
@@ -253,7 +261,7 @@ func runProgramAction(pme *packagemanager.Explorer,
253
261
}
254
262
uploadToolID , err := getToolID (props , action , port .Protocol )
255
263
if err != nil {
256
- return err
264
+ return nil , err
257
265
}
258
266
259
267
var uploadToolPlatform * cores.PlatformRelease
@@ -268,7 +276,7 @@ func runProgramAction(pme *packagemanager.Explorer,
268
276
Trace ("Upload tool" )
269
277
270
278
if split := strings .Split (uploadToolID , ":" ); len (split ) > 2 {
271
- return & arduino.InvalidPlatformPropertyError {
279
+ return nil , & arduino.InvalidPlatformPropertyError {
272
280
Property : fmt .Sprintf ("%s.tool.%s" , action , port .Protocol ), // TODO: Can be done better, maybe inline getToolID(...)
273
281
Value : uploadToolID }
274
282
} else if len (split ) == 2 {
@@ -277,12 +285,12 @@ func runProgramAction(pme *packagemanager.Explorer,
277
285
PlatformArchitecture : boardPlatform .Platform .Architecture ,
278
286
})
279
287
if p == nil {
280
- return & arduino.PlatformNotFoundError {Platform : split [0 ] + ":" + boardPlatform .Platform .Architecture }
288
+ return nil , & arduino.PlatformNotFoundError {Platform : split [0 ] + ":" + boardPlatform .Platform .Architecture }
281
289
}
282
290
uploadToolID = split [1 ]
283
291
uploadToolPlatform = pme .GetInstalledPlatformRelease (p )
284
292
if uploadToolPlatform == nil {
285
- return & arduino.PlatformNotFoundError {Platform : split [0 ] + ":" + boardPlatform .Platform .Architecture }
293
+ return nil , & arduino.PlatformNotFoundError {Platform : split [0 ] + ":" + boardPlatform .Platform .Architecture }
286
294
}
287
295
}
288
296
@@ -309,7 +317,7 @@ func runProgramAction(pme *packagemanager.Explorer,
309
317
}
310
318
311
319
if ! uploadProperties .ContainsKey ("upload.protocol" ) && programmer == nil {
312
- return & arduino.ProgrammerRequiredForUploadError {}
320
+ return nil , & arduino.ProgrammerRequiredForUploadError {}
313
321
}
314
322
315
323
// Set properties for verbose upload
@@ -357,18 +365,35 @@ func runProgramAction(pme *packagemanager.Explorer,
357
365
if ! burnBootloader {
358
366
importPath , sketchName , err := determineBuildPathAndSketchName (importFile , importDir , sk , fqbn )
359
367
if err != nil {
360
- return & arduino.NotFoundError {Message : tr ("Error finding build artifacts" ), Cause : err }
368
+ return nil , & arduino.NotFoundError {Message : tr ("Error finding build artifacts" ), Cause : err }
361
369
}
362
370
if ! importPath .Exist () {
363
- return & arduino.NotFoundError {Message : tr ("Compiled sketch not found in %s" , importPath )}
371
+ return nil , & arduino.NotFoundError {Message : tr ("Compiled sketch not found in %s" , importPath )}
364
372
}
365
373
if ! importPath .IsDir () {
366
- return & arduino.NotFoundError {Message : tr ("Expected compiled sketch in directory %s, but is a file instead" , importPath )}
374
+ return nil , & arduino.NotFoundError {Message : tr ("Expected compiled sketch in directory %s, but is a file instead" , importPath )}
367
375
}
368
376
uploadProperties .SetPath ("build.path" , importPath )
369
377
uploadProperties .Set ("build.project_name" , sketchName )
370
378
}
371
379
380
+ // This context is kept alive for the entire duration of the upload
381
+ uploadCtx , uploadCompleted := context .WithCancel (context .Background ())
382
+ defer uploadCompleted ()
383
+
384
+ // Start the upload port change detector.
385
+ watcher , err := pme .DiscoveryManager ().Watch ()
386
+ if err != nil {
387
+ return nil , err
388
+ }
389
+ defer watcher .Close ()
390
+ updatedUploadPort := f .NewFuture [* discovery.Port ]()
391
+ go detectUploadPort (
392
+ uploadCtx ,
393
+ port , watcher .Feed (),
394
+ uploadProperties .GetBoolean ("upload.wait_for_upload_port" ),
395
+ updatedUploadPort )
396
+
372
397
// Force port wait to make easier to unbrick boards like the Arduino Leonardo, or similar with native USB,
373
398
// when a sketch causes a crash and the native USB serial port is lost.
374
399
// See https://github.com/arduino/arduino-cli/issues/1943 for the details.
@@ -385,7 +410,7 @@ func runProgramAction(pme *packagemanager.Explorer,
385
410
386
411
// If not using programmer perform some action required
387
412
// to set the board in bootloader mode
388
- actualPort := port
413
+ actualPort := port . Clone ()
389
414
if programmer == nil && ! burnBootloader && (port .Protocol == "serial" || forcedSerialPortWait ) {
390
415
// Perform reset via 1200bps touch if requested and wait for upload port also if requested.
391
416
touch := uploadProperties .GetBoolean ("upload.use_1200bps_touch" )
@@ -439,6 +464,7 @@ func runProgramAction(pme *packagemanager.Explorer,
439
464
} else {
440
465
if newPortAddress != "" {
441
466
actualPort .Address = newPortAddress
467
+ actualPort .AddressLabel = newPortAddress
442
468
}
443
469
}
444
470
}
@@ -455,34 +481,144 @@ func runProgramAction(pme *packagemanager.Explorer,
455
481
456
482
// Get Port properties gathered using pluggable discovery
457
483
uploadProperties .Set ("upload.port.address" , port .Address )
458
- uploadProperties .Set ("upload.port.label" , port .Label )
484
+ uploadProperties .Set ("upload.port.label" , port .AddressLabel )
459
485
uploadProperties .Set ("upload.port.protocol" , port .Protocol )
460
486
uploadProperties .Set ("upload.port.protocolLabel" , port .ProtocolLabel )
461
- for prop , value := range actualPort .Properties {
462
- uploadProperties .Set (fmt .Sprintf ("upload.port.properties.%s" , prop ), value )
487
+ if actualPort .Properties != nil {
488
+ for prop , value := range actualPort .Properties .AsMap () {
489
+ uploadProperties .Set (fmt .Sprintf ("upload.port.properties.%s" , prop ), value )
490
+ }
463
491
}
464
492
465
493
// Run recipes for upload
466
494
toolEnv := pme .GetEnvVarsForSpawnedProcess ()
467
495
if burnBootloader {
468
496
if err := runTool ("erase.pattern" , uploadProperties , outStream , errStream , verbose , dryRun , toolEnv ); err != nil {
469
- return & arduino.FailedUploadError {Message : tr ("Failed chip erase" ), Cause : err }
497
+ return nil , & arduino.FailedUploadError {Message : tr ("Failed chip erase" ), Cause : err }
470
498
}
471
499
if err := runTool ("bootloader.pattern" , uploadProperties , outStream , errStream , verbose , dryRun , toolEnv ); err != nil {
472
- return & arduino.FailedUploadError {Message : tr ("Failed to burn bootloader" ), Cause : err }
500
+ return nil , & arduino.FailedUploadError {Message : tr ("Failed to burn bootloader" ), Cause : err }
473
501
}
474
502
} else if programmer != nil {
475
503
if err := runTool ("program.pattern" , uploadProperties , outStream , errStream , verbose , dryRun , toolEnv ); err != nil {
476
- return & arduino.FailedUploadError {Message : tr ("Failed programming" ), Cause : err }
504
+ return nil , & arduino.FailedUploadError {Message : tr ("Failed programming" ), Cause : err }
477
505
}
478
506
} else {
479
507
if err := runTool ("upload.pattern" , uploadProperties , outStream , errStream , verbose , dryRun , toolEnv ); err != nil {
480
- return & arduino.FailedUploadError {Message : tr ("Failed uploading" ), Cause : err }
508
+ return nil , & arduino.FailedUploadError {Message : tr ("Failed uploading" ), Cause : err }
481
509
}
482
510
}
483
511
512
+ uploadCompleted ()
484
513
logrus .Tracef ("Upload successful" )
485
- return nil
514
+
515
+ updatedPort := updatedUploadPort .Await ()
516
+ if updatedPort == nil {
517
+ return nil , nil
518
+ }
519
+ return updatedPort .ToRPC (), nil
520
+ }
521
+
522
+ func detectUploadPort (
523
+ uploadCtx context.Context ,
524
+ uploadPort * discovery.Port , watch <- chan * discovery.Event ,
525
+ waitForUploadPort bool ,
526
+ result f.Future [* discovery.Port ],
527
+ ) {
528
+ log := logrus .WithField ("task" , "port_detection" )
529
+ log .Tracef ("Detecting new board port after upload" )
530
+
531
+ candidate := uploadPort .Clone ()
532
+ defer func () {
533
+ result .Send (candidate )
534
+ }()
535
+
536
+ // Ignore all events during the upload
537
+ for {
538
+ select {
539
+ case ev , ok := <- watch :
540
+ if ! ok {
541
+ log .Error ("Upload port detection failed, watcher closed" )
542
+ return
543
+ }
544
+ if candidate != nil && ev .Type == "remove" && ev .Port .Equals (candidate ) {
545
+ log .WithField ("event" , ev ).Trace ("User-specified port has been disconnected, forcing wait for upload port" )
546
+ waitForUploadPort = true
547
+ candidate = nil
548
+ } else {
549
+ log .WithField ("event" , ev ).Trace ("Ignored watcher event before upload" )
550
+ }
551
+ continue
552
+ case <- uploadCtx .Done ():
553
+ // Upload completed, move to the next phase
554
+ }
555
+ break
556
+ }
557
+
558
+ // Pick the first port that is detected after the upload
559
+ timeout := time .After (5 * time .Second )
560
+ if ! waitForUploadPort {
561
+ timeout = time .After (time .Second )
562
+ }
563
+ for {
564
+ select {
565
+ case ev , ok := <- watch :
566
+ if ! ok {
567
+ log .Error ("Upload port detection failed, watcher closed" )
568
+ return
569
+ }
570
+ if candidate != nil && ev .Type == "remove" && candidate .Equals (ev .Port ) {
571
+ log .WithField ("event" , ev ).Trace ("Candidate port is no longer available" )
572
+ candidate = nil
573
+ if ! waitForUploadPort {
574
+ waitForUploadPort = true
575
+ timeout = time .After (5 * time .Second )
576
+ log .Trace ("User-specified port has been disconnected, now waiting for upload port, timeout extended by 5 seconds" )
577
+ }
578
+ continue
579
+ }
580
+ if ev .Type != "add" {
581
+ log .WithField ("event" , ev ).Trace ("Ignored non-add event" )
582
+ continue
583
+ }
584
+
585
+ portPriority := func (port * discovery.Port ) int {
586
+ if port == nil {
587
+ return 0
588
+ }
589
+ prio := 0
590
+ if port .HardwareID == uploadPort .HardwareID {
591
+ prio += 1000
592
+ }
593
+ if port .Protocol == uploadPort .Protocol {
594
+ prio += 100
595
+ }
596
+ if port .Address == uploadPort .Address {
597
+ prio += 10
598
+ }
599
+ return prio
600
+ }
601
+ evPortPriority := portPriority (ev .Port )
602
+ candidatePriority := portPriority (candidate )
603
+ if evPortPriority <= candidatePriority {
604
+ log .WithField ("event" , ev ).Tracef ("New upload port candidate is worse than the current one (prio=%d)" , evPortPriority )
605
+ continue
606
+ }
607
+ log .WithField ("event" , ev ).Tracef ("Found new upload port candidate (prio=%d)" , evPortPriority )
608
+ candidate = ev .Port
609
+
610
+ // If the current candidate have the desired HW-ID return it quickly.
611
+ if candidate .HardwareID == ev .Port .HardwareID {
612
+ timeout = time .After (time .Second )
613
+ log .Trace ("New candidate port match the desired HW ID, timeout reduced to 1 second." )
614
+ continue
615
+ }
616
+
617
+ case <- timeout :
618
+ log .WithField ("selected_port" , candidate ).Trace ("Timeout waiting for candidate port" )
619
+ return
620
+ }
621
+ }
486
622
}
487
623
488
624
func runTool (recipeID string , props * properties.Map , outStream , errStream io.Writer , verbose bool , dryRun bool , toolEnv []string ) error {
0 commit comments