Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d132bd3154 | ||
|
|
d2eb635b36 | ||
|
|
da50aab27d | ||
|
|
9f34ea4230 | ||
|
|
8dd5377676 | ||
|
|
6c0ef72d02 | ||
|
|
c2624c6140 | ||
|
|
1de3e292a8 | ||
|
|
64949156d6 | ||
|
|
1aab2d880d | ||
|
|
7550b16f91 | ||
|
|
5778a469cf | ||
|
|
2ca305d439 | ||
|
|
0a2e4c7e0f | ||
|
|
211af899bd | ||
|
|
d292217c34 | ||
|
|
7eb5582646 | ||
|
|
a01937d6a9 | ||
|
|
0bce1e937f | ||
|
|
94b343d3d8 | ||
|
|
8ef885a7ba | ||
|
|
957b568a2d | ||
|
|
f0a67c5cb0 | ||
|
|
4a73482162 | ||
|
|
aad1a27cbf | ||
|
|
2b5e2b985f | ||
|
|
1997cbce40 | ||
|
|
ff1f02d164 | ||
|
|
2e5f44db16 | ||
|
|
1d595beab7 | ||
|
|
f382718f16 | ||
|
|
76a79dd8ec | ||
|
|
da236599d6 | ||
|
|
39908bfbd2 | ||
|
|
bcceff6f4d | ||
|
|
273bcdc4db | ||
|
|
a4eb6b0b63 | ||
|
|
5fa9b05d70 | ||
|
|
cccb181335 | ||
|
|
b365c94981 | ||
|
|
0d08680104 | ||
|
|
51a5957cf4 | ||
|
|
2bd1faa7af | ||
|
|
4271481300 | ||
|
|
64f415dc0f | ||
|
|
1d1ca1053b | ||
|
|
9a94e8bd9f |
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DLILYGO_TBEAM_BPF",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "t-beam-bpf"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "LilyGo TBeam-BPF",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "http://www.lilygo.cn/",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
@@ -1367,6 +1367,22 @@ bool Power::axpChipInit()
|
||||
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
|
||||
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
|
||||
PMU->disablePowerOutput(XPOWERS_VBACKUP);
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
|
||||
// T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
|
||||
// DCDC1 -> ESP32 + OLED 3V3 (always on, protected)
|
||||
// ALDO2 -> MicroSD 3V3 (OFF at reset, must enable)
|
||||
// ALDO4 -> L76K GNSS 3V3 (OFF at reset, must enable)
|
||||
// ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults.
|
||||
// LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16).
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||
|
||||
// Make sure nothing's driving into an unused rail
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC5);
|
||||
PMU->disablePowerOutput(XPOWERS_DLDO1);
|
||||
}
|
||||
|
||||
// disable all axp chip interrupt
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "memGet.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <assert.h>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
@@ -21,22 +20,6 @@
|
||||
#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
|
||||
@@ -298,10 +281,6 @@ 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]);
|
||||
|
||||
@@ -24,11 +24,6 @@ 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
|
||||
*/
|
||||
|
||||
@@ -219,6 +219,16 @@ static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool
|
||||
|
||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{
|
||||
#ifdef HAS_HAM_2M_ONLY
|
||||
// Hardware is restricted to the amateur 2m band — offer only the 2m regions
|
||||
// so the user cannot pick a sub-GHz region the RF path cannot emit or receive.
|
||||
static const LoraRegionOption regionOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
|
||||
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
|
||||
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
|
||||
};
|
||||
#else
|
||||
static const LoraRegionOption regionOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
|
||||
@@ -255,6 +265,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"ITU2_125CM (220-225)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM},
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
|
||||
static std::array<const char *, regionCount> regionLabels{};
|
||||
|
||||
+5
-8
@@ -1056,12 +1056,8 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
std::unique_ptr<RadioInterface> rIf;
|
||||
if (!config.lora.serial_hal_only) {
|
||||
rIf = initLoRa();
|
||||
} else {
|
||||
LOG_INFO("skipping LoRa radio init, for serialHal");
|
||||
}
|
||||
auto rIf = initLoRa();
|
||||
|
||||
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
@@ -1105,9 +1101,10 @@ void setup()
|
||||
|
||||
// Start airtime logger thread.
|
||||
airTime = new AirTime();
|
||||
if (!rIf && !config.lora.serial_hal_only)
|
||||
|
||||
if (!rIf)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
|
||||
else if (rIf) {
|
||||
else {
|
||||
// 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)))) *
|
||||
|
||||
@@ -1742,6 +1742,13 @@ void NodeDB::installDefaultDeviceState()
|
||||
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
|
||||
owner.has_is_unmessagable = true;
|
||||
owner.is_unmessagable = false;
|
||||
|
||||
#ifdef HAS_HAM_2M_ONLY
|
||||
// Ham-band-only hardware defaults to licensed operation. The user can still flip this off later
|
||||
// (e.g. a commercial operator on an adjacent allocation who wants to keep encryption on) — we
|
||||
// only set the default here, not on every boot.
|
||||
owner.is_licensed = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// We reserve a few nodenums for future use
|
||||
|
||||
@@ -113,6 +113,11 @@ void RF95Interface::setTransmitEnable(bool txon)
|
||||
/// \return true if initialisation succeeded.
|
||||
bool RF95Interface::init()
|
||||
{
|
||||
#ifdef RF95_POWER_EN
|
||||
pinMode(RF95_POWER_EN, OUTPUT);
|
||||
digitalWrite(RF95_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
RadioLibInterface::init();
|
||||
|
||||
#if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT)
|
||||
@@ -335,6 +340,10 @@ bool RF95Interface::sleep()
|
||||
setStandby(); // First cancel any active receiving/sending
|
||||
lora->sleep();
|
||||
|
||||
#ifdef RF95_POWER_EN
|
||||
digitalWrite(RF95_POWER_EN, LOW);
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
digitalWrite(RF95_FAN_EN, 0);
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#include "platform/portduino/SerialHal.h"
|
||||
#include "platform/portduino/SimRadio.h"
|
||||
#include "platform/portduino/USBHal.h"
|
||||
#endif
|
||||
@@ -353,9 +352,6 @@ 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;
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#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);
|
||||
};
|
||||
+17
-95
@@ -1,15 +1,12 @@
|
||||
#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()
|
||||
@@ -82,29 +79,18 @@ 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) { // 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 == 1) { // looking for START2
|
||||
if (c != START2)
|
||||
rxPtr = 0; // failed to find framing
|
||||
} 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
|
||||
uint32_t maxLen = rxIsSerialHal ? (uint32_t)meshtastic_SerialHalCommand_size : MAX_TO_FROM_RADIO_SIZE;
|
||||
if (len > maxLen)
|
||||
// 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)
|
||||
rxPtr = 0; // length is bogus, restart search for framing
|
||||
}
|
||||
|
||||
@@ -112,16 +98,8 @@ 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
|
||||
|
||||
// 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);
|
||||
// If we didn't just fail the packet and we now have the right # of bytes, parse it
|
||||
handleToRadio(rxBuf + HEADER_LEN, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,12 +114,7 @@ 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);
|
||||
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
|
||||
return recentRx ? 5 : 250;
|
||||
} else {
|
||||
while (stream->available()) { // Currently we never want to block
|
||||
int cInt = stream->read();
|
||||
@@ -162,30 +135,18 @@ int32_t StreamAPI::readStream()
|
||||
if (ptr == 0) { // looking for START1
|
||||
if (c != START1)
|
||||
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 == 1) { // looking for START2
|
||||
if (c != START2)
|
||||
rxPtr = 0; // failed to find framing
|
||||
} 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
|
||||
uint32_t maxLen = rxIsSerialHal ? (uint32_t)meshtastic_SerialHalCommand_size : MAX_TO_FROM_RADIO_SIZE;
|
||||
if (len > maxLen)
|
||||
// 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)
|
||||
rxPtr = 0; // length is bogus, restart search for framing
|
||||
}
|
||||
|
||||
@@ -193,16 +154,8 @@ 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
|
||||
|
||||
// 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);
|
||||
// If we didn't just fail the packet and we now have the right # of bytes, parse it
|
||||
handleToRadio(rxBuf + HEADER_LEN, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,10 +199,6 @@ 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
|
||||
@@ -300,31 +249,4 @@ 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);
|
||||
}
|
||||
+2
-26
@@ -4,15 +4,10 @@
|
||||
#include "Stream.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "generated/meshtastic/serial_hal.pb.h"
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
|
||||
// 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 To/FromRadio packet + our 32 bit header
|
||||
#define MAX_STREAM_BUF_SIZE (MAX_TO_FROM_RADIO_SIZE + sizeof(uint32_t))
|
||||
|
||||
/**
|
||||
* A version of our 'phone' API that talks over a Stream. So therefore well suited to use with serial links
|
||||
@@ -44,8 +39,6 @@ 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;
|
||||
@@ -63,17 +56,6 @@ 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
|
||||
@@ -93,12 +75,6 @@ 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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
|
||||
#elif defined(T_BEAM_1W)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
|
||||
#elif defined(T_BEAM_BPF)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF
|
||||
#elif defined(T_LORA_PAGER)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
|
||||
#elif defined(HELTEC_V4)
|
||||
|
||||
@@ -598,8 +598,7 @@ 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" || portduino_config.lora_spi_dev == "serial")) {
|
||||
if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
|
||||
continue;
|
||||
}
|
||||
if (i->enabled) {
|
||||
@@ -618,8 +617,7 @@ 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" || portduino_config.lora_spi_dev == "serial")) {
|
||||
if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
|
||||
continue;
|
||||
}
|
||||
if (i.enabled) {
|
||||
@@ -666,8 +664,7 @@ 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" || portduino_config.lora_spi_dev == "serial")) {
|
||||
if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
|
||||
continue;
|
||||
}
|
||||
if (i.enabled && i.default_high) {
|
||||
@@ -677,8 +674,7 @@ 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" &&
|
||||
portduino_config.lora_spi_dev != "serial") {
|
||||
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
|
||||
SPI.begin(portduino_config.lora_spi_dev.c_str());
|
||||
}
|
||||
|
||||
@@ -712,7 +708,6 @@ 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;
|
||||
}
|
||||
@@ -849,15 +844,12 @@ 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" && portduino_config.lora_spi_dev != "serial") {
|
||||
if (portduino_config.lora_spi_dev != "ch341") {
|
||||
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';
|
||||
|
||||
@@ -86,9 +86,6 @@ 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;
|
||||
@@ -279,12 +276,6 @@ 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) {
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
#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
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef Pins_Arduino_h
|
||||
#define Pins_Arduino_h
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define USB_VID 0x303a
|
||||
#define USB_PID 0x1001
|
||||
|
||||
// UART1 (qwiic)
|
||||
static const uint8_t TX = 43;
|
||||
static const uint8_t RX = 44;
|
||||
|
||||
// I2C for OLED and sensors
|
||||
static const uint8_t SDA = 8;
|
||||
static const uint8_t SCL = 9;
|
||||
|
||||
// Default SPI mapped to Radio/SD
|
||||
static const uint8_t SS = 1; // LoRa CS
|
||||
static const uint8_t MOSI = 11;
|
||||
static const uint8_t MISO = 13;
|
||||
static const uint8_t SCK = 12;
|
||||
|
||||
// SD Card CS
|
||||
#define SDCARD_CS 10
|
||||
|
||||
#endif /* Pins_Arduino_h */
|
||||
@@ -0,0 +1,23 @@
|
||||
; LilyGo T-Beam-BPF (144-148Mhz) ~5 Watt (37 dBm)
|
||||
[env:t-beam-bpf]
|
||||
custom_meshtastic_hw_model = 124
|
||||
custom_meshtastic_hw_model_slug = TBEAM_BPF
|
||||
custom_meshtastic_architecture = esp32s3
|
||||
custom_meshtastic_actively_supported = true
|
||||
custom_meshtastic_support_level = 3
|
||||
custom_meshtastic_display_name = LILYGO T-Beam BPF
|
||||
custom_meshtastic_images = tbeam-1w.svg
|
||||
custom_meshtastic_tags = LilyGo
|
||||
|
||||
extends = esp32s3_base
|
||||
board = t-beam-bpf
|
||||
board_build.partitions = default_16MB.csv
|
||||
board_check = true
|
||||
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
|
||||
build_flags =
|
||||
${esp32s3_base.build_flags}
|
||||
-I variants/esp32s3/t-beam-bpf
|
||||
-D T_BEAM_BPF
|
||||
@@ -0,0 +1,70 @@
|
||||
// LilyGo T-Beam-BPF variant.h
|
||||
// Configuration based on LilyGO utilities.h and RF documentation
|
||||
|
||||
// Hardware is restricted to the amateur 2m band (144-148 MHz).
|
||||
#define HAS_HAM_2M_ONLY 1
|
||||
|
||||
// I2C for OLED display (SH1106 at 0x3C)
|
||||
#define I2C_SDA 8
|
||||
#define I2C_SCL 9
|
||||
|
||||
// GPS - Quectel L76K. Per schematic sheet 7:
|
||||
|
||||
#define GPS_RX_PIN 5
|
||||
#define GPS_TX_PIN 6
|
||||
#define GPS_1PPS_PIN 7
|
||||
#define HAS_GPS 1
|
||||
#define GPS_BAUDRATE 9600
|
||||
|
||||
// Buttons
|
||||
#define BUTTON_PIN 0 // BUTTON 1
|
||||
#define ALT_BUTTON_PIN 3 // BUTTON 2
|
||||
|
||||
// SPI (shared by LoRa and SD)
|
||||
#define SPI_MOSI 11
|
||||
#define SPI_SCK 12
|
||||
#define SPI_MISO 13
|
||||
#define SPI_CS 10
|
||||
|
||||
// SD Card
|
||||
#define HAS_SDCARD
|
||||
#define SDCARD_USE_SPI1
|
||||
// #define SDCARD_CS SPI_CS (already defined in pins_arduino.h)
|
||||
|
||||
// LoRa Radio - SX1278 144-148Mhz
|
||||
#define USE_RF95
|
||||
#define LORA_SCK SPI_SCK
|
||||
#define LORA_MISO SPI_MISO
|
||||
#define LORA_MOSI SPI_MOSI
|
||||
#define LORA_CS 1
|
||||
#define LORA_RESET 18
|
||||
#define LORA_IRQ 14
|
||||
#define LORA_DIO0 LORA_IRQ
|
||||
#define LORA_DIO1 21
|
||||
#define RF95_RXEN 39 // LNA enable - HIGH during RX
|
||||
|
||||
// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()!
|
||||
// GPIO 16 powers the SX1278 via LDO
|
||||
#define RF95_POWER_EN 16
|
||||
|
||||
// +27dBm PA
|
||||
// LilyGo Docs specify SX1278 power must be capped at 10
|
||||
#define RF95_MAX_POWER 10
|
||||
// PA curve mapping (TX values 1-10)
|
||||
// Measurements taken with 5V USB power (max 37 dBm on battery power)
|
||||
// Input values: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
||||
// Output values: 0, 34, 35, 36, 36, 37, 37, 38, 38, 38
|
||||
#define NUM_PA_POINTS 10
|
||||
#define TX_GAIN_LORA 0, 32, 32, 32, 31, 31, 30, 30, 29, 28
|
||||
|
||||
// Display - SH1106 OLED (128x64)
|
||||
#define USE_SH1106
|
||||
#define OLED_WIDTH 128
|
||||
#define OLED_HEIGHT 64
|
||||
|
||||
// 32768 Hz crystal present
|
||||
#define HAS_32768HZ 1
|
||||
|
||||
// PMU
|
||||
#define HAS_AXP2101
|
||||
// #define PMU_IRQ 4 // Leave disabled for now
|
||||
Reference in New Issue
Block a user