Skip to content

Allow port, protocol and port settings to be specified in profiles. #2717

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 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
26 changes: 24 additions & 2 deletions docs/sketch-project-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Each profile will define:
- A possible core platform name and version, that is a dependency of the target core platform (with the 3rd party
platform index URL if needed)
- The libraries used in the sketch (including their version)
- The port and protocol to upload the sketch and monitor the board

The format of the file is the following:

Expand All @@ -33,7 +34,11 @@ profiles:
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)

port: <PORT_NAME>
port_config:
<PORT_SETTING_NAME>: <PORT_SETTING_VALUE>
...
protocol: <PORT_PROTOCOL>
...more profiles here...
```

Expand All @@ -54,6 +59,15 @@ otherwise below). The available fields are:
- `<USER_NOTES>` is a free text string available to the developer to add comments. This field is optional.
- `<PROGRAMMER>` is the programmer that will be used. This field is optional.

The following fields are available since Arduino CLI 1.1.0:

- `<PORT_NAME>` is the port that will be used to upload and monitor the board (unless explicitly set otherwise). This
field is optional.
- `port_config` section with `<PORT_SETTING_NAME>` and `<PORT_SETTING_VALUE>` defines the port settings that will be
used in the `monitor` command. Typically is used to set the baudrate for the serial port (for example
`baudrate: 115200`) but any setting/value can be specified. Multiple settings can be set. These fields are optional.
- `<PORT_PROTOCOL>` is the protocol for the port used to upload and monitor the board. This field is optional.

A complete example of a sketch project file may be the following:

```
Expand All @@ -76,6 +90,9 @@ profiles:
- VitconMQTT (1.0.1)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)
port: /dev/ttyACM0
port_config:
baudrate: 115200

tiny:
notes: testing the very limit of the AVR platform, it will be very unstable
Expand Down Expand Up @@ -139,6 +156,8 @@ particular:
- The `default_fqbn` key sets the default value for the `--fqbn` flag
- The `default_programmer` key sets the default value for the `--programmer` flag
- The `default_port` key sets the default value for the `--port` flag
- The `default_port_config` key sets the default values for the `--config` flag in the `monitor` command (available
since Arduino CLI 1.1.0)
- The `default_protocol` key sets the default value for the `--protocol` flag
- The `default_profile` key sets the default value for the `--profile` flag

