The README says who wrote it and the two projects it was built for, WII6 and OGLAS, with their sites; credits Ground Control for the modem and the library that does the protocol work; and gives the licence with contact details, where to report issues and what helps in a report, and that it is not a Ground Control product. The licence, every source file and the Python package's metadata carry the copyright and email; the package lists its homepage, repository, issues and changelog.
213 lines
8.8 KiB
Markdown
213 lines
8.8 KiB
Markdown
# 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 `<rb9704/rb9704.h>`:
|
|
|
|
```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 <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
|
|
|
|
```sh
|
|
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, 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 <scottp@dd.com.au>
|
|
|
|
* 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.
|