Skip to content

Commit cddbff3

Browse files
Bikappaper1234
andauthored
feat: filter boards by fqbn in connected list (#2052)
Co-authored-by: per1234 <[email protected]>
1 parent 58c6bc3 commit cddbff3

File tree

20 files changed

+233
-98
lines changed

20 files changed

+233
-98
lines changed

arduino/cores/fqbn.go

+20
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ func (fqbn *FQBN) String() string {
7676
return res
7777
}
7878

79+
// Match check if the target FQBN corresponds to the receiver one.
80+
// The core parts are checked for exact equality while board options are loosely
81+
// matched: the set of boards options of the target must be fully contained within
82+
// the one of the receiver and their values must be equal.
83+
func (fqbn *FQBN) Match(target *FQBN) bool {
84+
if fqbn.StringWithoutConfig() != target.StringWithoutConfig() {
85+
return false
86+
}
87+
88+
searchedProperties := target.Configs.Clone()
89+
actualConfigs := fqbn.Configs.AsMap()
90+
for neededKey, neededValue := range searchedProperties.AsMap() {
91+
targetValue, hasKey := actualConfigs[neededKey]
92+
if !hasKey || targetValue != neededValue {
93+
return false
94+
}
95+
}
96+
return true
97+
}
98+
7999
// StringWithoutConfig returns the FQBN without the Config part
80100
func (fqbn *FQBN) StringWithoutConfig() string {
81101
return fqbn.Package + ":" + fqbn.PlatformArch + ":" + fqbn.BoardID

arduino/cores/fqbn_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,37 @@ func TestFQBN(t *testing.T) {
121121
"properties.Map{\n \"cpu\": \"atmega\",\n \"speed\": \"1000\",\n \"extra\": \"core=arduino\",\n}",
122122
f.Configs.Dump())
123123
}
124+
125+
func TestMatch(t *testing.T) {
126+
expectedMatches := [][]string{
127+
{"arduino:avr:uno", "arduino:avr:uno"},
128+
{"arduino:avr:uno", "arduino:avr:uno:opt1=1,opt2=2"},
129+
{"arduino:avr:uno:opt1=1", "arduino:avr:uno:opt1=1,opt2=2"},
130+
{"arduino:avr:uno:opt1=1,opt2=2", "arduino:avr:uno:opt1=1,opt2=2"},
131+
{"arduino:avr:uno:opt3=3,opt1=1,opt2=2", "arduino:avr:uno:opt2=2,opt3=3,opt1=1,opt4=4"},
132+
}
133+
134+
for _, pair := range expectedMatches {
135+
a, err := ParseFQBN(pair[0])
136+
require.NoError(t, err)
137+
b, err := ParseFQBN(pair[1])
138+
require.NoError(t, err)
139+
require.True(t, b.Match(a))
140+
}
141+
142+
expectedMismatches := [][]string{
143+
{"arduino:avr:uno", "arduino:avr:due"},
144+
{"arduino:avr:uno", "arduino:avr:due:opt1=1,opt2=2"},
145+
{"arduino:avr:uno:opt1=1", "arduino:avr:uno"},
146+
{"arduino:avr:uno:opt1=1,opt2=", "arduino:avr:uno:opt1=1,opt2=3"},
147+
{"arduino:avr:uno:opt1=1,opt2=2", "arduino:avr:uno:opt2=2"},
148+
}
149+
150+
for _, pair := range expectedMismatches {
151+
a, err := ParseFQBN(pair[0])
152+
require.NoError(t, err)
153+
b, err := ParseFQBN(pair[1])
154+
require.NoError(t, err)
155+
require.False(t, b.Match(a))
156+
}
157+
}

commands/board/list.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, discoveryStartError
205205
}
206206
defer release()
207207

