scottp beb22fb70c Docs: the README, JSPR, Cloudloop, testing
README says what the module adds to Ground Control's library and what it
works around, and how to use it. JSPR.md is the protocol as the library
speaks it, from its source, with its surprises. CLOUDLOOP.md is account
to first message, from WII6's IRIDIUM.md. TESTING.md is the levels, the
rig, and what has passed: Python, native_sim, an ESP32-S3 and an STM32U5
against the fake, and free checks against a real 9704.
2026-10-09 12:48:16 +11:00

rockblock9704-zephyr

The RockBLOCK 9704, Ground Control's Iridium Certus modem, on Zephyr: a module that builds Ground Control's own C library for any board with a UART, gives it the one thread it needs, and offers a small API, a shell command and a devicetree binding. With it come host tools in Python and a fake modem, so the same tests run against Ground Control's Python binding, against Zephyr on native_sim, and against Zephyr on a board.

The 9704 sends and receives IMT messages of up to 100 kB through the Iridium network, and speaks JSPR, JSON lines over a 230400 8N1 UART. There is no AT mode; it is not a 9602 or 9603.

Ground Control's library

This module does not reimplement or fork the protocol. Ground Control's repository, rock7/RockBLOCK-9704 (MIT), is pinned untouched in west.yml at modules/lib/rockblock9704, and CMakeLists.txt compiles its portable core. What each part does:

Ground Control's library This module
JSPR, segmenting, CRC, provisioning check yes uses it unchanged
Platforms Linux, Windows, Arduino serial presets a Zephyr serial port, on the UART in devicetree
Threads none: no locking, shared static buffers one worker thread owns every call into it
API blocking calls, or async calls with callbacks and rbPoll() requests with a timeout each, callbacks from the worker, a status snapshot
Polling the caller calls rbPoll(), one line per call the worker wakes on a byte from the modem, so an idle modem costs nothing
Power pins rbBeginGpio with libgpiod, Linux only power-gpios, iridium-enable-gpios, booted-gpios in devicetree
Python rockblock9704 on PyPI, a binding of the C the rb9704 command line uses it; the fake modem tests it

The library's source shows several things a caller has to work around; docs/JSPR.md, section 8, lists them all. The ones this module handles:

  • Provisioning is asked once and kept for good, a miss included. A modem provisioned after the first send would refuse every topic until a reboot. Before a send whose topic is not in what the library kept, and after the modem refuses one, the module clears the library's cache so it asks again: one free JSPR round trip.
  • The blocking calls stop working once callbacks are registered. The module uses the async path only.
  • A query during a send swallows the modem's segment requests, since the reader discards lines it is not waiting for. Info and signal requests are refused with -EBUSY while a message is moving.
  • No locking. Every library call is on the worker thread.

Others are for upstream to fix, and are listed in docs/JSPR.md for that: setState waits for a misspelt target, a line with no status code reads past its buffer, MT segment data is cut at 1446 characters, and the Python binding never releases the GIL.

Using it

In the application's west.yml; the import brings Ground Control's library, pinned, at the path the module looks in, and nothing else:

    - name: rockblock9704-zephyr
      url: https://github.com/SH3D/rockblock9704-zephyr
      revision: v0.1.0
      path: modules/lib/rockblock9704-zephyr
      import:
        name-allowlist:
          - rockblock9704

In the board overlay, a node on the modem's UART. The pins are optional; without them the modem is taken to be powered:

&usart3 {
	current-speed = <230400>;
	status = "okay";

	rockblock {
		compatible = "rock7,rockblock-9704";
		power-gpios = <&gpiob 0 GPIO_ACTIVE_HIGH>;          /* P_EN */
		iridium-enable-gpios = <&gpiob 1 GPIO_ACTIVE_HIGH>; /* I_EN */
		booted-gpios = <&gpiob 2 GPIO_ACTIVE_HIGH>;         /* I_BTD */
	};
};

CONFIG_RB9704 follows the node. Then <rb9704/rb9704.h>:

static void mo_done(uint16_t topic, uint16_t id, int result, void *user)
{
	/* result 0: the network has it */
}

