Skip to content

Commit

Permalink
Merge pull request #84 from benlye/lpc1768
Browse files Browse the repository at this point in the history
* Adds support for LPC1768 boards (MKS SBASE, SKR, Re-ARM, etc.)
* Adds option for pre-flash and post-flash system commands
  • Loading branch information
benlye authored Apr 26, 2019
2 parents 22b1693 + 03316f4 commit 4569003
Show file tree
Hide file tree
Showing 5 changed files with 464 additions and 32 deletions.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ This plugin can be used to flash pre-compiled firmware images to your printer fr

<p align="center"><img alt="Firmware Updater" src="extras/img/firmware-updater.png"></p>

Works with boards with Atmel AVR family 8-bit MCUs (Atmega1280, Atmega1284p, and Atmega2560) MCUs, and Atmel SAM family 32-bit MCUs (Arduino DUE).
## Works with
* Atmel AVR family 8-bit MCUs (Atmega644, Atmega1280, Atmega1284p, and Atmega2560, etc.)
* Atmel SAM family 32-bit MCUs (Arduino DUE, etc.)
* LPC1768 boards (MKS SBASE, SKR v1.1 and v1.3, etc.)

## Setup

Expand Down Expand Up @@ -51,6 +54,53 @@ cd BOSSA-1.7.0
sudo cp ~/BOSSA-1.7.0/bin/bossac /usr/local/bin/
```

### LPC 1768 Installation
Flashing an LPC1768 board requires that the host can mount the board's on-board SD card to a known mount point in the host filesystem.

There are several ways to do this, but using [usbmount](https://github.com/rbrito/usbmount) works well and is documented here. It will mount the SD card to `/media/usb`.

**Note:** The Marlin board configuration must have `USB_SD_ONBOARD` enabled so that the on-board SD card is presented to the host via the USB connection. This seems to be the default configuration for Marlin's LPC1768 boards. It is configured in the board's pins file.

Once installed, usbmount requires some tweaking to make it work well on the Raspberry Pi. The instructions below assume that you are running OctoPrint on a Raspberry Pi, as the user 'pi'.

1. Install usbmount

`sudo apt-get install usbmount`

2. Configure usbmount so that the mount has the correct permissions for the 'pi' user

`sudo nano /etc/usbmount/usbmount.conf`

Find FS_MOUNTOPTIONS and change it to:

`FS_MOUNTOPTIONS="-fstype=vfat,gid=pi,uid=pi,dmask=0022,fmask=0111`

3. Configure systemd-udevd so that the mount is accessible

`sudo systemctl edit systemd-udevd`

Insert these lines then save and close the file:
```
[Service]
MountFlags=shared
```

Then run:
```
sudo systemctl daemon-reload
sudo service systemd-udevd --full-restart
```

Once usbmount is installed and configured the LPC1768 on-board SD card should be mounted at `/media/usb` the next time it is plugged in or restarted.

#### Troubleshooting LPC1768 Uploads
The firmware upload will fail if the SD card is not accessible, either because it is not mounted on the host, or because the printer firmware has control over it.

Try:
* Reset the board
* Check that the 'Path to firmware folder' 'Test' button gives a successful result
* Use the OctoPrint terminal to send an `M22` command to release the SD card from the firmware

## Configuration

In order to be able to flash firmware we need to select and configure a flash method. Once the flash method is selected additional options will be available.
Expand All @@ -75,6 +125,9 @@ Typical MCU/programmer combinations are:
<p align="center"><img alt="Firmware Updater Settings" src="extras/img/bossac-config.png"></p>
The only required setting is the path to the bossac binary.

### LPC1768 Configuration
The only required setting is the path to the firmware update folder. If using usbmount it will probably be `/media/usb`.

### Customizing the Command Lines
The command lines for avrdude and bossac can be customized by editing the string in the advanced settings for the flash method. Text in braces (`{}`) will be substituted for preconfigured values if present.

Expand Down
235 changes: 233 additions & 2 deletions octoprint_firmwareupdater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import time
import re
import serial
import shutil
from serial import SerialException

import octoprint.plugin
Expand Down Expand Up @@ -49,8 +50,8 @@ def __init__(self):
def initialize(self):
# TODO: make method configurable via new plugin hook "octoprint.plugin.firmwareupdater.flash_methods",
# also include prechecks
self._flash_prechecks = dict(avrdude=self._check_avrdude, bossac=self._check_bossac)
self._flash_methods = dict(avrdude=self._flash_avrdude, bossac=self._flash_bossac)
self._flash_prechecks = dict(avrdude=self._check_avrdude, bossac=self._check_bossac, lpc1768=self._check_lpc1768)
self._flash_methods = dict(avrdude=self._flash_avrdude, bossac=self._flash_bossac, lpc1768=self._flash_lpc1768)

