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
Show file tree
Hide file tree
Changes from 5 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
23 changes: 23 additions & 0 deletions arduino/cores/fqbn.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ 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 {
return false
}
if 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
Expand Down
34 changes: 34 additions & 0 deletions arduino/cores/fqbn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
12 changes: 10 additions & 2 deletions internal/cli/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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})
Expand Down
33 changes: 33 additions & 0 deletions internal/integrationtest/board/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading