Skip to content

Commit

Permalink
Optimized upload 1200 bps-touch and fixed some rare corner cases (ard…
Browse files Browse the repository at this point in the history
…uino#1267)

* Refactored upload output and debug info and removed unused functions

* Skip serial port touch if the port is not connected

* Perform touch only if required

* Slightly change test to set serial port properties
  • Loading branch information
cmaglie authored Apr 22, 2021
1 parent 495a5f1 commit 5aa4248
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 85 deletions.
150 changes: 99 additions & 51 deletions arduino/serialutils/serialutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,13 @@
package serialutils

import (
"fmt"
"time"

"github.com/pkg/errors"
"go.bug.st/serial"
)

// Reset a board using the 1200 bps port-touch. If wait is true, it will wait
// for a new port to appear (which could change sometimes) and returns that.
// The error is set if the port listing fails.
func Reset(port string, wait bool) (string, error) {
// Touch port at 1200bps
if err := TouchSerialPortAt1200bps(port); err != nil {
return "", errors.New("1200bps Touch")
}

if wait {
// Wait for port to disappear and reappear
if p, err := WaitForNewSerialPortOrDefaultTo(port); err == nil {
port = p
} else {
return "", errors.WithMessage(err, "detecting upload port")
}
}

return port, nil
}

// TouchSerialPortAt1200bps open and close the serial port at 1200 bps. This
// is used on many Arduino boards as a signal to put the board in "bootloader"
// mode.
Expand Down Expand Up @@ -71,59 +51,127 @@ func TouchSerialPortAt1200bps(port string) error {
return nil
}

// WaitForNewSerialPortOrDefaultTo is meant to be called just after a reset. It watches the ports connected
// to the machine until a port appears. The new appeared port is returned or, if the operation
// timeouts, the default port provided as parameter is returned.
func WaitForNewSerialPortOrDefaultTo(defaultPort string) (string, error) {
if p, err := WaitForNewSerialPort(); err != nil {
return "", errors.WithMessage(err, "detecting upload port")
} else if p != "" {
// on OS X, if the port is opened too quickly after it is detected,
// a "Resource busy" error occurs, add a delay to workaround.
// This apply to other platforms as well.
time.Sleep(500 * time.Millisecond)

return p, nil
func getPortMap() (map[string]bool, error) {
ports, err := serial.GetPortsList()
if err != nil {
return nil, errors.WithMessage(err, "listing serial ports")
}
return defaultPort, nil
res := map[string]bool{}
for _, port := range ports {
res[port] = true
}
return res, nil
}

// WaitForNewSerialPort is meant to be called just after a reset. It watches the ports connected
// to the machine until a port appears. The new appeared port is returned.
func WaitForNewSerialPort() (string, error) {
getPortMap := func() (map[string]bool, error) {
ports, err := serial.GetPortsList()
if err != nil {
return nil, errors.WithMessage(err, "listing serial ports")
}
res := map[string]bool{}
for _, port := range ports {
res[port] = true
}
return res, nil
}
// ResetProgressCallbacks is a struct that defines a bunch of function callback
// to observe the Reset function progress.
type ResetProgressCallbacks struct {
// TouchingPort is called to signal the 1200-bps touch of the reported port
TouchingPort func(port string)
// WaitingForNewSerial is called to signal that we are waiting for a new port
WaitingForNewSerial func()
// BootloaderPortFound is called to signal that the wait is completed and to
// report the port found, or the empty string if no ports have been found and
// the wait has timed-out.
BootloaderPortFound func(port string)
// Debug reports messages useful for debugging purposes. In normal conditions
// these messages should not be displayed to the user.
Debug func(msg string)
}

// Reset a board using the 1200 bps port-touch and wait for new ports.
// Both reset and wait are optional:
// - if port is "" touch will be skipped
// - if wait is false waiting will be skipped
// If wait is true, this function will wait for a new port to appear and returns that
// one, otherwise the empty string is returned if the new port can not be detected or
// if the wait parameter is false.
// The error is set if the port listing fails.
func Reset(portToTouch string, wait bool, cb *ResetProgressCallbacks) (string, error) {
last, err := getPortMap()
if cb != nil && cb.Debug != nil {
cb.Debug(fmt.Sprintf("LAST: %v", last))
}
if err != nil {
return "", err
}

if portToTouch != "" && last[portToTouch] {
if cb != nil && cb.Debug != nil {
cb.Debug(fmt.Sprintf("TOUCH: %v", portToTouch))
}
if cb != nil && cb.TouchingPort != nil {
cb.TouchingPort(portToTouch)
}
if err := TouchSerialPortAt1200bps(portToTouch); err != nil {
fmt.Println("TOUCH: error during reset:", err)
}
}

if !wait {
return "", nil
}
if cb != nil && cb.WaitingForNewSerial != nil {
cb.WaitingForNewSerial()
}

deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
now, err := getPortMap()
if err != nil {
return "", err
}

if cb != nil && cb.Debug != nil {
cb.Debug(fmt.Sprintf("WAIT: %v", now))
}
hasNewPorts := false
for p := range now {
if !last[p] {
return p, nil // Found it!
hasNewPorts = true
break
}
}

if hasNewPorts {
if cb != nil && cb.Debug != nil {
cb.Debug("New ports found!")
}

// on OS X, if the port is opened too quickly after it is detected,
// a "Resource busy" error occurs, add a delay to workaround.
// This apply to other platforms as well.
time.Sleep(time.Second)

// Some boards have a glitch in the bootloader: some user experienced
// the USB serial port appearing and disappearing rapidly before
// settling.
// This check ensure that the port is stable after one second.
check, err := getPortMap()
if err != nil {
return "", err
}
if cb != nil && cb.Debug != nil {
cb.Debug(fmt.Sprintf("CHECK: %v", check))
}
for p := range check {
if !last[p] {
if cb != nil && cb.BootloaderPortFound != nil {
cb.BootloaderPortFound(p)
}
return p, nil // Found it!
}
}
if cb != nil && cb.Debug != nil {
cb.Debug("Port check failed... still waiting")
}
}

last = now
time.Sleep(250 * time.Millisecond)
}

if cb != nil && cb.BootloaderPortFound != nil {
cb.BootloaderPortFound("")
}
return "", nil
}
80 changes: 46 additions & 34 deletions commands/upload/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import (
properties "github.com/arduino/go-properties-orderedmap"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.bug.st/serial"
)

// Upload FIXMEDOC
Expand Down Expand Up @@ -293,46 +292,59 @@ func runProgramAction(pm *packagemanager.PackageManager,
// to set the board in bootloader mode
actualPort := port
if programmer == nil && !burnBootloader {
// Perform reset via 1200bps touch if requested
if uploadProperties.GetBoolean("upload.use_1200bps_touch") {
if port == "" {
outStream.Write([]byte(fmt.Sprintln("Skipping 1200-bps touch reset: no serial port selected!")))
} else {
ports, err := serial.GetPortsList()
if err != nil {
return fmt.Errorf("cannot get serial port list: %s", err)
}
for _, p := range ports {
if p == port {
if verbose {
outStream.Write([]byte(fmt.Sprintf("Performing 1200-bps touch reset on serial port %s", p)))
outStream.Write([]byte(fmt.Sprintln()))
}
logrus.Infof("Touching port %s at 1200bps", port)
if err := serialutils.TouchSerialPortAt1200bps(p); err != nil {
outStream.Write([]byte(fmt.Sprintf("Cannot perform port reset: %s", err)))
outStream.Write([]byte(fmt.Sprintln()))
}
break
// Perform reset via 1200bps touch if requested and wait for upload port if requested.

touch := uploadProperties.GetBoolean("upload.use_1200bps_touch")
wait := uploadProperties.GetBoolean("upload.wait_for_upload_port")
portToTouch := ""
if touch {
portToTouch = port
}

// if touch is requested but port is not specified, print a warning
if touch && portToTouch == "" {
outStream.Write([]byte(fmt.Sprintln("Skipping 1200-bps touch reset: no serial port selected!")))
}

var cb *serialutils.ResetProgressCallbacks
if verbose {
cb = &serialutils.ResetProgressCallbacks{
TouchingPort: func(port string) {
logrus.WithField("phase", "board reset").Infof("Performing 1200-bps touch reset on serial port %s", port)
outStream.Write([]byte(fmt.Sprintf("Performing 1200-bps touch reset on serial port %s", port)))
outStream.Write([]byte(fmt.Sprintln()))
},
WaitingForNewSerial: func() {
logrus.WithField("phase", "board reset").Info("Waiting for upload port...")
outStream.Write([]byte(fmt.Sprintln("Waiting for upload port...")))
},
BootloaderPortFound: func(port string) {
if port != "" {
logrus.WithField("phase", "board reset").Infof("Upload port found on %s", port)
outStream.Write([]byte(fmt.Sprintf("Upload port found on %s", port)))
outStream.Write([]byte(fmt.Sprintln()))
} else {
logrus.WithField("phase", "board reset").Infof("No upload port found, using %s as fallback", actualPort)
outStream.Write([]byte(fmt.Sprintf("No upload port found, using %s as fallback", actualPort)))
outStream.Write([]byte(fmt.Sprintln()))
}
}
},
Debug: func(msg string) {
logrus.WithField("phase", "board reset").Debug(msg)
},
}
}

// Wait for upload port if requested
if uploadProperties.GetBoolean("upload.wait_for_upload_port") {
if verbose {
outStream.Write([]byte(fmt.Sprintln("Waiting for upload port...")))
}

actualPort, err = serialutils.WaitForNewSerialPortOrDefaultTo(actualPort)
if err != nil {
return errors.WithMessage(err, "detecting serial port")
if newPort, err := serialutils.Reset(portToTouch, wait, cb); err != nil {
outStream.Write([]byte(fmt.Sprintf("Cannot perform port reset: %s", err)))
outStream.Write([]byte(fmt.Sprintln()))
} else {
if newPort != "" {
actualPort = newPort
}
}
}

if port != "" {
if actualPort != "" {
// Set serial port property
uploadProperties.Set("serial.port", actualPort)
if strings.HasPrefix(actualPort, "/dev/") {
Expand Down

0 comments on commit 5aa4248

Please sign in to comment.