console_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="console"), maxBytes=2*1024*1024)
console_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
Expand Down Expand Up @@ -193,6 +194,18 @@ def _start_flash_process(self, method, hex_file, printer_port):
return True

def _flash_worker(self, method, firmware, printer_port):
# Run pre-flash commandline here
preflash_command = self._settings.get(["preflash_commandline"])
if preflash_command is not None and self._settings.get_boolean(["enable_preflash_commandline"]):
self._logger.info("Executing pre-flash commandline '{}'".format(preflash_command))
try:
r = os.system(preflash_command)
except:
e = sys.exc_info()[0]
self._logger.error("Error executing pre-flash commandline '{}'".format(preflash_command))

self._logger.info("Pre-flash command '{}' returned: {}".format(preflash_command, r))

try:
self._logger.info("Firmware update started")

Expand Down Expand Up @@ -230,6 +243,18 @@ def _flash_worker(self, method, firmware, printer_port):
self._console_logger.info(message)
self._send_status("success")

# Run post-flash commandline here
postflash_command = self._settings.get(["postflash_commandline"])
if postflash_command is not None and self._settings.get_boolean(["enable_postflash_commandline"]):
self._logger.info("Executing post-flash commandline '{}'".format(postflash_command))
try:
r = os.system(postflash_command)
except:
e = sys.exc_info()[0]
self._logger.error("Error executing post-flash commandline '{}'".format(postflash_command))

self._logger.info("Post-flash command '{}' returned: {}".format(postflash_command, r))

postflash_gcode = self._settings.get(["postflash_gcode"])
if postflash_gcode is not None and self._settings.get_boolean(["enable_postflash_gcode"]):
self._logger.info(u"Setting run_postflash_gcode flag to true")
Expand Down Expand Up @@ -476,6 +501,82 @@ def _check_bossac(self):
else:
return True

def _flash_lpc1768(self, firmware=None, printer_port=None):
assert(firmware is not None)
assert(printer_port is not None)

lpc1768_path = self._settings.get(["lpc1768_path"])
working_dir = os.path.dirname(lpc1768_path)

if self._settings.get_boolean(["lpc1768_preflashreset"]):
self._send_status("progress", subtype="boardreset")
self._logger.info(u"Pre-flash reset: attempting to reset the board")
if not self._reset_lpc1768(printer_port):
self._logger.error(u"Reset failed")
return False

# Release the SD card
if not self._unmount_sd(printer_port):
self._send_status("flasherror", message="Unable to unmount SD card")
return False

# loop until the mount is available; timeout after 60s
count = 1
timeout = 60
interval = 1
sdstarttime = time.time()
self._logger.info(u"Waiting for SD card to be avaialble at '{}'".format(lpc1768_path))
self._send_status("progress", subtype="waitforsd")
while (time.time() < (sdstarttime + timeout) and not os.access(lpc1768_path, os.W_OK)):
self._logger.debug(u"Waiting for firmware folder path to become available [{}/{}]".format(count, int(timeout / interval)))
count = count + 1
time.sleep(interval)

if not os.access(lpc1768_path, os.W_OK):
self._send_status("flasherror", message="Unable to access firmware folder")
self._logger.error(u"Firmware folder path is not writeable: {path}".format(path=lpc1768_path))
return False

self._logger.info(u"Firmware update folder '{}' available for writing after {} seconds".format(lpc1768_path, round((time.time() - sdstarttime),0)))

target_path = lpc1768_path + '/firmware.bin'
self._logger.info(u"Copying firmware to update folder '{}' -> '{}'".format(firmware, target_path))

self._send_status("progress", subtype="writing")

try:
shutil.copyfile(firmware, target_path)
except:
self._logger.exception(u"Flashing failed. Unable to copy file.")
self._send_status("flasherror")
return False

self._logger.info(u"Firmware update reset: attempting to reset the board")
if not self._reset_lpc1768(printer_port):
self._logger.error(u"Reset failed")
return False

return True

def _check_lpc1768(self):
lpc1768_path = self._settings.get(["lpc1768_path"])
pattern = re.compile("^(\/[^\0/]+)+$")

if not pattern.match(lpc1768_path):
self._logger.error(u"Firmware folder path is not valid: {path}".format(path=lpc1768_path))
return False
elif lpc1768_path is None:
self._logger.error(u"Firmware folder path is not set.")
return False
if not os.path.exists(lpc1768_path):
self._logger.error(u"Firmware folder path does not exist: {path}".format(path=lpc1768_path))
return False
elif not os.path.isdir(lpc1768_path):
self._logger.error(u"Firmware folder path is not a folder: {path}".format(path=lpc1768_path))
return False
else:
return True

