Skip to content

feat: filter boards by fqbn in connected list #2052

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 6 commits into from
Jan 30, 2023
Merged
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
20 changes: 20 additions & 0 deletions arduino/cores/fqbn.go
Original file line number Diff line number Diff line change
@@ -76,6 +76,26 @@ func (fqbn *FQBN) String() string {
return res
}

// Match check if the target FQBN corresponds to the receiver one.
// The core parts are checked for exact equality while board options are loosely
// matched: the set of boards options of the target must be fully contained within
// the one of the receiver and their values must be equal.
func (fqbn *FQBN) Match(target *FQBN) bool {
if fqbn.StringWithoutConfig() != target.StringWithoutConfig() {
return false
}

searchedProperties := target.Configs.Clone()
actualConfigs := fqbn.Configs.AsMap()
for neededKey, neededValue := range searchedProperties.AsMap() {
targetValue, hasKey := actualConfigs[neededKey]
if !hasKey || targetValue != neededValue {
return false
}
}
return true
}

// StringWithoutConfig returns the FQBN without the Config part
func (fqbn *FQBN) StringWithoutConfig() string {
return fqbn.Package + ":" + fqbn.PlatformArch + ":" + fqbn.BoardID
34 changes: 34 additions & 0 deletions arduino/cores/fqbn_test.go
Original file line number Diff line number Diff line change
@@ -121,3 +121,37 @@ func TestFQBN(t *testing.T) {
"properties.Map{\n \"cpu\": \"atmega\",\n \"speed\": \"1000\",\n \"extra\": \"core=arduino\",\n}",
f.Configs.Dump())
}

func TestMatch(t *testing.T) {
expectedMatches := [][]string{
{"arduino:avr:uno", "arduino:avr:uno"},
{"arduino:avr:uno", "arduino:avr:uno:opt1=1,opt2=2"},
{"arduino:avr:uno:opt1=1", "arduino:avr:uno:opt1=1,opt2=2"},
{"arduino:avr:uno:opt1=1,opt2=2", "arduino:avr:uno:opt1=1,opt2=2"},
{"arduino:avr:uno:opt3=3,opt1=1,opt2=2", "arduino:avr:uno:opt2=2,opt3=3,opt1=1,opt4=4"},
}

for _, pair := range expectedMatches {
a, err := ParseFQBN(pair[0])
require.NoError(t, err)
b, err := ParseFQBN(pair[1])
require.NoError(t, err)
require.True(t, b.Match(a))
}

expectedMismatches := [][]string{
{"arduino:avr:uno", "arduino:avr:due"},
{"arduino:avr:uno", "arduino:avr:due:opt1=1,opt2=2"},
{"arduino:avr:uno:opt1=1", "arduino:avr:uno"},
{"arduino:avr:uno:opt1=1,opt2=", "arduino:avr:uno:opt1=1,opt2=3"},
{"arduino:avr:uno:opt1=1,opt2=2", "arduino:avr:uno:opt2=2"},
}

for _, pair := range expectedMismatches {
a, err := ParseFQBN(pair[0])
require.NoError(t, err)
b, err := ParseFQBN(pair[1])
require.NoError(t, err)
require.False(t, b.Match(a))
}
}
27 changes: 26 additions & 1 deletion commands/board/list.go
Original file line number Diff line number Diff line change
@@ -205,6 +205,15 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, discoveryStartError
}
defer release()

var fqbnFilter *cores.FQBN
if f := req.GetFqbn(); f != "" {
var err error
fqbnFilter, err = cores.ParseFQBN(f)
if err != nil {
return nil, nil, &arduino.InvalidFQBNError{Cause: err}
}
}

dm := pme.DiscoveryManager()
discoveryStartErrors = dm.Start()
time.Sleep(time.Duration(req.GetTimeout()) * time.Millisecond)
@@ -222,11 +231,27 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, discoveryStartError
Port: port.ToRPC(),
MatchingBoards: boards,
}
retVal = append(retVal, b)

if fqbnFilter == nil || hasMatchingBoard(b, fqbnFilter) {
retVal = append(retVal, b)
}
}
return retVal, discoveryStartErrors, nil
}

