Getting started

This guide shows the basic lifecycle for the Joulescope driver in Python (pyjoulescope_driver) and in C (joulescope_driver): find a device, open it, configure it, receive statistics and streaming samples, and shut down cleanly. It applies to the JS110, JS220 and JS320. The driver also has Node.js bindings in node_api/, which this guide does not cover.

The payload formats for statistics and streaming samples are in streaming_topics.md. The per-model topic references are js220.txt and js320.json.

Install

Python:

python -m pip install pyjoulescope_driver
python -m pyjoulescope_driver scan

On Linux, also install the udev rules described in the README. scan lists the connected device paths.

C: build the static library, the jsdrv command-line tool and the examples with CMake as described in the README. example/quickstart.c is the C version of this guide and builds as jsdrv_quickstart.

Concepts

  • Publish-subscribe: every setting, command and measurement is a topic with a value. Set a value with publish, read a retained value with query, and receive updates with subscribe.

  • Device path: {backend}/{model}/{serial_number}, such as u/js320/8W2A. Device topics are {device_path}/{topic}, such as u/js320/8W2A/s/i/range/mode. The driver lists every USB device it recognizes, including non-Joulescope devices such as u/mb/93NP, so select by brand or model.

  • Retained and command topics: a topic whose last segment starts with ! is a command or a data stream and is not retained. All other topics retain their last value. Subscribing with the retain flag delivers the retained value immediately.

  • Topic prefixes: h/ host-side settings such as the sample rate, s/ sensor streaming and ranging, c/ controller, and @/ driver and device lifecycle. The full list for a connected device: python -m pyjoulescope_driver metadata.

  • Open modes: defaults pushes the host’s retained values, else the metadata defaults, to the device. restore reads the device’s current state into the host and leaves it unchanged. raw is for firmware update only.

  • Callbacks run on the driver thread. Keep them short and copy the data out. A driver call from a callback cannot wait, so the driver forces it asynchronous whatever its timeout: publish, subscribe and unsubscribe return at once and report errors only to the log, and query returns zeros. jsdrv_is_driver_thread() and Driver.is_driver_thread() tell the two cases apart.

  • Finalize: each Python Driver must be finalized, by the context manager or finalize(), and each C context must be passed to jsdrv_finalize(). Finalize stops the driver threads and closes the devices that are still open.

Python

The program below measures for two seconds on the first Joulescope. It works on all three models.

import time
from pyjoulescope_driver import Driver


def on_statistics(topic, value):
    # value is a nested dict, see doc/streaming_topics.md
    i = value['signals']['current']['avg']['value']
    v = value['signals']['voltage']['avg']['value']
    print(f'{i:.6f} A, {v:.3f} V')


def on_current(topic, value):
    # value['data'] is a numpy float32 array in amperes
    print(f"{len(value['data'])} samples from sample_id {value['sample_id']}")


with Driver() as d:
    # Select by brand, or by spec such as 'js320', '8W2A' or 'js320-8W2A'.
    device_path = d.find_one_device(brand='joulescope')
    print(f'Using {device_path}: model={device_path.model}')

    with d.open(device_path, mode='defaults') as device:
        # Topics passed to device are relative to device_path.
        if device_path.model == 'js110':
            device.publish('s/i/range/select', 'auto')
        else:
            device.publish('s/i/range/mode', 'auto')
        for signal in ['i', 'v', 'p']:
            device.publish(f's/{signal}/ctrl', 1)      # enable streaming
        device.publish('s/stats/scnt', 500_000)         # samples per block
        device.publish('s/stats/ctrl', 1)               # enable statistics
        device.subscribe('s/stats/value', 'pub', on_statistics)
        device.subscribe('s/i/!data', 'pub', on_current)
        time.sleep(2.0)
    # Leaving the block unsubscribes and closes the device.
# Leaving the block finalizes the driver.

The same operations with absolute topics:

d.publish(f'{device_path}/s/i/range/mode', 'auto')
d.subscribe(f'{device_path}/s/stats/value', 'pub', on_statistics)
value = d.query(f'{device_path}/s/i/range/mode')

Topic values accept the metadata option names, such as 'auto', or the underlying numeric value. Boolean topics accept 1 and 0. python -m pyjoulescope_driver values --device js320 prints every retained value for a device, and metadata prints each topic’s type, default and options.

See the Driver, DeviceContext and SubscribeContext docstrings for the full API, and pyjoulescope_driver/entry_points/ for complete programs.

C

example/quickstart.c is the complete, buildable program. It uses only the public headers and performs these steps:

#include "jsdrv.h"
#include "jsdrv/union.h"
#include "jsdrv/topic.h"
#include "jsdrv/os_thread.h"

struct jsdrv_context_s * context = NULL;
jsdrv_initialize(&context, NULL, JSDRV_TIMEOUT_MS_INIT);

// List the devices: a comma-separated string of device paths.
char devices[1024];
struct jsdrv_union_s list = jsdrv_union_str(devices);
list.size = sizeof(devices);
jsdrv_query(context, JSDRV_MSG_DEVICE_LIST, &list, JSDRV_TIMEOUT_MS_DEFAULT);

