From 803be7e96c7c117193cbd66d1b83750d5f511301 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Thu, 24 Sep 2020 16:23:19 +0200 Subject: [PATCH] Factoring out serial subrotuines to perform board reset So they can be reused in other projects. --- arduino/serialutils/serialutils.go | 129 +++++++++++++++++++++++++++++ commands/upload/upload.go | 85 ++----------------- 2 files changed, 136 insertions(+), 78 deletions(-) create mode 100644 arduino/serialutils/serialutils.go diff --git a/arduino/serialutils/serialutils.go b/arduino/serialutils/serialutils.go new file mode 100644 index 00000000000..e45fe130ce6 --- /dev/null +++ b/arduino/serialutils/serialutils.go @@ -0,0 +1,129 @@ +// This file is part of arduino-cli +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package serialutils + +import ( + "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. +func TouchSerialPortAt1200bps(port string) error { + // Open port + p, err := serial.Open(port, &serial.Mode{BaudRate: 1200}) + if err != nil { + return errors.WithMessage(err, "opening port at 1200bps") + } + + // Set DTR to false + if err = p.SetDTR(false); err != nil { + p.Close() + return errors.WithMessage(err, "setting DTR to OFF") + } + + // Close serial port + p.Close() + + // Scanning for available ports seems to open the port or + // otherwise assert DTR, which would cancel the WDT reset if + // it happens within 250 ms. So we wait until the reset should + // have already occurred before going on. + time.Sleep(500 * time.Millisecond) + + 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 + } + return defaultPort, 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 + } + + last, err := getPortMap() + if err != nil { + return "", err + } + + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + now, err := getPortMap() + if err != nil { + return "", err + } + + for p := range now { + if !last[p] { + return p, nil // Found it! + } + } + + last = now + time.Sleep(250 * time.Millisecond) + } + + return "", nil +} diff --git a/commands/upload/upload.go b/commands/upload/upload.go index 1e1dc9701e6..a26bdb6ceff 100644 --- a/commands/upload/upload.go +++ b/commands/upload/upload.go @@ -22,12 +22,11 @@ import ( "net/url" "path/filepath" "strings" - "time" "github.com/arduino/arduino-cli/arduino/cores" "github.com/arduino/arduino-cli/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/arduino/serialutils" "github.com/arduino/arduino-cli/arduino/sketches" - "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cli/commands" "github.com/arduino/arduino-cli/executils" rpc "github.com/arduino/arduino-cli/rpc/commands" @@ -275,19 +274,14 @@ func runProgramAction(pm *packagemanager.PackageManager, outStream.Write([]byte(fmt.Sprintf("Performing 1200-bps touch reset on serial port %s", p))) outStream.Write([]byte(fmt.Sprintln())) } - if err := touchSerialPortAt1200bps(p); err != nil { + 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 } } - - // Scanning for available ports seems to open the port or - // otherwise assert DTR, which would cancel the WDT reset if - // it happened within 250 ms. So we wait until the reset should - // have already occurred before we start scanning. - time.Sleep(500 * time.Millisecond) } // Wait for upload port if requested @@ -295,18 +289,11 @@ func runProgramAction(pm *packagemanager.PackageManager, if verbose { outStream.Write([]byte(fmt.Sprintln("Waiting for upload port..."))) } - if p, err := waitForNewSerialPort(); err != nil { - return fmt.Errorf("cannot detect serial ports: %s", err) - } else if p == "" { - feedback.Print("No new serial port detected.") - } else { - actualPort = 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) + actualPort, err = serialutils.WaitForNewSerialPortOrDefaultTo(actualPort) + if err != nil { + return errors.WithMessage(err, "detecting serial port") + } } } @@ -382,64 +369,6 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri return nil } -func touchSerialPortAt1200bps(port string) error { - logrus.Infof("Touching port %s at 1200bps", port) - - // Open port - p, err := serial.Open(port, &serial.Mode{BaudRate: 1200}) - if err != nil { - return fmt.Errorf("opening port: %s", err) - } - defer p.Close() - - if err = p.SetDTR(false); err != nil { - return fmt.Errorf("cannot set DTR") - } - return 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) { - logrus.Infof("Waiting for upload port...") - - getPortMap := func() (map[string]bool, error) { - ports, err := serial.GetPortsList() - if err != nil { - return nil, err - } - res := map[string]bool{} - for _, port := range ports { - res[port] = true - } - return res, nil - } - - last, err := getPortMap() - if err != nil { - return "", fmt.Errorf("scanning serial port: %s", err) - } - - deadline := time.Now().Add(10 * time.Second) - for time.Now().Before(deadline) { - now, err := getPortMap() - if err != nil { - return "", fmt.Errorf("scanning serial port: %s", err) - } - - for p := range now { - if !last[p] { - return p, nil // Found it! - } - } - - last = now - time.Sleep(250 * time.Millisecond) - } - - return "", nil -} - func determineBuildPathAndSketchName(importFile, importDir string, sketch *sketches.Sketch, fqbn *cores.FQBN) (*paths.Path, string, error) { // In general, compiling a sketch will produce a set of files that are // named as the sketch but have different extensions, for example Sketch.ino