Skip to content

Query the backend to get the fqbn when a core is not installed #336

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 4 commits into from
Aug 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion arduino/cores/packagemanager/identify.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
properties "github.com/arduino/go-properties-orderedmap"
)

// IdentifyBoard returns a list of baords matching the provided identification properties.
// IdentifyBoard returns a list of boards matching the provided identification properties.
func (pm *PackageManager) IdentifyBoard(idProps *properties.Map) []*cores.Board {
if idProps.Size() == 0 {
return []*cores.Board{}
Expand Down
11 changes: 5 additions & 6 deletions cli/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,18 @@ func runListCommand(cmd *cobra.Command, args []string) {
time.Sleep(timeout)
}

resp, err := board.List(instance.CreateInstance().GetId())
ports, err := board.List(instance.CreateInstance().GetId())
if err != nil {
formatter.PrintError(err, "Error detecting boards")
os.Exit(errorcodes.ErrNetwork)
}

if output.JSONOrElse(resp) {
outputListResp(resp)
if output.JSONOrElse(ports) {
outputListResp(ports)
}
}

func outputListResp(resp *rpc.BoardListResp) {
ports := resp.GetPorts()
func outputListResp(ports []*rpc.DetectedPort) {
if len(ports) == 0 {
formatter.Print("No boards found.")
return
Expand All @@ -84,7 +83,7 @@ func outputListResp(resp *rpc.BoardListResp) {
})
table := output.NewTable()
table.SetHeader("Port", "Type", "Board Name", "FQBN")
for _, port := range resp.GetPorts() {
for _, port := range ports {
address := port.GetProtocol() + "://" + port.GetAddress()
if port.GetProtocol() == "serial" {
address = port.GetAddress()
Expand Down
4 changes: 2 additions & 2 deletions cli/output/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ func (t *Table) makeTableRow(columns ...interface{}) *TableRow {
case TextBox:
cells[i] = text
case string:
cells[i] = Sprintf("%s", text)
cells[i] = sprintf("%s", text)
case fmt.Stringer:
cells[i] = Sprintf("%s", text.String())
cells[i] = sprintf("%s", text.String())
default:
panic(fmt.Sprintf("invalid column argument type: %t", col))
}
Expand Down
3 changes: 1 addition & 2 deletions cli/output/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ func spaces(n int) string {
return res
}

// Sprintf FIXMEDOC
func Sprintf(format string, args ...interface{}) TextBox {
func sprintf(format string, args ...interface{}) TextBox {
cleanArgs := make([]interface{}, len(args))
for i, arg := range args {
if text, ok := arg.(*Text); ok {
Expand Down
84 changes: 79 additions & 5 deletions commands/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,65 @@
package board

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"github.com/arduino/arduino-cli/cli/globals"
"github.com/arduino/arduino-cli/commands"
rpc "github.com/arduino/arduino-cli/rpc/commands"
"github.com/pkg/errors"
)

var (
// ErrNotFound is returned when the API returns 404
ErrNotFound = errors.New("board not found")
)

func apiByVidPid(url string) ([]*rpc.BoardListItem, error) {
retVal := []*rpc.BoardListItem{}
req, _ := http.NewRequest("GET", url, nil)
req.Header = globals.HTTPClientHeader
req.Header.Set("Content-Type", "application/json")
Copy link
Contributor

@mattiabertorello mattiabertorello Aug 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you put the user-agent header?
An example could be this one https://github.com/arduino/Arduino/blob/324a9bcbbbe540d133604767b5c4d30c5f62c753/arduino-core/src/cc/arduino/utils/network/HttpConnectionManager.java#L59
Example

final String defaultUserAgent = String.format(
      "ArduinoIDE/%s (%s; %s; %s; %s) Java/%s (%s)",
      BaseNoGui.VERSION_NAME,
      System.getProperty("os.name"),
      System.getProperty("os.version"),
      System.getProperty("os.arch"),
      System.getProperty("user.language"),
      System.getProperty("java.version"),
      System.getProperty("java.vendor")
    );

Copy link
Contributor Author

@masci masci Aug 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user agent is already there, see how globals.HTTPClientHeader is initialized


if res, err := http.DefaultClient.Do(req); err == nil {
if res.StatusCode >= 400 {
if res.StatusCode == 404 {
return nil, ErrNotFound
}
return nil, errors.Errorf("the server responded with status %s", res.Status)
}

body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()

var dat map[string]interface{}
err = json.Unmarshal(body, &dat)
if err != nil {
return nil, errors.Wrap(err, "error processing response from server")
}

name, nameFound := dat["name"].(string)
fqbn, fbqnFound := dat["fqbn"].(string)

if !nameFound || !fbqnFound {
return nil, errors.New("wrong format in server response")
}

retVal = append(retVal, &rpc.BoardListItem{
Name: name,
FQBN: fqbn,
})
} else {
return nil, errors.Wrap(err, "error querying Arduino Cloud Api")
}

return retVal, nil
}

// List FIXMEDOC
func List(instanceID int32) (*rpc.BoardListResp, error) {
func List(instanceID int32) ([]*rpc.DetectedPort, error) {
pm := commands.GetPackageManager(instanceID)
if pm == nil {
return nil, errors.New("invalid instance")
Expand All @@ -40,29 +92,51 @@ func List(instanceID int32) (*rpc.BoardListResp, error) {
}
defer serialDiscovery.Close()

resp := &rpc.BoardListResp{Ports: []*rpc.DetectedPort{}}

ports, err := serialDiscovery.List()
if err != nil {
return nil, errors.Wrap(err, "error getting port list from serial-discovery")
}

retVal := []*rpc.DetectedPort{}
for _, port := range ports {
b := []*rpc.BoardListItem{}

// first query installed cores through the Package Manager
for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) {
b = append(b, &rpc.BoardListItem{
Name: board.Name(),
FQBN: board.FQBN(),
})
}

// if installed cores didn't recognize the board, try querying
// the builder API
if len(b) == 0 {
url := fmt.Sprintf("https://builder.arduino.cc/v3/boards/byVidPid/%s/%s",
port.IdentificationPrefs.Get("vid"),
port.IdentificationPrefs.Get("pid"))
items, err := apiByVidPid(url)
if err == ErrNotFound {
// the board couldn't be detected, keep going with the next port
continue
} else if err != nil {
// this is bad, bail out
return nil, errors.Wrap(err, "error getting board info from Arduino Cloud")
}

b = items
}

// boards slice can be empty at this point if neither the cores nor the
// API managed to recognize the connected board
p := &rpc.DetectedPort{
Address: port.Address,
Protocol: port.Protocol,
ProtocolLabel: port.ProtocolLabel,
Boards: b,
}
resp.Ports = append(resp.Ports, p)
retVal = append(retVal, p)
}

return resp, nil
return retVal, nil
}
89 changes: 89 additions & 0 deletions commands/board/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// This file is part of arduino-cli.
//
// Copyright 2019 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 [email protected].

package board

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestGetByVidPid(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
{
"architecture": "samd",
"fqbn": "arduino:samd:mkr1000",
"href": "/v3/boards/arduino:samd:mkr1000",
"id": "mkr1000",
"name": "Arduino/Genuino MKR1000",
"package": "arduino",
"plan": "create-free"
}
`)
}))
defer ts.Close()

res, err := apiByVidPid(ts.URL)
require.Nil(t, err)
require.Len(t, res, 1)
require.Equal(t, "Arduino/Genuino MKR1000", res[0].Name)
require.Equal(t, "arduino:samd:mkr1000", res[0].FQBN)

// wrong url
res, err = apiByVidPid("http://0.0.0.0")
require.NotNil(t, err)
}

func TestGetByVidPidNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()

res, err := apiByVidPid(ts.URL)
require.NotNil(t, err)
require.Equal(t, "board not found", err.Error())
require.Len(t, res, 0)
}

func TestGetByVidPid5xx(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Ooooops!"))
}))
defer ts.Close()

res, err := apiByVidPid(ts.URL)
require.NotNil(t, err)
require.Equal(t, "the server responded with status 500 Internal Server Error", err.Error())
require.Len(t, res, 0)
}

func TestGetByVidPidMalformedResponse(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "{}")
}))
defer ts.Close()

res, err := apiByVidPid(ts.URL)
require.NotNil(t, err)
require.Equal(t, "wrong format in server response", err.Error())
require.Len(t, res, 0)
}
9 changes: 8 additions & 1 deletion commands/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ func (s *ArduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board

// BoardList FIXMEDOC
func (s *ArduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardListReq) (*rpc.BoardListResp, error) {
return board.List(req.GetInstance().GetId())
ports, err := board.List(req.GetInstance().GetId())
if err != nil {
return nil, err
}

return &rpc.BoardListResp{
Ports: ports,
}, nil
}

// BoardListAll FIXMEDOC
Expand Down