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.
595 lines
14 KiB
C
595 lines
14 KiB
C
/* SPDX-License-Identifier: MIT
|
|
* Copyright (c) 2026 Scott Penrose <scottp@dd.com.au>
|
|
*
|
|
* 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);
|
|
}
|