@@ -17,10 +17,26 @@ package commands
1717
1818import (
1919 "context"
20+ "encoding/json"
21+ "errors"
22+ "fmt"
23+ "io"
24+ "net/http"
25+ "regexp"
26+ "sort"
27+ "strings"
28+ "time"
2029
30+ "github.com/arduino/arduino-cli/commands/cmderrors"
2131 "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"
2237 rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
2338 "github.com/arduino/go-properties-orderedmap"
39+ "github.com/sirupsen/logrus"
2440)
2541
2642// BoardIdentify identifies the board based on the provided properties
@@ -40,3 +56,162 @@ func (s *arduinoCoreServerImpl) BoardIdentify(ctx context.Context, req *rpc.Boar
4056 Boards : res ,
4157 }, nil
4258}
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+ }
0 commit comments