Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Added BoardIdentify gRPC call. #2794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Moved functions into proper compilation unit
  • Loading branch information
cmaglie committed Jan 7, 2025
commit 5ef3b82cbd9194e3f59adeef74330f07a37189c4
175 changes: 175 additions & 0 deletions 175 commands/service_board_identify.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,26 @@ 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
Expand All @@ -40,3 +56,162 @@ func (s *arduinoCoreServerImpl) BoardIdentify(ctx context.Context, req *rpc.Boar
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
}
170 changes: 0 additions & 170 deletions 170 commands/service_board_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,188 +17,18 @@ 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"
f "github.com/arduino/arduino-cli/internal/algorithms"
"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"
)

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
}

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)
}

// 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
}

// BoardList returns a list of boards found by the loaded discoveries.
// In case of errors partial results from discoveries that didn't fail
// are returned.
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.