Source code for pyjoulescope_driver.program_js320
# SPDX-FileCopyrightText: Copyright 2026 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.
"""JS320 firmware update via the in-driver fwup manager.
Mirrors the C definitions in
``include_private/jsdrv_prv/devices/js320/js320_fwup_mgr.h``.
The full update sequence runs in a worker thread inside the driver;
this module just publishes the request and watches the status topic.
"""
import json
import logging
import struct
import threading
from pyjoulescope_driver import Driver
_log = logging.getLogger(__name__)
FWUP_TOPIC_ADD = 'fwup/@/!add'
FWUP_FLAG_SKIP_CTRL = 1 << 0
FWUP_FLAG_SKIP_FPGA = 1 << 1
FWUP_FLAG_SKIP_RESOURCES = 1 << 2
# struct jsdrv_fwup_add_header_s: char[32] device_prefix, u32 flags, u32 zip_size
_FWUP_ADD_HEADER = struct.Struct('<32sII')
[docs]
def program_js320(driver: Driver, device_path: str,
package_path: str = None, flags: int = 0,
progress=None, timeout: float = 600.0):
"""Program a JS320 device using the firmware update manager.
Publishes the request to ``fwup/@/!add`` and monitors the
per-worker ``fwup/NNN/status`` topic until the worker reports a
terminal ``DONE`` or ``ERROR`` state.
The driver must be initialized but the device must NOT already be
open: the worker opens the device internally in raw mode.
:param driver: The driver instance.
:param device_path: The target device path (e.g., ``u/js320/8w2a``).
:param package_path: The path to the manufacturing ZIP package.
``None`` (default) uses the firmware embedded in the driver build.
:param flags: Bitmask of ``FWUP_FLAG_SKIP_*`` flags.
:param progress: An optional callable(completion: float, msg: str).
:param timeout: Maximum total time to wait, in seconds.
:raise RuntimeError: If the firmware update fails.
:raise TimeoutError: If the firmware update does not complete in time.
"""
if progress is None:
progress = lambda x, y: None
if package_path is None:
zip_data = b''
else:
with open(package_path, 'rb') as f:
zip_data = f.read()
device_prefix = device_path.encode('utf-8')
if len(device_prefix) >= 32:
raise ValueError(f'device path too long: {device_path}')
payload = _FWUP_ADD_HEADER.pack(device_prefix, flags, len(zip_data)) + zip_data
done = threading.Event()
last = {'state': '', 'msg': '', 'rc': 0}
def on_status(topic, value):
if not isinstance(value, str) or not topic.endswith('/status'):
return
try:
entry = json.loads(value)
except (ValueError, TypeError):
return
state = str(entry.get('state', '?'))
msg = str(entry.get('msg', ''))
try:
rc = int(entry.get('rc', 0))
except (TypeError, ValueError):
rc = 0
try:
pct = float(entry.get('pct', 0.0))
except (TypeError, ValueError):
pct = 0.0
last['state'] = state
last['msg'] = msg
last['rc'] = rc
_log.info('fwup %s %.1f%%: %s (rc=%d) [%s]',
state, pct, msg, rc, topic)
progress(pct / 100.0, f'{state}: {msg}')
if state in ('DONE', 'ERROR'):
done.set()
progress(0.0, 'Starting JS320 firmware update')
driver.subscribe('fwup/', 'pub', on_status)
try:
driver.publish(FWUP_TOPIC_ADD, payload)
if not done.wait(timeout):
raise TimeoutError(
f'JS320 firmware update timed out after {timeout:.0f} s '
f'(last state {last["state"]}: {last["msg"]})')
finally:
driver.unsubscribe('fwup/', on_status)
if last['state'] != 'DONE' or last['rc'] != 0:
raise RuntimeError(
f'JS320 firmware update failed: {last["msg"]} '
f'(state={last["state"]}, rc={last["rc"]})')
progress(1.0, 'Complete')