Skip to content

Commit ed88bd8

Browse files
authored
Added BoardIdentify gRPC call. (#2794)
* Board identification function do not require a full Port but just the properties * Added BoardIdentify gRPC call * Added implementation of BoardIdentify * Removed unused functions * Added option to query cloud API * Moved functions into proper compilation unit * Added integration test * Moved code for better readability * Use BoardIdentify internally This commit also fix a bug (the package manager was used after release).
1 parent d09cc76 commit ed88bd8

File tree

11 files changed

+992
-662
lines changed

11 files changed

+992
-662
lines changed

Diff for: commands/service_board_identify.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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 [email protected].
15+
16+
package commands
17+
18+
import (
19+
"context"
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
"io"
24+
"net/http"
25+
"regexp"
26+
"sort"
27+
"strings"
28+
"time"
29+
30+
"github.com/arduino/arduino-cli/commands/cmderrors"
31+
"github.com/arduino/arduino-cli/commands/internal/instances"
32+
"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
33+
"github.com/arduino/arduino-cli/internal/cli/configuration"
34+
"github.com/arduino/arduino-cli/internal/i18n"
35+
"github.com/arduino/arduino-cli/internal/inventory"
36+
"github.com/arduino/arduino-cli/pkg/fqbn"
37+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
38+
"github.com/arduino/go-properties-orderedmap"
39+
"github.com/sirupsen/logrus"
40+
)
41+
42+
// BoardIdentify identifies the board based on the provided properties
43+
func (s *arduinoCoreServerImpl) BoardIdentify(ctx context.Context, req *rpc.BoardIdentifyRequest) (*rpc.BoardIdentifyResponse, error) {
44+
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
45+
if err != nil {
46+
return nil, err
47+
}
48+
defer release()
49+
50+
props := properties.NewFromHashmap(req.GetProperties())
51+
res, err := identify(pme, props, s.settings, !req.GetUseCloudApiForUnknownBoardDetection())
52+
if err != nil {
53+
return nil, err
54+
}
55+
return &rpc.BoardIdentifyResponse{
56+
Boards: res,
57+
}, nil
58+
}
59+
60+
// identify returns a list of boards checking first the installed platforms or the Cloud API
61+
func identify(pme *packagemanager.Explorer, properties *properties.Map, settings *configuration.Settings, skipCloudAPI bool) ([]*rpc.BoardListItem, error) {
62+
if properties == nil {
63+
return nil, nil
64+
}
65+
66+
// first query installed cores through the Package Manager
67+
boards := []*rpc.BoardListItem{}
68+
logrus.Debug("Querying installed cores for board identification...")
69+
for _, board := range pme.IdentifyBoard(properties) {
70+
fqbn, err := fqbn.Parse(board.FQBN())
71+
if err != nil {
72+
return nil, &cmderrors.InvalidFQBNError{Cause: err}
73+
}
74+
fqbn.Configs = board.IdentifyBoardConfiguration(properties)
75+
76+
// We need the Platform maintaner for sorting so we set it here
77+
platform := &rpc.Platform{
78+
Metadata: &rpc.PlatformMetadata{
79+
Maintainer: board.PlatformRelease.Platform.Package.Maintainer,
80+
},
81+
}
82+
boards = append(boards, &rpc.BoardListItem{
83+
Name: board.Name(),
84+
Fqbn: fqbn.String(),
85+
IsHidden: board.IsHidden(),
86+
Platform: platform,
87+
})
88+
}
89+
90+
// if installed cores didn't recognize the board, try querying
91+
// the builder API if the board is a USB device port
92+
if len(boards) == 0 && !skipCloudAPI && !settings.SkipCloudApiForBoardDetection() {
93+
items, err := identifyViaCloudAPI(properties, settings)
94+
if err != nil {
95+
// this is bad, but keep going
96+
logrus.WithError(err).Debug("Error querying builder API")
97+
}
98+
boards = items
99+
}
100+
101+
// Sort by FQBN alphabetically
102+
sort.Slice(boards, func(i, j int) bool {
103+
return strings.ToLower(boards[i].GetFqbn()) < strings.ToLower(boards[j].GetFqbn())
104+
})
105+
106+
// Put Arduino boards before others in case there are non Arduino boards with identical VID:PID combination
107+
sort.SliceStable(boards, func(i, j int) bool {
108+
if boards[i].GetPlatform().GetMetadata().GetMaintainer() == "Arduino" && boards[j].GetPlatform().GetMetadata().GetMaintainer() != "Arduino" {
109+
return true
110+
}
111+
return false
112+
})
113+
114+
// We need the Board's Platform only for sorting but it shouldn't be present in the output
115+
for _, board := range boards {
116+
board.Platform = nil
117+
}
118+
119+
return boards, nil
120+
}
121+
122+
func identifyViaCloudAPI(props *properties.Map, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
123+
// If the port is not USB do not try identification via cloud
124+
if !props.ContainsKey("vid") || !props.ContainsKey("pid") {
125+
return nil, nil
126+
}
127+
128+
logrus.Debug("Querying builder API for board identification...")
129+
return cachedAPIByVidPid(props.Get("vid"), props.Get("pid"), settings)
130+
}
131+
132+
var (
133+
vidPidURL = "https://builder.arduino.cc/v3/boards/byVidPid"
134+
validVidPid = regexp.MustCompile(`0[xX][a-fA-F\d]{4}`)
135+
)
136+
137+
func cachedAPIByVidPid(vid, pid string, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
138+
var resp []*rpc.BoardListItem
139+
140+
cacheKey := fmt.Sprintf("cache.builder-api.v3/boards/byvid/pid/%s/%s", vid, pid)
141+
if cachedResp := inventory.Store.GetString(cacheKey + ".data"); cachedResp != "" {
142+
ts := inventory.Store.GetTime(cacheKey + ".ts")
143+
if time.Since(ts) < time.Hour*24 {
144+
// Use cached response
145+
if err := json.Unmarshal([]byte(cachedResp), &resp); err == nil {
146+
return resp, nil
147+
}
148+
}
149+
}
150+
151+
resp, err := apiByVidPid(vid, pid, settings) // Perform API requrest
152+
153+
if err == nil {
154+
if cachedResp, err := json.Marshal(resp); err == nil {
155+
inventory.Store.Set(cacheKey+".data", string(cachedResp))
156+
inventory.Store.Set(cacheKey+".ts", time.Now())
157+
inventory.WriteStore()
158+
}
159+
}
160+
return resp, err
161+
}
162+
163+
func apiByVidPid(vid, pid string, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
164+
// ensure vid and pid are valid before hitting the API
165+
if !validVidPid.MatchString(vid) {
166+
return nil, errors.New(i18n.Tr("Invalid vid value: '%s'", vid))
167+
}
168+
if !validVidPid.MatchString(pid) {
169+
return nil, errors.New(i18n.Tr("Invalid pid value: '%s'", pid))
170+
}
171+
172+
url := fmt.Sprintf("%s/%s/%s", vidPidURL, vid, pid)
173+
req, _ := http.NewRequest("GET", url, nil)
174+
req.Header.Set("Content-Type", "application/json")
175+
176+
httpClient, err := settings.NewHttpClient()
177+
if err != nil {
178+
return nil, fmt.Errorf("%s: %w", i18n.Tr("failed to initialize http client"), err)
179+
}
180+
181+
res, err := httpClient.Do(req)
182+
if err != nil {
183+
return nil, fmt.Errorf("%s: %w", i18n.Tr("error querying Arduino Cloud Api"), err)
184+
}
185+
if res.StatusCode == 404 {
186+
// This is not an error, it just means that the board is not recognized
187+
return nil, nil
188+
}
189+
if res.StatusCode >= 400 {
190+
return nil, errors.New(i18n.Tr("the server responded with status %s", res.Status))
191+
}
192+
193+
resp, err := io.ReadAll(res.Body)
194+
if err != nil {
195+
return nil, err
196+
}
197+
if err := res.Body.Close(); err != nil {
198+
return nil, err
199+
}
200+
201+
var dat map[string]interface{}
202+
if err := json.Unmarshal(resp, &dat); err != nil {
203+
return nil, fmt.Errorf("%s: %w", i18n.Tr("error processing response from server"), err)
204+
}
205+
name, nameFound := dat["name"].(string)
206+
fqbn, fbqnFound := dat["fqbn"].(string)
207+
if !nameFound || !fbqnFound {
208+
return nil, errors.New(i18n.Tr("wrong format in server response"))
209+
}
210+
211+
return []*rpc.BoardListItem{
212+
{
213+
Name: name,
214+
Fqbn: fqbn,
215+
},
216+
}, nil
217+
}

Diff for: commands/service_board_list_test.go renamed to commands/service_board_identify_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/arduino/arduino-cli/internal/cli/configuration"
2626
"github.com/arduino/go-paths-helper"
2727
"github.com/arduino/go-properties-orderedmap"
28-
discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
2928
"github.com/stretchr/testify/require"
3029
"go.bug.st/downloader/v2"
3130
semver "go.bug.st/relaxed-semver"
@@ -157,7 +156,7 @@ func TestBoardIdentifySorting(t *testing.T) {
157156
defer release()
158157

159158
settings := configuration.NewSettings()
160-
res, err := identify(pme, &discovery.Port{Properties: idPrefs}, settings, true)
159+
res, err := identify(pme, idPrefs, settings, true)
161160
require.NoError(t, err)
162161
require.NotNil(t, res)
163162
require.Len(t, res, 4)

0 commit comments

Comments
 (0)