# 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. rockblock9704-zephyr is written and maintained by Scott Penrose ([Digital Dimensions](https://www.dd.com.au/)). It was built for two projects that send data home by satellite: [WII6](https://wii6.au/), a wave-measuring buoy, and [OGLAS](https://oglas.au/), a fleet of LoRa sensors, repeaters and hubs. Both run it, and neither is needed to use it. ## Ground Control's library This module does not reimplement or fork the protocol. Ground Control's repository, [rock7/RockBLOCK-9704](https://github.com/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: ```yaml - 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: ```dts &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 ``: ```c 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 | sendhex 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 ```sh python/install.sh python/.venv/bin/pytest -q python/tests # Python python/.venv/bin/pytest -q python/tests --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, ESP32-S3-DevKitC, ESP32 DevKitC and LilyGO T3-S3 (an out-of-tree board in OGLAST2: build with its `-DBOARD_ROOT`). It has run on the first three; all six build. ## 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 ``` ## Credits - [Ground Control](https://www.groundcontrol.com/), for the RockBLOCK 9704 and its [C library](https://github.com/rock7/RockBLOCK-9704) (MIT), which does all of the protocol work. This module builds it unchanged. - The serial port and the worker began in OGLAS's satellite bench and were carried through WII6 before moving here. ## Licence MIT, as Ground Control's library is. See `LICENSE`. Copyright (c) 2026 Scott Penrose * https://www.sh3d.com.au/ - Sh3d * https://www.dd.com.au/ - Digital Dimensions ## Support Report issues on the [GitHub mirror](https://github.com/SH3D/rockblock9704-zephyr/issues) (the Gitea instance does not allow public sign-ups). Most helpful in a report: the output of `rb9704 spy on` around the problem, and the modem's firmware version from `rb9704 info`. ## Disclaimer This is not an official Ground Control product. Use at your own risk, and remember that every `send` costs airtime.