Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 13c6edf

Browse files
authoredMar 10, 2021
Add board search command and gRPC interface function (#1210)
* Add board search command * board search now searches only on board name and results are sorted alphabetically * Remove fuzzy search from board search command
1 parent d35a3c9 commit 13c6edf

File tree

12 files changed

+902
-311
lines changed

12 files changed

+902
-311
lines changed
 

‎arduino/utils/search.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package utils
17+
18+
import (
19+
"strings"
20+
"unicode"
21+
22+
"golang.org/x/text/runes"
23+
"golang.org/x/text/transform"
24+
"golang.org/x/text/unicode/norm"
25+
)
26+
27+
// removeDiatrics removes accents and similar diatrics from unicode characters.
28+
// An empty string is returned in case of errors.
29+
// This might not be the best solution but it works well enough for our usecase,
30+
// in the future we might want to use the golang.org/x/text/secure/precis package
31+
// when its API will be finalized.
32+
// From https://stackoverflow.com/a/26722698
33+
func removeDiatrics(s string) (string, error) {
34+
transformer := transform.Chain(
35+
norm.NFD,
36+
runes.Remove(runes.In(unicode.Mn)),
37+
norm.NFC,
38+
)
39+
s, _, err := transform.String(transformer, s)
40+
if err != nil {
41+
return "", err
42+
}
43+
return s, nil
44+
}
45+
46+
// Match returns true if all substrings are contained in str.
47+
// Both str and substrings are transforms to lower case and have their
48+
// accents and other unicode diatrics removed.
49+
// If strings transformation fails an error is returned.
50+
func Match(str string, substrings []string) (bool, error) {
51+
str, err := removeDiatrics(strings.ToLower(str))
52+
if err != nil {
53+
return false, err
54+
}
55+
56+
for _, sub := range substrings {
57+
cleanSub, err := removeDiatrics(strings.ToLower(sub))
58+
if err != nil {
59+
return false, err
60+
}
61+
if !strings.Contains(str, cleanSub) {
62+
return false, nil
63+
}
64+
}
65+
return true, nil
66+
}

‎cli/board/board.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func NewCommand() *cobra.Command {
3737
boardCommand.AddCommand(initDetailsCommand())
3838
boardCommand.AddCommand(initListCommand())
3939
boardCommand.AddCommand(initListAllCommand())
40+
boardCommand.AddCommand(initSearchCommand())
4041

4142
return boardCommand
4243
}

‎cli/board/search.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package board
17+
18+
import (
19+
"context"
20+
"os"
21+
"sort"
22+
"strings"
23+
24+
"github.com/arduino/arduino-cli/cli/errorcodes"
25+
"github.com/arduino/arduino-cli/cli/feedback"
26+
"github.com/arduino/arduino-cli/cli/instance"
27+
"github.com/arduino/arduino-cli/commands/board"
28+
rpc "github.com/arduino/arduino-cli/rpc/commands"
29+
"github.com/arduino/arduino-cli/table"
30+
"github.com/spf13/cobra"
31+
)
32+
33+
func initSearchCommand() *cobra.Command {
34+
var searchCommand = &cobra.Command{
35+
Use: "search [boardname]",
36+
Short: "List all known boards and their corresponding FQBN.",
37+
Long: "" +
38+
"List all boards that have the support platform installed. You can search\n" +
39+
"for a specific board if you specify the board name",
40+
Example: "" +
41+
" " + os.Args[0] + " board search\n" +
42+
" " + os.Args[0] + " board search zero",
43+
Args: cobra.ArbitraryArgs,
44+
Run: runSearchCommand,
45+
}
46+
searchCommand.Flags().BoolVarP(&searchFlags.showHiddenBoard, "show-hidden", "a", false, "Show also boards marked as 'hidden' in the platform")
47+
return searchCommand
48+
}
49+
50+
var searchFlags struct {
51+
showHiddenBoard bool
52+
}
53+
54+
func runSearchCommand(cmd *cobra.Command, args []string) {
55+
inst, err := instance.CreateInstance()
56+
if err != nil {
57+
feedback.Errorf("Error searching boards: %v", err)
58+
os.Exit(errorcodes.ErrGeneric)
59+
}
60+
61+
res, err := board.Search(context.Background(), &rpc.BoardSearchReq{
62+
Instance: inst,
63+
SearchArgs: strings.Join(args, " "),
64+
IncludeHiddenBoards: searchFlags.showHiddenBoard,
65+
})
66+
if err != nil {
67+
feedback.Errorf("Error searching boards: %v", err)
68+
os.Exit(errorcodes.ErrGeneric)
69+
}
70+
71+
feedback.PrintResult(searchResults{res.Boards})
72+
}
73+
74+
// output from this command requires special formatting so we create a dedicated
75+
// feedback.Result implementation
76+
type searchResults struct {
77+
boards []*rpc.BoardListItem
78+
}
79+
80+
func (r searchResults) Data() interface{} {
81+
return r.boards
82+
}
83+
84+
func (r searchResults) String() string {
85+
sort.Slice(r.boards, func(i, j int) bool {
86+
return r.boards[i].GetName() < r.boards[j].GetName()
87+
})
88+
89+
t := table.New()
90+
t.SetHeader("Board Name", "FQBN", "Platform ID", "")
91+
for _, item := range r.boards {
92+
hidden := ""
93+
if item.IsHidden {
94+
hidden = "(hidden)"
95+
}
96+
t.AddRow(item.GetName(), item.GetFQBN(), item.Platform.ID, hidden)
97+
}
98+
return t.Render()
99+
}

‎client_example/go.sum

Lines changed: 4 additions & 42 deletions
Large diffs are not rendered by default.

‎client_example/main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ func main() {
150150
log.Println("calling BoardDetails(arduino:samd:mkr1000)")
151151
callBoardsDetails(client, instance)
152152

153+
log.Println("calling BoardSearch()")
154+
callBoardSearch(client, instance)
155+
153156
// Attach a board to a sketch.
154157
// Uncomment if you do have an actual board connected.
155158
// log.Println("calling BoardAttach(serial:///dev/ttyACM0)")
@@ -524,6 +527,22 @@ func callBoardsDetails(client rpc.ArduinoCoreClient, instance *rpc.Instance) {
524527
log.Printf("Config options: %s", details.GetConfigOptions())
525528
}
526529

530+
func callBoardSearch(client rpc.ArduinoCoreClient, instance *rpc.Instance) {
531+
res, err := client.BoardSearch(context.Background(),
532+
&rpc.BoardSearchReq{
533+
Instance: instance,
534+
SearchArgs: "",
535+
})
536+
537+
if err != nil {
538+
log.Fatalf("Error getting board data: %s\n", err)
539+
}
540+
541+
for _, board := range res.Boards {
542+
log.Printf("Board Name: %s, Board Platform: %s\n", board.Name, board.Platform.ID)
543+
}
544+
}
545+
527546
func callBoardAttach(client rpc.ArduinoCoreClient, instance *rpc.Instance) {
528547
currDir, _ := os.Getwd()
529548
boardattachresp, err := client.BoardAttach(context.Background(),

‎commands/board/search.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package board
17+
18+
import (
19+
"context"
20+
"errors"
21+
"sort"
22+
"strings"
23+
24+
"github.com/arduino/arduino-cli/arduino/utils"
25+
"github.com/arduino/arduino-cli/commands"
26+
rpc "github.com/arduino/arduino-cli/rpc/commands"
27+
)
28+
29+
// Search returns all boards that match the search arg.
30+
// Boards are searched in all platforms, including those in the index that are not yet
31+
// installed. Note that platforms that are not installed don't include boards' FQBNs.
32+
// If no search argument is used all boards are returned.
33+
func Search(ctx context.Context, req *rpc.BoardSearchReq) (*rpc.BoardSearchResp, error) {
34+
pm := commands.GetPackageManager(req.GetInstance().GetId())
35+
if pm == nil {
36+
return nil, errors.New("invalid instance")
37+
}
38+
39+
searchArgs := strings.Split(strings.Trim(req.SearchArgs, " "), " ")
40+
41+
match := func(toTest []string) (bool, error) {
42+
if len(searchArgs) == 0 {
43+
return true, nil
44+
}
45+
46+
for _, t := range toTest {
47+
matches, err := utils.Match(t, searchArgs)
48+
if err != nil {
49+
return false, err
50+
}
51+
if matches {
52+
return matches, nil
53+
}
54+
}
55+
return false, nil
56+
}
57+
58+
res := &rpc.BoardSearchResp{Boards: []*rpc.BoardListItem{}}
59+
for _, targetPackage := range pm.Packages {
60+
for _, platform := range targetPackage.Platforms {
61+
latestPlatformRelease := platform.GetLatestRelease()
62+
if latestPlatformRelease == nil {
63+
continue
64+
}
65+
installedVersion := ""
66+
if installedPlatformRelease := pm.GetInstalledPlatformRelease(platform); installedPlatformRelease != nil {
67+
installedVersion = installedPlatformRelease.Version.String()
68+
}
69+
70+
rpcPlatform := &rpc.Platform{
71+
ID: platform.String(),
72+
Installed: installedVersion,
73+
Latest: latestPlatformRelease.Version.String(),
74+
Name: platform.Name,
75+
Maintainer: platform.Package.Maintainer,
76+
Website: platform.Package.WebsiteURL,
77+
Email: platform.Package.Email,
78+
ManuallyInstalled: platform.ManuallyInstalled,
79+
}
80+
81+
// Platforms that are not installed don't have a list of boards
82+
// generated from their boards.txt file so we need two different
83+
// ways of reading board data.
84+
// The only boards information for platforms that are not installed
85+
// is that found in the index, usually that's only a board name.
86+
if len(latestPlatformRelease.Boards) != 0 {
87+
for _, board := range latestPlatformRelease.Boards {
88+
if !req.GetIncludeHiddenBoards() && board.IsHidden() {
89+
continue
90+
}
91+
92+
toTest := append(strings.Split(board.Name(), " "), board.Name(), board.FQBN())
93+
if ok, err := match(toTest); err != nil {
94+
return nil, err
95+
} else if !ok {
96+
continue
97+
}
98+
99+
res.Boards = append(res.Boards, &rpc.BoardListItem{
100+
Name: board.Name(),
101+
FQBN: board.FQBN(),
102+
IsHidden: board.IsHidden(),
103+
Platform: rpcPlatform,
104+
})
105+
}
106+
} else {
107+
for _, board := range latestPlatformRelease.BoardsManifest {
108+
toTest := append(strings.Split(board.Name, " "), board.Name)
109+
if ok, err := match(toTest); err != nil {
110+
return nil, err
111+
} else if !ok {
112+
continue
113+
}
114+
115+
res.Boards = append(res.Boards, &rpc.BoardListItem{
116+
Name: strings.Trim(board.Name, " \n"),
117+
Platform: rpcPlatform,
118+
})
119+
}
120+
}
121+
}
122+
}
123+
124+
sort.Slice(res.Boards, func(i, j int) bool {
125+
return res.Boards[i].Name < res.Boards[j].Name
126+
})
127+
return res, nil
128+
}

‎commands/daemon/daemon.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (s *ArduinoCoreServerImpl) BoardListAll(ctx context.Context, req *rpc.Board
5959
return board.ListAll(ctx, req)
6060
}
6161

62+
// BoardSearch exposes to the gRPC interface the board search command
63+
func (s *ArduinoCoreServerImpl) BoardSearch(ctx context.Context, req *rpc.BoardSearchReq) (*rpc.BoardSearchResp, error) {
64+
return board.Search(ctx, req)
65+
}
66+
6267
// BoardListWatch FIXMEDOC
6368
func (s *ArduinoCoreServerImpl) BoardListWatch(stream rpc.ArduinoCore_BoardListWatchServer) error {
6469
msg, err := stream.Recv()

‎rpc/commands/board.pb.go

Lines changed: 181 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎rpc/commands/board.proto

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,17 @@ message BoardListItem {
238238
// Platform this board belongs to
239239
Platform platform = 6;
240240
}
241+
242+
message BoardSearchReq {
243+
// Arduino Core Service instance from the `Init` response.
244+
Instance instance = 1;
245+
// The search query to filter the board list by.
246+
string search_args = 2;
247+
// Set to true to get also the boards marked as "hidden" in installed platforms
248+
bool include_hidden_boards = 3;
249+
}
250+
251+
message BoardSearchResp {
252+
// List of installed and installable boards.
253+
repeated BoardListItem boards = 1;
254+
}

‎rpc/commands/commands.pb.go

Lines changed: 294 additions & 246 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎rpc/commands/commands.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ service ArduinoCore {
8080
// List all the boards provided by installed platforms.
8181
rpc BoardListAll(BoardListAllReq) returns (BoardListAllResp);
8282

83+
// Search boards in installed and not installed Platforms.
84+
rpc BoardSearch(BoardSearchReq) returns (BoardSearchResp);
85+
8386
// List boards connection and disconnected events.
8487
rpc BoardListWatch(stream BoardListWatchReq) returns (stream BoardListWatchResp);
8588

‎test/test_board.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,91 @@ def test_board_details_list_programmers_flag(run_command):
587587
assert "edbg Atmel EDBG" in lines
588588
assert "atmel_ice Atmel-ICE" in lines
589589
assert "sam_ice Atmel SAM-ICE" in lines
590+
591+
592+
def test_board_search(run_command, data_dir):
593+
assert run_command("update")
594+
595+
res = run_command("board search --format json")
596+
assert res.ok
597+
data = json.loads(res.stdout)
598+
# Verifies boards are returned
599+
assert len(data) > 0
600+
# Verifies no board has FQBN set since no platform is installed
601+
assert len([board["FQBN"] for board in data if "FQBN" in board]) == 0
602+
names = [board["name"] for board in data if "name" in board]
603+
assert "Arduino Uno" in names
604+
assert "Arduino Yún" in names
605+
assert "Arduino Zero" in names
606+
assert "Arduino Nano 33 BLE" in names
607+
assert "Arduino Portenta H7" in names
608+
609+
# Search in non installed boards
610+
res = run_command("board search --format json nano 33")
611+
assert res.ok
612+
data = json.loads(res.stdout)
613+
# Verifies boards are returned
614+
assert len(data) > 0
615+
# Verifies no board has FQBN set since no platform is installed
616+
assert len([board["FQBN"] for board in data if "FQBN" in board]) == 0
617+
names = [board["name"] for board in data if "name" in board]
618+
assert "Arduino Nano 33 BLE" in names
619+
assert "Arduino Nano 33 IoT" in names
620+
621+
# Install a platform from index
622+
assert run_command("core install arduino:avr@1.8.3")
623+
624+
res = run_command("board search --format json")
625+
assert res.ok
626+
data = json.loads(res.stdout)
627+
assert len(data) > 0
628+
# Verifies some FQBNs are now returned after installing a platform
629+
assert len([board["FQBN"] for board in data if "FQBN" in board]) == 26
630+
installed_boards = {board["FQBN"]: board for board in data if "FQBN" in board}
631+
assert "arduino:avr:uno" in installed_boards
632+
assert "Arduino Uno" == installed_boards["arduino:avr:uno"]["name"]
633+
assert "arduino:avr:yun" in installed_boards
634+
assert "Arduino Yún" == installed_boards["arduino:avr:yun"]["name"]
635+
636+
res = run_command("board search --format json arduino yun")
637+
assert res.ok
638+
data = json.loads(res.stdout)
639+
assert len(data) > 0
640+
installed_boards = {board["FQBN"]: board for board in data if "FQBN" in board}
641+
assert "arduino:avr:yun" in installed_boards
642+
assert "Arduino Yún" == installed_boards["arduino:avr:yun"]["name"]
643+
644+
# Manually installs a core in sketchbooks hardware folder
645+
git_url = "https://github.com/arduino/ArduinoCore-samd.git"
646+
repo_dir = Path(data_dir, "hardware", "arduino-beta-development", "samd")
647+
assert Repo.clone_from(git_url, repo_dir, multi_options=["-b 1.8.11"])
648+
649+
res = run_command("board search --format json")
650+
assert res.ok
651+
data = json.loads(res.stdout)
652+
assert len(data) > 0
653+
# Verifies some FQBNs are now returned after installing a platform
654+
assert len([board["FQBN"] for board in data if "FQBN" in board]) == 43
655+
installed_boards = {board["FQBN"]: board for board in data if "FQBN" in board}
656+
assert "arduino:avr:uno" in installed_boards
657+
assert "Arduino Uno" == installed_boards["arduino:avr:uno"]["name"]
658+
assert "arduino:avr:yun" in installed_boards
659+
assert "Arduino Yún" == installed_boards["arduino:avr:yun"]["name"]
660+
assert "arduino-beta-development:samd:mkrwifi1010" in installed_boards
661+
assert "Arduino MKR WiFi 1010" == installed_boards["arduino-beta-development:samd:mkrwifi1010"]["name"]
662+
assert "arduino-beta-development:samd:mkr1000" in installed_boards
663+
assert "Arduino MKR1000" == installed_boards["arduino-beta-development:samd:mkr1000"]["name"]
664+
assert "arduino-beta-development:samd:mkrzero" in installed_boards
665+
assert "Arduino MKRZERO" == installed_boards["arduino-beta-development:samd:mkrzero"]["name"]
666+
assert "arduino-beta-development:samd:nano_33_iot" in installed_boards
667+
assert "Arduino NANO 33 IoT" == installed_boards["arduino-beta-development:samd:nano_33_iot"]["name"]
668+
assert "arduino-beta-development:samd:arduino_zero_native" in installed_boards
669+
670+
res = run_command("board search --format json mkr1000")
671+
assert res.ok
672+
data = json.loads(res.stdout)
673+
assert len(data) > 0
674+
# Verifies some FQBNs are now returned after installing a platform
675+
installed_boards = {board["FQBN"]: board for board in data if "FQBN" in board}
676+
assert "arduino-beta-development:samd:mkr1000" in installed_boards
677+
assert "Arduino MKR1000" == installed_boards["arduino-beta-development:samd:mkr1000"]["name"]

0 commit comments

Comments
 (0)
Please sign in to comment.