<!-- SPDX-FileCopyrightText: Copyright 2023-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. -->

<a id="driver"></a>

# Driver

<a id="module-pyjoulescope_driver"></a>

### *class* pyjoulescope_driver.DeviceContext(driver, device_path)

An open device returned by [`Driver.open()`](#pyjoulescope_driver.Driver.open).

The topic arguments are relative to the device path, such as
“s/i/range/mode”.  Call [`close()`](#pyjoulescope_driver.DeviceContext.close) or exit the context to
unsubscribe all subscriptions made through this instance and then
close the device.

#### close(timeout=None)

Unsubscribe all subscriptions made through this instance, and
then close the device.  Calling close again does nothing.

See [`Driver.close()`](#pyjoulescope_driver.Driver.close).

#### publish(topic, value, timeout=None)

Publish a value.  See [`Driver.publish()`](#pyjoulescope_driver.Driver.publish).

#### publish_and_wait(publish_topic, publish_value, response_topic, timeout=None, match=None)

Publish and wait for a response.  See [`Driver.publish_and_wait()`](#pyjoulescope_driver.Driver.publish_and_wait).

#### query(topic, timeout=None)

Query a value.  See [`Driver.query()`](#pyjoulescope_driver.Driver.query).

#### subscribe(topic, flags, fn, timeout=None)

Subscribe to a topic.  See [`Driver.subscribe()`](#pyjoulescope_driver.Driver.subscribe).

[`close()`](#pyjoulescope_driver.DeviceContext.close) unsubscribes the subscription, if still active.

* **Returns:**
  The [`SubscribeContext`](#pyjoulescope_driver.SubscribeContext).

#### topic(topic)

Get the absolute topic.

* **Parameters:**
  **topic** – The topic relative to the device path.
* **Returns:**
  The absolute topic, “{device_path}/{topic}”.

#### unsubscribe(topic, fn, timeout=None)

Unsubscribe from a topic.  See [`Driver.unsubscribe()`](#pyjoulescope_driver.Driver.unsubscribe).

### *class* pyjoulescope_driver.Driver

The Joulescope driver class.

* **Parameters:**
  **timeout** – The optional timeout for open.
  None (default) uses the default timeout.

The native driver log forwards to the Python “jsdrv” logger at
[`log_level`](#pyjoulescope_driver.Driver.log_level), which defaults to “error”.

The application must finalize each Driver instance, either with a
context manager or by calling [`finalize()`](#pyjoulescope_driver.Driver.finalize) directly.  Finalize
stops the driver threads and closes the devices that the instance
opened.  A Driver that is never finalized keeps its threads running
and its devices open, which is a program bug.  Typical usage:

```default
from pyjoulescope_driver import Driver

with Driver() as d:
    device_path = d.find_one_device('js320', brand='joulescope')
    with d.open(device_path, mode='restore') as device:
        device.publish('s/i/range/mode', 'auto')
        with device.subscribe('s/stats/value', 'pub', on_statistics):
            time.sleep(1.0)
```

Use [`device_paths()`](#pyjoulescope_driver.Driver.device_paths), [`find_devices()`](#pyjoulescope_driver.Driver.find_devices) and
[`find_one_device()`](#pyjoulescope_driver.Driver.find_one_device) to select devices, and [`device_watch()`](#pyjoulescope_driver.Driver.device_watch)
to track device additions and removals.  [`open()`](#pyjoulescope_driver.Driver.open) returns a
[`DeviceContext`](#pyjoulescope_driver.DeviceContext), and [`subscribe()`](#pyjoulescope_driver.Driver.subscribe) returns a
[`SubscribeContext`](#pyjoulescope_driver.SubscribeContext).

The driver thread invokes the subscriber callbacks and completes
every command, so a callback cannot wait on the driver.  A Driver
method called from a callback is forced asynchronous, whatever its
timeout: [`publish()`](#pyjoulescope_driver.Driver.publish), [`subscribe()`](#pyjoulescope_driver.Driver.subscribe) and [`unsubscribe()`](#pyjoulescope_driver.Driver.unsubscribe)
return at once and report any error only to the log, and
[`query()`](#pyjoulescope_driver.Driver.query) returns a zeroed value.  Use [`is_driver_thread()`](#pyjoulescope_driver.Driver.is_driver_thread)
to tell the two cases apart, and defer work that needs a result,
such as [`open()`](#pyjoulescope_driver.Driver.open), to another thread.

#### close(device_prefix, timeout=None)

Close an attached device.

* **Parameters:**
  * **device_prefix** – The prefix name for the device.
  * **timeout** – The timeout in seconds.  None uses the default timeout.

#### device_paths(specs=None, brand=None, timeout=None)

List the currently connected devices.

* **Parameters:**
  * **specs** – The device specifications.  None (default) lists
    all devices.  See [`device_filter.find()`](https://docs.joulescope.com/en/software/joulescope_driver/device_selection.md#pyjoulescope_driver.device_filter.find).
  * **brand** – The case-insensitive brand, such as “Joulescope”.
    None (default) lists all brands.
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.
* **Returns:**
  The sorted list of matching `DevicePath`
  instances, which are str instances.
* **Raises:**
  * **TypeError** – If specs or brand has an invalid type.
  * **ValueError** – If brand is not supported.

Before 2.5.0, the first positional argument was timeout.  A numeric
specs is still treated as timeout, with a DeprecationWarning.

#### device_watch(on_add, on_remove, specs=None, brand=None, timeout=None)

Watch for matching devices.

* **Parameters:**
  * **on_add** – The callable(device_path) called with the
    `DevicePath` for each added device.  Before returning,
    this method calls on_add for each connected device.
  * **on_remove** – The callable(device_path) called with the
    `DevicePath` for each removed device.
  * **specs** – The device specifications.  None (default) matches
    all devices.  See [`device_filter.find()`](https://docs.joulescope.com/en/software/joulescope_driver/device_selection.md#pyjoulescope_driver.device_filter.find).
  * **brand** – The case-insensitive brand, such as “Joulescope”.
    None (default) matches all brands.
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.
* **Returns:**
  The [`SubscribeContext`](#pyjoulescope_driver.SubscribeContext).  Call unsubscribe() or
  exit its context to stop watching.

This method calls on_add for the connected devices from the
calling thread, so these on_add calls may use any Driver method.
Afterwards, the driver thread calls on_add and on_remove, once for
each device addition and removal, in order.  Driver calls from
these callbacks are forced asynchronous, see
[`is_driver_thread()`](#pyjoulescope_driver.Driver.is_driver_thread), so defer work that needs a result,
such as [`open()`](#pyjoulescope_driver.Driver.open), to another thread.

#### finalize(timeout=None)

Finalize the driver and release all resources.

Finalize first closes each device that this instance opened and
that is still connected, ignoring close errors.  It then stops the
driver, which stops all subscriptions.  Afterwards, close,
unsubscribe and unsubscribe_all do nothing, and the other methods
raise RuntimeError.  Exiting a `with Driver() as d:` block calls
finalize.

* **Parameters:**
  **timeout** – The timeout in seconds.  None (default) uses
  the driver’s finalize timeout, which allows time for every
  device and backend thread to exit.

#### find_devices(specs=None, brand=None, timeout=None)

Find the connected devices.

This method is an alias for [`device_paths()`](#pyjoulescope_driver.Driver.device_paths), which reads
better next to [`find_one_device()`](#pyjoulescope_driver.Driver.find_one_device).

#### find_one_device(specs=None, brand=None, timeout=None)

Find exactly one connected device.

* **Parameters:**
  * **specs** – The device specifications.  None (default) matches
    all devices.  See [`device_filter.find()`](https://docs.joulescope.com/en/software/joulescope_driver/device_selection.md#pyjoulescope_driver.device_filter.find).
  * **brand** – The case-insensitive brand, such as “Joulescope”.
    None (default) matches all brands.
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.
* **Returns:**
  The matching `DevicePath`.
* **Raises:**
  [**device_filter.DeviceFilterError**](https://docs.joulescope.com/en/software/joulescope_driver/device_selection.md#pyjoulescope_driver.device_filter.DeviceFilterError) – If zero or multiple
  devices match.

#### is_driver_thread()

Check if the caller is the driver thread.

* **Returns:**
  True when called from the driver thread, which invokes
  the subscriber callbacks.  False otherwise, including after
  [`finalize()`](#pyjoulescope_driver.Driver.finalize).

Driver methods called from the driver thread are forced
asynchronous, whatever their timeout.  See [`Driver`](#pyjoulescope_driver.Driver).

#### log_level

The current log level.

See [`LogLevel`](#pyjoulescope_driver.LogLevel).

#### open(device_prefix, mode=None, timeout=None)

Open an attached device.

* **Parameters:**
  * **device_prefix** – The prefix name for the device.
  * **mode** – 

    The open mode which is one of:
    * ’defaults’: Push state to the device: the host’s retained
      value for each writable topic when present, else the
      metadata default.
    * ’restore’: Push nothing; adopt the device’s current state
      into the host cache.
    * ’raw’: Open the device in raw mode for development or firmware update.
    * None: equivalent to ‘defaults’.
  * **timeout** – The timeout in seconds.  None uses the default open
    timeout, 3 seconds, which allows a device time to recover from
    a host that exited without closing it.
* **Returns:**
  The [`DeviceContext`](#pyjoulescope_driver.DeviceContext) for the open device, which
  closes the device on close() or when used as a context manager:
  ```default
  with d.open(device_path) as device:
      device.publish('s/i/range/mode', 'auto')
  ```

  Finalize closes all devices that remain open.

#### publish(topic, value, timeout=None)

Publish a value to a topic.

* **Parameters:**
  * **topic** – The topic string.
  * **value** – The value, which must pass validation for the topic.
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout and waits for the return code.  0 does
    not wait, so publish cannot raise for an invalid topic or value.
    The driver logs the error at ERROR level to the “jsdrv” logger
    instead, such as “publish u/js320/8W2A/s/nope failed: 16 NOT_FOUND”.
    Use a blocking publish when the caller must handle the error.
* **Raise:**
  On error.

#### publish_and_wait(publish_topic, publish_value, response_topic, timeout=None, match=None)

Publish a value and wait for a response on another topic.

* **Parameters:**
  * **publish_topic** – The topic to publish to.
  * **publish_value** – The value to publish.
  * **response_topic** – The topic to subscribe to for
    the response.
  * **timeout** – The timeout in float seconds.
    None (default) uses the default timeout.
  * **match** – The optional callable(value) that returns True
    for the expected response.  Other responses, such as those
    for another transaction, are ignored.  None (default)
    accepts the first response.
* **Returns:**
  The value received on response_topic.
* **Raises:**
  **TimeoutError** – If no response arrives in time.

#### query(topic, timeout=None)

Query the value for a topic.

* **Parameters:**
  * **topic** – The topic name.
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.
* **Returns:**
  The value for the topic.
* **Raise:**
  On error.

#### subscribe(topic, flags, fn, timeout=None)

Subscribe to receive topic updates.

* **Parameters:**
  * **self** – The driver instance.
  * **topic** – Subscribe to this topic string.
  * **flags** – 

    The flags or list of flags for this subscription.
    The flags can be int32 jsdrv_subscribe_flag_e or string
    mnemonics, which are:
    - pub: Subscribe to normal values
    - pub_retain: Subscribe to normal values and immediately publish
      all matching retained values.  With timeout, this function does
      not return successfully until all retained values have been
      published.
    - metadata_req: Subscribe to metadata requests (not normally useful).
    - metadata_rsp: Subscribe to metadata updates.
    - metadata_rsp_retain: Subscribe to metadata updates and immediately
      publish all matching retained metadata values.
    - query_req: Subscribe to all query requests (not normally useful).
    - query_rsp: Subscribe to all query responses.
    - return_code: Subscribe to all return code responses.
  * **fn** – The function to call on each publish.  Note that python
    dynamically constructs bound methods.  To unsubscribe a method,
    provide the exact  same bound method instance to unsubscribe.
  * **timeout** – The timeout in float seconds to wait for this operation
    to complete.  None waits the default amount.
    0 does not wait and subscription will occur asynchronously.
* **Returns:**
  The [`SubscribeContext`](#pyjoulescope_driver.SubscribeContext), which unsubscribes on
  unsubscribe() or when used as a context manager:
  ```default
  with d.subscribe(topic, 'pub', fn):
      ...
  ```
* **Raises:**
  **RuntimeError** – on subscribe failure.

#### unsubscribe(topic, fn, timeout=None)

Unsubscribe from a topic.

* **Parameters:**
  * **topic** – The topic name string.
  * **fn** – The function previously provided to [`subscribe()`](#pyjoulescope_driver.Driver.subscribe).
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.  0 returns at once, and fn may still
    be called until the driver thread handles the request.
* **Raise:**
  On error.

When this method returns after waiting, fn is no longer called.
From a callback, this method cannot wait, see
[`is_driver_thread()`](#pyjoulescope_driver.Driver.is_driver_thread).

#### unsubscribe_all(fn, timeout=None)

Unsubscribe a callback from all topics.

* **Parameters:**
  * **fn** – The function previously provided to [`subscribe()`](#pyjoulescope_driver.Driver.subscribe).
  * **timeout** – The timeout in seconds.  None (default) uses
    the default timeout.  0 returns at once, and fn may still
    be called until the driver thread handles the request.
* **Raise:**
  On error.

See [`unsubscribe()`](#pyjoulescope_driver.Driver.unsubscribe).

### *class* pyjoulescope_driver.ElementType

The element type enumeration.

### *class* pyjoulescope_driver.ErrorCode

The error code enumeration.

### *class* pyjoulescope_driver.Field

The field enumeration.

### *class* pyjoulescope_driver.LogLevel

The log level enumeration.

### *class* pyjoulescope_driver.MemClient(driver, device_path, cmd_topic, target=0)

Generic mb_stdmsg_mem_s protocol client.

* **Parameters:**
  * **driver** – The Driver instance.
  * **device_path** – The device path (e.g. ‘u/js320/000000’).
  * **cmd_topic** – The memory command sub-topic
    (e.g. ‘s/flash/!cmd’).
  * **target** – The target memory region ID (0-255).

#### cmd(op, offset=0, length=0, data=b'', timeout_ms=5000, target=None)

Send a memory command and wait for the response.

* **Parameters:**
  * **op** – The memory operation (MEM_OP_READ, etc.).
  * **offset** – The byte offset within the target region.
  * **length** – The operation length in bytes.
  * **data** – The payload data for write operations.
  * **timeout_ms** – The device-side timeout in milliseconds.
  * **target** – Override the instance target for this call.
* **Returns:**
  The response data bytes.
* **Raises:**
  * **TimeoutError** – If no response within timeout.
  * **RuntimeError** – If the device returns a non-zero status.

#### erase_64k(offset)

Erase a 64 KB block.

* **Parameters:**
  **offset** – The block-aligned byte offset.

#### read(offset, length)

Read data in 256-byte page chunks.

* **Parameters:**
  * **offset** – The starting byte offset.
  * **length** – The number of bytes to read.
* **Returns:**
  The read bytes.

#### read_personality()

Read the public mb_personality_app_s from the sys task.

The MemClient must be pointed at the sys task command topic
(e.g. `MemClient(driver, device, 'c/sys/!cmd')`).

* **Returns:**
  A dict with the public personality fields.
* **Raises:**
  **ValueError** – If the response is too short.

#### write_pages(offset, data)

Write data in 256-byte page chunks.

* **Parameters:**
  * **offset** – The starting byte offset.
  * **data** – The bytes to write.

#### write_verify(offset, data, retries=3)

Erase, write, and verify with retries.

* **Parameters:**
  * **offset** – The flash byte offset.
  * **data** – The data to write.
  * **retries** – Number of attempts before raising.
* **Raises:**
  **RuntimeError** – If verification fails after all retries.

#### write_verify_records(records, retries=3)

Erase, write, and verify multiple records with retries.

* **Parameters:**
  * **records** – The list of (offset, data) pairs.
  * **retries** – Number of attempts before raising.
* **Raises:**
  **RuntimeError** – If verification fails after all retries.

### *class* pyjoulescope_driver.StdMsg

Binary value published as JSDRV_UNION_STDMSG.

Use this instead of `bytes` when publishing a value that
starts with an `mb_stdmsg_header_s` so the device receives
it as `MB_VALUE_STDMSG`.

### *class* pyjoulescope_driver.SubscribeContext(driver, subscriptions)

The subscriptions returned by [`Driver.subscribe()`](#pyjoulescope_driver.Driver.subscribe).

Call [`unsubscribe()`](#pyjoulescope_driver.SubscribeContext.unsubscribe) or exit the context to unsubscribe.  After
[`Driver.finalize()`](#pyjoulescope_driver.Driver.finalize), unsubscribe does nothing.

#### extend(other)

Move the subscriptions from another SubscribeContext into this one.

#### unsubscribe(timeout=None)

Unsubscribe.  Calling unsubscribe again does nothing.

### *class* pyjoulescope_driver.SubscribeFlags

The available subscribe flags.

### *class* pyjoulescope_driver.TimeMap

Python wrapper for jsdrv_tmap_s instance.

Each instance owns its underlying jsdrv_tmap_s.  TimeMap values
returned from driver responses are independent snapshots; copy()
and deepcopy() produce further independent instances.

#### time_map_get(index=None)

Get the time map data.

* **Parameters:**
  **index** – 

  The index for the time map which is one of:
  - None: return all time maps in the instance.
  - int index: Return only this index.
  - (int start_index, int end_index): Return this range.
    start_index is inclusive, end_index is exclusive.
* **Returns:**
  Return an np.ndarray with the the named columns
  [‘offset_time’, ‘offset_counter’, ‘counter_rate’].
  Access as either a[0][‘offset_counter’] or a[0][1].
