Skip to content

Commit

Permalink
Merge pull request #195 from benlye/marlin-bft
Browse files Browse the repository at this point in the history
Add Binary File Transfer support
  • Loading branch information
benlye authored Feb 6, 2021
2 parents 03d9426 + 055163d commit 0e90022
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 126 deletions.
47 changes: 42 additions & 5 deletions octoprint_firmwareupdater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from octoprint_firmwareupdater.methods import dfuprog
from octoprint_firmwareupdater.methods import lpc1768
from octoprint_firmwareupdater.methods import stm32flash
from octoprint_firmwareupdater.methods import marlinbft

class FirmwareupdaterPlugin(octoprint.plugin.BlueprintPlugin,
octoprint.plugin.TemplatePlugin,
Expand All @@ -40,8 +41,22 @@ 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=avrdude._check_avrdude, bossac=bossac._check_bossac, dfuprogrammer=dfuprog._check_dfuprog, lpc1768=lpc1768._check_lpc1768, stm32flash=stm32flash._check_stm32flash)
self._flash_methods = dict(avrdude=avrdude._flash_avrdude, bossac=bossac._flash_bossac, dfuprogrammer=dfuprog._flash_dfuprog, lpc1768=lpc1768._flash_lpc1768, stm32flash=stm32flash._flash_stm32flash)
self._flash_prechecks = dict(
avrdude=avrdude._check_avrdude,
bossac=bossac._check_bossac,
dfuprogrammer=dfuprog._check_dfuprog,
lpc1768=lpc1768._check_lpc1768,
stm32flash=stm32flash._check_stm32flash,
marlinbft=marlinbft._check_marlinbft
)
self._flash_methods = dict(
avrdude=avrdude._flash_avrdude,
bossac=bossac._flash_bossac,
dfuprogrammer=dfuprog._flash_dfuprog,
lpc1768=lpc1768._flash_lpc1768,
stm32flash=stm32flash._flash_stm32flash,
marlinbft=marlinbft._flash_marlinbft
)

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 @@ -101,8 +116,11 @@ def flash_firmware(self):

if method in self._flash_prechecks:
if not self._flash_prechecks[method](self):
error_message = "Cannot flash firmware, flash method {} is not fully configured".format(method)
self._send_status("flasherror", subtype="method", message=error_message)
if method == "marlinbft":
error_message = "Marlin BINARY_FILE_TRANSFER capability is not supported"
else:
error_message = "Cannot flash firmware, flash method {} is not fully configured".format(method)
self._send_status("flasherror", subtype="method", message=error_message)
return flask.make_response(error_message, 400)

file_to_flash = None
Expand Down Expand Up @@ -324,6 +342,10 @@ def get_settings_defaults(self):
"lpc1768_path": None,
"lpc1768_unmount_command": "sudo umount {mountpoint}",
"lpc1768_preflashreset": True,
"marlinbft_waitafterconnect": 0,
"marlinbft_timeout": 1000,
"marlinbft_progresslogging": False,
"marlinbft_hascapability": False,
"postflash_delay": "0",
"preflash_delay": "3",
"postflash_gcode": None,
Expand All @@ -350,6 +372,11 @@ def get_assets(self):

#~~ Extra methods

def _send_capability(self, capability, enabled):
self._plugin_manager.send_plugin_message(self._identifier, dict(type="capability",
capability=capability,
enabled=enabled))

