diff --git a/README.md b/README.md index 89f0081..39a03aa 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ This plugin can be used to flash pre-compiled firmware images to your printer fr
-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 @@ -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. @@ -75,6 +125,9 @@ Typical MCU/programmer combinations are: 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. diff --git a/octoprint_firmwareupdater/__init__.py b/octoprint_firmwareupdater/__init__.py index a262214..8cbcb5f 100644 --- a/octoprint_firmwareupdater/__init__.py +++ b/octoprint_firmwareupdater/__init__.py @@ -11,6 +11,7 @@ import time import re import serial +import shutil from serial import SerialException import octoprint.plugin @@ -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")) @@ -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") @@ -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") @@ -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)) @@ -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): @@ -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 diff --git a/octoprint_firmwareupdater/static/js/firmwareupdater.js b/octoprint_firmwareupdater/static/js/firmwareupdater.js index 2eea835..eb36940 100644 --- a/octoprint_firmwareupdater/static/js/firmwareupdater.js +++ b/octoprint_firmwareupdater/static/js/firmwareupdater.js @@ -12,12 +12,17 @@ $(function() { self.showAdvancedConfig = ko.observable(false); self.showAvrdudeConfig = ko.observable(false); self.showBossacConfig = ko.observable(false); + self.showLpc1768Config = ko.observable(false); self.showPostflashConfig = ko.observable(false); self.configEnablePostflashDelay = ko.observable(); self.configPostflashDelay = ko.observable(); self.configEnablePostflashGcode = ko.observable(); self.configPostflashGcode = ko.observable(); self.configDisableBootloaderCheck = ko.observable(); + self.configEnablePreflashCommandline = ko.observable(); + self.configPreflashCommandline = ko.observable(); + self.configEnablePostflashCommandline = ko.observable(); + self.configPostflashCommandline = ko.observable(); // Config settings for avrdude self.configAvrdudeMcu = ko.observable(); @@ -53,6 +58,17 @@ $(function() { return self.bossacPathBroken() || self.bossacPathOk(); }); + // Config settings for lpc1768 + self.configLpc1768Path = ko.observable(); + self.configLpc1768ResetBeforeFlash = ko.observable(); + + self.lpc1768PathBroken = ko.observable(false); + self.lpc1768PathOk = ko.observable(false); + self.lpc1768PathText = ko.observable(); + self.lpc1768PathHelpVisible = ko.computed(function() { + return self.lpc1768PathBroken() || self.lpc1768PathOk(); + }); + self.flashPort = ko.observable(undefined); self.firmwareFileName = ko.observable(undefined); @@ -87,14 +103,21 @@ $(function() { self.configFlashMethod.subscribe(function(value) { if(value == 'avrdude') { - self.showBossacConfig(false); self.showAvrdudeConfig(true); + self.showBossacConfig(false); + self.showLpc1768Config(false); } else if(value == 'bossac') { + self.showAvrdudeConfig(false); self.showBossacConfig(true); + self.showLpc1768Config(false); + } else if(value == 'lpc1768'){ self.showAvrdudeConfig(false); - } else { self.showBossacConfig(false); + self.showLpc1768Config(true); + } else { self.showAvrdudeConfig(false); + self.showBossacConfig(false); + self.showLpc1768Config(false); } }); @@ -156,6 +179,10 @@ $(function() { alert = gettext("The bossac path is not configured."); } + if (self.settingsViewModel.settings.plugins.firmwareupdater.flash_method() == "lpc1768" && !self.settingsViewModel.settings.plugins.firmwareupdater.lpc1768_path()) { + alert = gettext("The lpc1768 firmware folder path is not configured."); + } + if (!self.flashPort()) { alert = gettext("The printer port is not selected."); } @@ -298,6 +325,10 @@ $(function() { message = gettext("Starting flash..."); break; } + case "waitforsd": { + message = gettext("Waiting for SD card to mount on host..."); + break; + } case "writing": { message = gettext("Writing memory..."); break; @@ -314,6 +345,10 @@ $(function() { message = gettext("Post-flash delay..."); break; } + case "boardreset": { + message = gettext("Resetting the board..."); + break; + } case "reconnecting": { message = gettext("Reconnecting to printer..."); break; @@ -338,15 +373,28 @@ $(function() { self.showPluginConfig = function() { // Load the general settings + self.configFlashMethod(self.settingsViewModel.settings.plugins.firmwareupdater.flash_method()); + self.configPreflashCommandline(self.settingsViewModel.settings.plugins.firmwareupdater.preflash_commandline()); + self.configPostflashCommandline(self.settingsViewModel.settings.plugins.firmwareupdater.postflash_commandline()); self.configPostflashDelay(self.settingsViewModel.settings.plugins.firmwareupdater.postflash_delay()); + self.configPostflashGcode(self.settingsViewModel.settings.plugins.firmwareupdater.postflash_gcode()); + + if(self.settingsViewModel.settings.plugins.firmwareupdater.enable_preflash_commandline() != 'false') { + self.configEnablePreflashCommandline(self.settingsViewModel.settings.plugins.firmwareupdater.enable_preflash_commandline()); + } + + if(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_commandline() != 'false') { + self.configEnablePostflashCommandline(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_commandline()); + } + if(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_delay() != 'false') { self.configEnablePostflashDelay(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_delay()); } - self.configFlashMethod(self.settingsViewModel.settings.plugins.firmwareupdater.flash_method()); + if(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_gcode() != 'false') { self.configEnablePostflashGcode(self.settingsViewModel.settings.plugins.firmwareupdater.enable_postflash_gcode()); } - self.configPostflashGcode(self.settingsViewModel.settings.plugins.firmwareupdater.postflash_gcode()); + if(self.settingsViewModel.settings.plugins.firmwareupdater.disable_bootloadercheck() != 'false') { self.configDisableBootloaderCheck(self.settingsViewModel.settings.plugins.firmwareupdater.disable_bootloadercheck()); } @@ -366,6 +414,12 @@ $(function() { self.configBossacPath(self.settingsViewModel.settings.plugins.firmwareupdater.bossac_path()); self.configBossacDisableVerification(self.settingsViewModel.settings.plugins.firmwareupdater.bossac_disableverify()); self.configBossacCommandLine(self.settingsViewModel.settings.plugins.firmwareupdater.bossac_commandline()); + + // Load the lpc1768 settings + self.configLpc1768Path(self.settingsViewModel.settings.plugins.firmwareupdater.lpc1768_path()); + if(self.settingsViewModel.settings.plugins.firmwareupdater.lpc1768_preflashreset() != 'false') { + self.configLpc1768ResetBeforeFlash(self.settingsViewModel.settings.plugins.firmwareupdater.lpc1768_preflashreset()); + } self.configurationDialog.modal(); }; @@ -392,6 +446,12 @@ $(function() { bossac_path: self.configBossacPath(), bossac_disableverify: self.configBossacDisableVerification(), bossac_commandline: self.configBossacCommandLine(), + lpc1768_path: self.configLpc1768Path(), + lpc1768_preflashreset: self.configLpc1768ResetBeforeFlash(), + enable_preflash_commandline: self.configEnablePreflashCommandline(), + preflash_commandline: self.configPreflashCommandline(), + enable_postflash_commandline: self.configEnablePostflashCommandline(), + postflash_commandline: self.configPostflashCommandline(), postflash_delay: self.configPostflashDelay(), postflash_gcode: self.configPostflashGcode(), enable_postflash_delay: self.configEnablePostflashDelay(), @@ -526,6 +586,37 @@ $(function() { }) }; + self.testLpc1768Path = function() { + $.ajax({ + url: API_BASEURL + "util/test", + type: "POST", + dataType: "json", + data: JSON.stringify({ + command: "path", + path: self.configLpc1768Path(), + check_type: "path", + check_access: ["r", "w"], + check_writable_dir: "true" + }), + contentType: "application/json; charset=UTF-8", + success: function(response) { + if (!response.result) { + if (!response.exists) { + self.lpc1768PathText(gettext("The path doesn't exist")); + } else if (!response.typeok) { + self.lpc1768PathText(gettext("The path is not a folder")); + } else if (!response.access) { + self.lpc1768PathText(gettext("The path is not writeable")); + } + } else { + self.lpc1768PathText(gettext("The path is valid")); + } + self.lpc1768PathOk(response.result); + self.lpc1768PathBroken(!response.result); + } + }) + }; + self.onSettingsShown = function() { self.inSettingsDialog = true; }; diff --git a/octoprint_firmwareupdater/templates/firmwareupdater_settings.jinja2 b/octoprint_firmwareupdater/templates/firmwareupdater_settings.jinja2 index 4f9e0cb..1969e55 100644 --- a/octoprint_firmwareupdater/templates/firmwareupdater_settings.jinja2 +++ b/octoprint_firmwareupdater/templates/firmwareupdater_settings.jinja2 @@ -88,6 +88,7 @@ + @@ -150,9 +151,25 @@ + +