208+
var fqbnFilter *cores.FQBN
209+
if f := req.GetFqbn(); f != "" {
210+
var err error
211+
fqbnFilter, err = cores.ParseFQBN(f)
212+
if err != nil {
213+
return nil, nil, &arduino.InvalidFQBNError{Cause: err}
214+
}
215+
}
216+
208217
dm := pme.DiscoveryManager()
209218
discoveryStartErrors = dm.Start()
210219
time.Sleep(time.Duration(req.GetTimeout()) * time.Millisecond)
@@ -222,11 +231,27 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, discoveryStartError
222231
Port: port.ToRPC(),
223232
MatchingBoards: boards,
224233
}
225-
retVal = append(retVal, b)
234+
235+
if fqbnFilter == nil || hasMatchingBoard(b, fqbnFilter) {
236+
retVal = append(retVal, b)
237+
}
226238
}
227239
return retVal, discoveryStartErrors, nil
228240
}
229241

242+
func hasMatchingBoard(b *rpc.DetectedPort, fqbnFilter *cores.FQBN) bool {
243+
for _, detectedBoard := range b.MatchingBoards {
244+
detectedFqbn, err := cores.ParseFQBN(detectedBoard.Fqbn)
245+
if err != nil {
246+
continue
247+
}
248+
if detectedFqbn.Match(fqbnFilter) {
249+
return true
250+
}
251+
}
252+
return false
253+
}
254+
230255
// Watch returns a channel that receives boards connection and disconnection events.
231256
// It also returns a callback function that must be used to stop and dispose the watch.
232257
func Watch(req *rpc.BoardListWatchRequest) (<-chan *rpc.BoardListWatchResponse, func(), error) {

internal/cli/board/list.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package board
1717

1818
import (
19+
"errors"
1920
"fmt"
2021
"os"
2122
"sort"
2223

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

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

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

66-
ports, discvoeryErrors, err := board.List(&rpc.BoardListRequest{
69+
ports, discoveryErrors, err := board.List(&rpc.BoardListRequest{
6770
Instance: inst,
6871
Timeout: timeoutArg.Get().Milliseconds(),
72+
Fqbn: fqbn.String(),
6973
})
74+
var invalidFQBNErr *arduino.InvalidFQBNError
75+
if errors.As(err, &invalidFQBNErr) {
76+
feedback.Fatal(tr(err.Error()), feedback.ErrBadArgument)
77+
}
7078
if err != nil {
7179
feedback.Warning(tr("Error detecting boards: %v", err))
7280
}
73-
for _, err := range discvoeryErrors {
81+
for _, err := range discoveryErrors {
7482
feedback.Warning(tr("Error starting discovery: %v", err))
7583
}
7684
feedback.PrintResult(result{ports})

internal/integrationtest/board/board_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,39 @@ func TestBoardList(t *testing.T) {
9191
MustBeEmpty()
9292
}
9393

94+
func TestBoardListWithFqbnFilter(t *testing.T) {
95+
if os.Getenv("CI") != "" {
96+
t.Skip("VMs have no serial ports")
97+
}
98+
99+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
100+
defer env.CleanUp()
101+
102+
_, _, err := cli.Run("core", "update-index")
103+
require.NoError(t, err)
104+
stdout, _, err := cli.Run("board", "list", "-b", "foo:bar:baz", "--format", "json")
105+
require.NoError(t, err)
106+
// this is a bit of a passpartout test, it actually filters the "bluetooth boards" locally
107+
// but it would succeed even if the filtering wasn't working properly
108+
// TODO: find a way to simulate connected boards or create a unit test which
109+
// mocks or initializes multiple components
110+
requirejson.Parse(t, stdout).
111+
MustBeEmpty()
112+
}
113+
114+
func TestBoardListWithFqbnFilterInvalid(t *testing.T) {
115+
if os.Getenv("CI") != "" {
116+
t.Skip("VMs have no serial ports")
117+
}
118+
119+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
120+
defer env.CleanUp()
121+
122+
_, stderr, err := cli.Run("board", "list", "-b", "yadayada", "--format", "json")
123+
require.Error(t, err)
124+
requirejson.Query(t, stderr, ".error", `"Invalid FQBN: not an FQBN: yadayada"`)
125+
}
126+
94127
func TestBoardListWithInvalidDiscovery(t *testing.T) {
95128
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
96129
defer env.CleanUp()

0 commit comments

Comments
 (0)