Skip to content

Commit

Permalink
Query the backend to get the fqbn when a core is not installed (#336)
Browse files Browse the repository at this point in the history
* query the backend to get the fqbn

* Update commands/board/list.go

Co-Authored-By: Maurizio Branca <m.branca@arduino.cc>
  • Loading branch information
masci and Maurizio Branca committed Aug 9, 2019
1 parent 5858721 commit f2d14b2
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 17 deletions.
2 changes: 1 addition & 1 deletion arduino/cores/packagemanager/identify.go
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
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
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
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
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")

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
@@ -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 license@arduino.cc.

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

0 comments on commit f2d14b2

Please sign in to comment.