func hasMatchingBoard(b *rpc.DetectedPort, fqbnFilter *cores.FQBN) bool {
for _, detectedBoard := range b.MatchingBoards {
detectedFqbn, err := cores.ParseFQBN(detectedBoard.Fqbn)
if err != nil {
continue
}
if detectedFqbn.Match(fqbnFilter) {
return true
}
}
return false
}

// Watch returns a channel that receives boards connection and disconnection events.
// It also returns a callback function that must be used to stop and dispose the watch.
func Watch(req *rpc.BoardListWatchRequest) (<-chan *rpc.BoardListWatchResponse, func(), error) {
12 changes: 10 additions & 2 deletions internal/cli/board/list.go
Original file line number Diff line number Diff line change
@@ -16,10 +16,12 @@
package board

import (
"errors"
"fmt"
"os"
"sort"

"github.com/arduino/arduino-cli/arduino"
"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/commands/board"
"github.com/arduino/arduino-cli/internal/cli/arguments"
@@ -47,6 +49,7 @@ func initListCommand() *cobra.Command {
}

timeoutArg.AddToCommand(listCommand)
fqbn.AddToCommand(listCommand)
listCommand.Flags().BoolVarP(&watch, "watch", "w", false, tr("Command keeps running and prints list of connected boards whenever there is a change."))

return listCommand
@@ -63,14 +66,19 @@ func runListCommand(cmd *cobra.Command, args []string) {
return
}

ports, discvoeryErrors, err := board.List(&rpc.BoardListRequest{
ports, discoveryErrors, err := board.List(&rpc.BoardListRequest{
Instance: inst,
Timeout: timeoutArg.Get().Milliseconds(),
Fqbn: fqbn.String(),
})
var invalidFQBNErr *arduino.InvalidFQBNError
if errors.As(err, &invalidFQBNErr) {
feedback.Fatal(tr(err.Error()), feedback.ErrBadArgument)
}
if err != nil {
feedback.Warning(tr("Error detecting boards: %v", err))
}
for _, err := range discvoeryErrors {
for _, err := range discoveryErrors {
feedback.Warning(tr("Error starting discovery: %v", err))
}
feedback.PrintResult(result{ports})
33 changes: 33 additions & 0 deletions internal/integrationtest/board/board_test.go
Original file line number Diff line number Diff line change
@@ -91,6 +91,39 @@ func TestBoardList(t *testing.T) {
MustBeEmpty()
}

func TestBoardListWithFqbnFilter(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("VMs have no serial ports")
}

env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()

_, _, err := cli.Run("core", "update-index")
require.NoError(t, err)
stdout, _, err := cli.Run("board", "list", "-b", "foo:bar:baz", "--format", "json")
require.NoError(t, err)
// this is a bit of a passpartout test, it actually filters the "bluetooth boards" locally
// but it would succeed even if the filtering wasn't working properly
// TODO: find a way to simulate connected boards or create a unit test which
// mocks or initializes multiple components
requirejson.Parse(t, stdout).
MustBeEmpty()
}

func TestBoardListWithFqbnFilterInvalid(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("VMs have no serial ports")
}

env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()

_, stderr, err := cli.Run("board", "list", "-b", "yadayada", "--format", "json")
require.Error(t, err)
requirejson.Query(t, stderr, ".error", `"Invalid FQBN: not an FQBN: yadayada"`)
}

func TestBoardListWithInvalidDiscovery(t *testing.T) {
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()
176 changes: 94 additions & 82 deletions rpc/cc/arduino/cli/commands/v1/board.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/board.proto
Original file line number Diff line number Diff line change
@@ -152,6 +152,9 @@ message BoardListRequest {
Instance instance = 1;
// Search for boards for the given time (in milliseconds)
int64 timeout = 2;
// The fully qualified board name of the board you want information about
// (e.g., `arduino:avr:uno`).
string fqbn = 3;
}

message BoardListResponse {
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/commands.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/commands_grpc.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/common.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/compile.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/core.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/lib.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/monitor.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/port.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/commands/v1/upload.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/debug/v1/debug.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/debug/v1/debug_grpc.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/settings/v1/settings.pb.go
2 changes: 1 addition & 1 deletion rpc/cc/arduino/cli/settings/v1/settings_grpc.pb.go