Compare commits

...
11 Commits
12 changed files with 1350 additions and 29 deletions
+21
View File
@@ -7,6 +7,7 @@
#include "memGet.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <assert.h>
#include <atomic>
#include <cstring>
#include <memory>
#include <stdexcept>
@@ -20,6 +21,22 @@
#if HAS_NETWORKING
extern meshtastic::Syslog syslog;
#endif
namespace
{
std::atomic<bool> serialHalLogSuppressed{false};
}
void RedirectablePrint::setSerialHalLogSuppressed(bool suppressed)
{
serialHalLogSuppressed.store(suppressed);
}
bool RedirectablePrint::isSerialHalLogSuppressed()
{
return serialHalLogSuppressed.load();
}
void RedirectablePrint::rpInit()
{
#ifdef HAS_FREE_RTOS
@@ -281,6 +298,10 @@ meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel)
void RedirectablePrint::log(const char *logLevel, const char *format, ...)
{
if (isSerialHalLogSuppressed()) {
return;
}
// append \n to format
size_t len = strlen(format);
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
+5
View File
@@ -24,6 +24,11 @@ class RedirectablePrint : public Print
public:
explicit RedirectablePrint(Print *_dest) : dest(_dest) {}
/// Suppress all log output while a SerialHal transaction is in progress.
// Unclear if this is necessary, but it seems to help with response speeds.
static void setSerialHalLogSuppressed(bool suppressed);
static bool isSerialHalLogSuppressed();
/**
* Set a new destination
*/
+8 -5
View File
@@ -1056,8 +1056,12 @@ void setup()
#endif
#endif
auto rIf = initLoRa();
std::unique_ptr<RadioInterface> rIf;
if (!config.lora.serial_hal_only) {
rIf = initLoRa();
} else {
LOG_INFO("skipping LoRa radio init, for serialHal");
}
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
#if !MESHTASTIC_EXCLUDE_MQTT
@@ -1101,10 +1105,9 @@ void setup()
// Start airtime logger thread.
airTime = new AirTime();
if (!rIf)
if (!rIf && !config.lora.serial_hal_only)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
else {
else if (rIf) {
// Log bit rate to debug output
LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) /
(float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) *
+4
View File
@@ -26,6 +26,7 @@
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#include "platform/portduino/SerialHal.h"
#include "platform/portduino/SimRadio.h"
#include "platform/portduino/USBHal.h"
#endif
@@ -352,6 +353,9 @@ std::unique_ptr<RadioInterface> initLoRa()
portduino_config.lora_spi_dev.c_str());
if (portduino_config.lora_spi_dev == "ch341") {
RadioLibHAL = ch341Hal;
} else if (portduino_config.lora_spi_dev == "serial") {
RadioLibHAL = new SerialHal(portduino_config.lora_serial_device, portduino_config.lora_serial_baud,
(uint32_t)portduino_config.lora_serial_timeout_ms);
} else {
if (RadioLibHAL != nullptr) {
delete RadioLibHAL;
+385
View File
@@ -0,0 +1,385 @@
#include "mesh/SerialHalDevice.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "concurrency/Periodic.h"
#include "configuration.h"
#include "mesh/StreamAPI.h"
#include "mesh/generated/meshtastic/config.pb.h"
#include <Arduino.h>
#include <SPI.h>
#include <cstring>
#include <stdint.h>
#if defined(ARCH_ESP32)
#if defined(HW_SPI1_DEVICE)
extern SPIClass SPI1;
#endif
#endif
namespace
{
constexpr uint32_t SERIAL_PI_RISING = 1;
constexpr uint32_t SERIAL_PI_FALLING = 2;
constexpr uint32_t SERIAL_PI_INPUT = 0;
constexpr uint32_t SERIAL_PI_OUTPUT = 1;
constexpr size_t MAX_INTERRUPT_SLOTS = 8;
constexpr int32_t INTERRUPT_POLL_MS = 5;
struct InterruptSlot {
bool used = false;
uint32_t pin = 0;
uint32_t mode = 0;
volatile bool pending = false;
};
concurrency::Lock interruptMutex;
InterruptSlot interruptSlots[MAX_INTERRUPT_SLOTS];
StreamAPI *interruptStreamApi = nullptr;
concurrency::Periodic *interruptEmitter = nullptr;
int findSlotByPinLocked(uint32_t pin)
{
for (size_t i = 0; i < MAX_INTERRUPT_SLOTS; ++i) {
if (interruptSlots[i].used && interruptSlots[i].pin == pin) {
return (int)i;
}
}
return -1;
}
int allocateSlotLocked()
{
for (size_t i = 0; i < MAX_INTERRUPT_SLOTS; ++i) {
if (!interruptSlots[i].used) {
return (int)i;
}
}
return -1;
}
#if defined(ARCH_PORTDUINO) || defined(ARCH_RP2040)
PinStatus toInterruptMode(uint32_t serialMode)
{
if (serialMode == SERIAL_PI_RISING) {
return PinStatus::RISING;
}
if (serialMode == SERIAL_PI_FALLING) {
return PinStatus::FALLING;
}
return PinStatus::CHANGE;
}
#else
int toInterruptMode(uint32_t serialMode)
{
if (serialMode == SERIAL_PI_RISING) {
return RISING;
}
if (serialMode == SERIAL_PI_FALLING) {
return FALLING;
}
return CHANGE;
}
#endif
int32_t pumpInterruptEvents();
void ensureInterruptEmitter()
{
if (!interruptEmitter) {
interruptEmitter = new concurrency::Periodic("SerialHalIrqEmitter", pumpInterruptEvents);
}
}
void emitInterruptEvent(uint32_t pin, StreamAPI *streamApi)
{
if (streamApi == nullptr) {
return;
}
meshtastic_SerialHalResponse event = meshtastic_SerialHalResponse_init_zero;
event.transaction_id = 0; // asynchronous interrupt notification
event.result = meshtastic_SerialHalResponse_Result_OK;
event.value = pin; // host-side SerialHal treats value as interrupt pin
SerialHalDevice::emitResponse(event, streamApi);
}
void markPendingBySlot(uint8_t slot)
{
if (slot < MAX_INTERRUPT_SLOTS && interruptSlots[slot].used) {
interruptSlots[slot].pending = true;
}
}
void isr0()
{
markPendingBySlot(0);
}
void isr1()
{
markPendingBySlot(1);
}
void isr2()
{
markPendingBySlot(2);
}
void isr3()
{
markPendingBySlot(3);
}
void isr4()
{
markPendingBySlot(4);
}
void isr5()
{
markPendingBySlot(5);
}
void isr6()
{
markPendingBySlot(6);
}
void isr7()
{
markPendingBySlot(7);
}
void (*const isrTable[MAX_INTERRUPT_SLOTS])() = {isr0, isr1, isr2, isr3, isr4, isr5, isr6, isr7};
int32_t pumpInterruptEvents()
{
uint32_t toEmit[MAX_INTERRUPT_SLOTS] = {0};
size_t emitCount = 0;
StreamAPI *streamApi = nullptr;
{
concurrency::LockGuard lock(&interruptMutex);
streamApi = interruptStreamApi;
for (size_t i = 0; i < MAX_INTERRUPT_SLOTS; ++i) {
if (interruptSlots[i].used && interruptSlots[i].pending) {
interruptSlots[i].pending = false;
toEmit[emitCount++] = interruptSlots[i].pin;
}
}
}
for (size_t i = 0; i < emitCount; ++i) {
emitInterruptEvent(toEmit[i], streamApi);
}
return INTERRUPT_POLL_MS;
}
} // namespace
// Helper to safely set response result
static inline void setResponseError(meshtastic_SerialHalResponse &response, meshtastic_SerialHalResponse_Result result,
const char *error = nullptr)
{
response.result = result;
if (error != nullptr) {
snprintf(response.error, sizeof(response.error), "%s", error);
}
}
void SerialHalDevice::handleCommand(const uint8_t *buf, size_t len, StreamAPI *streamApi)
{
if (buf == nullptr || streamApi == nullptr) {
return;
}
// Validate role - SerialHal commands only handled when config.lora.serial_hal_only
if (!config.lora.serial_hal_only) {
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
response.result = meshtastic_SerialHalResponse_Result_UNSUPPORTED;
snprintf(response.error, sizeof(response.error), "SerialHal not enabled for this role");
emitResponse(response, streamApi);
return;
}
// Decode the command
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
if (!pb_decode_from_bytes(buf, len, &meshtastic_SerialHalCommand_msg, &cmd)) {
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
response.result = meshtastic_SerialHalResponse_Result_BAD_REQUEST;
snprintf(response.error, sizeof(response.error), "Failed to decode SerialHalCommand");
emitResponse(response, streamApi);
return;
}
// Initialize response with matching transaction_id
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
response.transaction_id = cmd.transaction_id;
response.result = meshtastic_SerialHalResponse_Result_OK;
// Dispatch to operation handler
switch (cmd.type) {
case meshtastic_SerialHalCommand_Type_PIN_MODE:
handlePinMode(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_DIGITAL_WRITE:
handleDigitalWrite(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_DIGITAL_READ:
handleDigitalRead(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_ATTACH_INTERRUPT:
handleAttachInterrupt(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_DETACH_INTERRUPT:
handleDetachInterrupt(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_SPI_TRANSFER:
handleSpiTransfer(cmd, response);
break;
case meshtastic_SerialHalCommand_Type_NOOP:
// NOOP: just return OK
break;
default:
response.result = meshtastic_SerialHalResponse_Result_UNSUPPORTED;
snprintf(response.error, sizeof(response.error), "Unknown SerialHal operation type");
break;
}
emitResponse(response, streamApi);
}
void SerialHalDevice::handlePinMode(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
// LOG_DEBUG("SerialHalDevice: pinMode pin=%u mode=%u", cmd.pin, cmd.mode);
if (cmd.mode == SERIAL_PI_INPUT) {
pinMode((int)cmd.pin, INPUT);
} else if (cmd.mode == SERIAL_PI_OUTPUT) {
pinMode((int)cmd.pin, OUTPUT);
} else {
setResponseError(response, meshtastic_SerialHalResponse_Result_BAD_REQUEST, "Unsupported pin mode");
}
}
void SerialHalDevice::handleDigitalWrite(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
// LOG_DEBUG("SerialHalDevice: digitalWrite pin=%u value=%u", cmd.pin, cmd.value);
digitalWrite((int)cmd.pin, cmd.value ? HIGH : LOW);
}
void SerialHalDevice::handleDigitalRead(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
// LOG_DEBUG("SerialHalDevice: digitalRead pin=%u", cmd.pin);
response.value = (uint32_t)digitalRead((int)cmd.pin);
}
void SerialHalDevice::handleAttachInterrupt(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
// LOG_DEBUG("SerialHalDevice: attachInterrupt pin=%u mode=%u", cmd.pin, cmd.mode);
ensureInterruptEmitter();
int slot = -1;
{
concurrency::LockGuard lock(&interruptMutex);
slot = findSlotByPinLocked(cmd.pin);
if (slot < 0) {
slot = allocateSlotLocked();
}
if (slot >= 0) {
interruptSlots[slot].used = true;
interruptSlots[slot].pin = cmd.pin;
interruptSlots[slot].mode = cmd.mode;
interruptSlots[slot].pending = false;
}
}
if (slot < 0) {
setResponseError(response, meshtastic_SerialHalResponse_Result_ERROR, "No interrupt slots available");
return;
}
::attachInterrupt((int)cmd.pin, isrTable[slot], toInterruptMode(cmd.mode));
}
void SerialHalDevice::handleDetachInterrupt(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
// LOG_DEBUG("SerialHalDevice: detachInterrupt pin=%u", cmd.pin);
::detachInterrupt((int)cmd.pin);
{
concurrency::LockGuard lock(&interruptMutex);
const int slot = findSlotByPinLocked(cmd.pin);
if (slot >= 0) {
interruptSlots[slot] = InterruptSlot{};
}
}
}
void SerialHalDevice::handleSpiTransfer(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response)
{
if (cmd.data.size == 0) {
return;
}
#if !ARCH_PORTDUINO
if (spiLock == nullptr) {
setResponseError(response, meshtastic_SerialHalResponse_Result_ERROR, "SPI lock not initialized");
return;
}
#if defined(HW_SPI1_DEVICE)
SPIClass &spiBus = SPI1;
#else
SPIClass &spiBus = SPI;
#endif
response.data.size = cmd.data.size;
{
concurrency::LockGuard guard(spiLock);
spiBus.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
#ifdef ARCH_ESP32
spiBus.transferBytes(cmd.data.bytes, response.data.bytes, cmd.data.size);
#else
spiBus.transfer(cmd.data.bytes, response.data.bytes, cmd.data.size);
#endif
spiBus.endTransaction();
}
#else
// SPI wiring is board/radio-specific; keep this explicit for now.
response.result = meshtastic_SerialHalResponse_Result_UNSUPPORTED;
snprintf(response.error, sizeof(response.error), "SPI not supported on this platform");
#endif
}
void SerialHalDevice::emitResponse(const meshtastic_SerialHalResponse &response, StreamAPI *streamApi)
{
if (streamApi == nullptr) {
return;
}
// Encode the response
uint8_t encoded[meshtastic_SerialHalResponse_size] = {0};
const size_t responseLen =
pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_SerialHalResponse_msg, static_cast<const void *>(&response));
if (responseLen == 0 || responseLen > 0xFFFF) {
LOG_ERROR("SerialHalDevice: Failed to encode response (len=%zu)", responseLen);
return;
}
// Build frame with StreamAPI framing: START1 SERIALHAL_MAGIC LEN_H LEN_L [payload]
constexpr uint8_t START1 = 0x94;
constexpr uint8_t SERIALHAL_MAGIC = 0xA5;
uint8_t hdr[4];
hdr[0] = START1;
hdr[1] = SERIALHAL_MAGIC;
hdr[2] = (uint8_t)((responseLen >> 8) & 0xFF); // LEN_H
hdr[3] = (uint8_t)(responseLen & 0xFF); // LEN_L
// Emit via StreamAPI (this uses the internal txBuf + framing)
streamApi->emitSerialHalResponse(hdr, sizeof(hdr), encoded, responseLen);
// Keep a recent stream instance so async interrupt events can be emitted.
{
concurrency::LockGuard lock(&interruptMutex);
interruptStreamApi = streamApi;
}
}
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include "mesh/generated/meshtastic/serial_hal.pb.h"
#include <cstdint>
/**
* @brief Device-side handler for SerialHal GPIO/SPI operations over StreamAPI framing.
*
* This module decodes SerialHalCommand protobufs received from a host and executes
* the requested GPIO (pinMode, digitalWrite, digitalRead, attach/detachInterrupt) or
* SPI operations, then returns results via SerialHalResponse.
*
* Usage:
* 1. Override StreamAPI::handleSerialHalCommand() in a subclass
* 2. Call SerialHalDevice::handleCommand(buf, len, streamApi)
* 3. SerialHalDevice will decode, execute, and emit the response
*
* The handler is only active when config.lora.serial_hal_only is true.
*/
class StreamAPI; // forward declaration
class SerialHalDevice
{
public:
/**
* @brief Process a SerialHalCommand and emit a response.
*
* Decodes the protobuf, validates the operation, executes it on the device,
* and writes the response back via the StreamAPI instance.
*
* @param buf Pointer to the encoded SerialHalCommand protobuf payload (not including framing)
* @param len Length of the encoded payload
* @param streamApi Pointer to the StreamAPI instance (used for emitting responses)
*/
static void handleCommand(const uint8_t *buf, size_t len, StreamAPI *streamApi);
/**
* @brief Emit a SerialHalResponse back to the host via StreamAPI framing.
*
* Encodes the response protobuf and sends it with proper framing (START1 SERIALHAL_MAGIC LEN_H LEN_L payload).
*
* @param response The response to send
* @param streamApi Pointer to the StreamAPI instance
*/
static void emitResponse(const meshtastic_SerialHalResponse &response, StreamAPI *streamApi);
private:
/**
* @brief Execute a GPIO pinMode operation.
* @param cmd Decoded SerialHalCommand with PIN_MODE type
* @param response Response object to fill with result
*/
static void handlePinMode(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
/**
* @brief Execute a GPIO digitalWrite operation.
* @param cmd Decoded SerialHalCommand with DIGITAL_WRITE type
* @param response Response object to fill with result
*/
static void handleDigitalWrite(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
/**
* @brief Execute a GPIO digitalRead operation.
* @param cmd Decoded SerialHalCommand with DIGITAL_READ type
* @param response Response object to fill with result (value field contains read result)
*/
static void handleDigitalRead(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
/**
* @brief Execute an attachInterrupt operation.
* @param cmd Decoded SerialHalCommand with ATTACH_INTERRUPT type
* @param response Response object to fill with result
*/
static void handleAttachInterrupt(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
/**
* @brief Execute a detachInterrupt operation.
* @param cmd Decoded SerialHalCommand with DETACH_INTERRUPT type
* @param response Response object to fill with result
*/
static void handleDetachInterrupt(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
/**
* @brief Execute an SPI transfer operation.
* @param cmd Decoded SerialHalCommand with SPI_TRANSFER type and data to send
* @param response Response object to fill with result (data field contains received bytes)
*/
static void handleSpiTransfer(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse &response);
};
+95 -17
View File
@@ -1,12 +1,15 @@
#include "StreamAPI.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "RedirectablePrint.h"
#include "SerialHalDevice.h"
#include "Throttle.h"
#include "concurrency/LockGuard.h"
#include "configuration.h"
#define START1 0x94
#define START2 0xc3
#define SERIALHAL_MAGIC 0xa5 // second framing byte for SerialHal frames (START1 SH_MAGIC LEN_H LEN_L PAYLOAD)
#define HEADER_LEN 4
int32_t StreamAPI::runOncePart()
@@ -79,18 +82,29 @@ int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
if (ptr == 0) { // looking for START1
if (c != START1)
rxPtr = 0; // failed to find framing
} else if (ptr == 1) { // looking for START2
if (c != START2)
rxPtr = 0; // failed to find framing
} else if (ptr == 1) { // discriminate frame type on second byte
if (c == START2) {
rxIsSerialHal = false; // standard ToRadio frame
serialHalRxActive.store(false);
RedirectablePrint::setSerialHalLogSuppressed(false);
} else if (c == SERIALHAL_MAGIC) {
rxIsSerialHal = true; // SerialHal command frame
serialHalRxActive.store(true);
RedirectablePrint::setSerialHalLogSuppressed(true);
} else {
rxPtr = 0; // unrecognised second byte — not our frame
serialHalRxActive.store(false);
RedirectablePrint::setSerialHalLogSuppressed(false);
}
} else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing
uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing
// console->printf("len %d\n", len);
if (ptr == HEADER_LEN - 1) {
// we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid
// protobuf also)
if (len > MAX_TO_FROM_RADIO_SIZE)
// we _just_ finished our 4 byte header, validate length now
uint32_t maxLen = rxIsSerialHal ? (uint32_t)meshtastic_SerialHalCommand_size : MAX_TO_FROM_RADIO_SIZE;
if (len > maxLen)
rxPtr = 0; // length is bogus, restart search for framing
}
@@ -98,8 +112,16 @@ int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload?
rxPtr = 0; // start over again on the next packet
// If we didn't just fail the packet and we now have the right # of bytes, parse it
handleToRadio(rxBuf + HEADER_LEN, len);
// Dispatch based on which frame type we identified at byte 1
if (rxIsSerialHal)
handleSerialHalCommand(rxBuf + HEADER_LEN, len);
else
handleToRadio(rxBuf + HEADER_LEN, len);
if (rxIsSerialHal)
serialHalRxActive.store(false);
if (rxIsSerialHal)
RedirectablePrint::setSerialHalLogSuppressed(false);
}
}
}
@@ -114,7 +136,12 @@ int32_t StreamAPI::readStream()
if (!stream->available()) {
// Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time
bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000);
return recentRx ? 5 : 250;
if (!recentRx)
return 250; // Sleep a long time if we haven't heard from the computer in a while
if (serialHalRxActive.load())
return 0; // If we are in the middle of a SerialHal transaction, don't sleep at all because we want to be as
// responsive as possible to incoming SerialHal bytes
return 5; // Otherwise, poll frequently for new data
} else {
while (stream->available()) { // Currently we never want to block
int cInt = stream->read();
@@ -135,18 +162,30 @@ int32_t StreamAPI::readStream()
if (ptr == 0) { // looking for START1
if (c != START1)
rxPtr = 0; // failed to find framing
} else if (ptr == 1) { // looking for START2
if (c != START2)
rxPtr = 0; // failed to find framing
} else if (ptr == 1) { // discriminate frame type on second byte
if (c == START2) {
rxIsSerialHal = false; // standard ToRadio frame
serialHalRxActive.store(false);
RedirectablePrint::setSerialHalLogSuppressed(false);
} else if (c == SERIALHAL_MAGIC) {
rxIsSerialHal = true; // SerialHal command frame
serialHalRxActive.store(true);
RedirectablePrint::setSerialHalLogSuppressed(true);
LOG_WARN("StreamAPI: Detected SerialHal command frame");
} else {
rxPtr = 0; // unrecognised second byte — not our frame
serialHalRxActive.store(false);
RedirectablePrint::setSerialHalLogSuppressed(false);
}
} else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing
uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing
// console->printf("len %d\n", len);
if (ptr == HEADER_LEN - 1) {
// we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid
// protobuf also)
if (len > MAX_TO_FROM_RADIO_SIZE)
// we _just_ finished our 4 byte header, validate length now
uint32_t maxLen = rxIsSerialHal ? (uint32_t)meshtastic_SerialHalCommand_size : MAX_TO_FROM_RADIO_SIZE;
if (len > maxLen)
rxPtr = 0; // length is bogus, restart search for framing
}
@@ -154,8 +193,16 @@ int32_t StreamAPI::readStream()
if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload?
rxPtr = 0; // start over again on the next packet
// If we didn't just fail the packet and we now have the right # of bytes, parse it
handleToRadio(rxBuf + HEADER_LEN, len);
// Dispatch based on which frame type we identified at byte 1
if (rxIsSerialHal)
handleSerialHalCommand(rxBuf + HEADER_LEN, len);
else
handleToRadio(rxBuf + HEADER_LEN, len);
if (rxIsSerialHal)
serialHalRxActive.store(false);
if (rxIsSerialHal)
RedirectablePrint::setSerialHalLogSuppressed(false);
}
}
}
@@ -199,6 +246,10 @@ void StreamAPI::emitRebooted()
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
{
if (serialHalRxActive.load()) {
return;
}
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here — those
// belong to the main packet-emission path and a LOG_ firing during
// `writeStream()` would corrupt an in-flight encode. We keep a
@@ -249,4 +300,31 @@ void StreamAPI::onConnectionChanged(bool connected)
// received a packet in a while
powerFSM.trigger(EVENT_SERIAL_DISCONNECTED);
}
}
void StreamAPI::handleSerialHalCommand(const uint8_t *buf, size_t len)
{
// Default implementation: dispatch to SerialHalDevice for GPIO/SPI handling
SerialHalDevice::handleCommand(buf, len, this);
}
void StreamAPI::emitSerialHalResponse(const uint8_t *hdr, size_t hdrLen, const uint8_t *payload, size_t payloadLen)
{
if (hdr == nullptr || hdrLen != 4 || payload == nullptr || payloadLen > meshtastic_SerialHalResponse_size) {
LOG_ERROR("StreamAPI: Invalid SerialHal response parameters");
return;
}
// Build complete frame in a temporary buffer
uint8_t frame[4 + meshtastic_SerialHalResponse_size];
memcpy(frame, hdr, hdrLen);
memcpy(frame + hdrLen, payload, payloadLen);
size_t totalLen = hdrLen + payloadLen;
// Serialize stream writes against other emit operations via streamLock
concurrency::LockGuard guard(&streamLock);
stream->write(frame, totalLen);
stream->flush();
LOG_WARN("StreamAPI: Emitted SerialHal response frame (len=%zu)", totalLen);
}
+26 -2
View File
@@ -4,10 +4,15 @@
#include "Stream.h"
#include "concurrency/Lock.h"
#include "concurrency/OSThread.h"
#include "generated/meshtastic/serial_hal.pb.h"
#include <atomic>
#include <cstdarg>
// A To/FromRadio packet + our 32 bit header
#define MAX_STREAM_BUF_SIZE (MAX_TO_FROM_RADIO_SIZE + sizeof(uint32_t))
// Buffer sized for the larger of a full ToRadio/FromRadio payload or a full SerialHalCommand payload, plus header.
#define MAX_STREAM_PAYLOAD_SIZE \
(MAX_TO_FROM_RADIO_SIZE > (int)meshtastic_SerialHalCommand_size ? MAX_TO_FROM_RADIO_SIZE \
: (int)meshtastic_SerialHalCommand_size)
#define MAX_STREAM_BUF_SIZE (MAX_STREAM_PAYLOAD_SIZE + (int)sizeof(uint32_t))
/**
* A version of our 'phone' API that talks over a Stream. So therefore well suited to use with serial links
@@ -39,6 +44,8 @@ class StreamAPI : public PhoneAPI
uint8_t rxBuf[MAX_STREAM_BUF_SIZE] = {0};
size_t rxPtr = 0;
bool rxIsSerialHal = false; ///< true when the current in-progress frame is a SerialHal frame (START1 SH_MAGIC ...)
std::atomic<bool> serialHalRxActive{false};
/// time of last rx, used, to slow down our polling if we haven't heard from anyone
uint32_t lastRxMsec = 0;
@@ -56,6 +63,17 @@ class StreamAPI : public PhoneAPI
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override = 0;
/**
* Emit a SerialHal response frame with proper framing (START1 SERIALHAL_MAGIC LEN_H LEN_L payload).
* Called by SerialHalDevice to send responses back to the host.
*
* @param hdr 4-byte header (START1 SERIALHAL_MAGIC LEN_H LEN_L)
* @param hdrLen Length of header (should be 4)
* @param payload Encoded SerialHalResponse protobuf payload
* @param payloadLen Length of payload
*/
void emitSerialHalResponse(const uint8_t *hdr, size_t hdrLen, const uint8_t *payload, size_t payloadLen);
private:
/**
* Read any rx chars from the link and call handleToRadio
@@ -75,6 +93,12 @@ class StreamAPI : public PhoneAPI
*/
void emitRebooted();
/**
* Called when a complete SerialHal-framed packet has been received.
* Default implementation dispatches to SerialHalDevice for GPIO/SPI handling.
*/
virtual void handleSerialHalCommand(const uint8_t *buf, size_t len);
virtual void onConnectionChanged(bool connected) override;
/**
+13 -5
View File
@@ -598,7 +598,8 @@ void portduinoSetup()
for (const auto *i : portduino_config.all_pins) {
// In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora
// Those GPIO are handled in our usermode driver instead.
if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
if (i->config_section == "Lora" &&
(portduino_config.lora_spi_dev == "ch341" || portduino_config.lora_spi_dev == "serial")) {
continue;
}
if (i->enabled) {
@@ -617,7 +618,8 @@ void portduinoSetup()
for (auto i : portduino_config.extra_pins) {
// In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora
// Those GPIO are handled in our usermode driver instead.
if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
if (i.config_section == "Lora" &&
(portduino_config.lora_spi_dev == "ch341" || portduino_config.lora_spi_dev == "serial")) {
continue;
}
if (i.enabled) {
@@ -664,7 +666,8 @@ void portduinoSetup()
for (auto i : portduino_config.extra_pins) {
// In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora
// Those GPIO are handled in our usermode driver instead.
if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
if (i.config_section == "Lora" &&
(portduino_config.lora_spi_dev == "ch341" || portduino_config.lora_spi_dev == "serial")) {
continue;
}
if (i.enabled && i.default_high) {
@@ -674,7 +677,8 @@ void portduinoSetup()
}
// Only initialize the radio pins when dealing with real, kernel controlled SPI hardware
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341" &&
portduino_config.lora_spi_dev != "serial") {
SPI.begin(portduino_config.lora_spi_dev.c_str());
}
@@ -708,6 +712,7 @@ void portduinoSetup()
}
if (portduino_config.lora_spi_dev != "") {
portduinoSetOptions({.realHardware = true});
LOG_DEBUG("Running with real hardware SPI device %s", portduino_config.lora_spi_dev.c_str());
}
return;
}
@@ -844,12 +849,15 @@ bool loadConfig(const char *configPath)
}
portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as<int>(2000000);
portduino_config.lora_serial_device = yamlConfig["Lora"]["SerialDevice"].as<std::string>("");
portduino_config.lora_serial_baud = yamlConfig["Lora"]["SerialBaud"].as<int>(115200);
portduino_config.lora_serial_timeout_ms = yamlConfig["Lora"]["SerialTimeoutMs"].as<int>(500);
portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as<std::string>("");
portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as<int>(0x5512);
portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as<int>(0x1A86);
portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as<std::string>("spidev0.0");
if (portduino_config.lora_spi_dev != "ch341") {
if (portduino_config.lora_spi_dev != "ch341" && portduino_config.lora_spi_dev != "serial") {
portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev;
if (portduino_config.lora_spi_dev.length() == 14) {
int x = portduino_config.lora_spi_dev.at(11) - '0';
+9
View File
@@ -86,6 +86,9 @@ extern struct portduino_config_struct {
bool has_device_id = false;
uint8_t device_id[16] = {0};
std::string lora_spi_dev = "";
std::string lora_serial_device = "";
int lora_serial_baud = 115200;
int lora_serial_timeout_ms = 500;
std::string lora_usb_serial_num = "";
int lora_spi_dev_int = 0;
int lora_default_gpiochip = 0;
@@ -276,6 +279,12 @@ extern struct portduino_config_struct {
}
if (lora_usb_serial_num != "")
out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num;
if (lora_serial_device != "")
out << YAML::Key << "SerialDevice" << YAML::Value << lora_serial_device;
if (lora_serial_baud != 115200)
out << YAML::Key << "SerialBaud" << YAML::Value << lora_serial_baud;
if (lora_serial_timeout_ms != 500)
out << YAML::Key << "SerialTimeoutMs" << YAML::Value << lora_serial_timeout_ms;
if (spiSpeed != 2000000)
out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed;
if (rfswitch_dio_pins[0] != RADIOLIB_NC) {
+595
View File
@@ -0,0 +1,595 @@
#include "platform/portduino/SerialHal.h"
#include "mesh/mesh-pb-constants.h"
#include "platform/portduino/PortduinoGlue.h"
#include <cerrno>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <poll.h>
#include <sched.h>
#include <sys/time.h>
#include <termios.h>
#include <unistd.h>
#include <utility>
namespace
{
constexpr uint8_t START1 = 0x94;
constexpr uint8_t SERIALHAL_MAGIC = 0xA5;
constexpr size_t HEADER_SIZE = 4; // START1 + SERIALHAL_MAGIC + LEN_H + LEN_L
constexpr uint8_t START2 = 0xC3; // second byte of a normal FromRadio frame
speed_t toTermiosBaud(uint32_t baud)
{
switch (baud) {
case 9600:
return B9600;
case 19200:
return B19200;
case 38400:
return B38400;
case 57600:
return B57600;
case 115200:
return B115200;
case 230400:
return B230400;
case 460800:
return B460800;
case 921600:
return B921600;
default:
return B115200;
}
}
} // namespace
SerialHal::SerialHal(const std::string &devicePath, uint32_t baudRate, uint32_t opTimeoutMs)
: RadioLibHal(SERIAL_PI_INPUT, SERIAL_PI_OUTPUT, SERIAL_PI_LOW, SERIAL_PI_HIGH, SERIAL_PI_RISING, SERIAL_PI_FALLING),
device(devicePath), baud(baudRate), timeoutMs(opTimeoutMs)
{
if (!openPort()) {
setTransportError("unable to open serial device");
}
}
SerialHal::~SerialHal()
{
closePort();
}
bool SerialHal::openPort()
{
closePort();
fd = ::open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
return false;
}
termios tty = {};
if (tcgetattr(fd, &tty) != 0) {
closePort();
return false;
}
// Force raw mode to avoid line discipline byte mangling on binary frames.
cfmakeraw(&tty);
cfsetospeed(&tty, toTermiosBaud(baud));
cfsetispeed(&tty, toTermiosBaud(baud));
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 0;
tty.c_cflag |= (CLOCAL | CREAD);
tty.c_cflag &= ~(PARENB | PARODD);
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CRTSCTS;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
closePort();
return false;
}
tcflush(fd, TCIOFLUSH);
inError = false;
startReaderThread();
return true;
}
void SerialHal::closePort()
{
stopReaderThread();
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
void SerialHal::setTransportError(const char *msg)
{
if (!inError.load() || !hasWarned) {
LOG_ERROR("SerialHal: %s (%s)", msg, device.c_str());
}
inError = true;
hasWarned = true;
portduino_status.LoRa_in_error = true;
}
bool SerialHal::waitForReadable(int timeout)
{
if (fd < 0) {
return false;
}
pollfd pfd = {};
pfd.fd = fd;
pfd.events = POLLIN;
int ret = poll(&pfd, 1, timeout);
return ret > 0 && (pfd.revents & POLLIN);
}
bool SerialHal::writeAll(const uint8_t *data, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t rc = ::write(fd, data + off, len - off);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
off += (size_t)rc;
}
return true;
}
bool SerialHal::readExact(uint8_t *data, size_t len)
{
size_t off = 0;
auto start = std::chrono::steady_clock::now();
while (off < len) {
auto now = std::chrono::steady_clock::now();
int elapsed = (int)std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
int remaining = (int)timeoutMs - elapsed;
if (remaining <= 0 || !waitForReadable(remaining)) {
return false;
}
ssize_t rc = ::read(fd, data + off, len - off);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (rc == 0) {
return false;
}
off += (size_t)rc;
}
return true;
}
uint16_t SerialHal::crc16(const uint8_t *data, size_t len) const
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= ((uint16_t)data[i] << 8);
for (int bit = 0; bit < 8; ++bit) {
if (crc & 0x8000) {
crc = (uint16_t)((crc << 1) ^ 0x1021);
} else {
crc = (uint16_t)(crc << 1);
}
}
}
return crc;
}
bool SerialHal::sendRequest(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse *response)
{
if (fd < 0 && !openPort()) {
setTransportError("serial open failed");
return false;
}
uint8_t encoded[meshtastic_SerialHalCommand_size] = {0};
const size_t payloadLen =
pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_SerialHalCommand_msg, static_cast<const void *>(&cmd));
if (payloadLen == 0 || payloadLen > 0xFFFF) {
setTransportError("serial command encode failed");
return false;
}
// Build frame with StreamAPI canonical framing: START1 SERIALHAL_MAGIC LEN_H LEN_L [payload]
std::vector<uint8_t> frame;
frame.resize(HEADER_SIZE + payloadLen);
frame[0] = START1;
frame[1] = SERIALHAL_MAGIC;
frame[2] = (uint8_t)((payloadLen >> 8) & 0xFF); // LEN_H (big-endian)
frame[3] = (uint8_t)(payloadLen & 0xFF); // LEN_L
memcpy(frame.data() + HEADER_SIZE, encoded, payloadLen);
{
std::lock_guard<std::mutex> writeGuard(writeMutex);
if (!writeAll(frame.data(), frame.size())) {
setTransportError("serial write failed");
return false;
}
}
meshtastic_SerialHalResponse got = meshtastic_SerialHalResponse_init_zero;
{
std::unique_lock<std::mutex> lock(stateMutex);
const auto timeout = std::chrono::milliseconds(timeoutMs);
const bool arrived = responseCv.wait_for(lock, timeout, [&]() { return pendingResponses.count(cmd.transaction_id) > 0; });
if (!arrived) {
setTransportError("serial response timeout");
LOG_WARN("SerialHal: response timeout for transaction_id %u, cmd type %u", cmd.transaction_id, cmd.type);
return false;
}
got = pendingResponses[cmd.transaction_id];
pendingResponses.erase(cmd.transaction_id);
}
if (got.result != meshtastic_SerialHalResponse_Result_OK) {
setTransportError("serial response reported error");
LOG_WARN("SerialHal: response error: %s, %u, %u", got.error, cmd.type, cmd.data.size);
return false;
}
if (response != nullptr) {
*response = got;
}
inError = false;
hasWarned = false;
return true;
}
void SerialHal::pinMode(uint32_t pin, uint32_t mode)
{
if (checkError() || pin == RADIOLIB_NC) {
return;
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_PIN_MODE;
cmd.pin = pin;
cmd.mode = mode;
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
sendRequest(cmd, &response);
}
void SerialHal::digitalWrite(uint32_t pin, uint32_t value)
{
if (checkError() || pin == RADIOLIB_NC) {
return;
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_DIGITAL_WRITE;
cmd.pin = pin;
cmd.value = value;
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
sendRequest(cmd, &response);
}
uint32_t SerialHal::digitalRead(uint32_t pin)
{
if (checkError() || pin == RADIOLIB_NC) {
return 0;
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_DIGITAL_READ;
cmd.pin = pin;
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
if (!sendRequest(cmd, &response)) {
return 0;
}
return response.value;
}
void SerialHal::attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode)
{
if (checkError() || interruptNum == RADIOLIB_NC) {
return;
}
{
std::lock_guard<std::mutex> lock(stateMutex);
interruptCallbacks[interruptNum] = interruptCb;
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_ATTACH_INTERRUPT;
cmd.pin = interruptNum;
cmd.mode = mode;
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
sendRequest(cmd, &response);
}
void SerialHal::detachInterrupt(uint32_t interruptNum)
{
if (checkError() || interruptNum == RADIOLIB_NC) {
return;
}
{
std::lock_guard<std::mutex> lock(stateMutex);
interruptCallbacks.erase(interruptNum);
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_DETACH_INTERRUPT;
cmd.pin = interruptNum;
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
sendRequest(cmd, &response);
}
void SerialHal::delay(unsigned long ms)
{
delayMicroseconds(ms * 1000);
}
void SerialHal::delayMicroseconds(unsigned long us)
{
if (us == 0) {
sched_yield();
return;
}
usleep(us);
}
void SerialHal::yield()
{
sched_yield();
}
unsigned long SerialHal::millis()
{
struct timeval tv;
gettimeofday(&tv, nullptr);
return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL);
}
unsigned long SerialHal::micros()
{
struct timeval tv;
gettimeofday(&tv, nullptr);
return (tv.tv_sec * 1000000ULL) + tv.tv_usec;
}
long SerialHal::pulseIn(uint32_t pin, uint32_t state, unsigned long timeout)
{
(void)pin;
(void)state;
(void)timeout;
LOG_WARN("SerialHal pulseIn is not supported");
return 0;
}
void SerialHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
{
if (checkError()) {
return;
}
if (len == 0) {
return;
}
meshtastic_SerialHalCommand cmd = meshtastic_SerialHalCommand_init_zero;
cmd.transaction_id = txId.fetch_add(1);
cmd.type = meshtastic_SerialHalCommand_Type_SPI_TRANSFER;
const size_t maxTx = sizeof(cmd.data.bytes);
const size_t txLen = len < maxTx ? len : maxTx;
cmd.data.size = txLen;
if (out != nullptr) {
memcpy(cmd.data.bytes, out, txLen);
} else {
memset(cmd.data.bytes, 0, txLen);
}
meshtastic_SerialHalResponse response = meshtastic_SerialHalResponse_init_zero;
if (!sendRequest(cmd, &response)) {
return;
}
if (in != nullptr) {
size_t copyLen = response.data.size < len ? response.data.size : len;
memcpy(in, response.data.bytes, copyLen);
if (copyLen < len) {
memset(in + copyLen, 0, len - copyLen);
}
}
}
bool SerialHal::checkError()
{
if (inError.load()) {
if (!hasWarned) {
LOG_ERROR("SerialHal in_error detected");
hasWarned = true;
}
portduino_status.LoRa_in_error = true;
return true;
}
hasWarned = false;
return false;
}
bool SerialHal::readFrame(std::vector<uint8_t> &payload, int firstByteTimeoutMs)
{
payload.clear();
// Loop so that normal FromRadio frames (START1 START2 ...) emitted by the
// device on the same serial port are drained and discarded rather than
// causing the byte stream to desync.
for (;;) {
uint8_t hdr[HEADER_SIZE] = {0};
for (;;) {
ssize_t rc = ::read(fd, &hdr[0], 1);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (rc == 0) {
return false;
}
if (hdr[0] == START1) {
break;
}
}
if (!readExact(hdr + 1, HEADER_SIZE - 1)) {
return false;
}
const uint16_t len = ((uint16_t)hdr[2] << 8) | (uint16_t)hdr[3];
if (hdr[1] == SERIALHAL_MAGIC) {
// SerialHal response frame — this is what we want.
if (len > meshtastic_SerialHalResponse_size) {
return false;
}
payload.resize(len);
if (len > 0 && !readExact(payload.data(), len)) {
payload.clear();
return false;
}
return true;
} else if (hdr[1] == START2) {
// Normal FromRadio frame emitted by the device — drain and discard
// its payload so we stay in sync, then loop to find a SerialHal frame.
if (len > 0) {
std::vector<uint8_t> discard(len);
if (!readExact(discard.data(), len)) {
return false;
}
}
// continue looping, look for next frame
} else {
// Unknown second byte after START1 — restart search for framing.
continue;
}
}
}
void SerialHal::readerLoop()
{
readerRunning = true;
while (!readerStopRequested.load()) {
if (fd < 0) {
break;
}
if (!waitForReadable(100)) {
continue;
}
std::vector<uint8_t> payload;
if (!readFrame(payload, 40)) {
continue;
}
meshtastic_SerialHalResponse resp = meshtastic_SerialHalResponse_init_zero;
if (payload.empty() || !pb_decode_from_bytes(payload.data(), payload.size(), &meshtastic_SerialHalResponse_msg, &resp)) {
continue;
}
if (resp.transaction_id == 0) {
LOG_WARN("SerialHal: received unsolicited interrupt event: pin=%u", resp.value);
// transaction_id 0 is reserved for unsolicited interrupt events.
// The device reports the triggered pin in resp.value instead of
// matching one of the synchronous request/response transactions.
{
std::lock_guard<std::mutex> lock(stateMutex);
if (interruptCallbacks.count(resp.value) > 0) {
pendingInterruptPins.push_back(resp.value);
}
}
interruptCv.notify_one();
continue;
}
{
std::lock_guard<std::mutex> lock(stateMutex);
pendingResponses[resp.transaction_id] = resp;
}
responseCv.notify_all();
}
readerRunning = false;
}
void SerialHal::interruptDispatchLoop()
{
interruptDispatcherRunning = true;
while (!readerStopRequested.load()) {
uint32_t pin = 0;
void (*cb)(void) = nullptr;
{
std::unique_lock<std::mutex> lock(stateMutex);
interruptCv.wait(lock, [&]() { return readerStopRequested.load() || !pendingInterruptPins.empty(); });
if (readerStopRequested.load()) {
break;
}
pin = pendingInterruptPins.front();
pendingInterruptPins.pop_front();
auto it = interruptCallbacks.find(pin);
if (it != interruptCallbacks.end()) {
cb = it->second;
}
}
if (cb != nullptr) {
cb();
}
}
interruptDispatcherRunning = false;
}
void SerialHal::startReaderThread()
{
stopReaderThread();
readerStopRequested = false;
readerThread = std::thread(&SerialHal::readerLoop, this);
interruptThread = std::thread(&SerialHal::interruptDispatchLoop, this);
}
void SerialHal::stopReaderThread()
{
readerStopRequested = true;
interruptCv.notify_all();
if (readerThread.joinable()) {
readerThread.join();
}
if (interruptThread.joinable()) {
interruptThread.join();
}
std::lock_guard<std::mutex> lock(stateMutex);
pendingInterruptPins.clear();
}
+99
View File
@@ -0,0 +1,99 @@
#ifndef PI_HAL_SERIAL_H
#define PI_HAL_SERIAL_H
#include <RadioLib.h>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <mesh/generated/meshtastic/serial_hal.pb.h>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#define SERIAL_PI_INPUT (0)
#define SERIAL_PI_OUTPUT (1)
#define SERIAL_PI_LOW (0)
#define SERIAL_PI_HIGH (1)
#define SERIAL_PI_RISING (1)
#define SERIAL_PI_FALLING (2)
class SerialHal : public RadioLibHal
{
public:
explicit SerialHal(const std::string &device, uint32_t baud = 115200, uint32_t timeoutMs = 500);
~SerialHal() override;
void init() override {}
void term() override {}
void pinMode(uint32_t pin, uint32_t mode) override;
void digitalWrite(uint32_t pin, uint32_t value) override;
uint32_t digitalRead(uint32_t pin) override;
void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override;
void detachInterrupt(uint32_t interruptNum) override;
void delay(unsigned long ms) override;
void delayMicroseconds(unsigned long us) override;
void yield() override;
unsigned long millis() override;
unsigned long micros() override;
long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override;
void spiBegin() override {}
void spiBeginTransaction() override {}
void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override;
void spiEndTransaction() override {}
void spiEnd() override {}
bool checkError();
private:
bool openPort();
void closePort();
bool sendRequest(const meshtastic_SerialHalCommand &cmd, meshtastic_SerialHalResponse *response);
bool writeAll(const uint8_t *data, size_t len);
bool readExact(uint8_t *data, size_t len);
bool waitForReadable(int timeoutMs);
bool readFrame(std::vector<uint8_t> &payload, int firstByteTimeoutMs);
void readerLoop();
void interruptDispatchLoop();
void startReaderThread();
void stopReaderThread();
uint16_t crc16(const uint8_t *data, size_t len) const;
void setTransportError(const char *msg);
std::string device;
uint32_t baud;
uint32_t timeoutMs;
int fd = -1;
bool hasWarned = false;
std::atomic<bool> inError{false};
std::atomic<uint16_t> txId{1};
std::mutex fdMutex;
std::mutex writeMutex;
std::mutex stateMutex;
std::condition_variable responseCv;
std::thread readerThread;
std::thread interruptThread;
std::atomic<bool> readerStopRequested{false};
std::atomic<bool> readerRunning{false};
std::atomic<bool> interruptDispatcherRunning{false};
std::condition_variable interruptCv;
std::deque<uint32_t> pendingInterruptPins;
std::unordered_map<uint32_t, void (*)(void)> interruptCallbacks;
std::unordered_map<uint16_t, meshtastic_SerialHalResponse> pendingResponses;
};
#endif