def _send_status(self, status, subtype=None, message=None):
self._plugin_manager.send_plugin_message(self._identifier, dict(type="status",
status=status,
Expand All @@ -358,6 +385,15 @@ def _send_status(self, status, subtype=None, message=None):

#~~ Hooks

##~~ capabilites hook
def firmware_capability_hook(self, comm_instance, capability, enabled, already_defined):
del comm_instance, already_defined
if capability.lower() == "BINARY_FILE_TRANSFER".lower():
self._logger.info("Setting BINARY_FILE_TRANSFER capability to %s" % (enabled))
self._settings.set_boolean(["marlinbft_hascapability"], enabled)
self._settings.save()
self._send_capability("BINARY_FILE_TRANSFER", enabled)

def bodysize_hook(self, current_max_body_sizes, *args, **kwargs):
return [("POST", r"/flash", 1000 * 1024)]

Expand Down Expand Up @@ -415,5 +451,6 @@ def __plugin_load__():

__plugin_hooks__ = {
"octoprint.server.http.bodysize": __plugin_implementation__.bodysize_hook,
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.update_hook
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.update_hook,
"octoprint.comm.protocol.firmware.capabilities": __plugin_implementation__.firmware_capability_hook
}
2 changes: 1 addition & 1 deletion octoprint_firmwareupdater/methods/avrdude.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _check_avrdude(self):
else:
return True

def _flash_avrdude(self, firmware=None, printer_port=None):
def _flash_avrdude(self, firmware=None, printer_port=None, **kwargs):
assert(firmware is not None)
assert(printer_port is not None)

Expand Down
2 changes: 1 addition & 1 deletion octoprint_firmwareupdater/methods/bossac.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _check_bossac(self):
else:
return True

def _flash_bossac(self, firmware=None, printer_port=None):
def _flash_bossac(self, firmware=None, printer_port=None, **kwargs):
assert(firmware is not None)
assert(printer_port is not None)

Expand Down
2 changes: 1 addition & 1 deletion octoprint_firmwareupdater/methods/dfuprog.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def _check_dfuprog(self):
else:
return True

def _flash_dfuprog(self, firmware=None, printer_port=None):
def _flash_dfuprog(self, firmware=None, printer_port=None, **kwargs):
assert(firmware is not None)

if not _erase_dfuprog(self):
Expand Down
2 changes: 1 addition & 1 deletion octoprint_firmwareupdater/methods/lpc1768.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def _check_lpc1768(self):
else:
return True

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

Expand Down
196 changes: 196 additions & 0 deletions octoprint_firmwareupdater/methods/marlinbft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import os
import time
import shutil
import subprocess
import sys
import binproto2 as mbp

current_baudrate = None

def _check_marlinbft(self):
self._logger.info("Marlin BINARY_FILE_TRANSFER capability is %s" % (self._settings.get_boolean(["marlinbft_hascapability"])))
if not self._settings.get_boolean(["marlinbft_hascapability"]):
self._logger.error("Marlin BINARY_FILE_TRANSFER capability is not supported")
self._send_status("flasherror", subtype="nobftcap")
return False
elif not self._printer.is_operational():
self._logger.error("Printer is not connected")
self._send_status("flasherror", subtype="notconnected")
return False
else:
global current_baudrate
_, current_port, current_baudrate, current_profile = self._printer.get_current_connection()
return True

def _flash_marlinbft(self, firmware=None, printer_port=None, **kwargs):
assert(firmware is not None)
assert(printer_port is not None)
assert(current_baudrate is not None)

# Get the settings
bft_waitafterconnect = self._settings.get_int(["marlinbft_waitafterconnect"])
bft_timeout = self._settings.get_int(["marlinbft_timeout"])
bft_verbose = self._settings.get_boolean(["marlinbft_progresslogging"])

# Loggging
if bft_verbose:
transfer_logger = self._logger
else:
transfer_logger = None

try:
# Open the binary protocol connection
self._logger.info("Current Baud: %s" % current_baudrate)

self._logger.info(u"Initializing Marlin BFT protocol")
self._send_status("progress", subtype="bftinit")
protocol = mbp.Protocol(printer_port, current_baudrate, 512, bft_timeout, self._logger)

# Wait after connect protocol
if bft_waitafterconnect > 0:
self._logger.info("waiting %ss after protocol connect" % bft_waitafterconnect)
time.sleep(bft_waitafterconnect)

# Make sure temperature auto-reporting is disabled
protocol.send_ascii("M155 S0")

# Connect
self._logger.info(u"Connecting to printer at '{}' using Marlin BFT protocol".format(printer_port))
self._send_status("progress", subtype="bftconnect")
protocol.connect()

# Copy the file
self._logger.info(u"Transfering file to printer using Marlin BFT '{}' -> /firmware.bin".format(firmware))
self._send_status("progress", subtype="sending")
filetransfer = mbp.FileTransferProtocol(protocol, logger=transfer_logger)
filetransfer.copy(firmware, 'firmware.bin', True, False)
self._logger.info(u"Binary file transfer complete")

# Disconnect
protocol.disconnect()

# Display a message on the LCD
protocol.send_ascii("M117 Resetting...")

except mbp.exceptions.ConnectionLost:
self._logger.exception(u"Flashing failed. Unable to connect to printer.")
self._send_status("flasherror", message="Unable to open binary file transfer connection")
return False

except mbp.exceptions.FatalError:
self._logger.exception(u"Flashing failed. Too many retries.")
self._send_status("flasherror", message="Unable to transfer firmware file to printer - too many retries")
return False

except:
self._logger.exception(u"Flashing failed. Unable to transfer file.")
self._send_status("flasherror", message="Unable to transfer firmware file to printer")
return False

finally:
if (protocol):
protocol.shutdown()

self._send_status("progress", subtype="boardreset")
self._logger.info(u"Firmware update reset: attempting to reset the board")
if not _reset_board(self, printer_port, current_baudrate):
self._logger.error(u"Reset failed")
return False

return True

def _reset_board(self, printer_port=None, current_baudrate=None):
assert(printer_port is not None)
assert(current_baudrate is not None)

self._logger.info(u"Resetting printer at '{port}'".format(port=printer_port))

# Configure the port
try:
os.system('stty -F ' + printer_port + ' speed ' + str(current_baudrate) + ' -echo > /dev/null')
except:
self._logger.exception(u"Error configuring serial port.")
self._send_status("flasherror", message="Board reset failed")
return False

# Smoothie reset command
try:
os.system('echo reset >> ' + printer_port)
except:
self._logger.exception(u"Error sending Smoothie 'reset' command.")
self._send_status("flasherror", message="Board reset failed")
return False

# Marlin reset command
try:
os.system('echo M997 >> ' + printer_port)
except:
self._logger.exception(u"Error sending Marlin 'M997' command.")
self._send_status("flasherror", message="Board reset failed")
return False

if _wait_for_board(self, 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_board(self, printer_port=None):
assert(printer_port is not None)
self._logger.info(u"Waiting for printer at '{port}' to reset".format(port=printer_port))

check_command = 'ls ' + printer_port + ' > /dev/null 2>&1'
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

if not os.system(check_command):
connected = True
time.sleep(interval)

else:
time.sleep(interval)
connected = False

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

self._logger.info(u"Printer 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

if not os.system(check_command):
connected = True
time.sleep(interval)

else:
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"Printer at '{port}' reset in {duration} seconds".format(port=printer_port, duration=(round((end - start),2))))
return True
2 changes: 1 addition & 1 deletion octoprint_firmwareupdater/methods/stm32flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _check_stm32flash(self):
else:
return True

def _flash_stm32flash(self, firmware=None, printer_port=None):
def _flash_stm32flash(self, firmware=None, printer_port=None, **kwargs):
assert(firmware is not None)
assert(printer_port is not None)

Expand Down
Loading

0 comments on commit 0e90022

Please sign in to comment.