Skip to content

Add flag to board list command to watch for connected and disconnected boards #1032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions cli/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
5 changes: 5 additions & 0 deletions cli/feedback/exported.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions cli/feedback/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
66 changes: 38 additions & 28 deletions commands/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions commands/bundled_tools_serial_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down