Expand All @@ -148,11 +167,14 @@ For example:
default_fqbn: arduino:samd:mkr1000
default_programmer: atmel_ice
default_port: /dev/ttyACM0
default_port_config:
baudrate: 115200
default_protocol: serial
default_profile: myprofile
```

With this configuration set, it is not necessary to specify the `--fqbn`, `--programmer`, `--port`, `--protocol` or
`--profile` flags to the [`arduino-cli compile`](commands/arduino-cli_compile.md),
[`arduino-cli upload`](commands/arduino-cli_upload.md) or [`arduino-cli debug`](commands/arduino-cli_debug.md) commands
when compiling, uploading or debugging the sketch.
when compiling, uploading or debugging the sketch. Moreover in the `monitor` command it is not necessary to specify the
`--config baudrate=115200` to communicate with the monitor port of the board.
49 changes: 43 additions & 6 deletions internal/arduino/sketch/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ import (

// projectRaw is a support struct used only to unmarshal the yaml
type projectRaw struct {
ProfilesRaw yaml.Node `yaml:"profiles"`
DefaultProfile string `yaml:"default_profile"`
DefaultFqbn string `yaml:"default_fqbn"`
DefaultPort string `yaml:"default_port,omitempty"`
DefaultProtocol string `yaml:"default_protocol,omitempty"`
DefaultProgrammer string `yaml:"default_programmer,omitempty"`
ProfilesRaw yaml.Node `yaml:"profiles"`
DefaultProfile string `yaml:"default_profile"`
DefaultFqbn string `yaml:"default_fqbn"`
DefaultPort string `yaml:"default_port,omitempty"`
DefaultPortConfig map[string]string `yaml:"default_port_config,omitempty"`
DefaultProtocol string `yaml:"default_protocol,omitempty"`
DefaultProgrammer string `yaml:"default_programmer,omitempty"`
}

// Project represents the sketch project file
Expand All @@ -48,6 +49,7 @@ type Project struct {
DefaultProfile string
DefaultFqbn string
DefaultPort string
DefaultPortConfig map[string]string
DefaultProtocol string
DefaultProgrammer string
}
Expand All @@ -70,6 +72,12 @@ func (p *Project) AsYaml() string {
if p.DefaultPort != "" {
res += fmt.Sprintf("default_port: %s\n", p.DefaultPort)
}
if len(p.DefaultPortConfig) > 0 {
res += "default_port_config:\n"
for k, v := range p.DefaultPortConfig {
res += fmt.Sprintf(" %s: %s\n", k, v)
}
}
if p.DefaultProtocol != "" {
res += fmt.Sprintf("default_protocol: %s\n", p.DefaultProtocol)
}
Expand Down Expand Up @@ -103,17 +111,33 @@ type Profile struct {
Name string
Notes string `yaml:"notes"`
FQBN string `yaml:"fqbn"`
Port string `yaml:"port"`
PortConfig map[string]string `yaml:"port_config"`
Protocol string `yaml:"protocol"`
Programmer string `yaml:"programmer"`
Platforms ProfileRequiredPlatforms `yaml:"platforms"`
Libraries ProfileRequiredLibraries `yaml:"libraries"`
}

// ToRpc converts this Profile to an rpc.SketchProfile
func (p *Profile) ToRpc() *rpc.SketchProfile {
var portConfig *rpc.MonitorPortConfiguration
if len(p.PortConfig) > 0 {
portConfig = &rpc.MonitorPortConfiguration{}
for k, v := range p.PortConfig {
portConfig.Settings = append(portConfig.Settings, &rpc.MonitorPortSetting{
SettingId: k,
Value: v,
})
}
}
return &rpc.SketchProfile{
Name: p.Name,
Fqbn: p.FQBN,
Programmer: p.Programmer,
Port: p.Port,
PortConfig: portConfig,
Protocol: p.Protocol,
}
}

Expand All @@ -127,6 +151,18 @@ func (p *Profile) AsYaml() string {
if p.Programmer != "" {
res += fmt.Sprintf(" programmer: %s\n", p.Programmer)
}
if p.Port != "" {
res += fmt.Sprintf(" port: %s\n", p.Port)
}
if p.Protocol != "" {
res += fmt.Sprintf(" protocol: %s\n", p.Protocol)
}
if len(p.PortConfig) > 0 {
res += " port_config:\n"
for k, v := range p.PortConfig {
res += fmt.Sprintf(" %s: %s\n", k, v)
}
}
res += p.Platforms.AsYaml()
res += p.Libraries.AsYaml()
return res
Expand Down Expand Up @@ -291,6 +327,7 @@ func LoadProjectFile(file *paths.Path) (*Project, error) {
DefaultProfile: raw.DefaultProfile,
DefaultFqbn: raw.DefaultFqbn,
DefaultPort: raw.DefaultPort,
DefaultPortConfig: raw.DefaultPortConfig,
DefaultProtocol: raw.DefaultProtocol,
DefaultProgrammer: raw.DefaultProgrammer,
}, nil
Expand Down
11 changes: 11 additions & 0 deletions internal/arduino/sketch/sketch.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ func (s *Sketch) Hash() string {
// ToRpc converts this Sketch into a rpc.LoadSketchResponse
func (s *Sketch) ToRpc() *rpc.Sketch {
defaultPort, defaultProtocol := s.GetDefaultPortAddressAndProtocol()
var defaultPortConfig *rpc.MonitorPortConfiguration
if len(s.Project.DefaultPortConfig) > 0 {
defaultPortConfig = &rpc.MonitorPortConfiguration{}
for k, v := range s.Project.DefaultPortConfig {
defaultPortConfig.Settings = append(defaultPortConfig.Settings, &rpc.MonitorPortSetting{
SettingId: k,
Value: v,
})
}
}
res := &rpc.Sketch{
MainFile: s.MainFile.String(),
LocationPath: s.FullPath.String(),
Expand All @@ -297,6 +307,7 @@ func (s *Sketch) ToRpc() *rpc.Sketch {
RootFolderFiles: s.RootFolderFiles.AsStrings(),
DefaultFqbn: s.GetDefaultFQBN(),
DefaultPort: defaultPort,
DefaultPortConfig: defaultPortConfig,
DefaultProtocol: defaultProtocol,
DefaultProgrammer: s.GetDefaultProgrammer(),
Profiles: f.Map(s.Project.Profiles, (*Profile).ToRpc),
Expand Down
8 changes: 7 additions & 1 deletion internal/cli/arguments/fqbn.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (f *Fqbn) Set(fqbn string) {
// - the port is not found, in this case nil is returned
// - the FQBN autodetection fail, in this case the function prints an error and
// terminates the execution
func CalculateFQBNAndPort(ctx context.Context, portArgs *Port, fqbnArg *Fqbn, instance *rpc.Instance, srv rpc.ArduinoCoreServiceServer, defaultFQBN, defaultAddress, defaultProtocol string) (string, *rpc.Port) {
func CalculateFQBNAndPort(ctx context.Context, portArgs *Port, fqbnArg *Fqbn, instance *rpc.Instance, srv rpc.ArduinoCoreServiceServer, defaultFQBN, defaultAddress, defaultProtocol string, profile *rpc.SketchProfile) (string, *rpc.Port) {
fqbn := fqbnArg.String()
if fqbn == "" {
fqbn = defaultFQBN
Expand All @@ -87,6 +87,12 @@ func CalculateFQBNAndPort(ctx context.Context, portArgs *Port, fqbnArg *Fqbn, in
return fqbn, port
}

if profile.GetPort() != "" {
defaultAddress = profile.GetPort()
}
if profile.GetProtocol() != "" {
defaultProtocol = profile.GetProtocol()
}
port, err := portArgs.GetPort(ctx, instance, srv, defaultAddress, defaultProtocol)
if err != nil {
feedback.Fatal(i18n.Tr("Error getting port metadata: %v", err), feedback.ErrGeneric)
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func runCompileCommand(cmd *cobra.Command, args []string, srv rpc.ArduinoCoreSer
fqbnArg.Set(profile.GetFqbn())
}

fqbn, port := arguments.CalculateFQBNAndPort(ctx, &portArgs, &fqbnArg, inst, srv, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol())
fqbn, port := arguments.CalculateFQBNAndPort(ctx, &portArgs, &fqbnArg, inst, srv, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol(), profile)

if keysKeychain != "" || signKey != "" || encryptKey != "" {
arguments.CheckFlagsMandatory(cmd, "keys-keychain", "sign-key", "encrypt-key")
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func runDebugCommand(ctx context.Context, srv rpc.ArduinoCoreServiceServer, args
fqbnArg.Set(profile.GetFqbn())
}

fqbn, port := arguments.CalculateFQBNAndPort(ctx, portArgs, fqbnArg, inst, srv, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol())
fqbn, port := arguments.CalculateFQBNAndPort(ctx, portArgs, fqbnArg, inst, srv, sk.GetDefaultFqbn(), sk.GetDefaultPort(), sk.GetDefaultProtocol(), profile)

prog := profile.GetProgrammer()
if prog == "" || programmer.GetProgrammer() != "" {
Expand Down
112 changes: 65 additions & 47 deletions internal/cli/monitor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package monitor

import (
"bytes"
"cmp"
"context"
"errors"
"io"
Expand Down Expand Up @@ -163,50 +164,52 @@ func runMonitorCmd(
return
}

actualConfigurationLabels := properties.NewMap()
for _, setting := range defaultSettings.GetSettings() {
actualConfigurationLabels.Set(setting.GetSettingId(), setting.GetValue())
}

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 defaultSettings.GetSettings() {
if k == "" {
if contains(s.GetEnumValues(), v) {
setting = s
break
}
} else {
if strings.EqualFold(s.GetSettingId(), k) {
if !contains(s.GetEnumValues(), v) {
feedback.Fatal(i18n.Tr("invalid port configuration value for %s: %s", k, v), feedback.ErrBadArgument)
}
setting = s
break
// This utility finds the settings descriptor from key/value or only from key.
// It fails fatal if the key or value are invalid.
searchSettingDescriptor := func(k, v string) *rpc.MonitorPortSettingDescriptor {
for _, s := range defaultSettings.GetSettings() {
if k == "" {
if contains(s.GetEnumValues(), v) {
return s
}
} else {
if strings.EqualFold(s.GetSettingId(), k) {
if !contains(s.GetEnumValues(), v) {
feedback.Fatal(i18n.Tr("invalid port configuration value for %s: %s", k, v), feedback.ErrBadArgument)
}
return s
}
}
if setting == nil {
feedback.Fatal(i18n.Tr("invalid port configuration: %s", config), feedback.ErrBadArgument)
}
configuration.Settings = append(configuration.GetSettings(), &rpc.MonitorPortSetting{
SettingId: setting.GetSettingId(),
Value: v,
})
actualConfigurationLabels.Set(setting.GetSettingId(), v)
}
feedback.Fatal(i18n.Tr("invalid port configuration: %s=%s", k, v), feedback.ErrBadArgument)
return nil
}

// Build configuration by layering
layeredPortConfig := properties.NewMap()
setConfig := func(k, v string) {
settingDesc := searchSettingDescriptor(k, v)
layeredPortConfig.Set(settingDesc.GetSettingId(), v)
}

// Layer 1: apply configuration from sketch profile...
profileConfig := profile.GetPortConfig()
if profileConfig == nil {
// ...or from sketch default...
profileConfig = sketch.GetDefaultPortConfig()
}
for _, setting := range profileConfig.GetSettings() {
setConfig(setting.SettingId, setting.Value)
}

// Layer 2: apply configuration from command line...
for _, config := range configs {
if split := strings.SplitN(config, "=", 2); len(split) == 2 {
setConfig(split[0], split[1])
} else {
setConfig("", config)
}
}
ttyIn, ttyOut, err := feedback.InteractiveStreams()
if err != nil {
feedback.FatalError(err, feedback.ErrGeneric)
Expand All @@ -233,26 +236,41 @@ func runMonitorCmd(
}
ttyIn = io.TeeReader(ttyIn, ctrlCDetector)
}
var portConfiguration []*rpc.MonitorPortSetting
for k, v := range layeredPortConfig.AsMap() {
portConfiguration = append(portConfiguration, &rpc.MonitorPortSetting{
SettingId: k,
Value: v,
})
}
monitorServer, portProxy := commands.MonitorServerToReadWriteCloser(ctx, &rpc.MonitorPortOpenRequest{
Instance: inst,
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
Fqbn: fqbn,
PortConfiguration: configuration,
Instance: inst,
Port: &rpc.Port{Address: portAddress, Protocol: portProtocol},
Fqbn: fqbn,
PortConfiguration: &rpc.MonitorPortConfiguration{
Settings: portConfiguration,
},
})
go func() {
if !quiet {
if len(configs) == 0 {
if layeredPortConfig.Size() == 0 {
if fqbn != "" {
feedback.Print(i18n.Tr("Using default monitor configuration for board: %s", fqbn))
} else if portProtocol == "serial" {
feedback.Print(i18n.Tr("Using generic monitor configuration.\nWARNING: Your board may require different settings to work!\n"))
}
}
feedback.Print(i18n.Tr("Monitor port settings:"))
keys := actualConfigurationLabels.Keys()
slices.Sort(keys)
for _, k := range keys {
feedback.Printf(" %s=%s", k, actualConfigurationLabels.Get(k))
slices.SortFunc(defaultSettings.GetSettings(), func(a, b *rpc.MonitorPortSettingDescriptor) int {
return cmp.Compare(a.GetSettingId(), b.GetSettingId())
})
for _, defaultSetting := range defaultSettings.GetSettings() {
k := defaultSetting.GetSettingId()
v, ok := layeredPortConfig.GetOk(k)
if !ok {
v = defaultSetting.GetValue()
}
feedback.Printf(" %s=%s", k, v)
}
feedback.Print("")

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/upload/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func runUploadCommand(ctx context.Context, srv rpc.ArduinoCoreServiceServer, arg
defaultFQBN := sketch.GetDefaultFqbn()
defaultAddress := sketch.GetDefaultPort()
defaultProtocol := sketch.GetDefaultProtocol()
fqbn, port := arguments.CalculateFQBNAndPort(ctx, &portArgs, &fqbnArg, inst, srv, defaultFQBN, defaultAddress, defaultProtocol)
fqbn, port := arguments.CalculateFQBNAndPort(ctx, &portArgs, &fqbnArg, inst, srv, defaultFQBN, defaultAddress, defaultProtocol, profile)

userFieldRes, err := srv.SupportedUserFields(ctx, &rpc.SupportedUserFieldsRequest{
Instance: inst,
Expand Down
Loading
Loading