Skip to content

Commit 385d93a

Browse files
authored
Merge pull request #1491 from arduino/feature/pluggable-monitor
Add support for pluggable monitor (tracks branch `feature/pluggable-monitor`)
2 parents a8cd004 + 605ea1b commit 385d93a

30 files changed

+2831
-1361
lines changed

Diff for: arduino/cores/cores.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,18 @@ type PlatformRelease struct {
5252
ToolDependencies ToolDependencies
5353
DiscoveryDependencies DiscoveryDependencies
5454
MonitorDependencies MonitorDependencies
55-
Help PlatformReleaseHelp `json:"-"`
56-
Platform *Platform `json:"-"`
57-
Properties *properties.Map `json:"-"`
58-
Boards map[string]*Board `json:"-"`
59-
Programmers map[string]*Programmer `json:"-"`
60-
Menus *properties.Map `json:"-"`
61-
InstallDir *paths.Path `json:"-"`
62-
IsIDEBundled bool `json:"-"`
63-
IsTrusted bool `json:"-"`
64-
PluggableDiscoveryAware bool `json:"-"` // true if the Platform supports pluggable discovery (no compatibility layer required)
55+
Help PlatformReleaseHelp `json:"-"`
56+
Platform *Platform `json:"-"`
57+
Properties *properties.Map `json:"-"`
58+
Boards map[string]*Board `json:"-"`
59+
Programmers map[string]*Programmer `json:"-"`
60+
Menus *properties.Map `json:"-"`
61+
InstallDir *paths.Path `json:"-"`
62+
IsIDEBundled bool `json:"-"`
63+
IsTrusted bool `json:"-"`
64+
PluggableDiscoveryAware bool `json:"-"` // true if the Platform supports pluggable discovery (no compatibility layer required)
65+
Monitors map[string]*MonitorDependency `json:"-"`
66+
MonitorsDevRecipes map[string]string `json:"-"`
6567
}
6668

6769
// BoardManifest contains information about a board. These metadata are usually

Diff for: arduino/cores/packagemanager/loader.go

+23
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
326326
} else {
327327
platform.Properties.Set("pluggable_discovery.required.0", "builtin:serial-discovery")
328328
platform.Properties.Set("pluggable_discovery.required.1", "builtin:mdns-discovery")
329+
platform.Properties.Set("pluggable_monitor.required.serial", "builtin:serial-monitor")
329330
}
330331

331332
if platform.Platform.Name == "" {
@@ -359,6 +360,28 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
359360
if !platform.PluggableDiscoveryAware {
360361
convertLegacyPlatformToPluggableDiscovery(platform)
361362
}
363+
364+
// Build pluggable monitor references
365+
platform.Monitors = map[string]*cores.MonitorDependency{}
366+
for protocol, ref := range platform.Properties.SubTree("pluggable_monitor.required").AsMap() {
367+
split := strings.Split(ref, ":")
368+
if len(split) != 2 {
369+
return fmt.Errorf(tr("invalid pluggable monitor reference: %s"), ref)
370+
}
371+
pm.Log.WithField("protocol", protocol).WithField("tool", ref).Info("Adding monitor tool")
372+
platform.Monitors[protocol] = &cores.MonitorDependency{
373+
Packager: split[0],
374+
Name: split[1],
375+
}
376+
}
377+
378+
// Support for pluggable monitors in debugging/development environments
379+
platform.MonitorsDevRecipes = map[string]string{}
380+
for protocol, recipe := range platform.Properties.SubTree("pluggable_monitor.pattern").AsMap() {
381+
pm.Log.WithField("protocol", protocol).WithField("recipe", recipe).Info("Adding monitor recipe")
382+
platform.MonitorsDevRecipes[protocol] = recipe
383+
}
384+
362385
return nil
363386
}
364387

Diff for: arduino/monitor/monitor.go

+7-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// Arduino software without disclosing the source code of your own applications.
1414
// To purchase a commercial license, send an email to [email protected].
1515

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

