#!/usr/bin/env python import glob import serial import slip import os import subprocess import sys import time import logging import logging.config import param_stored_vals_msg # Used for reading stick calibration values import slip, struct # Used for writing stick calibration values sololink_conf = "/etc/sololink.conf" ARTOO_SYSINFO_ID = chr(0x3) ARTOO_UPDATE_ID = chr(0x12) ARTOO_LOCKOUT_ID = chr(0x13) ARTOO_CALIBRATE_ID = chr(0x2) ARTOO_UPDATE_SUCCESS = chr(1) ARTOO_UPDATE_FAILED = chr(2) ARTOO_LOCKOUT_FALSE = chr(0) ARTOO_LOCKOUT_TRUE = chr(1) ARTOO_BAUD = 115200 # update_result should be either ARTOO_UPDATE_SUCCESS or ARTOO_UPDATE_FAILED def setArtooUpdateComplete(update_result): ser = serial.Serial("/dev/ttymxc1", 115200, timeout=1) slipdev = slip.SlipDevice(ser) slipdev.write("".join([ARTOO_UPDATE_ID, update_result])) ser.close() # lockout should be either ARTOO_LOCKOUT_FALSE or ARTOO_LOCKOUT_TRUE def setArtooLockout(lockout): ser = serial.Serial("/dev/ttymxc1", 115200, timeout=1) slipdev = slip.SlipDevice(ser) slipdev.write("".join([ARTOO_LOCKOUT_ID, lockout])) ser.close() def mkdir_p(path): try: os.makedirs(path) except OSError: pass # already exists def doUpdateComplete(): setArtooLockout(ARTOO_LOCKOUT_FALSE) if not os.path.exists("/log/updates/READY"): if os.path.exists("/log/updates/UPDATEFAILED"): f = open("/log/updates/UPDATEFAILED", "r") # file should be one line; read it all r = f.read(1000) f.close() r = r.strip("\r\n\t\0 ") logger.info("request \"update failed\" screen (%s)", r) setArtooUpdateComplete(ARTOO_UPDATE_FAILED) else: logger.info("request \"update success\" screen") setArtooUpdateComplete(ARTOO_UPDATE_SUCCESS) mkdir_p("/log/updates") open("/log/updates/READY", "w").close() # "touch" else: logger.info("no screen update (READY exists)") # return tuple (filename, version), or None def getFirmwareInfo(): files = sorted(glob.glob("/firmware/artoo_*.bin")) if not files: return None filename = files[-1] # Filename may be of the form # "/firmware/artoo_0.0.0.bin", or # "/firmware/artoo_v0.0.0.bin". # Get it without the 'v'. if filename[16] == 'v': version = filename[17:-4] else: version = filename[16:-4] return (filename, version) # return version as string ("unknown" if can't get version) def getArtooVersion(): #Check the version of the stm32 firmware over serial #The STM32 might be emitting packets already, so we try a few times to get the #version string. This has been observed to get the version with one retry on #several occasions. logger.info("requesting stm32 version") version = "unknown" ser = serial.Serial("/dev/ttymxc1", 115200, timeout=1) slipdev = slip.SlipDevice(ser) for i in range(5): slipdev.write("".join([ARTOO_SYSINFO_ID])) pkt = slipdev.read() if not pkt: logger.info("no data received from stm32, retrying") continue pkt = "".join(pkt) if pkt[0] == ARTOO_SYSINFO_ID: # SysInfo packet is: artoo/src/hostprotocol.cpp # start size # 0 1 ARTOO_SYSINFO_ID artoo/src/hostprotocol.h # 1 12 UniqueId artoo/src/stm32/sys.h # 13 2 hwversion artoo/src/hostprotocol.cpp # 15 var Version artoo/src/version.h # Version may start with an initial 'v', e.g. v0.6.10, # but we want it starting with the numeric part. if pkt[15] != 'v': version = pkt[15:] else: version = pkt[16:] break logger.info("got %s/%d, retrying", str(hex(ord(pkt[0]))), len(pkt)) ser.close() return version # Attempt to update the STM32 multiple times def updateStm32(filename): # Total attempts will be retry+1, with the last one doing the unprotect. # Since doing the unprotect results in a support call, retries is set # such that the unprotect is a last resort. Allow for two init failures # and two erase failures (truly degenerate) before doing the unprotect # and (hopefully) fixing a protected stm32. Getting to the unprotect # (where it is really needed) should happen only once in a controller's # lifetime, if at all (it should never be needed, really). stick_cal = None retry = 4 while retry >= 0: if retry > 0: # don't unprotect chip the first few times an erase fails logger.info("updating without readout protection disable") if call_stm32loader(filename, disable_readout_protect=False): break # Move on if the firmware is successfully loaded. else: # last attempt - unprotect chip if erase fails logger.info("Updating the STM32 has failed several times. We suspect that readout protection is enabled.") logger.info("Saving Stick Calibration: ") stick_cal = read_stick_cal() logger.info(stick_cal) logger.info("updating with readout protection disable. This will wipe flash.") call_stm32loader(filename, disable_readout_protect=True) retry -= 1 logger.info("Update complete. Stick cal before update was: {}. (None means it wasn't read before update)".format(stick_cal)) logger.info("stick cal after update: {}".format(read_stick_cal())) # Only write stick calibration if we did disabled readout protection: if stick_cal is not None: # Try to write_stick cal multiple times in case it fails. # This is defensive. I've never seen it fail. for i in range(3): write_stick_cal(stick_cal) time.sleep(1) logger.info("Stick cal written. Reading it back again as a check:") new_cal = read_stick_cal() logger.info(new_cal) if stick_cal == new_cal: break else: logger.info("Stick cal failed to be written correctly:") logger.info("Calibration attempted to be written: {}".format(stick_cal)) logger.info("Calibration found in flash: {}".format(new_cal)) # Initiate the stm32loader process to actually update the STM32 chip. Return True if successful. def call_stm32loader(filename, disable_readout_protect=False): # subprocess.check_output raises an exception if stm32loader.py returns a nonzero exit code for any reason # including if readout_protect is enabled. # Disable readout protect if specified. This will erase all flash. if disable_readout_protect: flags = "-wvqu" else: flags = "-wvq" try: s = subprocess.check_output(["stm32loader.py", flags, "-s", "127", "-b", "115200", "-p", "/dev/ttymxc1", filename], stderr=subprocess.STDOUT) logger.info("stm32loader.py returned normally; output:") success = True except subprocess.CalledProcessError as cpe: s = cpe.output logger.info("stm32loader.py returned error; output:") success = False # this might be ugly, but it gets the output in the log s = s.strip("\r\n\t\0 ") logger.info(s) # Wait a second for the STM32 to come back up before we send it a message later time.sleep(1) return success def writeVersionFile(version): f = open("/STM_VERSION", 'w') f.write(version + '\n') f.close() # return version from /STM_VERSION def getVersionFile(): try: f = open("/STM_VERSION", 'r') version = f.readline() f.close() version = version.strip() if version == "": version = "unknown" except: version = "unknown" return version def read_stick_cal(): # Start the STM32 process because sololink_config sets runlevel to 2 before an update, # but we need the (runlevel 3) STM32 process to read a stick cal. p = subprocess.Popen('stm32', shell=True) time.sleep(3) msg = param_stored_vals_msg.fetch() # String containing all artoo params (including stick cal values) p.terminate() # Kill the STM32 process. time.sleep(1) # Give time for the STM32 process to die. Not sure if needed. # msg is an empty string if the stm32 process is not running (runlevel 2) if not msg: logger.info("Could not read stick calibration.") return None else: msg = param_stored_vals_msg.unpack(msg) # Converts string to Dict axes = msg.get('stickCals') # 6-item list of 3-item tuples # Pack datastructure into a single list that can be written later stick_cals = [] for a in axes: stick_cals.extend(a) return stick_cals def write_stick_cal(values): logger.info("Writing stick cal: {}".format(values)) serialpath = '/dev/ttymxc1' serialport = serial.Serial(serialpath, ARTOO_BAUD) slipdev = slip.SlipDevice(serialport) packed_vals = struct.pack("