Skip to content
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

Add support for pluggable monitor (tracks branch feature/pluggable-monitor) #1491

Merged
merged 9 commits into from
Oct 11, 2021
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
22 changes: 12 additions & 10 deletions arduino/cores/cores.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ type PlatformRelease struct {
ToolDependencies ToolDependencies
DiscoveryDependencies DiscoveryDependencies
MonitorDependencies MonitorDependencies
Help PlatformReleaseHelp `json:"-"`
Platform *Platform `json:"-"`
Properties *properties.Map `json:"-"`
Boards map[string]*Board `json:"-"`
Programmers map[string]*Programmer `json:"-"`
Menus *properties.Map `json:"-"`
InstallDir *paths.Path `json:"-"`
IsIDEBundled bool `json:"-"`
IsTrusted bool `json:"-"`
PluggableDiscoveryAware bool `json:"-"` // true if the Platform supports pluggable discovery (no compatibility layer required)
Help PlatformReleaseHelp `json:"-"`
Platform *Platform `json:"-"`
Properties *properties.Map `json:"-"`
Boards map[string]*Board `json:"-"`
Programmers map[string]*Programmer `json:"-"`
Menus *properties.Map `json:"-"`
InstallDir *paths.Path `json:"-"`
IsIDEBundled bool `json:"-"`
IsTrusted bool `json:"-"`
PluggableDiscoveryAware bool `json:"-"` // true if the Platform supports pluggable discovery (no compatibility layer required)
Monitors map[string]*MonitorDependency `json:"-"`
MonitorsDevRecipes map[string]string `json:"-"`
}

// BoardManifest contains information about a board. These metadata are usually
Expand Down
23 changes: 23 additions & 0 deletions arduino/cores/packagemanager/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
} else {
platform.Properties.Set("pluggable_discovery.required.0", "builtin:serial-discovery")
platform.Properties.Set("pluggable_discovery.required.1", "builtin:mdns-discovery")
platform.Properties.Set("pluggable_monitor.required.serial", "builtin:serial-monitor")
}

if platform.Platform.Name == "" {
Expand Down Expand Up @@ -359,6 +360,28 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
if !platform.PluggableDiscoveryAware {
convertLegacyPlatformToPluggableDiscovery(platform)
}

// Build pluggable monitor references
platform.Monitors = map[string]*cores.MonitorDependency{}
for protocol, ref := range platform.Properties.SubTree("pluggable_monitor.required").AsMap() {
split := strings.Split(ref, ":")
if len(split) != 2 {
return fmt.Errorf(tr("invalid pluggable monitor reference: %s"), ref)
}
pm.Log.WithField("protocol", protocol).WithField("tool", ref).Info("Adding monitor tool")
platform.Monitors[protocol] = &cores.MonitorDependency{
Packager: split[0],
Name: split[1],
}
}

// Support for pluggable monitors in debugging/development environments
platform.MonitorsDevRecipes = map[string]string{}
for protocol, recipe := range platform.Properties.SubTree("pluggable_monitor.pattern").AsMap() {
pm.Log.WithField("protocol", protocol).WithField("recipe", recipe).Info("Adding monitor recipe")
platform.MonitorsDevRecipes[protocol] = recipe
}

return nil
}

Expand Down
22 changes: 7 additions & 15 deletions arduino/monitor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

// Package monitor provides a client for Pluggable Monitors.
// Documentation is available here:
// https://arduino.github.io/arduino-cli/latest/pluggable-monitor-specification/
package monitor