def _reset_1200(self, printer_port=None):
assert(printer_port is not None)
self._logger.info(u"Toggling '{port}' at 1200bps".format(port=printer_port))
Expand All @@ -496,6 +597,130 @@ def _reset_1200(self, printer_port=None):

return True

def _reset_lpc1768(self, printer_port=None):
assert(printer_port is not None)
self._logger.info(u"Resetting LPC1768 at '{port}'".format(port=printer_port))
try:
ser = serial.Serial(port=printer_port, \
baudrate=9600, \
parity=serial.PARITY_NONE, \
stopbits=serial.STOPBITS_ONE , \
bytesize=serial.EIGHTBITS, \
timeout=2000)

# Marlin reset command
ser.write("M997\r")
# Smoothie reset command
ser.write("reset\r")

ser.close()

except SerialException as ex:
self._logger.exception(u"Board reset failed: {error}".format(error=str(ex)))
self._send_status("flasherror", message="Board reset failed")
return False

if self._wait_for_lpc1768(printer_port):
return True
else:
self._logger.error(u"Board reset failed")
self._send_status("flasherror", message="Board reset failed")
return False

def _wait_for_lpc1768(self, printer_port=None):
assert(printer_port is not None)
self._logger.info(u"Waiting for LPC1768 at '{port}' to reset".format(port=printer_port))

start = time.time()
timeout = 10
interval = 0.2
count = 1
connected = True

loopstarttime = time.time()

while (time.time() < (loopstarttime + timeout) and connected):
self._logger.debug(u"Waiting for reset to init [{}/{}]".format(count, int(timeout / interval)))
count = count + 1
try:
ser = serial.Serial(port=printer_port, \
baudrate=9600, \
parity=serial.PARITY_NONE, \
stopbits=serial.STOPBITS_ONE , \
bytesize=serial.EIGHTBITS, \
timeout=2000)

ser.close()
connected = True
time.sleep(interval)

except SerialException as ex:
time.sleep(interval)
connected = False

if connected:
self._logger.error(u"Timeout waiting for board reset to init")
return False

self._logger.info(u"LPC1768 at '{port}' is resetting".format(port=printer_port))

time.sleep(3)

timeout = 20
interval = 0.2
count = 1
connected = False

loopstarttime = time.time()
while (time.time() < (loopstarttime + timeout) and not connected):
self._logger.debug(u"Waiting for reset to complete [{}/{}]".format(count, int(timeout / interval)))
count = count + 1
try:
ser = serial.Serial(port=printer_port, \
baudrate=9600, \
parity=serial.PARITY_NONE, \
stopbits=serial.STOPBITS_ONE , \
bytesize=serial.EIGHTBITS, \
timeout=2000)

ser.close()
connected = True
time.sleep(interval)

except SerialException as ex:
time.sleep(interval)
connected = False

if not connected:
self._logger.error(u"Timeout waiting for board reset to complete")
return False

end = time.time()
self._logger.info(u"LPC1768 at '{port}' reset in {duration} seconds".format(port=printer_port, duration=(round((end - start),2))))
return True

def _unmount_sd(self, printer_port=None):
assert(printer_port is not None)
self._logger.info(u"Release the firmware lock on the SD Card by sending 'M22' to '{port}'".format(port=printer_port))
try:
ser = serial.Serial(port=printer_port, \
baudrate=9600, \
parity=serial.PARITY_NONE, \
stopbits=serial.STOPBITS_ONE , \
bytesize=serial.EIGHTBITS, \
timeout=2000)

ser.write("M22\r")
time.sleep(1)
ser.close()

except SerialException as ex:
self._logger.exception(u"Card unmount failed: {error}".format(error=str(ex)))
self._send_status("flasherror", message="Card unmount failed")
return False

return True

#~~ SettingsPlugin API

def get_settings_defaults(self):
Expand All @@ -511,9 +736,15 @@ def get_settings_defaults(self):
"bossac_path": None,
"bossac_commandline": "{bossac} -i -p {port} -U true -e -w {disableverify} -b {firmware} -R",
"bossac_disableverify": None,
"lpc1768_path": None,
"lpc1768_preflashreset": True,
"postflash_delay": "0",
"postflash_gcode": None,
"run_postflash_gcode": False,
"preflash_commandline": None,
"postflash_commandline": None,
"enable_preflash_commandline": None,
"enable_postflash_commandline": None,
"enable_postflash_delay": None,
"enable_postflash_gcode": None,
"disable_bootloadercheck": None
Expand Down
Loading

0 comments on commit 4569003

Please sign in to comment.