diff --git a/arduino/cores/packagemanager/identify.go b/arduino/cores/packagemanager/identify.go index 834cd95d00e..926fa4dd1bf 100644 --- a/arduino/cores/packagemanager/identify.go +++ b/arduino/cores/packagemanager/identify.go @@ -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{} diff --git a/cli/board/list.go b/cli/board/list.go index fdaf4f3fdb0..63018402a28 100644 --- a/cli/board/list.go +++ b/cli/board/list.go @@ -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 @@ -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() diff --git a/cli/output/table.go b/cli/output/table.go index 5273576126d..efc9aca92cd 100644 --- a/cli/output/table.go +++ b/cli/output/table.go @@ -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)) } diff --git a/cli/output/text.go b/cli/output/text.go index e8151dfd701..f22d65fa3b6 100644 --- a/cli/output/text.go +++ b/cli/output/text.go @@ -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 { diff --git a/commands/board/list.go b/commands/board/list.go index 0f58a6e3852..11e4c3cc3d8 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -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") @@ -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 } diff --git a/commands/board/list_test.go b/commands/board/list_test.go new file mode 100644 index 00000000000..65fcaaa8cfb --- /dev/null +++ b/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) +} diff --git a/commands/daemon/daemon.go b/commands/daemon/daemon.go index aa5feafc5f0..b77b8f1d5ff 100644 --- a/commands/daemon/daemon.go +++ b/commands/daemon/daemon.go @@ -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