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

Optimized 1200-bps touch and upload port detection. Fixed some rare upload-fail corner cases. #1267

Merged
merged 5 commits into from
Apr 22, 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
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