Python: the rb9704 command line, a fake modem, and the tests

rb9704 drives a 9704 on USB and the Cloudloop account behind it, moved
from WII6's host/rockblock with its project names taken out. rb9704-fake
speaks JSPR on a pty or a serial port: everything the library asks, MO in
segments with the CRC checked, MT, signal, provisioning, refusals and
failures on command.

The tests run against the fake at every level: the fake itself, the
command line, Ground Control's Python binding, the shell sample on
native_sim, and the sample on a board (test_zephyr.py, one suite through
two consoles). test_real.py makes free checks against a real modem.
This commit is contained in:
scottp committed 2026-10-09 12:48:16 +11:00
1 parent dbed9bbc88
commit 917522d1f4
12 files changed
+1861

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
# rockblock9704-tools: the RockBLOCK 9704 from a computer
Two commands and a test suite, in one Python package:
- `rb9704`, a command line for a RockBLOCK 9704 plugged into USB and for
the Cloudloop account behind it, for proving the modem, the account and
the delivery path with no firmware in the loop;
- `rb9704-fake`, a fake modem that speaks JSPR on a pty or a serial port,
for testing anything that drives a 9704 without one;
- `tests/`, which run Ground Control's own Python binding, Zephyr on
`native_sim`, and Zephyr on a board against the fake
(`../docs/TESTING.md`).
`../docs/CLOUDLOOP.md` is the account, the provisioning and the order to
test things in; this is the tools.
## Install
```sh
python/install.sh # python/.venv, this package, rockblock9704, pytest
python/install.sh --udev # ... and keep ModemManager off the port (sudo)
```
Or by hand: `pip install -e 'python[modem,test]'`. Python 3.10 to 3.14,
which is where `rockblock9704` has wheels (Linux x86_64 and aarch64,
macOS, Windows). Without the `modem` extra, the fake modem, `jspr`,
`provision` and the cloud verbs work and the library verbs do not.
## Verbs
| Verb | Costs | What it does |
| --- | --- | --- |
| `ports` | | USB serial ports, the FT234XD (`0403:6015`) first |
| `info [--json]` | | hardware and firmware, serial, IMEI, ICCID, SIM, temperature, signal |
| `signal [--watch S] [--every S]` | | signal bars 0 to 5, once or for S seconds with a summary |
| `provision [--timeout S]` | | the setup `begin` does, then signal and provisioning every 30 s until topics appear |
| `jspr [--setup] [LINE ...]` | | raw JSPR: the lines given, or a prompt; every answer and unsolicited line printed |
| `send --text/--hex/--file [--topic N]` | airtime | one MO, blocking, through the library |
| `listen [--out DIR] [--for S]` | | MT as it arrives, each written to `DIR/MT-<utc>.bin` |
| `session --text/--hex/--file [--quiet S]` | airtime | one MO, then MT until S seconds pass with nothing new |
| `cloud ping` | | the token works |
| `cloud things` | | every Thing on the account, with its Certus subscriber id |
| `cloud mo [--thing ID] [--hours H]` | | message records for a Thing over the last H hours |
| `cloud msg ID` | | one message record and its Lingo form |
| `cloud mt --text/--hex/--file [--thing ID]` | airtime | queue an MT on `IMT_TOPIC_RAW` |
| `cloud status ID` | | an MT's delivery status |
The verbs that cost airtime print the size and ask; `--yes` skips the
question and is the only way to send from a script.
Options that go with any verb, before or after it:
- `-p PORT`: the serial port. Otherwise `$RB9704_PORT`, otherwise the one
9704 bridge attached (`0403:6015`); two attached is an error.
- `--log FILE`: append one JSON line per event (`{"t": ..., "ev": ...}`,
UTC), for the bench log.
The cloud verbs read the token from `$CLOUDLOOP_TOKEN` or
`~/.config/rb9704/cloudloop.token`, and the Thing from `--thing` or
`$CLOUDLOOP_THING`.
## The fake modem
```sh
rb9704-fake # a pty; prints READY /dev/pts/N
rb9704-fake --port /dev/ttyUSB0 # a real port, e.g. an adapter wired to a board
rb9704-fake --topics 244,313 # the provisioning it reports
```
It answers everything Ground Control's library asks (`../docs/JSPR.md`),
takes MO messages segment by segment and checks their CRC, and on a
command on stdin sends an MT, a signal change, a refusal or a failed MO.
The commands and the events it prints are in `rb9704tools/fakemodem.py`'s
docstring. Run it in a process of its own beside the library's Python
binding, which holds the GIL through every blocking call.
## How it is built
- The modem verbs (`info`, `signal`, `send`, `listen`, `session`) use
Ground Control's `rockblock9704`, a binding of the C library this
repository builds into Zephyr. `listen` and `session` use its
callback API, polled every 10 ms as the library asks, with the
callbacks only recording and the loop doing the work.
- `provision` and `jspr` speak JSPR over pyserial, because the library
asks for the provisioning only inside a send. `Jspr.setup` repeats
`rbBegin`'s sequence: an active API version, the internal SIM, the
state set active. JSPR lines are `<code> <target> <json>` ending in CR;
299 is unsolicited.
- The cloud verbs are plain `urllib` POSTs to `api.cloudloop.com`, every
argument in the query string as the API's Swagger
(<https://api.cloudloop.com/swagger/index.html>) describes it.
Only one program can hold the port: `jspr` and the library verbs cannot
run at once.