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

commands/service_board_identify.go

Lines changed: 217 additions & 0 deletions
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 license@arduino.cc.
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+
}

commands/service_board_list_test.go renamed to commands/service_board_identify_test.go

Lines changed: 1 addition & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy