The module: Ground Control's library on Zephyr, with one worker thread

Ground Control's RockBLOCK 9704 library is pinned untouched in west.yml
and compiled here for any board with a UART: a serial port on the bus of
the rock7,rockblock-9704 devicetree node, the library's timing calls on
the kernel, and one worker thread that owns every call into it, since the
library has no locking and shares static buffers.

The worker sleeps until a request or a byte from the modem arrives.
Requests (begin, end, power, send, info, signal) wait as long as the
caller says; a send with K_NO_WAIT queues and reports through mo_done.
Callbacks for a sent message, a received one, the signal and the
provisioning; a status snapshot that never touches the modem.

The library asks for provisioning once and keeps a miss for good; the
worker clears its cache before a send whose topic is missing and after
the modem refuses one, so a modem provisioned later is seen.

The serial port and worker began in the OGLAS satellite bench and were
carried through WII6's lib/wii6-sat.
This commit is contained in:
scottp committed 2026-10-09 12:48:16 +11:00
commit c0e92a7f02
16 files changed
+1608

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_glue.h - what Ground Control's library needs declared on Zephyr.
*
* Force-included into every library file (CMakeLists.txt): none of its
* platform branches (__linux__, _WIN32, ARDUINO) is taken on Zephyr, so its
* calls to the serial-setup hook and to millis() and delay() would otherwise
* be implicit declarations. rb9704_serial.c and rb9704_shims.c define them.
* usleep() is not declared here: picolibc's <unistd.h> does.
*/
#ifndef RB9704_GLUE_H_
#define RB9704_GLUE_H_
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* SERIAL_CONTEXT_SETUP_FUNC: fills the library's serialContext with this
* port. `port` is ignored; the UART is the devicetree node's bus. */
bool rb9704_serial_set_context(const char *port, uint32_t baud);
unsigned long millis(void);
void delay(uint32_t ms);
#ifdef __cplusplus
}
#endif
#endif /* RB9704_GLUE_H_ */
+31
View File
@@ -0,0 +1,31 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_internal.h - between this module's own files.
*/
#ifndef RB9704_INTERNAL_H_
#define RB9704_INTERNAL_H_
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/devicetree.h>
#include <zephyr/kernel.h>
#define RB9704_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(rock7_rockblock_9704)
/* rb9704_serial.c */
int rb9704_serial_available(void);
uint32_t rb9704_serial_dropped(void);
void rb9704_serial_spy(bool on);
/* rb9704_worker.c */
k_tid_t rb9704_worker_tid_get(void);
void rb9704_worker_rx(void);
/* rb9704_power.c, or the stubs in rb9704_worker.c without the pins */
int rb9704_power_init(void);
int rb9704_power_set(bool on);
bool rb9704_power_on(void);
bool rb9704_power_present(void);
#endif /* RB9704_INTERNAL_H_ */
+94
View File
@@ -0,0 +1,94 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_power.c - the board's control pins, as the devicetree gives them.
*
* power-gpios P_EN, the supply
* iridium-enable-gpios I_EN, the modem, once the supply is up
* booted-gpios I_BTD, the modem saying it has booted
*
* Up: supply, a moment, enable, then the booted pin or boot-delay-ms.
* Down: enable, then supply. Called on the worker thread only.
*/
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <errno.h>
#include "rb9704_internal.h"
LOG_MODULE_DECLARE(rb9704, CONFIG_RB9704_LOG_LEVEL);
#define BOOT_TIMEOUT_MS 30000
static const struct gpio_dt_spec pwr = GPIO_DT_SPEC_GET_OR(RB9704_NODE, power_gpios, {0});
static const struct gpio_dt_spec en = GPIO_DT_SPEC_GET_OR(RB9704_NODE, iridium_enable_gpios, {0});
static const struct gpio_dt_spec btd = GPIO_DT_SPEC_GET_OR(RB9704_NODE, booted_gpios, {0});
static bool on;
bool rb9704_power_present(void)
{
return pwr.port != NULL;
}
bool rb9704_power_on(void)
{
return on;
}
int rb9704_power_init(void)
{
if (pwr.port == NULL) {
return -ENOTSUP;
}
if (!gpio_is_ready_dt(&pwr) || gpio_pin_configure_dt(&pwr, GPIO_OUTPUT_INACTIVE) != 0) {
return -EIO;
}
if (en.port != NULL &&
(!gpio_is_ready_dt(&en) || gpio_pin_configure_dt(&en, GPIO_OUTPUT_INACTIVE) != 0)) {
return -EIO;
}
if (btd.port != NULL && (!gpio_is_ready_dt(&btd) || gpio_pin_configure_dt(&btd, GPIO_INPUT) != 0)) {
return -EIO;
}
return 0;
}
int rb9704_power_set(bool want)
{
if (pwr.port == NULL) {
return -ENOTSUP;
}
if (!want) {
if (en.port != NULL) {
(void)gpio_pin_set_dt(&en, 0);
}
(void)gpio_pin_set_dt(&pwr, 0);
on = false;
return 0;
}
if (on) {
return 0;
}
(void)gpio_pin_set_dt(&pwr, 1);
k_msleep(10);
if (en.port != NULL) {
(void)gpio_pin_set_dt(&en, 1);
}
if (btd.port != NULL) {
int64_t until = k_uptime_get() + BOOT_TIMEOUT_MS;
while (gpio_pin_get_dt(&btd) != 1) {
if (k_uptime_get() > until) {
LOG_ERR("modem did not signal booted in %d ms", BOOT_TIMEOUT_MS);
return -ETIMEDOUT;
}
k_msleep(50);
}
} else {
k_msleep(DT_PROP(RB9704_NODE, boot_delay_ms));
}
on = true;
return 0;
}
+198
View File
@@ -0,0 +1,198 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_serial.c - the library's serial port on a Zephyr UART. This is the
* whole port: five function pointers filled into the library's global
* serialContext by rb9704_serial_set_context(), which rbBegin() reaches
* through the SERIAL_CONTEXT_SETUP_FUNC macro. The UART is the bus of the
* "rock7,rockblock-9704" devicetree node.
*
* RX rides the UART ISR into a ring buffer sized for the library's largest
* JSPR line (8 kB, matching its jspr RX buffer); the library then pulls
* bytes one at a time from receiveJspr(). TX is polled: a worst-case 2 kB
* command is about 90 ms at 230400, and only the worker thread ever writes.
*
* The spy (rb9704_spy): JSPR is printable JSON lines, so the spy reassembles
* the lines in each direction and printk()s them. Do not use the library's
* own -DDEBUG for this: its TX debug path truncates the caller's buffer in
* place.
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/printk.h>
#include "rb9704_glue.h"
#include "rb9704_internal.h"
#include "serial.h" /* the library's serialContext */
/* Defined in the library's serial.c; each preset declares it itself. */
extern serialContext context;
static const struct device *const modem_uart = DEVICE_DT_GET(DT_BUS(RB9704_NODE));
RING_BUF_DECLARE(rb9704_rx_ring, CONFIG_RB9704_RX_RING_SIZE);
static K_SEM_DEFINE(rb9704_rx_sem, 0, 1);
static atomic_t rx_dropped;
static atomic_t spy_on;
/* Per-byte RX timeout. receiveJspr() treats a 0-return mid-line as
* end-of-data and discards the partial line, so this must comfortably
* exceed any inter-byte gap the modem produces (gaps at 230400 are
* microseconds; 500 ms is one bounded worker stall, worst case). */
#define RX_BYTE_TIMEOUT_MS 500
/* --- the spy ------------------------------------------------------ */
struct spy_line {
char buf[192];
size_t len;
};
static struct spy_line spy_rx = { .len = 0 };
static struct spy_line spy_tx = { .len = 0 };
static void spy_flush(struct spy_line *l, const char *tag)
{
if (l->len == 0) {
return;
}
l->buf[l->len] = '\0';
printk("rb9704 %s: %s\n", tag, l->buf);
l->len = 0;
}
static void spy_feed(struct spy_line *l, const char *tag,
const char *data, size_t len)
{
for (size_t i = 0; i < len; i++) {
char c = data[i];
if (c == '\n' || c == '\r') {
spy_flush(l, tag);
continue;
}
l->buf[l->len++] = (c >= 0x20 && c < 0x7f) ? c : '.';
if (l->len >= sizeof(l->buf) - 1) {
spy_flush(l, tag);
}
}
}
void rb9704_serial_spy(bool on)
{
atomic_set(&spy_on, on ? 1 : 0);
if (!on) {
spy_rx.len = 0;
spy_tx.len = 0;
}
}
/* --- ISR ---------------------------------------------------------------- */
static void modem_uart_isr(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
while (uart_irq_update(dev) && uart_irq_rx_ready(dev)) {
uint8_t tmp[64];
int n = uart_fifo_read(dev, tmp, sizeof(tmp));
if (n <= 0) {
break;
}
uint32_t put = ring_buf_put(&rb9704_rx_ring, tmp, n);
if (put < (uint32_t)n) {
atomic_add(&rx_dropped, n - put);
}
k_sem_give(&rb9704_rx_sem);
rb9704_worker_rx();
}
}
/* --- the five serialContext functions ----------------------------------- */
static bool ser_init(void)
{
if (!device_is_ready(modem_uart)) {
printk("rb9704: UART %s not ready\n", modem_uart->name);
return false;
}
ring_buf_reset(&rb9704_rx_ring);
k_sem_reset(&rb9704_rx_sem);
uart_irq_callback_user_data_set(modem_uart, modem_uart_isr, NULL);
uart_irq_rx_enable(modem_uart);
return true;
}
static bool ser_deinit(void)
{
uart_irq_rx_disable(modem_uart);
return true;
}
static int ser_read(char *bytes, const uint16_t length)
{
int64_t deadline = k_uptime_get() + RX_BYTE_TIMEOUT_MS;
for (;;) {
int n = ring_buf_get(&rb9704_rx_ring, (uint8_t *)bytes, length);
if (n > 0) {
if (atomic_get(&spy_on)) {
spy_feed(&spy_rx, "RX", bytes, n);
}
return n;
}
int64_t left = deadline - k_uptime_get();
if (left <= 0) {
return 0;
}
k_sem_take(&rb9704_rx_sem, K_MSEC(left));
}
}
static int ser_write(const char *data, const uint16_t length)
{
if (atomic_get(&spy_on)) {
spy_feed(&spy_tx, "TX", data, length);
}
for (uint16_t i = 0; i < length; i++) {
uart_poll_out(modem_uart, data[i]);
}
return length;
}
static int ser_peek(void)
{
return (int)ring_buf_size_get(&rb9704_rx_ring);
}
/* --- setup + worker-facing helpers -------------------------------------- */
bool rb9704_serial_set_context(const char *port, uint32_t baud)
{
ARG_UNUSED(port); /* the UART is the devicetree node's bus */
context.serialInit = ser_init;
context.serialDeInit = ser_deinit;
context.serialRead = ser_read;
context.serialWrite = ser_write;
context.serialPeek = ser_peek;
context.serialBaud = baud; /* informational; rate is set by DT */
return true;
}
int rb9704_serial_available(void)
{
return (int)ring_buf_size_get(&rb9704_rx_ring);
}
uint32_t rb9704_serial_dropped(void)
{
return (uint32_t)atomic_get(&rx_dropped);
}
+197
View File
@@ -0,0 +1,197 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_shell.c - `rb9704` on the Zephyr shell.
*
* Everything here is local serial traffic and free, except `send`, which
* costs airtime. Messages from the network arrive through the
* application's mt callback; the sample prints them.
*/
#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <rb9704/rb9704.h>
#define WAIT K_SECONDS(30)
/* The errno values differ between C libraries, so name the ones this
* module returns. */
static const char *err_name(int rc)
{
switch (-rc) {
case EALREADY: return "EALREADY";
case ENOTCONN: return "ENOTCONN";
case EBUSY: return "EBUSY";
case EACCES: return "EACCES";
case EAGAIN: return "EAGAIN";
case EINVAL: return "EINVAL";
case EIO: return "EIO";
case ETIMEDOUT: return "ETIMEDOUT";
case EINPROGRESS: return "EINPROGRESS";
case ENOTSUP: return "ENOTSUP";
case EDEADLK: return "EDEADLK";
default: return "?";
}
}
static int report(const struct shell *sh, const char *what, int rc)
{
if (rc < 0) {
shell_error(sh, "%s: %d %s", what, rc, err_name(rc));
return rc;
}
shell_print(sh, "%s: ok", what);
return 0;
}
static int cmd_begin(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc); ARG_UNUSED(argv);
return report(sh, "begin", rb9704_begin(WAIT));
}
static int cmd_end(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc); ARG_UNUSED(argv);
return report(sh, "end", rb9704_end(WAIT));
}
static int cmd_power(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc);
return report(sh, "power", rb9704_power(strcmp(argv[1], "on") == 0));
}
static int cmd_status(const struct shell *sh, size_t argc, char **argv)
{
struct rb9704_status s;
ARG_UNUSED(argc); ARG_UNUSED(argv);
rb9704_status(&s);
shell_print(sh, "powered %s, begun %s", s.powered ? "yes" : "no", s.begun ? "yes" : "no");
if (s.signal_known) {
shell_print(sh, "signal %u bars%s, %lld s ago", s.signal_bars,
!s.visibility_known ? ""
: s.constellation_visible ? ", constellation visible"
: ", constellation not visible",
(k_uptime_get() - s.signal_ms) / 1000);
} else {
shell_print(sh, "signal unknown");
}
if (s.provisioning_known) {
shell_print(sh, "topics %u", s.topic_count);
for (int i = 0; i < s.topic_count; i++) {
shell_print(sh, " %u %s", s.topics[i].id, s.topics[i].name);
}
} else {
shell_print(sh, "topics provisioning not seen yet");
}
shell_print(sh, "mo %u sent, %u failed%s", s.mo_sent, s.mo_failed,
s.mo_in_flight ? ", one in flight" : "");
shell_print(sh, "mt %u received, %u failed", s.mt_received, s.mt_failed);
shell_print(sh, "serial %u bytes dropped", s.rx_dropped);
return 0;
}
static int cmd_info(const struct shell *sh, size_t argc, char **argv)
{
struct rb9704_info i;
int rc;
ARG_UNUSED(argc); ARG_UNUSED(argv);
rc = rb9704_info(&i, WAIT);
if (rc != 0) {
return report(sh, "info", rc);
}
shell_print(sh, "imei %s", i.imei);
shell_print(sh, "serial %s", i.serial);
shell_print(sh, "hardware %s", i.hw_version);
shell_print(sh, "firmware %s", i.fw_version);
shell_print(sh, "iccid %s", i.iccid);
shell_print(sh, "board %d C", i.board_temp_c);
shell_print(sh, "sim card %s, %s", i.card_present ? "present" : "absent",
i.sim_connected ? "connected" : "not connected");
return 0;
}
static int cmd_signal(const struct shell *sh, size_t argc, char **argv)
{
int rc;
ARG_UNUSED(argc); ARG_UNUSED(argv);
rc = rb9704_signal(WAIT);
if (rc < 0) {
return report(sh, "signal", rc);
}
shell_print(sh, "signal: %d bars", rc);
return 0;
}
static int unhex(const char *s, uint8_t *out, size_t cap)
{
size_t n = 0;
while (s[0] != '\0' && s[1] != '\0') {
char b[3] = { s[0], s[1], '\0' };
char *end;
if (n == cap) {
return -ENOSPC;
}
out[n++] = (uint8_t)strtoul(b, &end, 16);
if (*end != '\0') {
return -EINVAL;
}
s += 2;
}
return s[0] == '\0' ? (int)n : -EINVAL;
}
static int cmd_send(const struct shell *sh, size_t argc, char **argv)
{
static uint8_t buf[CONFIG_RB9704_MESSAGE_MAX];
unsigned long topic = strtoul(argv[1], NULL, 10);
bool hex = strcmp(argv[0], "sendhex") == 0;
int len;
ARG_UNUSED(argc);
if (hex) {
len = unhex(argv[2], buf, sizeof(buf));
} else {
len = MIN(strlen(argv[2]), sizeof(buf));
memcpy(buf, argv[2], len);
}
if (len <= 0) {
shell_error(sh, "nothing to send (%d)", len);
return -EINVAL;
}
shell_print(sh, "%d bytes on topic %lu: this costs airtime", len, topic);
return report(sh, "send", rb9704_send((uint16_t)topic, buf, len, WAIT));
}
static int cmd_spy(const struct shell *sh, size_t argc, char **argv)
{
bool on = strcmp(argv[1], "on") == 0;
ARG_UNUSED(argc);
rb9704_spy(on);
shell_print(sh, "spy %s", on ? "on: every JSPR line printed" : "off");
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(rb9704_cmds,
SHELL_CMD(begin, NULL, "open the port and bring the library up", cmd_begin),
SHELL_CMD(end, NULL, "close it down", cmd_end),
SHELL_CMD_ARG(power, NULL, "power on|off: the board's supply, if wired", cmd_power, 2, 0),
SHELL_CMD(status, NULL, "what is known, without asking the modem", cmd_status),
SHELL_CMD(info, NULL, "IMEI, serial, versions, ICCID, SIM, temperature", cmd_info),
SHELL_CMD(signal, NULL, "signal bars now", cmd_signal),
SHELL_CMD_ARG(send, NULL, "send <topic> <text>: one message, costs airtime", cmd_send, 3, 0),
SHELL_CMD_ARG(sendhex, NULL, "sendhex <topic> <hex>: one message, costs airtime", cmd_send, 3, 0),
SHELL_CMD_ARG(spy, NULL, "spy on|off: print every JSPR line", cmd_spy, 2, 0),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(rb9704, &rb9704_cmds, "RockBLOCK 9704 Iridium modem", NULL);
+28
View File
@@ -0,0 +1,28 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_shims.c - the library's three timing calls on the Zephyr kernel,
* in place of its crossplatform.c, which has no Zephyr branch.
*/
/* useconds_t and usleep() sit behind a feature macro in picolibc. */
#define _DEFAULT_SOURCE
#include <zephyr/kernel.h>
#include <unistd.h>
#include "rb9704_glue.h"
unsigned long millis(void)
{
return k_uptime_get_32();
}
void delay(uint32_t ms)
{
k_msleep(ms);
}
/* picolibc declares usleep in <unistd.h>; the library calls it directly. */
int usleep(useconds_t us)
{
k_usleep(us);
return 0;
}
+593
View File
@@ -0,0 +1,593 @@
/* SPDX-License-Identifier: MIT
*
* rb9704_worker.c - the one thread that calls into Ground Control's library.
*
* The library has no locking and shares static buffers between its calls,
* so every rb* call happens here. Its callbacks fire inside rbPoll(), on
* this thread, and only record; the loop does the work after.
*
* loop sleep until a request or a byte from the modem arrives; then
* rbPoll() once for every line waiting (it reads one per call),
* hand any whole MT to the application and acknowledge it, and
* serve the request. The library's async path does nothing on a
* timer, so there is no polling and an idle modem costs no
* wakes.
* requests one slot. A caller takes it, fills it, kicks the worker and
* waits as long as it said. If it stops waiting, the request is
* abandoned: the worker finishes it, frees the slot itself, and
* a send's outcome still comes through mo_done.
*
* Two rules the library forces:
* - its async API with callbacks only: the blocking send and receive
* paths do nothing once callbacks are registered;
* - no modem queries while an MO is moving: receiveJspr() discards lines
* that do not match what it is waiting for, so a query would swallow
* the modem's request for the next segment and stall the send.
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/spinlock.h>
#include <errno.h>
#include <string.h>
/* rockblock_9704.h declares the library's private helpers static, so every
* file that includes it gets "declared static but never defined" warnings.
* GCC raises them at the end of the file, so the ignore has to stay on for
* the whole of it. */
#pragma GCC diagnostic ignored "-Wunused-function"
#include "rockblock_9704.h"
#include "imt_queue.h"
#include <rb9704/rb9704.h>
#include "rb9704_internal.h"
LOG_MODULE_REGISTER(rb9704, CONFIG_RB9704_LOG_LEVEL);
enum op { OP_BEGIN, OP_END, OP_SEND, OP_INFO, OP_SIGNAL, OP_POWER };
enum req_state { REQ_PENDING, REQ_DONE, REQ_ABANDONED };
static struct {
enum op op;
int result;
uint16_t topic;
size_t len;
bool on;
struct rb9704_info info;
enum req_state state;
} req;
static uint8_t mo_buf[CONFIG_RB9704_MESSAGE_MAX];
#define EV_REQ BIT(0)
#define EV_RX BIT(1)
static K_SEM_DEFINE(rb9704_slot, 1, 1);
static K_EVENT_DEFINE(rb9704_ev);
static K_SEM_DEFINE(rb9704_done, 0, 1);
static struct k_spinlock req_lock;
static struct rb9704_status st;
static K_MUTEX_DEFINE(rb9704_st_lock);
static const struct rb9704_callbacks *app;
static bool mt_waiting;
#if !IS_ENABLED(CONFIG_RB9704_POWER)
int rb9704_power_init(void)
{
return -ENOTSUP;
}
int rb9704_power_set(bool on)
{
ARG_UNUSED(on);
return -ENOTSUP;
}
bool rb9704_power_on(void)
{
return true;
}
bool rb9704_power_present(void)
{
return false;
}
#endif
/* --- library callbacks: inside rbPoll(), on this thread ------------------ */
static void cb_mo_started(const uint16_t id)
{
LOG_DBG("MO accepted by the modem: id %u", id);
}
static void cb_mo_complete(const uint16_t id, const rbMsgStatus_t status)
{
uint16_t topic;
int result = status == RB_MSG_STATUS_OK ? 0 : -EIO;
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.mo_in_flight = false;
topic = st.mo_topic;
if (result == 0) {
st.mo_sent++;
} else {
st.mo_failed++;
}
k_mutex_unlock(&rb9704_st_lock);
LOG_INF("MO %s: topic %u id %u", result == 0 ? "sent" : "failed", topic, id);
if (app != NULL && app->mo_done != NULL) {
app->mo_done(topic, id, result, app->user);
}
}
static void cb_mt_complete(const uint16_t id, const rbMsgStatus_t status)
{
if (status == RB_MSG_STATUS_OK) {
mt_waiting = true; /* delivered from the loop, outside rbPoll */
} else {
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.mt_failed++;
k_mutex_unlock(&rb9704_st_lock);
LOG_WRN("MT failed: id %u", id);
}
}
static void cb_constellation(const jsprConstellationState_t *cs)
{
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.signal_known = true;
st.visibility_known = true;
st.constellation_visible = cs->constellationVisible;
st.signal_bars = cs->signalBars;
st.signal_level = cs->signalLevel;
st.signal_ms = k_uptime_get();
k_mutex_unlock(&rb9704_st_lock);
LOG_DBG("signal: %u bars, visible %d", cs->signalBars, cs->constellationVisible);
if (app != NULL && app->signal != NULL) {
app->signal(cs->signalBars, cs->constellationVisible, app->user);
}
}
static void cb_provisioning(const jsprMessageProvisioning_t *mp)
{
uint8_t n;
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.provisioning_known = mp->provisioningSet;
n = MIN(mp->topicCount, RB9704_MAX_TOPICS);
st.topic_count = n;
for (int i = 0; i < n; i++) {
st.topics[i].id = mp->provisioning[i].topicId;
strncpy(st.topics[i].name, mp->provisioning[i].topicName,
sizeof(st.topics[i].name) - 1);
}
k_mutex_unlock(&rb9704_st_lock);
LOG_INF("provisioning: %u topic(s)", mp->topicCount);
if (app != NULL && app->provisioning != NULL) {
app->provisioning(st.topics, n, app->user);
}
}
static const rbCallbacks_t callbacks = {
.messageProvisioning = cb_provisioning,
.moMessageComplete = cb_mo_complete,
.mtMessageComplete = cb_mt_complete,
.constellationState = cb_constellation,
.moMessageStarted = cb_mo_started,
};
/* --- the requests, on this thread --------------------------------------- */
static int op_begin(void)
{
if (st.begun) {
return -EALREADY;
}
if (rb9704_power_present() && !rb9704_power_on()) {
int rc = rb9704_power_set(true);
if (rc != 0) {
return rc;
}
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.powered = true;
k_mutex_unlock(&rb9704_st_lock);
}
rbRegisterCallbacks(&callbacks);
if (!rbBegin("zephyr")) { /* the port name is unused: the UART is in devicetree */
return -EIO;
}
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.begun = true;
k_mutex_unlock(&rb9704_st_lock);
return 0;
}
static int op_end(void)
{
bool ok;
if (!st.begun) {
return -EALREADY;
}
ok = rbEnd();
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.begun = false;
st.mo_in_flight = false;
k_mutex_unlock(&rb9704_st_lock);
return ok ? 0 : -EIO;
}
static bool topic_provisioned(uint16_t topic)
{
for (int i = 0; i < st.topic_count; i++) {
if (st.topics[i].id == topic) {
return true;
}
}
return false;
}
/* The library asks the modem for its provisioning at the first send and
* keeps the answer for good, a miss included, so a modem provisioned after
* that would refuse every topic until a reboot. When the topic is not in
* what it kept, clear the cache and the library asks again: one free JSPR
* round trip. messageProvisioningInfo is the library's own global. */
extern jsprMessageProvisioning_t messageProvisioningInfo;
static int op_send(void)
{
bool known;
if (!st.begun) {
return -ENOTCONN;
}
if (st.mo_in_flight) {
return -EBUSY;
}
known = false;
for (int i = 0; messageProvisioningInfo.provisioningSet &&
i < MIN(messageProvisioningInfo.topicCount, JSPR_MAX_TOPICS); i++) {
known |= messageProvisioningInfo.provisioning[i].topicId == req.topic;
}
if (!known) {
messageProvisioningInfo.provisioningSet = false;
}
if (!rbSendMessageAsync(req.topic, (const char *)mo_buf, req.len)) {
if (st.provisioning_known && !topic_provisioned(req.topic)) {
return -EACCES;
}
/* The modem refused a topic the cache said was fine: the cache
* may be stale, so the next send asks again. */
messageProvisioningInfo.provisioningSet = false;
return -EIO;
}
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.mo_in_flight = true;
st.mo_topic = req.topic;
k_mutex_unlock(&rb9704_st_lock);
return 0;
}
static void copy_str(char *dst, size_t cap, const char *src)
{
if (src != NULL) {
strncpy(dst, src, cap - 1);
dst[cap - 1] = '\0';
} else {
dst[0] = '\0';
}
}
static int op_info(void)
{
struct rb9704_info *i = &req.info;
char *imei;
if (!st.begun) {
return -ENOTCONN;
}
if (st.mo_in_flight) {
return -EBUSY;
}
/* Each is one JSPR round trip: free, milliseconds at 230400. */
imei = rbGetImei();
copy_str(i->imei, sizeof(i->imei), imei);
copy_str(i->serial, sizeof(i->serial), rbGetSerialNumber());
copy_str(i->hw_version, sizeof(i->hw_version), rbGetHwVersion());
copy_str(i->fw_version, sizeof(i->fw_version), rbGetFirmwareVersion());
copy_str(i->iccid, sizeof(i->iccid), rbGetIccid());
i->board_temp_c = rbGetBoardTemp();
i->card_present = rbGetCardPresent();
i->sim_connected = rbGetSimConnected();
return imei != NULL ? 0 : -EIO;
}
static int op_signal(void)
{
int8_t bars;
if (!st.begun) {
return -ENOTCONN;
}
if (st.mo_in_flight) {
return -EBUSY;
}
bars = rbGetSignal();
if (bars < 0) {
return -EIO;
}
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.signal_known = true;
st.signal_bars = (uint8_t)bars;
st.signal_ms = k_uptime_get();
k_mutex_unlock(&rb9704_st_lock);
return bars;
}
static int op_power(void)
{
int rc;
if (!req.on && st.begun) {
(void)op_end();
}
rc = rb9704_power_set(req.on);
if (rc == 0) {
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.powered = req.on;
k_mutex_unlock(&rb9704_st_lock);
}
return rc;
}
static int dispatch(void)
{
switch (req.op) {
case OP_BEGIN: return op_begin();
case OP_END: return op_end();
case OP_SEND: return op_send();
case OP_INFO: return op_info();
case OP_SIGNAL: return op_signal();
case OP_POWER: return op_power();
}
return -EINVAL;
}
/* After dispatch: hand the result to a caller still waiting, or free the
* slot for one that stopped. */
static void complete(void)
{
k_spinlock_key_t key = k_spin_lock(&req_lock);
bool abandoned = req.state == REQ_ABANDONED;
if (!abandoned) {
req.state = REQ_DONE;
k_sem_give(&rb9704_done);
}
k_spin_unlock(&req_lock, key);
if (abandoned) {
if (req.op == OP_SEND && req.result != 0 && app != NULL && app->mo_done != NULL) {
app->mo_done(req.topic, 0, req.result, app->user);
}
k_sem_give(&rb9704_slot);
}
}
/* Caller side. The slot is held from here until the caller has read the
* result, or until the worker finishes a request the caller gave up on. */
static int submit(k_timeout_t timeout, bool async_ok)
{
k_spinlock_key_t key;
int rc;
req.state = REQ_PENDING;
k_sem_reset(&rb9704_done);
k_event_post(&rb9704_ev, EV_REQ);
rc = k_sem_take(&rb9704_done, timeout);
key = k_spin_lock(&req_lock);
if (rc != 0 && req.state != REQ_DONE) {
req.state = REQ_ABANDONED;
k_spin_unlock(&req_lock, key);
return async_ok ? -EINPROGRESS : -ETIMEDOUT;
}
k_spin_unlock(&req_lock, key);
return req.result;
}
static int take_slot(k_timeout_t timeout)
{
if (k_current_get() == rb9704_worker_tid_get()) {
return -EDEADLK; /* from a callback: the worker would wait on itself */
}
return k_sem_take(&rb9704_slot, timeout) == 0 ? 0 : -EAGAIN;
}
static int simple(enum op op, k_timeout_t timeout)
{
int rc = take_slot(timeout);
if (rc != 0) {
return rc;
}
req.op = op;
rc = submit(timeout, false);
if (rc != -ETIMEDOUT) {
k_sem_give(&rb9704_slot);
}
return rc;
}
/* --- the API -------------------------------------------------------------- */
void rb9704_set_callbacks(const struct rb9704_callbacks *cb)
{
app = cb;
}
int rb9704_power(bool on)
{
int rc;
if (!rb9704_power_present()) {
return -ENOTSUP;
}
rc = take_slot(K_FOREVER);
if (rc != 0) {
return rc;
}
req.op = OP_POWER;
req.on = on;
rc = submit(K_FOREVER, false);
k_sem_give(&rb9704_slot);
return rc;
}
int rb9704_begin(k_timeout_t timeout)
{
return simple(OP_BEGIN, timeout);
}
int rb9704_end(k_timeout_t timeout)
{
return simple(OP_END, timeout);
}
int rb9704_signal(k_timeout_t timeout)
{
return simple(OP_SIGNAL, timeout);
}
int rb9704_info(struct rb9704_info *out, k_timeout_t timeout)
{
int rc = take_slot(timeout);
if (rc != 0) {
return rc;
}
req.op = OP_INFO;
rc = submit(timeout, false);
if (rc == -ETIMEDOUT) {
return rc;
}
if (rc == 0) {
*out = req.info;
}
k_sem_give(&rb9704_slot);
return rc;
}
int rb9704_send(uint16_t topic, const void *data, size_t len, k_timeout_t timeout)
{
int rc;
if (topic < RB9704_TOPIC_MIN || data == NULL || len == 0 || len > sizeof(mo_buf)) {
return -EINVAL;
}
rc = take_slot(timeout);
if (rc != 0) {
return rc;
}
req.op = OP_SEND;
req.topic = topic;
req.len = len;
memcpy(mo_buf, data, len);
rc = submit(timeout, true);
if (rc != -EINPROGRESS) {
k_sem_give(&rb9704_slot);
}
return rc;
}
void rb9704_status(struct rb9704_status *out)
{
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
*out = st;
k_mutex_unlock(&rb9704_st_lock);
out->rx_dropped = rb9704_serial_dropped();
}
void rb9704_spy(bool on)
{
rb9704_serial_spy(on);
}
/* --- the loop -------------------------------------------------------------- */
/* Every whole MT in the library's queue, to the application, then
* acknowledged so the modem can send the next. */
static void deliver_mts(void)
{
char *payload = NULL;
size_t len;
while ((len = rbReceiveMessageAsync(&payload)) > 0 && payload != NULL) {
imt_t *head = imtQueueMtGetFirst();
struct rb9704_mt mt = {
.topic = head != NULL ? head->topic : 0,
.id = head != NULL ? head->id : 0,
.data = (const uint8_t *)payload,
.len = len,
};
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
st.mt_received++;
k_mutex_unlock(&rb9704_st_lock);
LOG_INF("MT received: topic %u id %u, %u bytes", mt.topic, mt.id,
(unsigned int)len);
if (app != NULL && app->mt != NULL) {
app->mt(&mt, app->user);
}
rbAcknowledgeReceiveHeadAsync();
payload = NULL;
}
}
static void worker(void *a, void *b, void *c)
{
ARG_UNUSED(a); ARG_UNUSED(b); ARG_UNUSED(c);
k_mutex_lock(&rb9704_st_lock, K_FOREVER);
/* No pins: the board is powered whenever this runs. */
st.powered = !rb9704_power_present();
if (rb9704_power_present() && rb9704_power_init() != 0) {
LOG_ERR("power pins not ready");
}
k_mutex_unlock(&rb9704_st_lock);
for (;;) {
uint32_t ev = k_event_wait(&rb9704_ev, EV_REQ | EV_RX, false, K_FOREVER);
/* Cleared before the work, so a byte or request that arrives
* during it wakes the next wait. */
k_event_clear(&rb9704_ev, ev);
if (st.begun) {
while (rb9704_serial_available() > 0) {
rbPoll(); /* one JSPR line per call */
}
if (mt_waiting) {
mt_waiting = false;
deliver_mts();
}
}
if (ev & EV_REQ) {
req.result = dispatch();
complete();
}
}
}
K_THREAD_DEFINE(rb9704_worker_tid, CONFIG_RB9704_WORKER_STACK_SIZE, worker, NULL, NULL, NULL,
CONFIG_RB9704_WORKER_PRIORITY, 0, 0);
k_tid_t rb9704_worker_tid_get(void)
{
return rb9704_worker_tid;
}
/* From the UART ISR: bytes have arrived for the library. */
void rb9704_worker_rx(void)
{
k_event_post(&rb9704_ev, EV_RX);
}