static void mt(const struct rb9704_mt *m, void *user)
{
	/* m->data, m->len: copy what you need, it is acknowledged after */
}

static const struct rb9704_callbacks cb = { .mo_done = mo_done, .mt = mt };

rb9704_set_callbacks(&cb);
rb9704_begin(K_SECONDS(30));
rb9704_send(RB9704_TOPIC_RAW, buf, len, K_SECONDS(10));	/* costs airtime */
rb9704_send(RB9704_TOPIC_RAW, buf, len, K_NO_WAIT);	/* queues, never blocks */

Every request takes a timeout and waits no longer; with K_NO_WAIT a send queues and returns -EINPROGRESS, and its outcome, a refusal included, comes through mo_done. Callbacks run on the worker thread. One modem per system: the library keeps global state.

Kconfig Default
CONFIG_RB9704_MESSAGE_MAX 2048 largest message each way; three static buffers of this size
CONFIG_RB9704_WORKER_STACK_SIZE 8192 measured: the library parks 1.5 kB on the stack in rbPoll()
CONFIG_RB9704_WORKER_PRIORITY 8
CONFIG_RB9704_RX_RING_SIZE 8192 the library's longest JSPR line
CONFIG_RB9704_POWER with power-gpios drive the pins
CONFIG_RB9704_SHELL with CONFIG_SHELL rb9704 on the shell
CONFIG_RB9704_UPSTREAM_DIR empty where Ground Control's library is, if not where west.yml puts it

The shell

rb9704 begin | end | power on|off
rb9704 status            what is known, without asking the modem
rb9704 info              IMEI, serial, versions, ICCID, SIM, temperature
rb9704 signal            bars now
rb9704 send <topic> <text>  |  sendhex <topic> <hex>     costs airtime
rb9704 spy on|off        every JSPR line, both ways

samples/shell is the shell plus callbacks that print what arrives.

Host tools

python/ is rb9704 (the modem and Cloudloop from a computer), rb9704-fake (a fake modem on a pty or a serial port), and the tests. python/README.md has every verb; docs/CLOUDLOOP.md is the account, the provisioning and the order to test a new modem in.

Testing

python/install.sh
python/.venv/bin/pytest -q python/tests                           # Python
python/.venv/bin/pytest -q python/tests --zephyr-exe <native_sim zephyr.exe>
python/.venv/bin/pytest -q python/tests/test_zephyr.py \
    --board-console /dev/ttyACM0 --board-modem /dev/ttyUSB0       # a board
python/.venv/bin/pytest -q python/tests/test_real.py --real-modem /dev/ttyUSB0

docs/TESTING.md has the levels, the two-board rig that needs no USB-serial adapter, and what has passed: the Python level, native_sim, an ESP32-S3 and an STM32U5 against the fake, and free checks against a real 9704 from Python and from native_sim.

Boards

Any board with a free UART. The sample has overlays for native_sim, NUCLEO-U575ZI-Q, Seeed XIAO ESP32S3 and ESP32-S3-DevKitC, and has run on the first three.

Layout

CMakeLists.txt, Kconfig, zephyr/module.yml   the module
dts/bindings/rock7,rockblock-9704.yaml       the devicetree binding
include/rb9704/rb9704.h                      the API
src/                                         serial port, shims, worker, power, shell
samples/shell/                               the shell sample, with board overlays
tests/rig/                                   the two-board test rig
python/                                      rb9704, rb9704-fake, the tests
docs/JSPR.md                                 the protocol as the library speaks it
docs/CLOUDLOOP.md                            account to first message
docs/TESTING.md                              the tests and how to run them
west.yml                                     the pins

Licence

MIT, as Ground Control's library is. The serial port and worker began in the OGLAS satellite bench and were carried through WII6 before moving here.

S
Description
Zephyr module for the RockBLOCK 9704 Iridium modem, built on Ground Control’s official library: thread-safe API, devicetree, shell, and a fake modem for testing
Readme MIT
115 KiB
0 Stars 2 Watchers 0 Forks
Languages
Python 56.5%
C 36.7%
Shell 3.8%
CMake 3%