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