// Open, configure and subscribe.  Build each topic from the device path.
jsdrv_open(context, device, JSDRV_DEVICE_OPEN_MODE_DEFAULTS, JSDRV_TIMEOUT_MS_DEFAULT);
struct jsdrv_topic_s t;
jsdrv_topic_set(&t, device);
jsdrv_topic_append(&t, "s/i/range/mode");
jsdrv_publish(context, t.topic, &jsdrv_union_cstr_r("auto"), JSDRV_TIMEOUT_MS_DEFAULT);
jsdrv_topic_set(&t, device);
jsdrv_topic_append(&t, "s/stats/ctrl");
jsdrv_publish(context, t.topic, &jsdrv_union_u8_r(1), JSDRV_TIMEOUT_MS_DEFAULT);
jsdrv_topic_set(&t, device);
jsdrv_topic_append(&t, "s/stats/value");
jsdrv_subscribe(context, t.topic, JSDRV_SFLAG_PUB, on_statistics, user_data,
                JSDRV_TIMEOUT_MS_DEFAULT);

jsdrv_thread_sleep_ms(2000);

jsdrv_close(context, device, JSDRV_TIMEOUT_MS_DEFAULT);
jsdrv_finalize(context, 0);

Subscriber callbacks receive a struct jsdrv_union_s. Statistics and streaming payloads are binary with value->app set to the payload type. Cast value->value.bin to struct jsdrv_statistics_s or struct jsdrv_stream_signal_s as described in streaming_topics.md.

Link against the jsdrv CMake target. include/jsdrv.h documents the API, and example/jsdrv/ contains the larger jsdrv tool.

Command-line tools

Command

Purpose

python -m pyjoulescope_driver scan

List the connected device paths.

python -m pyjoulescope_driver info

Driver, package and device summary.

python -m pyjoulescope_driver values -d js320

Every retained value for one device.

python -m pyjoulescope_driver metadata

Topic types, defaults and options.

python -m pyjoulescope_driver set -d js320 s/i/range/mode=auto

Set a value.

python -m pyjoulescope_driver statistics --duration 2

Print statistics.

python -m pyjoulescope_driver record --duration 10s out.jls

Capture to a JLS file.

python -m pyjoulescope_driver program

Update JS220 or JS320 firmware.

jsdrv scan, jsdrv statistics

The C equivalents.

Each command accepts --help. Most take --device (-d) with a device path, backend/model such as u/js320/, model, serial number or model-serial_number, and the generic commands also take --brand.

Model differences

JS110

JS220

JS320

Native sample rate

2 MHz

1 MHz

1 MHz

Sample rate topic

h/fs

h/fs

h/fs

Current range topic

s/i/range/select

s/i/range/mode

s/i/range/mode

Range after defaults open

auto

off

off

Statistics

s/sstats/value, or host-side (see below)

s/stats/value

s/stats/value

s/stats/scnt units

2 Msps samples

1 Msps samples

1 Msps samples

measure entry point

yes

yes

no

The JS220 and JS320 open with the current range off, so publish s/i/range/mode auto (or a fixed range) before expecting current readings. Before 2.5.0, a JS220 defaults open did not push anything, so the instrument kept the settings from the previous session until it was power cycled. Use restore to keep the instrument’s settings.

The JS220 and JS320 compute statistics on the instrument. The JS110 s/sstats/value is on-instrument at a fixed 2 Hz without standard deviation. For host-side JS110 statistics, enable s/i/ctrl, s/v/ctrl and s/p/ctrl, then s/stats/ctrl, as the example above does.

Pitfalls

  • A callback cannot wait for a result. Callbacks run on the driver thread, which completes every command, so the driver forces commands issued from a callback to timeout=0 (JSDRV_TIMEOUT_MS_ASYNC). A publish from a callback works, but its error is only logged, and a query returns zeros. Queue work that needs a result to another thread.

  • A C timeout of 0 means asynchronous for jsdrv_publish, jsdrv_subscribe, jsdrv_unsubscribe, jsdrv_open and jsdrv_close, but means the default timeout for jsdrv_initialize, jsdrv_query and jsdrv_finalize. Use JSDRV_TIMEOUT_MS_DEFAULT to block.

  • Finalize is mandatory. A Driver that is never finalized leaves threads running and devices open. Use with Driver() as d:.

  • The JS220 and JS320 current range defaults to off. See the table above.

  • defaults vs restore. defaults overwrites the device configuration. Use restore to observe a device that another process or the user configured, then set only what you need.

  • Device paths are not only Joulescopes. device_paths() returns every recognized device. Pass brand='joulescope' or a model.

  • record needs pyjls, which is not a declared dependency: python -m pip install pyjls.

  • Time is time64: a signed 64-bit integer with 2^30 ticks per second since 2018-01-01 00:00:00 UTC. Convert with pyjoulescope_driver.time64 or include/jsdrv/time.h.

  • Linux permissions. Without the udev rules, scan lists nothing.

  • Multiple Joulescopes make find_one_device(brand='joulescope') raise DeviceFilterError. Pass a serial number or model.

  • Closing a removed device raises in Python and returns an error in C. Subscribe to @/!remove, or use Driver.device_watch, to track removal.