import (
Expand All @@ -26,20 +29,9 @@ import (
"github.com/arduino/arduino-cli/cli/globals"
"github.com/arduino/arduino-cli/executils"
"github.com/arduino/arduino-cli/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/sirupsen/logrus"
)

// To work correctly a Pluggable Monitor must respect the state machine specifed on the documentation:
// https://arduino.github.io/arduino-cli/latest/pluggable-monitor-specification/#state-machine
// States a PluggableMonitor can be in
const (
Alive int = iota
Idle
Opened
Dead
)

// PluggableMonitor is a tool that communicates with a board through a communication port.
type PluggableMonitor struct {
id string
Expand Down Expand Up @@ -271,9 +263,9 @@ func (mon *PluggableMonitor) Configure(param, value string) error {
}

// Open connects to the given Port. A communication channel is opened
func (mon *PluggableMonitor) Open(port *rpc.Port) (io.ReadWriter, error) {
if port.Protocol != mon.supportedProtocol {
return nil, fmt.Errorf("invalid monitor protocol '%s': only '%s' is accepted", port.Protocol, mon.supportedProtocol)
func (mon *PluggableMonitor) Open(portAddress, portProtocol string) (io.ReadWriter, error) {
if portProtocol != mon.supportedProtocol {
return nil, fmt.Errorf("invalid monitor protocol '%s': only '%s' is accepted", portProtocol, mon.supportedProtocol)
}

tcpListener, err := net.Listen("tcp", "127.0.0.1:")
Expand All @@ -283,7 +275,7 @@ func (mon *PluggableMonitor) Open(port *rpc.Port) (io.ReadWriter, error) {
defer tcpListener.Close()
tcpListenerPort := tcpListener.Addr().(*net.TCPAddr).Port

if err := mon.sendCommand(fmt.Sprintf("OPEN 127.0.0.1:%d %s\n", tcpListenerPort, port.Address)); err != nil {
if err := mon.sendCommand(fmt.Sprintf("OPEN 127.0.0.1:%d %s\n", tcpListenerPort, portAddress)); err != nil {
return nil, err
}
if _, err := mon.waitMessage(time.Second*10, "open"); err != nil {
Expand Down
8 changes: 3 additions & 5 deletions arduino/monitor/monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"testing"
"time"

"github.com/arduino/arduino-cli/arduino/discovery"
"github.com/arduino/arduino-cli/executils"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -59,12 +58,11 @@ func TestDummyMonitor(t *testing.T) {
err = mon.Configure("speed", "38400")
require.NoError(t, err)

port := &discovery.Port{Address: "/dev/ttyACM0", Protocol: "test"}
rw, err := mon.Open(port.ToRPC())
rw, err := mon.Open("/dev/ttyACM0", "test")
require.NoError(t, err)

// Double open -> error: port already opened
_, err = mon.Open(port.ToRPC())
_, err = mon.Open("/dev/ttyACM0", "test")
require.Error(t, err)

// Write "TEST"
Expand Down Expand Up @@ -93,7 +91,7 @@ func TestDummyMonitor(t *testing.T) {
time.Sleep(100 * time.Millisecond)
require.Equal(t, int32(1), atomic.LoadInt32(&completed))

rw, err = mon.Open(port.ToRPC())
rw, err = mon.Open("/dev/ttyACM0", "test")
require.NoError(t, err)
n, err = rw.Write([]byte("TEST"))
require.NoError(t, err)
Expand Down
15 changes: 15 additions & 0 deletions cli/arguments/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ func (p *Port) AddToCommand(cmd *cobra.Command) {
cmd.Flags().DurationVar(&p.timeout, "discovery-timeout", 5*time.Second, tr("Max time to wait for port discovery, e.g.: 30s, 1m"))
}

// GetPortAddressAndProtocol returns only the port address and the port protocol
// without any other port metadata obtained from the discoveries. This method allows
// to bypass the discoveries unless the protocol is not specified: in this
// case the discoveries are needed to autodetect the protocol.
func (p *Port) GetPortAddressAndProtocol(instance *rpc.Instance, sk *sketch.Sketch) (string, string, error) {
if p.protocol != "" {
return p.address, p.protocol, nil
}
port, err := p.GetPort(instance, sk)
if err != nil {
return "", "", err
}
return port.Address, port.Protocol, nil
}

// GetPort returns the Port obtained by parsing command line arguments.
// The extra metadata for the ports is obtained using the pluggable discoveries.
func (p *Port) GetPort(instance *rpc.Instance, sk *sketch.Sketch) (*discovery.Port, error) {
Expand Down
2 changes: 2 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/arduino/arduino-cli/cli/generatedocs"
"github.com/arduino/arduino-cli/cli/globals"
"github.com/arduino/arduino-cli/cli/lib"
"github.com/arduino/arduino-cli/cli/monitor"
"github.com/arduino/arduino-cli/cli/outdated"
"github.com/arduino/arduino-cli/cli/output"
"github.com/arduino/arduino-cli/cli/sketch"
Expand Down Expand Up @@ -93,6 +94,7 @@ func createCliCommandTree(cmd *cobra.Command) {
cmd.AddCommand(daemon.NewCommand())
cmd.AddCommand(generatedocs.NewCommand())
cmd.AddCommand(lib.NewCommand())
cmd.AddCommand(monitor.NewCommand())
cmd.AddCommand(outdated.NewCommand())
cmd.AddCommand(sketch.NewCommand())
cmd.AddCommand(update.NewCommand())
Expand Down
210 changes: 210 additions & 0 deletions cli/monitor/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// 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 [email protected].

package monitor

import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strings"

"github.com/arduino/arduino-cli/cli/arguments"
"github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cli/cli/instance"
"github.com/arduino/arduino-cli/commands/monitor"
"github.com/arduino/arduino-cli/configuration"
"github.com/arduino/arduino-cli/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/arduino-cli/table"
"github.com/fatih/color"
"github.com/spf13/cobra"
)

var tr = i18n.Tr

var portArgs arguments.Port
var describe bool
var configs []string
var quiet bool
var fqbn string

// NewCommand created a new `monitor` command
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "monitor",
Short: tr("Open a communication port with a board."),
Long: tr("Open a communication port with a board."),
Example: "" +
" " + os.Args[0] + " monitor -p /dev/ttyACM0\n" +
" " + os.Args[0] + " monitor -p /dev/ttyACM0 --describe",
Run: runMonitorCmd,
}
portArgs.AddToCommand(cmd)
cmd.Flags().BoolVar(&describe, "describe", false, tr("Show all the settings of the communication port."))
cmd.Flags().StringSliceVarP(&configs, "config", "c", []string{}, tr("Configuration of the port."))
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, tr("Run in silent mode, show only monitor input and output."))
cmd.Flags().StringVarP(&fqbn, "fqbn", "b", "", tr("Fully Qualified Board Name, e.g.: arduino:avr:uno"))
cmd.MarkFlagRequired("port")
return cmd
}

func runMonitorCmd(cmd *cobra.Command, args []string) {
instance := instance.CreateAndInit()

if !configuration.HasConsole {
quiet = true
}

portAddress, portProtocol, err := portArgs.GetPortAddressAndProtocol(instance, nil)
if err != nil {
feedback.Error(err)
os.Exit(errorcodes.ErrGeneric)
}

enumerateResp, err := monitor.EnumerateMonitorPortSettings(context.Background(), &rpc.EnumerateMonitorPortSettingsRequest{
Instance: instance,
PortProtocol: portProtocol,
Fqbn: fqbn,
})
if err != nil {
feedback.Error(tr("Error getting port settings details: %s", err))
os.Exit(errorcodes.ErrGeneric)
}
if describe {
feedback.PrintResult(&detailsResult{Settings: enumerateResp.Settings})
return
}

tty, err := newStdInOutTerminal()
if err != nil {
feedback.Error(err)
os.Exit(errorcodes.ErrGeneric)
}
defer tty.Close()

configuration := &rpc.MonitorPortConfiguration{}
if len(configs) > 0 {
for _, config := range configs {
split := strings.SplitN(config, "=", 2)
k := ""
v := config
if len(split) == 2 {
k = split[0]
v = split[1]
}

var setting *rpc.MonitorPortSettingDescriptor
for _, s := range enumerateResp.GetSettings() {
if k == "" {
if contains(s.EnumValues, v) {
setting = s
break
}
} else {
if strings.EqualFold(s.SettingId, k) {
if !contains(s.EnumValues, v) {
feedback.Error(tr("invalid port configuration value for %s: %s", k, v))
os.Exit(errorcodes.ErrBadArgument)
}
setting = s
break
}
}
}
if setting == nil {
feedback.Error(tr("invalid port configuration: %s", config))
os.Exit(errorcodes.ErrBadArgument)
}
configuration.Settings = append(configuration.Settings, &rpc.MonitorPortSetting{
SettingId: setting.SettingId,
Value: v,
})
if !quiet {
feedback.Print(tr("Monitor port settings:"))
feedback.Print(fmt.Sprintf("%s=%s", setting.SettingId, v))
}
}
}
portProxy, _, err := monitor.Monitor(context.Background(), &rpc.MonitorRequest{
Instance: instance,
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
Fqbn: fqbn,
PortConfiguration: configuration,
})
if err != nil {
feedback.Error(err)
os.Exit(errorcodes.ErrGeneric)
}
defer portProxy.Close()

ctx, cancel := context.WithCancel(context.Background())
go func() {
_, err := io.Copy(tty, portProxy)
if err != nil && !errors.Is(err, io.EOF) {
feedback.Error(tr("Port closed:"), err)
}
cancel()
}()
go func() {
_, err := io.Copy(portProxy, tty)
if err != nil && !errors.Is(err, io.EOF) {
feedback.Error(tr("Port closed:"), err)
}
cancel()
}()

if !quiet {
feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress))
}

// Wait for port closed
<-ctx.Done()
}

type detailsResult struct {
Settings []*rpc.MonitorPortSettingDescriptor `json:"settings"`
}

func (r *detailsResult) Data() interface{} {
return r
}

func (r *detailsResult) String() string {
t := table.New()
green := color.New(color.FgGreen)
t.SetHeader(tr("ID"), tr("Setting"), tr("Default"), tr("Values"))
sort.Slice(r.Settings, func(i, j int) bool {
return r.Settings[i].Label < r.Settings[j].Label
})
for _, setting := range r.Settings {
values := strings.Join(setting.EnumValues, ", ")
t.AddRow(setting.SettingId, setting.Label, table.NewCell(setting.Value, green), values)
}
return t.Render()
}

func contains(s []string, searchterm string) bool {
for _, item := range s {
if strings.EqualFold(item, searchterm) {
return true
}
}
return false
}
Loading