Skip to content

Commit

Permalink
Added BoardIdentify gRPC call. (#2794)
Browse files Browse the repository at this point in the history
* 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).
  • Loading branch information
cmaglie authored Jan 13, 2025
1 parent d09cc76 commit ed88bd8
Show file tree
Hide file tree
Showing 11 changed files with 992 additions and 662 deletions.
217 changes: 217 additions & 0 deletions commands/service_board_identify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// This file is part of arduino-cli.
//
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package commands

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"time"

"github.com/arduino/arduino-cli/commands/cmderrors"
"github.com/arduino/arduino-cli/commands/internal/instances"
"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
"github.com/arduino/arduino-cli/internal/cli/configuration"
"github.com/arduino/arduino-cli/internal/i18n"
"github.com/arduino/arduino-cli/internal/inventory"
"github.com/arduino/arduino-cli/pkg/fqbn"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-properties-orderedmap"
"github.com/sirupsen/logrus"
)

// BoardIdentify identifies the board based on the provided properties
func (s *arduinoCoreServerImpl) BoardIdentify(ctx context.Context, req *rpc.BoardIdentifyRequest) (*rpc.BoardIdentifyResponse, error) {
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
if err != nil {
return nil, err
}
defer release()

props := properties.NewFromHashmap(req.GetProperties())
res, err := identify(pme, props, s.settings, !req.GetUseCloudApiForUnknownBoardDetection())
if err != nil {
return nil, err
}
return &rpc.BoardIdentifyResponse{
Boards: res,
}, nil
}

// identify returns a list of boards checking first the installed platforms or the Cloud API
func identify(pme *packagemanager.Explorer, properties *properties.Map, settings *configuration.Settings, skipCloudAPI bool) ([]*rpc.BoardListItem, error) {
if properties == nil {
return nil, nil
}

// first query installed cores through the Package Manager
boards := []*rpc.BoardListItem{}
logrus.Debug("Querying installed cores for board identification...")
for _, board := range pme.IdentifyBoard(properties) {
fqbn, err := fqbn.Parse(board.FQBN())
if err != nil {
return nil, &cmderrors.InvalidFQBNError{Cause: err}
}
fqbn.Configs = board.IdentifyBoardConfiguration(properties)

// We need the Platform maintaner for sorting so we set it here
platform := &rpc.Platform{
Metadata: &rpc.PlatformMetadata{
Maintainer: board.PlatformRelease.Platform.Package.Maintainer,
},
}
boards = append(boards, &rpc.BoardListItem{
Name: board.Name(),
Fqbn: fqbn.String(),
IsHidden: board.IsHidden(),
Platform: platform,
})
}

// if installed cores didn't recognize the board, try querying
// the builder API if the board is a USB device port
if len(boards) == 0 && !skipCloudAPI && !settings.SkipCloudApiForBoardDetection() {
items, err := identifyViaCloudAPI(properties, settings)
if err != nil {
// this is bad, but keep going
logrus.WithError(err).Debug("Error querying builder API")
}
boards = items
}

// Sort by FQBN alphabetically
sort.Slice(boards, func(i, j int) bool {
return strings.ToLower(boards[i].GetFqbn()) < strings.ToLower(boards[j].GetFqbn())
})

// Put Arduino boards before others in case there are non Arduino boards with identical VID:PID combination
sort.SliceStable(boards, func(i, j int) bool {
if boards[i].GetPlatform().GetMetadata().GetMaintainer() == "Arduino" && boards[j].GetPlatform().GetMetadata().GetMaintainer() != "Arduino" {
return true
}
return false
})

// We need the Board's Platform only for sorting but it shouldn't be present in the output
for _, board := range boards {
board.Platform = nil
}

return boards, nil
}

func identifyViaCloudAPI(props *properties.Map, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
// If the port is not USB do not try identification via cloud
if !props.ContainsKey("vid") || !props.ContainsKey("pid") {
return nil, nil
}

logrus.Debug("Querying builder API for board identification...")
return cachedAPIByVidPid(props.Get("vid"), props.Get("pid"), settings)
}

var (
vidPidURL = "https://builder.arduino.cc/v3/boards/byVidPid"
validVidPid = regexp.MustCompile(`0[xX][a-fA-F\d]{4}`)
)

func cachedAPIByVidPid(vid, pid string, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
var resp []*rpc.BoardListItem

cacheKey := fmt.Sprintf("cache.builder-api.v3/boards/byvid/pid/%s/%s", vid, pid)
if cachedResp := inventory.Store.GetString(cacheKey + ".data"); cachedResp != "" {
ts := inventory.Store.GetTime(cacheKey + ".ts")
if time.Since(ts) < time.Hour*24 {
// Use cached response
if err := json.Unmarshal([]byte(cachedResp), &resp); err == nil {
return resp, nil
}
}
}

resp, err := apiByVidPid(vid, pid, settings) // Perform API requrest

if err == nil {
if cachedResp, err := json.Marshal(resp); err == nil {
inventory.Store.Set(cacheKey+".data", string(cachedResp))
inventory.Store.Set(cacheKey+".ts", time.Now())
inventory.WriteStore()
}
}
return resp, err
}

func apiByVidPid(vid, pid string, settings *configuration.Settings) ([]*rpc.BoardListItem, error) {
// ensure vid and pid are valid before hitting the API
if !validVidPid.MatchString(vid) {
return nil, errors.New(i18n.Tr("Invalid vid value: '%s'", vid))
}
if !validVidPid.MatchString(pid) {
return nil, errors.New(i18n.Tr("Invalid pid value: '%s'", pid))
}

url := fmt.Sprintf("%s/%s/%s", vidPidURL, vid, pid)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Content-Type", "application/json")

httpClient, err := settings.NewHttpClient()
if err != nil {
return nil, fmt.Errorf("%s: %w", i18n.Tr("failed to initialize http client"), err)
}

res, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: %w", i18n.Tr("error querying Arduino Cloud Api"), err)
}
if res.StatusCode == 404 {
// This is not an error, it just means that the board is not recognized
return nil, nil
}
if res.StatusCode >= 400 {
return nil, errors.New(i18n.Tr("the server responded with status %s", res.Status))
}

resp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if err := res.Body.Close(); err != nil {
return nil, err
}

var dat map[string]interface{}
if err := json.Unmarshal(resp, &dat); err != nil {
return nil, fmt.Errorf("%s: %w", i18n.Tr("error processing response from server"), err)
}
name, nameFound := dat["name"].(string)
fqbn, fbqnFound := dat["fqbn"].(string)
if !nameFound || !fbqnFound {
return nil, errors.New(i18n.Tr("wrong format in server response"))
}

return []*rpc.BoardListItem{
{
Name: name,
Fqbn: fqbn,
},
}, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"github.com/arduino/arduino-cli/internal/cli/configuration"
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
semver "go.bug.st/relaxed-semver"
Expand Down Expand Up @@ -157,7 +156,7 @@ func TestBoardIdentifySorting(t *testing.T) {
defer release()

settings := configuration.NewSettings()
res, err := identify(pme, &discovery.Port{Properties: idPrefs}, settings, true)
res, err := identify(pme, idPrefs, settings, true)
require.NoError(t, err)
require.NotNil(t, res)
require.Len(t, res, 4)
Expand Down
Loading

0 comments on commit ed88bd8

Please sign in to comment.