Skip to content

Commit bfb2fb4

Browse files
committed
Upload port change detection (first draft)
1 parent 97d9e24 commit bfb2fb4

File tree

5 files changed

+150
-10
lines changed

5 files changed

+150
-10
lines changed

commands/upload/burnbootloader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func BurnBootloader(ctx context.Context, req *rpc.BurnBootloaderRequest, outStre
4141

4242
_, err := runProgramAction(
4343
pme,
44+
nil, // watcher
4445
nil, // sketch
4546
"", // importFile
4647
"", // importDir

commands/upload/upload.go

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"io"
2222
"path/filepath"
2323
"strings"
24+
"sync"
25+
"time"
2426

2527
"github.com/arduino/arduino-cli/arduino"
2628
"github.com/arduino/arduino-cli/arduino/cores"
@@ -29,8 +31,10 @@ import (
2931
"github.com/arduino/arduino-cli/arduino/serialutils"
3032
"github.com/arduino/arduino-cli/arduino/sketch"
3133
"github.com/arduino/arduino-cli/commands"
34+
"github.com/arduino/arduino-cli/commands/board"
3235
"github.com/arduino/arduino-cli/executils"
3336
"github.com/arduino/arduino-cli/i18n"
37+
f "github.com/arduino/arduino-cli/internal/algorithms"
3438
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
3539
paths "github.com/arduino/go-paths-helper"
3640
properties "github.com/arduino/go-properties-orderedmap"
@@ -134,15 +138,23 @@ func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, er
134138
return nil, &arduino.CantOpenSketchError{Cause: err}
135139
}
136140

137-
pme, release := commands.GetPackageManagerExplorer(req)
141+
pme, pmeRelease := commands.GetPackageManagerExplorer(req)
138142
if pme == nil {
139143
return nil, &arduino.InvalidInstanceError{}
140144
}
141-
defer release()
145+
defer pmeRelease()
146+
147+
watch, watchRelease, err := board.Watch(&rpc.BoardListWatchRequest{Instance: req.GetInstance()})
148+
if err != nil {
149+
logrus.WithError(err).Error("Error watching board ports")
150+
} else {
151+
defer watchRelease()
152+
}
142153

143154
updatedPort, err := runProgramAction(
144155
pme,
145156
sk,
157+
watch,
146158
req.GetImportFile(),
147159
req.GetImportDir(),
148160
req.GetFqbn(),
@@ -189,21 +201,43 @@ func UsingProgrammer(ctx context.Context, req *rpc.UploadUsingProgrammerRequest,
189201

190202
func runProgramAction(pme *packagemanager.Explorer,
191203
sk *sketch.Sketch,
204+
watch <-chan *rpc.BoardListWatchResponse,
192205
importFile, importDir, fqbnIn string, port *rpc.Port,
193206
programmerID string,
194207
verbose, verify, burnBootloader bool,
195208
outStream, errStream io.Writer,
196209
dryRun bool, userFields map[string]string) (*rpc.Port, error) {
197210

198-
if burnBootloader && programmerID == "" {
199-
return nil, &arduino.MissingProgrammerError{}
200-
}
201211
if port == nil || (port.Address == "" && port.Protocol == "") {
202212
// For no-port uploads use "default" protocol
203213
port = &rpc.Port{Protocol: "default"}
204214
}
205215
logrus.WithField("port", port).Tracef("Upload port")
206216

217+
// Default newPort
218+
uploadCompleted := func() *rpc.Port { return nil }
219+
if watch != nil {
220+
// Run port detector
221+
uploadCompletedCtx, cancel := context.WithCancel(context.Background())
222+
var newUploadPort *rpc.Port
223+
var wg sync.WaitGroup
224+
wg.Add(1)
225+
go func() {
226+
newUploadPort = detectUploadPort(port, watch, uploadCompletedCtx)
227+
wg.Done()
228+
}()
229+
uploadCompleted = func() *rpc.Port {
230+
cancel()
231+
wg.Wait()
232+
return newUploadPort
233+
}
234+
defer uploadCompleted() // defer in case of exit on error (ensures goroutine completion)
235+
}
236+
237+
if burnBootloader && programmerID == "" {
238+
return nil, &arduino.MissingProgrammerError{}
239+
}
240+
207241
fqbn, err := cores.ParseFQBN(fqbnIn)
208242
if err != nil {
209243
return nil, &arduino.InvalidFQBNError{Cause: err}
@@ -485,13 +519,73 @@ func runProgramAction(pme *packagemanager.Explorer,
485519
}
486520

487521
logrus.Tracef("Upload successful")
488-
return nil, nil // TODO: return new port
522+
return uploadCompleted(), nil
489523
}
490524

491-
func detectNewUploadPort(oldPort *rpc.Port) *rpc.Port {
492-
logrus.Tracef("Detecting new board port")
493-
// TODO
494-
return nil
525+
func detectUploadPort(uploadPort *rpc.Port, watch <-chan *rpc.BoardListWatchResponse, uploadCtx context.Context) *rpc.Port {
526+
log := logrus.WithField("task", "port_detection")
527+
log.Tracef("Detecting new board port after upload")
528+
529+
defer func() {
530+
// On exit, discard all events until the watcher is closed
531+
go f.DiscardCh(watch)
532+
}()
533+
534+
// Ignore all events during the upload
535+
for {
536+
select {
537+
case ev, ok := <-watch:
538+
if !ok {
539+
log.Error("Upload port detection failed, watcher closed")
540+
return nil
541+
}
542+
log.WithField("event", ev).Trace("Ignored watcher event before upload")
543+
continue
544+
case <-uploadCtx.Done():
545+
// Upload completed, move to the next phase
546+
}
547+
break
548+
}
549+
550+
// Pick the first port that is detected after the upload
551+
desiredHwID := uploadPort.HardwareId
552+
timeout := time.After(5 * time.Second)
553+
var candidate *rpc.Port
554+
for {
555+
select {
556+
case ev, ok := <-watch:
557+
if !ok {
558+
log.Error("Upload port detection failed, watcher closed")
559+
return candidate
560+
}
561+
if ev.EventType == "remove" && candidate != nil {
562+
if candidate.Equals(ev.Port.GetPort()) {
563+
log.WithField("event", ev).Trace("Candidate port is no more available")
564+
candidate = nil
565+
continue
566+
}
567+
}
568+
if ev.EventType != "add" {
569+
log.WithField("event", ev).Trace("Ignored non-add event")
570+
continue
571+
}
572+
candidate = ev.Port.GetPort()
573+
log.WithField("event", ev).Trace("New upload port candidate")
574+
575+
// If the current candidate port does not have the desired HW-ID do
576+
// not return it immediately.
577+
if desiredHwID != "" && candidate.GetHardwareId() != desiredHwID {
578+
log.Trace("New candidate port did not match desired HW ID, keep watching...")
579+
continue
580+
}
581+
582+
log.Trace("Found new upload port!")
583+
return candidate
584+
case <-timeout:
585+
log.Trace("Timeout waiting for candidate port")
586+
return candidate
587+
}
588+
}
495589
}
496590

497591
func runTool(recipeID string, props *properties.Map, outStream, errStream io.Writer, verbose bool, dryRun bool, toolEnv []string) error {

commands/upload/upload_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func TestUploadPropertiesComposition(t *testing.T) {
187187
_, err := runProgramAction(
188188
pme,
189189
nil, // sketch
190+
nil, // board watcher
190191
"", // importFile
191192
test.importDir.String(), // importDir
192193
test.fqbn, // FQBN

internal/algorithms/channels.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package f
17+
18+
// DiscardCh consume all incoming messages from the given channel until its closed.
19+
func DiscardCh[T any](ch <-chan T) {
20+
for range ch {
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package commands
17+
18+
// Equals return true if the given port has the same address and protocol
19+
// of the current port.
20+
func (p *Port) Equals(o *Port) bool {
21+
return p.GetAddress() == o.GetAddress() && p.GetProtocol() == o.GetProtocol()
22+
}

0 commit comments

Comments
 (0)