# SPDX-FileCopyrightText: Copyright 2022 Jetperch LLC
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Handle device programming for firmware and gateware.
For finer control on updating, use the jsdrv example application. For example:
jsdrv reset update1 && jsdrv mem_erase c/upd2 && jsdrv mem_write c/upd2 "C:/joulescope/js220/images/ctrl_release/updater2/js220_ctrl_updater2_1_0_1.img" && jsdrv reset update2
jsdrv reset update2 && jsdrv mem_erase c/upd1 && jsdrv mem_write c/upd1 "C:/joulescope/js220/images/ctrl_release/updater1/js220_ctrl_updater1_1_0_1.img" && jsdrv reset update1
jsdrv reset update1 && jsdrv mem_erase c/app && jsdrv mem_write c/app "C:/joulescope/js220/images/ctrl_release/app/js220_ctrl_app_1_0_1.img" && jsdrv reset app
jsdrv reset update1 && jsdrv mem_erase s/app1 && jsdrv mem_write s/app1 "C:/joulescope/js220/images/fpga_release/js220_fpga_1_0_1.img" && jsdrv reset app
"""
from pyjoulescope_driver import Driver
from pyjoulescope_driver.release import release_to_segments, \
SUBTYPE_CTRL_APP, SUBTYPE_CTRL_UPDATER2, \
SUBTYPE_CTRL_UPDATER1, SUBTYPE_SENSOR_FPGA, \
TARGETS
import logging
import time
_log = logging.getLogger(__name__)
SUBTYPE_RESET = {
SUBTYPE_CTRL_UPDATER1: 'update1',
SUBTYPE_CTRL_UPDATER2: 'update2',
SUBTYPE_CTRL_APP: 'app',
SUBTYPE_SENSOR_FPGA: None,
}
def version_to_str(version):
if version is None:
return '?.?.?'
if isinstance(version, str):
return version
v_patch = version & 0xffff
v_minor = (version >> 16) & 0xff
v_major = (version >> 24) & 0xff
return f'{v_major}.{v_minor}.{v_patch}'
class Programmer:
def __init__(self, driver, device_path):
self._driver = driver
self._path = device_path
if device_path[2] != '&':
self._running_subtype = SUBTYPE_CTRL_APP
self._path_app = device_path
self._path_updater = device_path[:2] + '&' + device_path[2:]
else:
self._running_subtype = None # either updater
self._path_app = device_path[:2] + device_path[3:]
self._path_updater = device_path
@property
def is_in_app(self):
return self._path == self._path_app
@property
def version(self):
return self._driver.query(self._path + '/c/fw/version')
@property
def fpga_version(self):
return self._driver.query(self._path + '/s/fpga/version')
@property
def target_id(self):
return self._driver.query(self._path + '/c/target')
def _device_await(self, device_path, timeout=None):
timeout = 5.0 if timeout is None else float(timeout)
t_end = time.time() + timeout
while True:
device_paths = self._driver.device_paths()
devices = [d for d in device_paths if d == self._path]
if len(devices):
if time.time() > t_end:
msg = f'timeout waiting for {self._path} removal'
_log.warning(msg)
raise TimeoutError(msg)
time.sleep(0.1)
else:
break
while True:
device_paths = self._driver.device_paths()
devices = [d for d in device_paths if d == device_path]
if len(devices):
self._path = devices[0]
return
if time.time() > t_end:
msg = f'timeout waiting for {device_path} in {device_paths}'
_log.warning(msg)
raise TimeoutError(msg)
time.sleep(0.1)
def reset_to(self, subtype):
if self._running_subtype == subtype:
return
reset_target = SUBTYPE_RESET[subtype]
_log.info(f'reset to {reset_target} {subtype}')
self._driver.publish(self._path + '/h/!reset', reset_target)
self._driver.close(self._path)
path = self._path_app if subtype == SUBTYPE_CTRL_APP else self._path_updater
self._device_await(path)
self._running_subtype = subtype
self._driver.open(self._path, mode='restore')
def segment_program(self, segment, progress, progress_msg, p1, p2):
subtype = segment['subtype']
target = TARGETS[subtype]
_log.info('segment_program %s', target['name'])
for attempt in range(10):
attempt_msg = '' if attempt == 0 else f' (retry {attempt})'
progress(p1, progress_msg + attempt_msg)
if subtype == SUBTYPE_CTRL_UPDATER1:
self.reset_to(SUBTYPE_CTRL_UPDATER2)
elif subtype == SUBTYPE_CTRL_UPDATER2:
self.reset_to(SUBTYPE_CTRL_UPDATER1)
progress(p1, progress_msg + ' erase' + attempt_msg)
self.publish(target['mem_region'] + '/!erase', 0, timeout=10)
progress(p2, progress_msg + ' write' + attempt_msg)
self.publish(target['mem_region'] + '/!write', segment['img'], timeout=10)
reset_to = SUBTYPE_RESET[subtype]
if reset_to is None:
return None
else:
self.reset_to(subtype)
version = self.version
if self.target_id == target['target_id'] and version == segment['version']:
return version
raise RuntimeError(f'Failed to program {target["name"]}')
def query(self, topic):
return self._driver.query(self._path + '/' + topic)
def publish(self, topic, value, timeout=None):
return self._driver.publish(self._path + '/' + topic, value, timeout=timeout)
def open(self, mode=None, timeout=None):
return self._driver.open(self._path, mode=mode, timeout=timeout)
def close(self, timeout=None):
return self._driver.close(self._path, timeout=timeout)
[docs]
def release_program(driver: Driver, device_path: str, image: bytes,
force_program=None, progress=None):
"""Program a device with an official release.
:param driver: The driver instance.
:param device_path: The device path string for the target device.
:param image: The binary release image to program. See
:func:`pyjoulescope_driver.release.release_get`.
:param force_program: Force device programming for all segments.
Normally, programming for segments with matching versions are skipped.
:param progress: An optional callable(completion: float, msg: str) callback.
When provided, this function will be called to provide progress updates.
* completion: The completion status from 0.0 (not started) to 1.0 (done).
* msg: A user-meaningful status message.
"""
if progress is None:
progress = lambda x, y: None
segments = release_to_segments(image)
progress(0.00, 'Check app version')
programmer = Programmer(driver, device_path)
if programmer.is_in_app:
ctrl_app_version = programmer.version
else:
ctrl_app_version = None
progress(0.02, 'Check updater2 version')
try:
programmer.reset_to(SUBTYPE_CTRL_UPDATER2)
update2_version = programmer.version
except TimeoutError:
update2_version = None
programmer.close()
programmer.open()
progress(0.05, 'Check updater1 version')
try:
programmer.reset_to(SUBTYPE_CTRL_UPDATER1)
update1_version = programmer.version
except TimeoutError:
update1_version = None
programmer.close()
programmer.open()
sensor_fpga_version = programmer.query('s/fpga/version')
versions_before = [
['app', version_to_str(ctrl_app_version)],
['fpga', version_to_str(sensor_fpga_version)],
['update1', version_to_str(update1_version)],
['update2', version_to_str(update2_version)],
]
v = ', '.join(['='.join(x) for x in versions_before])
_log.info('Existing versions: %s', v)
msg = 'Program updater2'
progress(0.1, msg)
segment = segments.get(SUBTYPE_CTRL_UPDATER2)
if segment is not None and (force_program or segment['version'] != update2_version):
update2_version = programmer.segment_program(segment, progress, msg, 0.1, 0.15)
msg = 'Program updater1'
progress(0.2, msg)
segment = segments.get(SUBTYPE_CTRL_UPDATER1)
if segment is not None and (force_program or segment['version'] != update1_version):
update1_version = programmer.segment_program(segment, progress, msg, 0.2, 0.25)
msg = 'Program sensor FPGA'
progress(0.3, msg)
segment = segments.get(SUBTYPE_SENSOR_FPGA)
if segment is not None and (force_program or segment['version'] != sensor_fpga_version):
programmer.segment_program(segment, progress, msg, 0.3, 0.55)
msg = 'Program controller application'
progress(0.65, msg)
segment = segments.get(SUBTYPE_CTRL_APP)
if segment is not None and (force_program or segment['version'] != ctrl_app_version):
ctrl_app_version = programmer.segment_program(segment, progress, msg, 0.65, 0.9)
else:
programmer.reset_to(SUBTYPE_CTRL_APP)
progress(0.97, 'Finalizing')
sensor_fpga_version = programmer.query('s/fpga/version')
programmer.close()
versions_after = [
['app', version_to_str(ctrl_app_version)],
['fpga', version_to_str(sensor_fpga_version)],
['update1', version_to_str(update1_version)],
['update2', version_to_str(update2_version)],
]
v = ', '.join(['='.join(x) for x in versions_after])
_log.info('Updated to versions: %s', v)
progress(1.0, 'Complete')
return versions_before, versions_after