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

+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);
}