diff --git a/README.md b/README.md index be8977f..1906fac 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,35 @@ Install a recent go enviroment (>=13.0) and run `go build`. The executable `seri ## Usage -After startup, the tool waits for commands. The available commands are: `START`, `STOP`, `QUIT`, `LIST` and `START_SYNC`. +After startup, the tool waits for commands. The available commands are: `HELLO`, `START`, `STOP`, `QUIT`, `LIST` and `START_SYNC`. + +#### HELLO command + +The `HELLO` command is used to establish the pluggable discovery protocol between client and discovery. +The format of the command is: + +`HELLO ""` + +for example: + +`HELLO 1 "Arduino IDE"` + +or: + +`HELLO 1 "arduino-cli"` + +in this case the protocol version requested by the client is `1` (at the moment of writing there were no other revisions of the protocol). +The response to the command is: + +```json +{ + "eventType": "hello", + "protocolVersion": 1, + "message": "OK" +} +``` + +`protocolVersion` is the protocol version that the discovery is going to use in the remainder of the communication. #### START command @@ -56,14 +84,10 @@ The `LIST` command returns a list of the currently available serial ports. The f { "address": "/dev/ttyACM0", "label": "/dev/ttyACM0", - "prefs": { - "productId": "0x804e", - "serialNumber": "EBEABFD6514D32364E202020FF10181E", - "vendorId": "0x2341" - }, - "identificationPrefs": { + "properties": { "pid": "0x804e", - "vid": "0x2341" + "vid": "0x2341", + "serialNumber": "EBEABFD6514D32364E202020FF10181E" }, "protocol": "serial", "protocolLabel": "Serial Port (USB)" @@ -72,13 +96,23 @@ The `LIST` command returns a list of the currently available serial ports. The f } ``` -The `ports` field contains a list of the available serial ports. If the serial port comes from an USB serial converter the USB VID/PID and USB SERIAL NUMBER properties are also reported inside `prefs`. Inside the `identificationPrefs` instead we have only the properties useful for product identification (in this case USB VID/PID only that may be useful to identify the board) +The `ports` field contains a list of the available serial ports. If the serial port comes from an USB serial converter the USB VID/PID and USB SERIAL NUMBER properties are also reported inside `properties`. The list command is a one-shot command, if you need continuos monitoring of ports you should use `START_SYNC` command. #### START_SYNC command The `START_SYNC` command puts the tool in "events" mode: the discovery will send `add` and `remove` events each time a new port is detected or removed respectively. +The immediate response to the command is: + +```json +{ + "eventType": "start_sync", + "message": "OK" +} +``` + +after that the discovery enters in "events" mode. The `add` events looks like the following: @@ -88,14 +122,10 @@ The `add` events looks like the following: "port": { "address": "/dev/ttyACM0", "label": "/dev/ttyACM0", - "prefs": { - "productId": "0x804e", - "serialNumber": "EBEABFD6514D32364E202020FF10181E", - "vendorId": "0x2341" - }, - "identificationPrefs": { + "properties": { "pid": "0x804e", - "vid": "0x2341" + "vid": "0x2341", + "serialNumber": "EBEABFD6514D32364E202020FF10181E" }, "protocol": "serial", "protocolLabel": "Serial Port (USB)" @@ -111,47 +141,66 @@ The `remove` event looks like this: { "eventType": "remove", "port": { - "address": "/dev/ttyACM0" + "address": "/dev/ttyACM0", + "protocol": "serial" } } ``` -in this case only the `address` field is reported. +in this case only the `address` and `protocol` fields are reported. ### Example of usage A possible transcript of the discovery usage: ``` -$ ./serial-discovery +$ ./serial-discovery START { "eventType": "start", "message": "OK" } +LIST +{ + "eventType": "list", + "ports": [ + { + "address": "/dev/ttyACM0", + "label": "/dev/ttyACM0", + "protocol": "serial", + "protocolLabel": "Serial Port (USB)", + "properties": { + "pid": "0x004e", + "serialNumber": "EBEABFD6514D32364E202020FF10181E", + "vid": "0x2341" + } + } + ] +} START_SYNC { + "eventType": "start_sync", + "message": "OK" +} +{ <--- this event has been immediately sent "eventType": "add", "port": { "address": "/dev/ttyACM0", "label": "/dev/ttyACM0", - "prefs": { - "productId": "0x804e", + "protocol": "serial", + "protocolLabel": "Serial Port (USB)", + "properties": { + "pid": "0x004e", "serialNumber": "EBEABFD6514D32364E202020FF10181E", - "vendorId": "0x2341" - }, - "identificationPrefs": { - "pid": "0x804e", "vid": "0x2341" - }, - "protocol": "serial", - "protocolLabel": "Serial Port (USB)" + } } } { <--- the board has been disconnected here "eventType": "remove", "port": { - "address": "/dev/ttyACM0" + "address": "/dev/ttyACM0", + "protocol": "serial" } } { <--- the board has been connected again @@ -159,17 +208,13 @@ START_SYNC "port": { "address": "/dev/ttyACM0", "label": "/dev/ttyACM0", - "prefs": { - "productId": "0x804e", + "protocol": "serial", + "protocolLabel": "Serial Port (USB)", + "properties": { + "pid": "0x004e", "serialNumber": "EBEABFD6514D32364E202020FF10181E", - "vendorId": "0x2341" - }, - "identificationPrefs": { - "pid": "0x804e", "vid": "0x2341" - }, - "protocol": "serial", - "protocolLabel": "Serial Port (USB)" + } } } QUIT @@ -196,5 +241,4 @@ The software is released under the GNU General Public License, which covers the of the serial-discovery code. The terms of this license can be found at: https://www.gnu.org/licenses/gpl-3.0.en.html -See [LICENSE.txt]() for details. - +See [LICENSE.txt](https://github.com/arduino/serial-discovery/blob/master/LICENSE.txt) for details. diff --git a/main.go b/main.go index 69041a3..7c36747 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,8 @@ import ( "encoding/json" "fmt" "os" + "regexp" + "strconv" "strings" "sync" @@ -42,48 +44,98 @@ func main() { reader := bufio.NewReader(os.Stdin) for { - cmd, err := reader.ReadString('\n') + fullCmd, err := reader.ReadString('\n') if err != nil { - outputError(err) + output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: err.Error(), + }) os.Exit(1) } - cmd = strings.ToUpper(strings.TrimSpace(cmd)) + split := strings.Split(fullCmd, " ") + cmd := strings.ToUpper(strings.TrimSpace(split[0])) switch cmd { + case "HELLO": + re := regexp.MustCompile(`(\d+) "([^"]+)"`) + matches := re.FindStringSubmatch(fullCmd[6:]) + if len(matches) != 3 { + output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: "Invalid HELLO command", + }) + continue + } + _ /* userAgent */ = matches[2] + _ /* reqProtocolVersion */, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: "Invalid protocol version: " + matches[2], + }) + } + output(&helloMessageJSON{ + EventType: "hello", + ProtocolVersion: 1, // Protocol version 1 is the only supported for now... + Message: "OK", + }) case "START": - outputMessage("start", "OK") + output(&genericMessageJSON{ + EventType: "start", + Message: "OK", + }) case "STOP": if syncStarted { syncCloseChan <- true syncStarted = false } - outputMessage("stop", "OK") + output(&genericMessageJSON{ + EventType: "stop", + Message: "OK", + }) case "LIST": outputList() case "QUIT": - outputMessage("quit", "OK") + output(&genericMessageJSON{ + EventType: "quit", + Message: "OK", + }) os.Exit(0) case "START_SYNC": if syncStarted { - outputMessage("startSync", "OK") + // sync already started, just acknowledge again... + output(&genericMessageJSON{ + EventType: "start_sync", + Message: "OK", + }) } else if close, err := startSync(); err != nil { - outputError(err) + output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: err.Error(), + }) } else { syncCloseChan = close syncStarted = true } default: - outputError(fmt.Errorf("Command %s not supported", cmd)) + output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: fmt.Sprintf("Command %s not supported", cmd), + }) } } } type boardPortJSON struct { - Address string `json:"address"` - Label string `json:"label,omitempty"` - Prefs *properties.Map `json:"prefs,omitempty"` - IdentificationPrefs *properties.Map `json:"identificationPrefs,omitempty"` - Protocol string `json:"protocol,omitempty"` - ProtocolLabel string `json:"protocolLabel,omitempty"` + Address string `json:"address"` + Label string `json:"label,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolLabel string `json:"protocolLabel,omitempty"` + Properties *properties.Map `json:"properties,omitempty"` } type listOutputJSON struct { @@ -94,7 +146,11 @@ type listOutputJSON struct { func outputList() { list, err := enumerator.GetDetailedPortsList() if err != nil { - outputError(err) + output(&genericMessageJSON{ + EventType: "list", + Error: true, + Message: err.Error(), + }) return } portsJSON := []*boardPortJSON{} @@ -102,60 +158,55 @@ func outputList() { portJSON := newBoardPortJSON(port) portsJSON = append(portsJSON, portJSON) } - d, err := json.MarshalIndent(&listOutputJSON{ + output(&listOutputJSON{ EventType: "list", Ports: portsJSON, - }, "", " ") - if err != nil { - outputError(err) - return - } - syncronizedPrintLn(string(d)) + }) } func newBoardPortJSON(port *enumerator.PortDetails) *boardPortJSON { prefs := properties.NewMap() - identificationPrefs := properties.NewMap() portJSON := &boardPortJSON{ - Address: port.Name, - Label: port.Name, - Protocol: "serial", - ProtocolLabel: "Serial Port", - Prefs: prefs, - IdentificationPrefs: identificationPrefs, + Address: port.Name, + Label: port.Name, + Protocol: "serial", + ProtocolLabel: "Serial Port", + Properties: prefs, } if port.IsUSB { portJSON.ProtocolLabel = "Serial Port (USB)" - portJSON.Prefs.Set("vendorId", "0x"+port.VID) - portJSON.Prefs.Set("productId", "0x"+port.PID) - portJSON.Prefs.Set("serialNumber", port.SerialNumber) - portJSON.IdentificationPrefs.Set("pid", "0x"+port.PID) - portJSON.IdentificationPrefs.Set("vid", "0x"+port.VID) + portJSON.Properties.Set("vid", "0x"+port.VID) + portJSON.Properties.Set("pid", "0x"+port.PID) + portJSON.Properties.Set("serialNumber", port.SerialNumber) } return portJSON } -type messageOutputJSON struct { +type helloMessageJSON struct { + EventType string `json:"eventType"` + ProtocolVersion int `json:"protocolVersion"` + Message string `json:"message"` +} + +type genericMessageJSON struct { EventType string `json:"eventType"` + Error bool `json:"error,omitempty"` Message string `json:"message"` } -func outputMessage(eventType, message string) { - d, err := json.MarshalIndent(&messageOutputJSON{ - EventType: eventType, - Message: message, - }, "", " ") +func output(msg interface{}) { + d, err := json.MarshalIndent(msg, "", " ") if err != nil { - outputError(err) + output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: err.Error(), + }) } else { syncronizedPrintLn(string(d)) } } -func outputError(err error) { - outputMessage("error", err.Error()) -} - var stdoutMutext sync.Mutex func syncronizedPrintLn(a ...interface{}) { diff --git a/sync.go b/sync.go index 5cadc52..272e614 100644 --- a/sync.go +++ b/sync.go @@ -17,18 +17,7 @@ package main -import "encoding/json" - type syncOutputJSON struct { EventType string `json:"eventType"` Port *boardPortJSON `json:"port"` } - -func outputSyncMessage(message *syncOutputJSON) { - d, err := json.MarshalIndent(message, "", " ") - if err != nil { - outputError(err) - } else { - syncronizedPrintLn(string(d)) - } -} diff --git a/sync_darwin.go b/sync_darwin.go index a00614c..1f40547 100644 --- a/sync_darwin.go +++ b/sync_darwin.go @@ -52,8 +52,12 @@ func startSync() (chan<- bool, error) { if err != nil { return nil, err } + output(&genericMessageJSON{ + EventType: "start_sync", + Message: "OK", + }) for _, port := range current { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), }) @@ -100,7 +104,11 @@ func startSync() (chan<- bool, error) { continue } if err != nil { - outputError(fmt.Errorf("error decoding START_SYNC event: %s", err)) + output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: fmt.Sprintf("error decoding START_SYNC event: %s", err), + }) } // if there is an event retry up to 5 times if n > 0 { @@ -115,16 +123,19 @@ func startSync() (chan<- bool, error) { for _, port := range current { if !portListHas(updates, port) { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "remove", - Port: &boardPortJSON{Address: port.Name}, + Port: &boardPortJSON{ + Address: port.Name, + Protocol: "serial", + }, }) } } for _, port := range updates { if !portListHas(current, port) { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), }) diff --git a/sync_linux.go b/sync_linux.go index feac941..d12d7e1 100644 --- a/sync_linux.go +++ b/sync_linux.go @@ -43,9 +43,14 @@ func startSync() (chan<- bool, error) { syncReader.Close() }() + output(&genericMessageJSON{ + EventType: "start_sync", + Message: "OK", + }) + // Ouput initial port state for _, port := range current { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), }) @@ -62,7 +67,12 @@ func startSync() (chan<- bool, error) { for { evt, err := dec.Decode() if err != nil { - outputError(fmt.Errorf("error decoding START_SYNC event: %s", err)) + output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: fmt.Sprintf("error decoding START_SYNC event: %s", err), + }) + // TODO: output "stop" msg? close? return } @@ -77,7 +87,7 @@ func startSync() (chan<- bool, error) { } for _, port := range portList { if port.IsUSB && port.Name == changedPort { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), }) @@ -86,9 +96,12 @@ func startSync() (chan<- bool, error) { } } if evt.Action == "remove" { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "remove", - Port: &boardPortJSON{Address: changedPort}, + Port: &boardPortJSON{ + Address: changedPort, + Protocol: "serial", + }, }) } } diff --git a/sync_windows.go b/sync_windows.go index 2a60a73..cf05cea 100644 --- a/sync_windows.go +++ b/sync_windows.go @@ -117,8 +117,12 @@ func startSync() (chan<- bool, error) { fmt.Println(err) return } + output(&genericMessageJSON{ + EventType: "start_sync", + Message: "OK", + }) for _, port := range current { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), }) @@ -164,16 +168,19 @@ func startSync() (chan<- bool, error) { for _, port := range current { if !portListHas(updates, port) { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "remove", - Port: &boardPortJSON{Address: port.Name}, + Port: &boardPortJSON{ + Address: port.Name, + Protocol: "serial", + }, }) } } for _, port := range updates { if !portListHas(current, port) { - outputSyncMessage(&syncOutputJSON{ + output(&syncOutputJSON{ EventType: "add", Port: newBoardPortJSON(port), })