Glowforge released firmware version 1.4.0-21 for their Pro and Basic model CNC lasers.
It appears to have been released on May 16, 2018. However, it has a build date of April 18th, 2018.
I haven’t completely analyzed all the changes yet, but here’s some diff’s. Feel free to get a head start (and maybe save me some effort, PLEASE )!
--- a/etc/build
+++ b/etc/build
@@ -2,17 +2,18 @@
Build Configuration: |
-----------------------
DISTRO = glowforge
-DISTRO_VERSION = 1.3.3-17
+DISTRO_VERSION = 1.4.0-21
MACHINE = glowforge
DEVICE_MODEL = glowforge
-FIRMWARE_VERSION = 1.3.3-17
+FIRMWARE_VERSION = 1.4.0-21
IMAGE_TYPE = production
-----------------------
Layer Revisions: |
-----------------------
meta = (detachedfromc53ddb2):c53ddb2006f04051b00df3189fe8d35bb49ef3c7
meta-fsl-arm = (nobranch):c9f259a4bf8472dfa3ff75f1c3fcbe5e0ded7aaf
-meta-glowforge = (detachedfrom027df84):027df84a675b0c729a5a4f581fc8ea841a03b88b
+meta-glowforge = (detachedfrom5c16cdc):5c16cdc8a5f4c4aa6ee3c2dc695344746913fafe
+meta-glowforge-gpl = (detachedfrom588a5a3):588a5a3ec093d5889752a30a239f0889f1048c17
meta-oe = (nobranch):df2f700d66bace65f5d802225232d01cf511fe81
meta-python = (nobranch):df2f700d66bace65f5d802225232d01cf511fe81
meta-networking = (nobranch):df2f700d66bace65f5d802225232d01cf511fe81
--- a/etc/network/if-pre-up.d/wpa-supplicant
+++ b/etc/network/if-pre-up.d/wpa-supplicant
@@ -8,11 +8,13 @@
VERBOSITY=0
-
+if [ -z "$IF_WPA_CONF" ]; then
+ exit 0
+fi
if [ -s "$IF_WPA_CONF" ]; then
WPA_SUP_CONF="-c $IF_WPA_CONF"
else
- exit 0
+ WPA_SUP_CONF="-C /var/run/wpa_supplicant"
fi
if [ ! -x "$WPA_SUP_BIN" ]; then
--- a/glowforge/python/glowforge/bugeggs/bugeggs.py
+++ b/glowforge/python/glowforge/bugeggs/bugeggs.py
@@ -2,14 +2,18 @@
# Crash report uploader
import argparse
+import errno
import logging
import os
import requests
import signal
import socket
+import subprocess
import sys
+import tempfile
import time
import urlparse
+from datetime import datetime
from setproctitle import setproctitle
import glowforge.util.http as http
@@ -19,12 +23,46 @@
from glowforge.util.build_info import BuildInfo
LOG_DIR = '/data/log'
-CRASH_REPORT_DIR = '/data/log/crash'
+CRASH_REPORT_DIR = os.path.join(LOG_DIR, 'crash')
+UNKNOWN_REPORT_DIR = os.path.join(CRASH_REPORT_DIR, 'unknown')
+
FILE_UPLOADED_SUFFIX = '.uploaded'
LOG_TAIL_SIZE_BYTES = 262144
received_signal = False
buildinfo = BuildInfo()
+
+def mkdirp(dir_name):
+ try:
+ os.makedirs(dir_name)
+ except OSError as e:
+ if e.errno == errno.EEXIST and os.path.isdir(dir_name):
+ pass
+ else:
+ raise
+
+
+def boot_device():
+ # Allow `rdev` value to be overridden by an environment variable for testing
+ return os.getenv('GF_BOOT_DEVICE', subprocess.check_output('rdev'))
+
+
+def in_recovery_partition():
+ try:
+ return boot_device().startswith('/dev/mmcblk2boot')
+ except:
+ return False
+
+
+def in_recovery_mode():
+ try:
+ # `in_recovery_mode`'s return code is 0 (shell truthiness) if in user-initiated recovery mode.
+ # Also allow it to be overridden with an environment variable for testing.
+ override = os.getenv('GF_RECOVERY_MODE')
+ return int(override) if override is not None else (subprocess.call('in_recovery_mode') == 0)
+ except:
+ return False
+
def abs_path_iter(dirpath, ignore_suffix=None):
for dirname, subdirs, files in os.walk(dirpath):
@@ -52,7 +90,7 @@
reachable = True
logging.debug('Host is reachable')
except Exception as e:
- logging.debug('Host is not reachable: {}'.format(e))
+ logging.exception('Host is not reachable')
finally:
if s is not None:
s.close()
@@ -86,7 +124,7 @@
logging.debug(r)
return True
except Exception as e:
- logging.debug('Upload failed: {}'.format(e))
+ logging.exception('Upload failed')
return False
@@ -118,6 +156,22 @@
logging.debug('No associated log data')
+def report_unknown_crash():
+ # Save a marker file that will be uploaded
+ message = 'In recovery OS but nothing to upload (possible CPU lockup?)'
+ logging.critical(message)
+ mkdirp(UNKNOWN_REPORT_DIR)
+ datestr = datetime.utcnow().strftime('%Y%m%d-%H%M%S')
+ filename = 'unknown-crash-{}-'.format(datestr)
+ try:
+ fd, abspath = tempfile.mkstemp(suffix='.dmp', prefix=filename, dir=UNKNOWN_REPORT_DIR)
+ f = os.fdopen(fd, 'w')
+ f.write('{} {}\n'.format(message, datestr))
+ f.close()
+ except OSError as e:
+ logging.exception('Unable to create file at {}'.format(abspath))
+
+
def get_file_tail(f, nbytes):
try:
f.seek(-nbytes, os.SEEK_END)
@@ -130,9 +184,14 @@
logging.debug('Marking {} as uploaded'.format(path))
if not dry_run:
try:
- os.rename(path, path+FILE_UPLOADED_SUFFIX)
+ newpath = path + FILE_UPLOADED_SUFFIX
+ os.rename(path, newpath)
except:
- pass
+ logging.exception('Renaming {} to {} failed'.format(path, newpath))
+
+
+def is_crash_report(path):
+ return not os.path.basename(path).startswith('head-debug-log-')
def signal_handler(signum, frame):
@@ -151,6 +210,20 @@
parser.add_argument('-k', '--keep', dest='keep', action='store_true', help='keep file name unmodified after uploading')
parser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true', help='dry run; do not upload or rename files')
args = parser.parse_args()
+
+ # If we're running from the recovery OS, but it wasn't user-initiated,
+ # this is a "postmortem" boot caused by a watchdog timeout
+ postmortem = (in_recovery_partition() and not in_recovery_mode())
+ # If this is a postmortem boot, but the previous boot didn't leave any crash logs,
+ # the watchdog reset could have been from a total CPU lockup
+ if postmortem:
+ have_dump = False
+ for abspath in abs_path_iter(args.src_dir, ignore_suffix=FILE_UPLOADED_SUFFIX):
+ if is_crash_report(abspath):
+ have_dump = True
+ break
+ if not have_dump:
+ report_unknown_crash()
# if we don't have an internet connection, don't start
if not args.dry_run:
--- a/glowforge/python/glowforge/devicesetup/fw_update.py
+++ b/glowforge/python/glowforge/devicesetup/fw_update.py
@@ -4,7 +4,7 @@
import subprocess
UPLOAD_BUF_SIZE = 8192
-MAX_FIRMWARE_UPDATE_SIZE = 50*1024*1024 # in bytes
+MAX_FIRMWARE_UPDATE_SIZE = 70*1024*1024 # in bytes
FIRMWARE_UPDATE_FILE_TEMP_PATH = '/var/volatile/tmp/update.fw'
UPDATER_PROCESS_NAME = 'glowforge-updater'
UPDATER_PATH = '/glowforge/python/glowforge/updater/glowforge-updater.sh'
@@ -21,7 +21,7 @@
else:
return None, None
-def run_updater(filepath):
+def run_updater(filepath, complete=False):
try:
args = [
UPDATER_PATH,
@@ -31,6 +31,8 @@
'-v', # capture verbose log output
filepath
]
+ if complete:
+ args.insert(1, '-c') # complete update
logging.info('Installing update')
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
logging.info('Update complete')
@@ -43,7 +45,7 @@
# (e.g. if the updater couldn't be launched)
-def install_firmware_update(response, fileobj):
+def install_firmware_update(response, fileobj, complete=False):
# Ensure the updater is not running
status, error = check_updater_status()
if status is not None:
@@ -97,7 +99,7 @@
if tmpfile is not None:
tmpfile.close()
- status, output = run_updater(FIRMWARE_UPDATE_FILE_TEMP_PATH)
+ status, output = run_updater(FIRMWARE_UPDATE_FILE_TEMP_PATH, complete)
delete_update_file()
response.status = status
--- a/glowforge/python/glowforge/devicesetup/webapp.py
+++ b/glowforge/python/glowforge/devicesetup/webapp.py
@@ -1,4 +1,4 @@
-WIFI_SETUP_API_VERSION = '0.9'
+WIFI_SETUP_API_VERSION = '1.0'
import SocketServer
@@ -84,7 +84,7 @@
@app.post('/setup/connect_and_check')
def connect():
params = request.forms
- return wifi.connect_and_check(params.get('ssid'), params.get('passphrase'), params.get('token'))
+ return wifi.connect_and_check(params.get('ssid'), params.get('passphrase'), params.get('token'), params.get('security'))
@app.route('/logs/<dirpath:path>/list')
def get_logs(dirpath):
@@ -155,11 +155,12 @@
@app.post('/firmware_update')
@recovery_mode_only
def firmware_update():
+ complete = (request.forms.get('complete') == '1')
f = request.files.get('file')
if len(request.files) != 1 or f is None:
response.status = 400
return None
- return fw_update.install_firmware_update(response, f)
+ return fw_update.install_firmware_update(response, f, complete=complete)
def stop():
server.stop()
--- a/glowforge/python/glowforge/devicesetup/wifi.py
+++ b/glowforge/python/glowforge/devicesetup/wifi.py
@@ -121,7 +121,7 @@
result['in_recovery_mode'] = machine.in_recovery_mode()
return result
-def connect_and_check(ssid, passphrase, token):
+def connect_and_check(ssid, passphrase, token, encryption):
logging.info('devicesetup:connect_and_check')
result = {}
@@ -139,8 +139,11 @@
logging.error(result)
return result
+ # default to WPA/WPA2 Personal if no encryption type specified
+ if encryption is None:
+ encryption = wifi_creds.WPA_WPA2_PERSONAL if passphrase else wifi_creds.UNSECURED
+
try:
- encryption = wifi_creds.WPA_WPA2_PERSONAL if passphrase else wifi_creds.UNSECURED
wifi_creds.set_credentials(ssid, passphrase, encryption, apply=True, iface=sta_interface)
except wifi_creds.InvalidSSIDLength as e:
response.status = 400
@@ -154,6 +157,14 @@
response.status = 400
result = {
'error': 'invalid_passphrase_length',
+ 'details': repr(e)
+ }
+ logging.error(result)
+ return result
+ except wifi_creds.InvalidPassphraseCharacters as e:
+ response.status = 400
+ result = {
+ 'error': 'invalid_passphrase_chars',
'details': repr(e)
}
logging.error(result)
@@ -248,19 +259,24 @@
logging.error(result)
return result
-def _iw_scan():
- return subprocess.check_output(['/usr/sbin/iw', 'dev', sta_interface, 'scan'], close_fds=True)
+def _wpa_cli_scan():
+ logging.info('devicesetup:_wpa_cli_scan calling /usr/sbin/wpa_cli scan')
+ subprocess.check_output(['/usr/sbin/wpa_cli', 'scan'], close_fds=True)
+ logging.info('devicesetup:_wpa_cli_scan calling /usr/sbin/wpa_cli scan_results')
+ return subprocess.check_output(['/usr/sbin/wpa_cli', 'scan_results'], close_fds=True)
def scan():
try:
- output = _iw_scan()
- seq = re.split('BSS ..:..:..:..:..:..', output)
- seq = map(parse_cell, seq)
- seq = filter(has_ssid, seq)
+ output = _wpa_cli_scan().splitlines()
+ # split into fields and filter out header lines
+ seq = map(lambda l: l.split('\t', 5), output)
+ seq = filter(lambda f: len(f) == 5, seq)
+ # parse information out of the fields
+ seq = map(parse_fields, seq)
+ seq = filter(supported_freq, seq)
seq = sort_by_ssid(seq)
- seq = filter(supported_freq, seq)
seq = unique_ssid(seq)
- return seq
+ return list(seq)
except Exception as e:
response.status = 500
response.content_type = 'application/json'
@@ -269,41 +285,31 @@
'details': e.message,
}
-def sort_by_ssid(seq):
- return sorted(seq, key=lambda v: v['ssid'].lower(), cmp=locale.strcoll)
-
-def has_ssid(dict):
- return dict.has_key('ssid')
+def parse_fields(fields):
+ return {
+ 'freq': int(fields[1]),
+ 'signal': fields[2] + ' dBm',
+ 'security': security_type_from_flags(fields[3]),
+ 'ssid': fields[4].decode('string_escape')
+ }
+
+def security_type_from_flags(flagstr):
+ # TODO: "[WPA2-EAP-" is WPA2 Enterprise, "[WPA-EAP-" is WPA Enterprise
+ if '[WPA2' in flagstr:
+ return wifi_creds.WPA2_PERSONAL
+ elif '[WPA' in flagstr:
+ return wifi_creds.WPA_PERSONAL
+ elif '[WEP' in flagstr:
+ return wifi_creds.WEP
+ else:
+ return wifi_creds.UNSECURED
def supported_freq(dict):
freq = dict['freq']
return freq >= 2400 and freq < 2500
+def sort_by_ssid(seq):
+ return sorted(seq, key=lambda v: v['ssid'].lower(), cmp=locale.strcoll)
+
def unique_ssid(lst):
return [] if lst==[] else [lst[0]] + unique_ssid(filter(lambda x: x['ssid'] != lst[0]['ssid'], lst[1:]))
-
-def parse_cell(cell):
- parsed = {}
-
- match = re.search('SSID: ([^\n]+)', cell)
- if match:
- parsed['ssid'] = match.group(1).decode('string_escape')
-
- match = re.search('freq: ([0-9]+)', cell)
- if match:
- parsed['freq'] = int(match.group(1))
-
- match = re.search('signal: (-[0-9]+.[0-9]+ dBm)', cell)
- if match:
- parsed['signal'] = match.group(1)
-
- if re.search('RSN:', cell):
- parsed['security'] = 'wpa2'
- elif re.search('WPA:\s+\* Version: 1\n', cell):
- parsed['security'] = 'wpa'
- elif re.search('WEP:', cell):
- parsed['security'] = 'wep'
- else:
- parsed['security'] = 'open'
-
- return parsed
--- a/glowforge/python/glowforge/devicesetup/wifi_creds.py
+++ b/glowforge/python/glowforge/devicesetup/wifi_creds.py
@@ -4,7 +4,8 @@
from hashlib import pbkdf2_hmac
# Encryption modes
-UNSECURED = 'unsecured'
+UNSECURED = 'open'
+WEP = 'wep'
WPA_PERSONAL = 'wpa'
WPA2_PERSONAL = 'wpa2'
WPA_WPA2_PERSONAL = 'wpa/wpa2'
@@ -19,6 +20,9 @@
class InvalidPassphraseLength(Exception):
pass
+class InvalidPassphraseCharacters(Exception):
+ pass
+
class InvalidEncryptionType(Exception):
pass
@@ -26,9 +30,7 @@
class EncryptionType(object):
pass_len_min = 0
pass_len_max = 0
- proto = None
key_mgmt = None
- group = None
@classmethod
def validate_passphrase_length(cls, passphrase):
length = len(passphrase) if passphrase is not None else 0
@@ -36,10 +38,12 @@
raise InvalidPassphraseLength(
'Length was {}, must be between {} and {}'.format(
length, cls.pass_len_min, cls.pass_len_max))
- return True
@classmethod
- def psk(cls, ssid, passphrase):
- return None
+ def validate_passphrase_character_set(cls, passphrase):
+ pass
+ @classmethod
+ def fields(cls, ssid, passphrase):
+ return {}
class EncryptionType_Unsecured(EncryptionType):
key_mgmt = 'NONE'
@@ -47,12 +51,35 @@
def validate_passphrase_length(cls, passphrase):
if passphrase is not None and len(passphrase) != 0:
raise InvalidPassphraseLength('Passphrase must be empty')
- return True
+
+class EncryptionType_WEP(EncryptionType):
+ key_mgmt = 'NONE'
+ # WEP keys must be 10 (64-bit), 26 (128-bit), or 32 (152-bit) hex digits
+ # wpa_supplicant does not support 256-bit WEP keys
+ valid_key_lengths = (10, 26, 32)
+ @classmethod
+ def validate_passphrase_length(cls, passphrase):
+ length = len(passphrase) if passphrase is not None else 0
+ if length not in cls.valid_key_lengths:
+ raise InvalidPassphraseLength(
+ 'Passphrase length was {}, must be one of {}'.format(
+ length, str(cls.valid_key_lengths)))
+ @classmethod
+ def validate_passphrase_character_set(cls, passphrase):
+ # Only accept hexadecimal keys. The conversion from ASCII "passphrase"
+ # to hex key is vendor-specific.
+ if re.search(r'[^0-9A-Fa-f]', passphrase):
+ raise InvalidPassphraseCharacters(
+ 'Passphrase can only contain 0-9, A-F, and a-f')
+ @classmethod
+ def fields(cls, ssid, passphrase):
+ return {'wep_key0': passphrase,
+ 'wep_tx_keyidx': '0'}
+
class EncryptionType_WPAPersonal(EncryptionType):
proto = 'WPA'
key_mgmt = 'WPA-PSK'
- group = 'CCMP TKIP'
pass_len_min = 8
pass_len_max = 63
@classmethod
@@ -61,6 +88,13 @@
# SSID as a salt. Python has a PBKDF2 implementation built in, so we
# don't have to shell out to wpa_passhprase.
return pbkdf2_hmac('sha1', passphrase, ssid, 4096, 32).encode('hex')
+ @classmethod
+ def fields(cls, ssid, passphrase):
+ # create hex key from SSID and passphrase to prevent issues with special
+ # characters
+ return {'proto': cls.proto,
+ 'group': 'CCMP TKIP',
+ 'psk': cls.psk(ssid, passphrase)}
class EncryptionType_WPA2Personal(EncryptionType_WPAPersonal):
proto = 'RSN'
@@ -71,6 +105,7 @@
ENCRYPTION_TYPES = {
UNSECURED: EncryptionType_Unsecured,
+ WEP: EncryptionType_WEP,
WPA_PERSONAL: EncryptionType_WPAPersonal,
WPA2_PERSONAL: EncryptionType_WPA2Personal,
WPA_WPA2_PERSONAL: EncryptionType_WPA_WPA2Personal
@@ -97,6 +132,8 @@
# raises InvalidPassphraseLength on failure
enctype.validate_passphrase_length(passphrase)
+ # raises InvalidPassphraseCharacters on failure
+ enctype.validate_passphrase_character_set(passphrase)
lines = [
'ctrl_interface=/var/run/wpa_supplicant',
@@ -107,17 +144,12 @@
]
# encode the SSID as hex to prevent issues with special characters
lines.append(' ssid='+ssid.encode('hex'))
- if enctype.proto:
- lines.append(' proto='+enctype.proto)
if enctype.key_mgmt:
lines.append(' key_mgmt='+enctype.key_mgmt)
- if enctype.group:
- lines.append(' group='+enctype.group)
- # create hex key from SSID and passphrase to prevent issues with special
- # characters
- psk = enctype.psk(ssid, passphrase)
- if psk:
- lines.append(' psk='+psk)
+ # add extra fields
+ for k, v in enctype.fields(ssid, passphrase).iteritems():
+ if v is not None:
+ lines.append(' {}={}'.format(k, v))
lines.append('}\n')
output = '\n'.join(lines)
--- a/glowforge/python/glowforge/updater/apply_update.py
+++ b/glowforge/python/glowforge/updater/apply_update.py
@@ -49,7 +49,12 @@
def apply(self):
apply_func = None
- if self.env.fwenv.is_device_a_active():
+ if self.env.args.complete:
+ if self.env.fwenv.is_recovery_device_active():
+ apply_func = self.apply_complete
+ else:
+ raise Exception("Complete update cannot be performed on currently mounted root device")
+ elif self.env.fwenv.is_device_a_active():
apply_func = self.apply_b
elif self.env.fwenv.is_device_b_active():
apply_func = self.apply_a
@@ -79,10 +84,15 @@
pass
def commit(self):
+ env_func = None
+ if self.env.args.complete:
+ env_func = self.env.fwenv.write_env_full
+ else:
+ env_func = self.env.fwenv.switch_env
logging.info("Committing update")
data = { 'firmware_version': self.update_version }
event.send('firmware_update:committing', data)
- self.env.fwenv.switch_env()
+ env_func()
if not self.env.args.no_reboot:
event.send('firmware_update:rebooting', data)
machine.reboot()
@@ -101,3 +111,6 @@
def apply_b(self):
self.fwup.apply_b()
+
+ def apply_complete(self):
+ self.fwup.apply_complete()
--- a/glowforge/python/glowforge/updater/fwup.py
+++ b/glowforge/python/glowforge/updater/fwup.py
@@ -49,10 +49,13 @@
def apply_b(self):
self.__apply(params.device_b, 'upgrade.b')
- def __apply(self, device, task):
+ def apply_complete(self):
+ self.__apply(params.root_device, 'complete', extra_args=['-U'])
+
+ def __apply(self, device, task, extra_args=[]):
logging.info('Applying update to device %s', device)
return self.__run_fwup(subprocess.check_call,
- ['-v', '-a', '-d', device, '-i', self.fw_file, '-t', task])
+ extra_args + ['-v', '-a', '-d', device, '-i', self.fw_file, '-t', task])
def __run_fwup(self, fn, cmdargs):
"""fn should be subprocess.check_call or subprocess.check_output."""
--- a/glowforge/python/glowforge/updater/params.py
+++ b/glowforge/python/glowforge/updater/params.py
@@ -1,12 +1,18 @@
import subprocess
+mmcdev_env_key = 'mmcdev'
+hwpart_env_key = 'mmchwpart'
bootpart_env_key = 'mmcpart'
rootdev_env_key = 'mmcroot'
+recovery_env_key = 'boot_recovery'
+mmc_hwpart_num = '0'
part_a = '1'
part_b = '2'
current_root = subprocess.check_output('/usr/sbin/rdev').split()[0] # rdev returns '/dev/mmcblkXpY /'
device = current_root[5:12] # 'mmcblkX'
+mmc_dev_num = str(int(current_root[11])-1)
device_a = '/dev/' + device + 'p' + part_a
device_b = '/dev/' + device + 'p' + part_b
+root_device = '/dev/' + device
recovery_device = '/dev/mmcblk2boot0p1'
uboot_config = '/etc/fw_env_' + device + '.config'
--- a/glowforge/python/glowforge/updater/uboot_env.py
+++ b/glowforge/python/glowforge/updater/uboot_env.py
@@ -41,6 +41,15 @@
raise Exception("Could not detect current root device")
self.set_multiple({
- params.bootpart_env_key : part,
- params.rootdev_env_key : root
+ params.bootpart_env_key: part,
+ params.rootdev_env_key: root
})
+
+ def write_env_full(self):
+ self.set_multiple({
+ params.mmcdev_env_key: params.mmc_dev_num,
+ params.hwpart_env_key: params.mmc_hwpart_num,
+ params.bootpart_env_key: params.part_a,
+ params.rootdev_env_key: params.device_a,
+ params.recovery_env_key: 'no'
+ })
--- a/glowforge/python/glowforge/updater/updater.py
+++ b/glowforge/python/glowforge/updater/updater.py
@@ -30,6 +30,7 @@
parser.add_argument('-L', dest='no_update_lock', action='store_true')
parser.add_argument('-R', dest='no_reboot', action='store_true')
parser.add_argument('-v', dest='verbose', action='store_true')
+ parser.add_argument('-c', dest='complete', action='store_true')
self.env.args = parser.parse_args()
if self.env.args.verbose:
logging.getLogger().level = logging.DEBUG