Skip to content

Commit a9c0a97

Browse files
authored
Add flag to board list command to watch for connected and disconnected boards (#1032)
1 parent 4777e1d commit a9c0a97

File tree

5 files changed

+189
-28
lines changed

5 files changed

+189
-28
lines changed

Diff for: cli/board/list.go

+113
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/arduino/arduino-cli/cli/errorcodes"
2626
"github.com/arduino/arduino-cli/cli/feedback"
2727
"github.com/arduino/arduino-cli/cli/instance"
28+
"github.com/arduino/arduino-cli/commands"
2829
"github.com/arduino/arduino-cli/commands/board"
2930
rpc "github.com/arduino/arduino-cli/rpc/commands"
3031
"github.com/arduino/arduino-cli/table"
@@ -43,15 +44,29 @@ func initListCommand() *cobra.Command {
4344

4445
listCommand.Flags().StringVar(&listFlags.timeout, "timeout", "0s",
4546
"The connected devices search timeout, raise it if your board doesn't show up (e.g. to 10s).")
47+
listCommand.Flags().BoolVarP(&listFlags.watch, "watch", "w", false,
48+
"Command keeps running and prints list of connected boards whenever there is a change.")
49+
4650
return listCommand
4751
}
4852

4953
var listFlags struct {
5054
timeout string // Expressed in a parsable duration, is the timeout for the list and attach commands.
55+
watch bool
5156
}
5257

5358
// runListCommand detects and lists the connected arduino boards
5459
func runListCommand(cmd *cobra.Command, args []string) {
60+
if listFlags.watch {
61+
inst, err := instance.CreateInstance()
62+
if err != nil {
63+
feedback.Errorf("Error detecting boards: %v", err)
64+
os.Exit(errorcodes.ErrGeneric)
65+
}
66+
watchList(cmd, inst)
67+
os.Exit(0)
68+
}
69+
5570
if timeout, err := time.ParseDuration(listFlags.timeout); err != nil {
5671
feedback.Errorf("Invalid timeout: %v", err)
5772
os.Exit(errorcodes.ErrBadArgument)
@@ -74,6 +89,48 @@ func runListCommand(cmd *cobra.Command, args []string) {
7489
feedback.PrintResult(result{ports})
7590
}
7691

92+
func watchList(cmd *cobra.Command, inst *rpc.Instance) {
93+
pm := commands.GetPackageManager(inst.Id)
94+
eventsChan, err := commands.WatchListBoards(pm)
95+
if err != nil {
96+
feedback.Errorf("Error detecting boards: %v", err)
97+
os.Exit(errorcodes.ErrNetwork)
98+
}
99+
100+
// This is done to avoid printing the header each time a new event is received
101+
if feedback.GetFormat() == feedback.Text {
102+
t := table.New()
103+
t.SetHeader("Port", "Type", "Event", "Board Name", "FQBN", "Core")
104+
feedback.Print(t.Render())
105+
}
106+
107+
for event := range eventsChan {
108+
boards := []*rpc.BoardListItem{}
109+
if event.Type == "add" {
110+
boards, err = board.Identify(pm, &commands.BoardPort{
111+
Address: event.Port.Address,
112+
Label: event.Port.AddressLabel,
113+
Prefs: event.Port.Properties,
114+
IdentificationPrefs: event.Port.IdentificationProperties,
115+
Protocol: event.Port.Protocol,
116+
ProtocolLabel: event.Port.ProtocolLabel,
117+
})
118+
if err != nil {
119+
feedback.Errorf("Error identifying board: %v", err)
120+
os.Exit(errorcodes.ErrNetwork)
121+
}
122+
}
123+
124+
feedback.PrintResult(watchEvent{
125+
Type: event.Type,
126+
Address: event.Port.Address,
127+
Protocol: event.Port.Protocol,
128+
ProtocolLabel: event.Port.ProtocolLabel,
129+
Boards: boards,
130+
})
131+
}
132+
}
133+
77134
// output from this command requires special formatting, let's create a dedicated
78135
// feedback.Result implementation
79136
type result struct {
@@ -134,3 +191,59 @@ func (dr result) String() string {
134191
}
135192
return t.Render()
136193
}
194+
195+
type watchEvent struct {
196+
Type string `json:"type"`
197+
Address string `json:"address,omitempty"`
198+
Protocol string `json:"protocol,omitempty"`
199+
ProtocolLabel string `json:"protocol_label,omitempty"`
200+
Boards []*rpc.BoardListItem `json:"boards,omitempty"`
201+
}
202+
203+
func (dr watchEvent) Data() interface{} {
204+
return dr
205+
}
206+
207+
func (dr watchEvent) String() string {
208+
t := table.New()
209+
210+
event := map[string]string{
211+
"add": "Connected",
212+
"remove": "Disconnected",
213+
}[dr.Type]
214+
215+
address := fmt.Sprintf("%s://%s", dr.Protocol, dr.Address)
216+
if dr.Protocol == "serial" || dr.Protocol == "" {
217+
address = dr.Address
218+
}
219+
protocol := dr.ProtocolLabel
220+
if boards := dr.Boards; len(boards) > 0 {
221+
sort.Slice(boards, func(i, j int) bool {
222+
x, y := boards[i], boards[j]
223+
return x.GetName() < y.GetName() || (x.GetName() == y.GetName() && x.GetFQBN() < y.GetFQBN())
224+
})
225+
for _, b := range boards {
226+
board := b.GetName()
227+
228+
// to improve the user experience, show on a dedicated column
229+
// the name of the core supporting the board detected
230+
var coreName = ""
231+
fqbn, err := cores.ParseFQBN(b.GetFQBN())
232+
if err == nil {
233+
coreName = fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch)
234+
}
235+
236+
t.AddRow(address, protocol, event, board, fqbn, coreName)
237+
238+
// reset address and protocol, we only show them on the first row
239+
address = ""
240+
protocol = ""
241+
}
242+
} else {
243+
board := ""
244+
fqbn := ""
245+
coreName := ""
246+
t.AddRow(address, protocol, event, board, fqbn, coreName)
247+
}
248+
return t.Render()
249+
}

Diff for: cli/feedback/exported.go

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ func SetFormat(f OutputFormat) {
3434
fb.SetFormat(f)
3535
}
3636

37+
// GetFormat returns the currently set output format
38+
func GetFormat() OutputFormat {
39+
return fb.GetFormat()
40+
}
41+
3742
// OutputWriter returns the underlying io.Writer to be used when the Print*
3843
// api is not enough
3944
func OutputWriter() io.Writer {

Diff for: cli/feedback/feedback.go

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ func (fb *Feedback) SetFormat(f OutputFormat) {
6868
fb.format = f
6969
}
7070

71+
// GetFormat returns the output format currently set
72+
func (fb *Feedback) GetFormat() OutputFormat {
73+
return fb.format
74+
}
75+
7176
// OutputWriter returns the underlying io.Writer to be used when the Print*
7277
// api is not enough.
7378
func (fb *Feedback) OutputWriter() io.Writer {

Diff for: commands/board/list.go

+38-28
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"regexp"
2424
"sync"
2525

26+
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
2627
"github.com/arduino/arduino-cli/commands"
2728
"github.com/arduino/arduino-cli/httpclient"
2829
rpc "github.com/arduino/arduino-cli/rpc/commands"
@@ -107,6 +108,39 @@ func identifyViaCloudAPI(port *commands.BoardPort) ([]*rpc.BoardListItem, error)
107108
return apiByVidPid(id.Get("vid"), id.Get("pid"))
108109
}
109110

111+
// Identify returns a list of boards checking first the installed platforms or the Cloud API
112+
func Identify(pm *packagemanager.PackageManager, port *commands.BoardPort) ([]*rpc.BoardListItem, error) {
113+
boards := []*rpc.BoardListItem{}
114+
115+
// first query installed cores through the Package Manager
116+
logrus.Debug("Querying installed cores for board identification...")
117+
for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) {
118+
boards = append(boards, &rpc.BoardListItem{
119+
Name: board.Name(),
120+
FQBN: board.FQBN(),
121+
})
122+
}
123+
124+
// if installed cores didn't recognize the board, try querying
125+
// the builder API if the board is a USB device port
126+
if len(boards) == 0 {
127+
items, err := identifyViaCloudAPI(port)
128+
if err == ErrNotFound {
129+
// the board couldn't be detected, print a warning
130+
logrus.Debug("Board not recognized")
131+
} else if err != nil {
132+
// this is bad, bail out
133+
return nil, errors.Wrap(err, "error getting board info from Arduino Cloud")
134+
}
135+
136+
// add a DetectedPort entry in any case: the `Boards` field will
137+
// be empty but the port will be shown anyways (useful for 3rd party
138+
// boards)
139+
boards = items
140+
}
141+
return boards, nil
142+
}
143+
110144
// List FIXMEDOC
111145
func List(instanceID int32) (r []*rpc.DetectedPort, e error) {
112146
m.Lock()
@@ -135,33 +169,9 @@ func List(instanceID int32) (r []*rpc.DetectedPort, e error) {
135169

136170
retVal := []*rpc.DetectedPort{}
137171
for _, port := range ports {
138-
b := []*rpc.BoardListItem{}
139-
140-
// first query installed cores through the Package Manager
141-
logrus.Debug("Querying installed cores for board identification...")
142-
for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) {
143-
b = append(b, &rpc.BoardListItem{
144-
Name: board.Name(),
145-
FQBN: board.FQBN(),
146-
})
147-
}
148-
149-
// if installed cores didn't recognize the board, try querying
150-
// the builder API if the board is a USB device port
151-
if len(b) == 0 {
152-
items, err := identifyViaCloudAPI(port)
153-
if err == ErrNotFound {
154-
// the board couldn't be detected, print a warning
155-
logrus.Debug("Board not recognized")
156-
} else if err != nil {
157-
// this is bad, bail out
158-
return nil, errors.Wrap(err, "error getting board info from Arduino Cloud")
159-
}
160-
161-
// add a DetectedPort entry in any case: the `Boards` field will
162-
// be empty but the port will be shown anyways (useful for 3rd party
163-
// boards)
164-
b = items
172+
boards, err := Identify(pm, port)
173+
if err != nil {
174+
return nil, err
165175
}
166176

167177
// boards slice can be empty at this point if neither the cores nor the
@@ -170,7 +180,7 @@ func List(instanceID int32) (r []*rpc.DetectedPort, e error) {
170180
Address: port.Address,
171181
Protocol: port.Protocol,
172182
ProtocolLabel: port.ProtocolLabel,
173-
Boards: b,
183+
Boards: boards,
174184
}
175185
retVal = append(retVal, p)
176186
}

Diff for: commands/bundled_tools_serial_discovery.go

+28
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/arduino/arduino-cli/arduino/cores"
2525
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
26+
"github.com/arduino/arduino-cli/arduino/discovery"
2627
"github.com/arduino/arduino-cli/arduino/resources"
2728
"github.com/arduino/arduino-cli/executils"
2829
"github.com/arduino/go-properties-orderedmap"
@@ -203,6 +204,33 @@ func ListBoards(pm *packagemanager.PackageManager) ([]*BoardPort, error) {
203204
return retVal, finalError
204205
}
205206

207+
// WatchListBoards returns a channel that receives events from the bundled discovery tool
208+
func WatchListBoards(pm *packagemanager.PackageManager) (<-chan *discovery.Event, error) {
209+
t, err := getBuiltinSerialDiscoveryTool(pm)
210+
if err != nil {
211+
return nil, err
212+
}
213+
214+
if !t.IsInstalled() {
215+
return nil, fmt.Errorf("missing serial-discovery tool")
216+
}
217+
218+
disc, err := discovery.New("serial-discovery", t.InstallDir.Join(t.Tool.Name).String())
219+
if err != nil {
220+
return nil, err
221+
}
222+
223+
if err = disc.Start(); err != nil {
224+
return nil, fmt.Errorf("starting discovery: %v", err)
225+
}
226+
227+
if err = disc.StartSync(); err != nil {
228+
return nil, fmt.Errorf("starting sync: %v", err)
229+
}
230+
231+
return disc.EventChannel(10), nil
232+
}
233+
206234
func getBuiltinSerialDiscoveryTool(pm *packagemanager.PackageManager) (*cores.ToolRelease, error) {
207235
builtinPackage := pm.Packages.GetOrCreatePackage("builtin")
208236
ctagsTool := builtinPackage.GetOrCreateTool("serial-discovery")

0 commit comments

Comments
 (0)