Compare commits

..
47 Commits
Author SHA1 Message Date
AustinandGitHub d132bd3154 Merge branch 'develop' into t-beam-bpf 2026-06-22 09:29:54 -04:00
AustinandGitHub d2eb635b36 Merge branch 'develop' into t-beam-bpf 2026-06-17 17:05:36 -04:00
AustinandGitHub da50aab27d Merge branch 'develop' into t-beam-bpf 2026-06-12 17:47:23 -04:00
AustinandGitHub 9f34ea4230 Merge branch 'develop' into t-beam-bpf 2026-06-09 20:23:02 -04:00
vidplace7 8dd5377676 This is a 5-6W radio!! Add TX_GAIN_LORA 2026-06-05 18:50:10 -04:00
vidplace7 6c0ef72d02 Add template for PA curve 2026-06-05 09:10:42 -04:00
AustinandGitHub c2624c6140 Merge branch 'develop' into t-beam-bpf 2026-06-04 13:51:51 -04:00
vidplace7 1de3e292a8 Add comment back 2026-06-04 13:51:04 -04:00
vidplace7 64949156d6 Remove mismatch guarding (we need a more comprehensive approach) 2026-06-04 13:49:32 -04:00
vidplace7 1aab2d880d Remove other-variant changes (BPF-only)
These have been moved to other PRs
2026-06-03 20:40:43 -04:00
AustinandGitHub 7550b16f91 Merge branch 'develop' into t-beam-bpf 2026-06-03 18:06:16 -04:00
vidplace7 5778a469cf Set RF95 pins for T-Beam Supreme
May cause regressions!!
2026-06-03 15:59:37 -04:00
vidplace7 2ca305d439 RadioLib doesn't accept 15.625 kHz
Use 15.6 instead
2026-06-03 13:49:44 -04:00
vidplace7 0a2e4c7e0f Adjust slotOverrides for TinyFast 2026-06-03 12:31:11 -04:00
vidplace7 211af899bd Switch to TinyFast
Still need to flesh out the default channels
2026-06-03 11:55:57 -04:00
AustinandGitHub d292217c34 Merge branch 'develop' into t-beam-bpf 2026-06-03 11:46:45 -04:00
AustinandGitHub 7eb5582646 Merge branch 'develop' into t-beam-bpf 2026-06-01 18:25:02 -04:00
AustinandGitHub a01937d6a9 Merge branch 'develop' into t-beam-bpf 2026-06-01 16:17:58 -04:00
vidplace7 0bce1e937f Initial default slots 2026-05-30 18:04:31 -04:00
vidplace7 94b343d3d8 Trunk fmt 2026-05-30 17:29:41 -04:00
vidplace7 8ef885a7ba Re-add PROFILE_HAM regionprofile
Accidentally removed in last merge
2026-05-30 17:24:15 -04:00
AustinandGitHub 957b568a2d Merge branch 'develop' into t-beam-bpf 2026-05-30 17:22:22 -04:00
vidplace7 f0a67c5cb0 Add HAS_HAM_2M definition to variant headers for 2M support 2026-05-26 22:46:56 -04:00
vidplace7 4a73482162 Add ITU region options to MenuAction enum 2026-05-26 22:07:47 -04:00
vidplace7 aad1a27cbf Fix for ITU 2/3 split 2026-05-26 20:19:05 -04:00
AustinandGitHub 2b5e2b985f Merge branch 'develop' into t-beam-bpf 2026-05-26 20:00:09 -04:00
AustinandGitHub 1997cbce40 Move T-Beam-BPF work from #9703 at vidplace7/t-beam-bpf
Initial work to add T-Beam BPF (144-148 Mhz LoRa)
2026-05-26 19:02:30 -04:00
AustinandGitHub ff1f02d164 Merge branch 'develop' into t-beam-bpf 2026-05-26 18:58:51 -04:00
vidplace7 2e5f44db16 Default PROFILE_HAM to slot 17
This is an appropriate default in the USA but not the EU.

The slot override really should follow the region itself, not the regionprofile.
2026-05-21 18:34:16 -04:00
vidplace7 1d595beab7 Default ham to narrow_fast 2026-05-21 18:13:05 -04:00
AustinandGitHub f382718f16 Merge branch 'develop' into t-beam-bpf 2026-05-21 17:16:44 -04:00
vidplace7 76a79dd8ec Fix compilation with pioarduino (USB_MODE) 2026-05-18 15:47:14 -04:00
AustinandGitHub da236599d6 Merge branch 'develop' into t-beam-bpf 2026-05-17 20:14:29 -04:00
Thomas Göttgens 39908bfbd2 Include the back option. 2026-04-21 21:22:33 +02:00
Thomas GöttgensandGitHub bcceff6f4d Merge branch 'develop' into t-beam-bpf 2026-04-21 17:33:56 +02:00
Thomas Göttgens 273bcdc4db Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-firmware into t-beam-bpf 2026-04-21 17:33:24 +02:00
Thomas Göttgens a4eb6b0b63 Lora CS is indeed 1, SD Card CS is 10 2026-04-21 17:32:40 +02:00
5fa9b05d70 Switch pin after defining it as output
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-21 17:26:57 +02:00
Thomas GöttgensandGitHub cccb181335 Merge branch 'develop' into t-beam-bpf 2026-04-21 17:15:51 +02:00
Thomas Göttgens b365c94981 Add ITU regions for this device and make GPS work. 2026-04-21 15:13:43 +02:00
Thomas Göttgens 0d08680104 Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-firmware into t-beam-bpf 2026-04-21 11:52:13 +02:00
Thomas GöttgensandGitHub 51a5957cf4 Merge branch 'develop' into t-beam-bpf 2026-04-21 11:49:30 +02:00
Thomas Göttgens 2bd1faa7af Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-firmware into t-beam-bpf 2026-04-21 09:20:53 +02:00
Thomas Göttgens 4271481300 minor correction to fix compiler warnings 2026-03-26 19:34:35 +01:00
Austin LaneandThomas Göttgens 64f415dc0f Add T-Beam BPF (144-148 Mhz LoRa) 2026-03-26 19:34:35 +01:00
Thomas Göttgens 1d1ca1053b minor correction to fix compiler warnings 2026-03-26 19:33:41 +01:00
Austin Lane 9a94e8bd9f Add T-Beam BPF (144-148 Mhz LoRa) 2026-02-20 21:13:16 -05:00
21 changed files with 232 additions and 1350 deletions
+39
View File
@@ -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"
}
+16
View File
@@ -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
-21
View File
@@ -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]);
-5
View File
@@ -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
*/
+11
View File
@@ -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
View File
@@ -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)))) *
+7
View File
@@ -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
+9
View File
@@ -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
-4
View File
@@ -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;
-385
View File
@@ -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;
}
}
-90
View File
@@ -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
View File
@@ -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
View File
@@ -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;
/**
+2
View File
@@ -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)
+5 -13
View File
@@ -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';
-9
View File
@@ -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) {
-595
View File
@@ -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();
}
-99
View File
@@ -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
+70
View File
@@ -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