1821
import (
@@ -26,20 +29,9 @@ import (
2629
"github.com/arduino/arduino-cli/cli/globals"
2730
"github.com/arduino/arduino-cli/executils"
2831
"github.com/arduino/arduino-cli/i18n"
29-
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
3032
"github.com/sirupsen/logrus"
3133
)
3234

33-
// To work correctly a Pluggable Monitor must respect the state machine specifed on the documentation:
34-
// https://arduino.github.io/arduino-cli/latest/pluggable-monitor-specification/#state-machine
35-
// States a PluggableMonitor can be in
36-
const (
37-
Alive int = iota
38-
Idle
39-
Opened
40-
Dead
41-
)
42-
4335
// PluggableMonitor is a tool that communicates with a board through a communication port.
4436
type PluggableMonitor struct {
4537
id string
@@ -271,9 +263,9 @@ func (mon *PluggableMonitor) Configure(param, value string) error {
271263
}
272264

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

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

286-
if err := mon.sendCommand(fmt.Sprintf("OPEN 127.0.0.1:%d %s\n", tcpListenerPort, port.Address)); err != nil {
278+
if err := mon.sendCommand(fmt.Sprintf("OPEN 127.0.0.1:%d %s\n", tcpListenerPort, portAddress)); err != nil {
287279
return nil, err
288280
}
289281
if _, err := mon.waitMessage(time.Second*10, "open"); err != nil {

Diff for: arduino/monitor/monitor_test.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"testing"
2525
"time"
2626

27-
"github.com/arduino/arduino-cli/arduino/discovery"
2827
"github.com/arduino/arduino-cli/executils"
2928
"github.com/arduino/go-paths-helper"
3029
"github.com/stretchr/testify/require"
@@ -59,12 +58,11 @@ func TestDummyMonitor(t *testing.T) {
5958
err = mon.Configure("speed", "38400")
6059
require.NoError(t, err)
6160

62-
port := &discovery.Port{Address: "/dev/ttyACM0", Protocol: "test"}
63-
rw, err := mon.Open(port.ToRPC())
61+
rw, err := mon.Open("/dev/ttyACM0", "test")
6462
require.NoError(t, err)
6563

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

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

96-
rw, err = mon.Open(port.ToRPC())
94+
rw, err = mon.Open("/dev/ttyACM0", "test")
9795
require.NoError(t, err)
9896
n, err = rw.Write([]byte("TEST"))
9997
require.NoError(t, err)

Diff for: cli/arguments/port.go

+15
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ func (p *Port) AddToCommand(cmd *cobra.Command) {
4646
cmd.Flags().DurationVar(&p.timeout, "discovery-timeout", 5*time.Second, tr("Max time to wait for port discovery, e.g.: 30s, 1m"))
4747
}
4848

49+
// GetPortAddressAndProtocol returns only the port address and the port protocol
50+
// without any other port metadata obtained from the discoveries. This method allows
51+
// to bypass the discoveries unless the protocol is not specified: in this
52+
// case the discoveries are needed to autodetect the protocol.
53+
func (p *Port) GetPortAddressAndProtocol(instance *rpc.Instance, sk *sketch.Sketch) (string, string, error) {
54+
if p.protocol != "" {
55+
return p.address, p.protocol, nil
56+
}
57+
port, err := p.GetPort(instance, sk)
58+
if err != nil {
59+
return "", "", err
60+
}
61+
return port.Address, port.Protocol, nil
62+
}
63+
4964
// GetPort returns the Port obtained by parsing command line arguments.
5065
// The extra metadata for the ports is obtained using the pluggable discoveries.
5166
func (p *Port) GetPort(instance *rpc.Instance, sk *sketch.Sketch) (*discovery.Port, error) {

Diff for: cli/cli.go

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/arduino/arduino-cli/cli/generatedocs"
3636
"github.com/arduino/arduino-cli/cli/globals"
3737
"github.com/arduino/arduino-cli/cli/lib"
38+
"github.com/arduino/arduino-cli/cli/monitor"
3839
"github.com/arduino/arduino-cli/cli/outdated"
3940
"github.com/arduino/arduino-cli/cli/output"
4041
"github.com/arduino/arduino-cli/cli/sketch"
@@ -93,6 +94,7 @@ func createCliCommandTree(cmd *cobra.Command) {
9394
cmd.AddCommand(daemon.NewCommand())
9495
cmd.AddCommand(generatedocs.NewCommand())
9596
cmd.AddCommand(lib.NewCommand())
97+
cmd.AddCommand(monitor.NewCommand())
9698
cmd.AddCommand(outdated.NewCommand())
9799
cmd.AddCommand(sketch.NewCommand())
98100
cmd.AddCommand(update.NewCommand())

Diff for: cli/monitor/monitor.go

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package monitor
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
"io"
23+
"os"
24+
"sort"
25+
"strings"
26+
27+
"github.com/arduino/arduino-cli/cli/arguments"
28+
"github.com/arduino/arduino-cli/cli/errorcodes"
29+
"github.com/arduino/arduino-cli/cli/feedback"
30+
"github.com/arduino/arduino-cli/cli/instance"
31+
"github.com/arduino/arduino-cli/commands/monitor"
32+
"github.com/arduino/arduino-cli/configuration"
33+
"github.com/arduino/arduino-cli/i18n"
34+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
35+
"github.com/arduino/arduino-cli/table"
36+
"github.com/fatih/color"
37+
"github.com/spf13/cobra"
38+
)
39+
40+
var tr = i18n.Tr
41+
42+
var portArgs arguments.Port
43+
var describe bool
44+
var configs []string
45+
var quiet bool
46+
var fqbn string
47+
48+
// NewCommand created a new `monitor` command
49+
func NewCommand() *cobra.Command {
50+
cmd := &cobra.Command{
51+
Use: "monitor",
52+
Short: tr("Open a communication port with a board."),
53+
Long: tr("Open a communication port with a board."),
54+
Example: "" +
55+
" " + os.Args[0] + " monitor -p /dev/ttyACM0\n" +
56+
" " + os.Args[0] + " monitor -p /dev/ttyACM0 --describe",
57+
Run: runMonitorCmd,
58+
}
59+
portArgs.AddToCommand(cmd)
60+
cmd.Flags().BoolVar(&describe, "describe", false, tr("Show all the settings of the communication port."))
61+
cmd.Flags().StringSliceVarP(&configs, "config", "c", []string{}, tr("Configuration of the port."))
62+
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, tr("Run in silent mode, show only monitor input and output."))
63+
cmd.Flags().StringVarP(&fqbn, "fqbn", "b", "", tr("Fully Qualified Board Name, e.g.: arduino:avr:uno"))
64+
cmd.MarkFlagRequired("port")
65+
return cmd
66+
}
67+
68+
func runMonitorCmd(cmd *cobra.Command, args []string) {
69+
instance := instance.CreateAndInit()
70+
71+
if !configuration.HasConsole {
72+
quiet = true
73+
}
74+
75+
portAddress, portProtocol, err := portArgs.GetPortAddressAndProtocol(instance, nil)
76+
if err != nil {
77+
feedback.Error(err)
78+
os.Exit(errorcodes.ErrGeneric)
79+
}
80+
81+
enumerateResp, err := monitor.EnumerateMonitorPortSettings(context.Background(), &rpc.EnumerateMonitorPortSettingsRequest{
82+
Instance: instance,
83+
PortProtocol: portProtocol,
84+
Fqbn: fqbn,
85+
})
86+
if err != nil {
87+
feedback.Error(tr("Error getting port settings details: %s", err))
88+
os.Exit(errorcodes.ErrGeneric)
89+
}
90+
if describe {
91+
feedback.PrintResult(&detailsResult{Settings: enumerateResp.Settings})
92+
return
93+
}
94+
95+
tty, err := newStdInOutTerminal()
96+
if err != nil {
97+
feedback.Error(err)
98+
os.Exit(errorcodes.ErrGeneric)
99+
}
100+
defer tty.Close()
101+
102+
configuration := &rpc.MonitorPortConfiguration{}
103+
if len(configs) > 0 {
104+
for _, config := range configs {
105+
split := strings.SplitN(config, "=", 2)
106+
k := ""
107+
v := config
108+
if len(split) == 2 {
109+
k = split[0]
110+
v = split[1]
111+
}
112+
113+
var setting *rpc.MonitorPortSettingDescriptor
114+
for _, s := range enumerateResp.GetSettings() {
115+
if k == "" {
116+
if contains(s.EnumValues, v) {
117+
setting = s
118+
break
119+
}
120+
} else {
121+
if strings.EqualFold(s.SettingId, k) {
122+
if !contains(s.EnumValues, v) {
123+
feedback.Error(tr("invalid port configuration value for %s: %s", k, v))
124+
os.Exit(errorcodes.ErrBadArgument)
125+
}
126+
setting = s
127+
break
128+
}
129+
}
130+
}
131+
if setting == nil {
132+
feedback.Error(tr("invalid port configuration: %s", config))
133+
os.Exit(errorcodes.ErrBadArgument)
134+
}
135+
configuration.Settings = append(configuration.Settings, &rpc.MonitorPortSetting{
136+
SettingId: setting.SettingId,
137+
Value: v,
138+
})
139+
if !quiet {
140+
feedback.Print(tr("Monitor port settings:"))
141+
feedback.Print(fmt.Sprintf("%s=%s", setting.SettingId, v))
142+
}
143+
}
144+
}
145+
portProxy, _, err := monitor.Monitor(context.Background(), &rpc.MonitorRequest{
146+
Instance: instance,
147+
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
148+
Fqbn: fqbn,
149+
PortConfiguration: configuration,
150+
})
151+
if err != nil {
152+
feedback.Error(err)
153+
os.Exit(errorcodes.ErrGeneric)
154+
}
155+
defer portProxy.Close()
156+
157+
ctx, cancel := context.WithCancel(context.Background())
158+
go func() {
159+
_, err := io.Copy(tty, portProxy)
160+
if err != nil && !errors.Is(err, io.EOF) {
161+
feedback.Error(tr("Port closed:"), err)
162+
}
163+
cancel()
164+
}()
165+
go func() {
166+
_, err := io.Copy(portProxy, tty)
167+
if err != nil && !errors.Is(err, io.EOF) {
168+
feedback.Error(tr("Port closed:"), err)
169+
}
170+
cancel()
171+
}()
172+
173+
if !quiet {
174+
feedback.Print(tr("Connected to %s! Press CTRL-C to exit.", portAddress))
175+
}
176+
177+
// Wait for port closed
178+
<-ctx.Done()
179+
}
180+
181+
type detailsResult struct {
182+
Settings []*rpc.MonitorPortSettingDescriptor `json:"settings"`
183+
}
184+
185+
func (r *detailsResult) Data() interface{} {
186+
return r
187+
}
188+
189+
func (r *detailsResult) String() string {
190+
t := table.New()
191+
green := color.New(color.FgGreen)
192+
t.SetHeader(tr("ID"), tr("Setting"), tr("Default"), tr("Values"))
193+
sort.Slice(r.Settings, func(i, j int) bool {
194+
return r.Settings[i].Label < r.Settings[j].Label
195+
})
196+
for _, setting := range r.Settings {
197+
values := strings.Join(setting.EnumValues, ", ")
198+
t.AddRow(setting.SettingId, setting.Label, table.NewCell(setting.Value, green), values)
199+
}
200+
return t.Render()
201+
}
202+
203+
func contains(s []string, searchterm string) bool {
204+
for _, item := range s {
205+
if strings.EqualFold(item, searchterm) {
206+
return true
207+
}
208+
}
209+
return false
210+
}

0 commit comments

Comments
 (0)