diff --git a/cli/board/list.go b/cli/board/list.go index c9c7551a91d..f622dc9556f 100644 --- a/cli/board/list.go +++ b/cli/board/list.go @@ -25,6 +25,7 @@ import ( "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cli/cli/instance" + "github.com/arduino/arduino-cli/commands" "github.com/arduino/arduino-cli/commands/board" rpc "github.com/arduino/arduino-cli/rpc/commands" "github.com/arduino/arduino-cli/table" @@ -43,15 +44,29 @@ func initListCommand() *cobra.Command { listCommand.Flags().StringVar(&listFlags.timeout, "timeout", "0s", "The connected devices search timeout, raise it if your board doesn't show up (e.g. to 10s).") + listCommand.Flags().BoolVarP(&listFlags.watch, "watch", "w", false, + "Command keeps running and prints list of connected boards whenever there is a change.") + return listCommand } var listFlags struct { timeout string // Expressed in a parsable duration, is the timeout for the list and attach commands. + watch bool } // runListCommand detects and lists the connected arduino boards func runListCommand(cmd *cobra.Command, args []string) { + if listFlags.watch { + inst, err := instance.CreateInstance() + if err != nil { + feedback.Errorf("Error detecting boards: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + watchList(cmd, inst) + os.Exit(0) + } + if timeout, err := time.ParseDuration(listFlags.timeout); err != nil { feedback.Errorf("Invalid timeout: %v", err) os.Exit(errorcodes.ErrBadArgument) @@ -74,6 +89,48 @@ func runListCommand(cmd *cobra.Command, args []string) { feedback.PrintResult(result{ports}) } +func watchList(cmd *cobra.Command, inst *rpc.Instance) { + pm := commands.GetPackageManager(inst.Id) + eventsChan, err := commands.WatchListBoards(pm) + if err != nil { + feedback.Errorf("Error detecting boards: %v", err) + os.Exit(errorcodes.ErrNetwork) + } + + // This is done to avoid printing the header each time a new event is received + if feedback.GetFormat() == feedback.Text { + t := table.New() + t.SetHeader("Port", "Type", "Event", "Board Name", "FQBN", "Core") + feedback.Print(t.Render()) + } + + for event := range eventsChan { + boards := []*rpc.BoardListItem{} + if event.Type == "add" { + boards, err = board.Identify(pm, &commands.BoardPort{ + Address: event.Port.Address, + Label: event.Port.AddressLabel, + Prefs: event.Port.Properties, + IdentificationPrefs: event.Port.IdentificationProperties, + Protocol: event.Port.Protocol, + ProtocolLabel: event.Port.ProtocolLabel, + }) + if err != nil { + feedback.Errorf("Error identifying board: %v", err) + os.Exit(errorcodes.ErrNetwork) + } + } + + feedback.PrintResult(watchEvent{ + Type: event.Type, + Address: event.Port.Address, + Protocol: event.Port.Protocol, + ProtocolLabel: event.Port.ProtocolLabel, + Boards: boards, + }) + } +} + // output from this command requires special formatting, let's create a dedicated // feedback.Result implementation type result struct { @@ -134,3 +191,59 @@ func (dr result) String() string { } return t.Render() } + +type watchEvent struct { + Type string `json:"type"` + Address string `json:"address,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolLabel string `json:"protocol_label,omitempty"` + Boards []*rpc.BoardListItem `json:"boards,omitempty"` +} + +func (dr watchEvent) Data() interface{} { + return dr +} + +func (dr watchEvent) String() string { + t := table.New() + + event := map[string]string{ + "add": "Connected", + "remove": "Disconnected", + }[dr.Type] + + address := fmt.Sprintf("%s://%s", dr.Protocol, dr.Address) + if dr.Protocol == "serial" || dr.Protocol == "" { + address = dr.Address + } + protocol := dr.ProtocolLabel + if boards := dr.Boards; len(boards) > 0 { + sort.Slice(boards, func(i, j int) bool { + x, y := boards[i], boards[j] + return x.GetName() < y.GetName() || (x.GetName() == y.GetName() && x.GetFQBN() < y.GetFQBN()) + }) + for _, b := range boards { + board := b.GetName() + + // to improve the user experience, show on a dedicated column + // the name of the core supporting the board detected + var coreName = "" + fqbn, err := cores.ParseFQBN(b.GetFQBN()) + if err == nil { + coreName = fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch) + } + + t.AddRow(address, protocol, event, board, fqbn, coreName) + + // reset address and protocol, we only show them on the first row + address = "" + protocol = "" + } + } else { + board := "" + fqbn := "" + coreName := "" + t.AddRow(address, protocol, event, board, fqbn, coreName) + } + return t.Render() +} diff --git a/cli/feedback/exported.go b/cli/feedback/exported.go index 17ecb4a9126..a9059a8cb00 100644 --- a/cli/feedback/exported.go +++ b/cli/feedback/exported.go @@ -34,6 +34,11 @@ func SetFormat(f OutputFormat) { fb.SetFormat(f) } +// GetFormat returns the currently set output format +func GetFormat() OutputFormat { + return fb.GetFormat() +} + // OutputWriter returns the underlying io.Writer to be used when the Print* // api is not enough func OutputWriter() io.Writer { diff --git a/cli/feedback/feedback.go b/cli/feedback/feedback.go index 8e64b8d560c..7284be4a522 100644 --- a/cli/feedback/feedback.go +++ b/cli/feedback/feedback.go @@ -68,6 +68,11 @@ func (fb *Feedback) SetFormat(f OutputFormat) { fb.format = f } +// GetFormat returns the output format currently set +func (fb *Feedback) GetFormat() OutputFormat { + return fb.format +} + // OutputWriter returns the underlying io.Writer to be used when the Print* // api is not enough. func (fb *Feedback) OutputWriter() io.Writer { diff --git a/commands/board/list.go b/commands/board/list.go index c47e7d6c474..93798cce0ab 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -23,6 +23,7 @@ import ( "regexp" "sync" + "github.com/arduino/arduino-cli/arduino/cores/packagemanager" "github.com/arduino/arduino-cli/commands" "github.com/arduino/arduino-cli/httpclient" rpc "github.com/arduino/arduino-cli/rpc/commands" @@ -107,6 +108,39 @@ func identifyViaCloudAPI(port *commands.BoardPort) ([]*rpc.BoardListItem, error) return apiByVidPid(id.Get("vid"), id.Get("pid")) } +// Identify returns a list of boards checking first the installed platforms or the Cloud API +func Identify(pm *packagemanager.PackageManager, port *commands.BoardPort) ([]*rpc.BoardListItem, error) { + boards := []*rpc.BoardListItem{} + + // first query installed cores through the Package Manager + logrus.Debug("Querying installed cores for board identification...") + for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) { + boards = append(boards, &rpc.BoardListItem{ + Name: board.Name(), + FQBN: board.FQBN(), + }) + } + + // if installed cores didn't recognize the board, try querying + // the builder API if the board is a USB device port + if len(boards) == 0 { + items, err := identifyViaCloudAPI(port) + if err == ErrNotFound { + // the board couldn't be detected, print a warning + logrus.Debug("Board not recognized") + } else if err != nil { + // this is bad, bail out + return nil, errors.Wrap(err, "error getting board info from Arduino Cloud") + } + + // add a DetectedPort entry in any case: the `Boards` field will + // be empty but the port will be shown anyways (useful for 3rd party + // boards) + boards = items + } + return boards, nil +} + // List FIXMEDOC func List(instanceID int32) (r []*rpc.DetectedPort, e error) { m.Lock() @@ -135,33 +169,9 @@ func List(instanceID int32) (r []*rpc.DetectedPort, e error) { retVal := []*rpc.DetectedPort{} for _, port := range ports { - b := []*rpc.BoardListItem{} - - // first query installed cores through the Package Manager - logrus.Debug("Querying installed cores for board identification...") - for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) { - b = append(b, &rpc.BoardListItem{ - Name: board.Name(), - FQBN: board.FQBN(), - }) - } - - // if installed cores didn't recognize the board, try querying - // the builder API if the board is a USB device port - if len(b) == 0 { - items, err := identifyViaCloudAPI(port) - if err == ErrNotFound { - // the board couldn't be detected, print a warning - logrus.Debug("Board not recognized") - } else if err != nil { - // this is bad, bail out - return nil, errors.Wrap(err, "error getting board info from Arduino Cloud") - } - - // add a DetectedPort entry in any case: the `Boards` field will - // be empty but the port will be shown anyways (useful for 3rd party - // boards) - b = items + boards, err := Identify(pm, port) + if err != nil { + return nil, err } // 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) { Address: port.Address, Protocol: port.Protocol, ProtocolLabel: port.ProtocolLabel, - Boards: b, + Boards: boards, } retVal = append(retVal, p) } diff --git a/commands/bundled_tools_serial_discovery.go b/commands/bundled_tools_serial_discovery.go index 428ddd5eec6..391a25c0aff 100644 --- a/commands/bundled_tools_serial_discovery.go +++ b/commands/bundled_tools_serial_discovery.go @@ -23,6 +23,7 @@ import ( "github.com/arduino/arduino-cli/arduino/cores" "github.com/arduino/arduino-cli/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/arduino/discovery" "github.com/arduino/arduino-cli/arduino/resources" "github.com/arduino/arduino-cli/executils" "github.com/arduino/go-properties-orderedmap" @@ -193,6 +194,33 @@ func ListBoards(pm *packagemanager.PackageManager) ([]*BoardPort, error) { return retVal, finalError } +// WatchListBoards returns a channel that receives events from the bundled discovery tool +func WatchListBoards(pm *packagemanager.PackageManager) (<-chan *discovery.Event, error) { + t, err := getBuiltinSerialDiscoveryTool(pm) + if err != nil { + return nil, err + } + + if !t.IsInstalled() { + return nil, fmt.Errorf("missing serial-discovery tool") + } + + disc, err := discovery.New("serial-discovery", t.InstallDir.Join(t.Tool.Name).String()) + if err != nil { + return nil, err + } + + if err = disc.Start(); err != nil { + return nil, fmt.Errorf("starting discovery: %v", err) + } + + if err = disc.StartSync(); err != nil { + return nil, fmt.Errorf("starting sync: %v", err) + } + + return disc.EventChannel(10), nil +} + func getBuiltinSerialDiscoveryTool(pm *packagemanager.PackageManager) (*cores.ToolRelease, error) { builtinPackage := pm.Packages.GetOrCreatePackage("builtin") ctagsTool := builtinPackage.GetOrCreateTool("serial-discovery")