mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-15 07:09:50 +00:00
474 lines
15 KiB
C++
474 lines
15 KiB
C++
// Copyright (c) 2024 microReticulum contributors
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "SX1262Interface.h"
|
|
#include "SX1262Bitrate.h"
|
|
#include <microReticulum/Log.h>
|
|
#include <microReticulum/Utilities/OS.h>
|
|
|
|
#ifdef ARDUINO
|
|
#include <SPI.h>
|
|
#endif
|
|
|
|
using namespace RNS;
|
|
|
|
#ifdef ARDUINO
|
|
// Static members for SPI mutex (shared with display and SD card)
|
|
SemaphoreHandle_t SX1262Interface::_spi_mutex = nullptr;
|
|
bool SX1262Interface::_mutex_initialized = false;
|
|
|
|
void SX1262Interface::set_spi_mutex(SemaphoreHandle_t mutex) {
|
|
_spi_mutex = mutex;
|
|
_mutex_initialized = (mutex != nullptr);
|
|
if (_mutex_initialized) {
|
|
DEBUG("SX1262Interface: Using external SPI mutex");
|
|
}
|
|
}
|
|
|
|
bool SX1262Interface::lock_activity(TickType_t timeout_ticks) const {
|
|
return _activity_mutex != nullptr &&
|
|
xSemaphoreTake(_activity_mutex, timeout_ticks) == pdTRUE;
|
|
}
|
|
|
|
void SX1262Interface::unlock_activity() const {
|
|
xSemaphoreGive(_activity_mutex);
|
|
}
|
|
#endif
|
|
|
|
SX1262Interface::SX1262Interface(const char* name) : InterfaceImpl(name) {
|
|
_IN = true;
|
|
_OUT = true;
|
|
_HW_MTU = HW_MTU;
|
|
_AUTOCONFIGURE_MTU = true;
|
|
|
|
_bitrate = calculate_lora_bitrate_bps(
|
|
_config.bandwidth,
|
|
_config.spreading_factor,
|
|
_config.coding_rate
|
|
);
|
|
#ifdef ARDUINO
|
|
_activity_mutex = xSemaphoreCreateMutex();
|
|
if (_activity_mutex == nullptr) {
|
|
ERROR("SX1262Interface: Failed to create activity mutex");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
SX1262Interface::~SX1262Interface() {
|
|
stop();
|
|
#ifdef ARDUINO
|
|
if (_activity_mutex != nullptr) {
|
|
vSemaphoreDelete(_activity_mutex);
|
|
_activity_mutex = nullptr;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void SX1262Interface::set_config(const SX1262Config& config) {
|
|
#ifdef ARDUINO
|
|
if (lock_activity(portMAX_DELAY)) {
|
|
_activity_history.reset();
|
|
_last_activity_sample_ms = millis();
|
|
unlock_activity();
|
|
}
|
|
#else
|
|
_activity_history.reset();
|
|
_last_activity_sample_ms = 0;
|
|
#endif
|
|
|
|
_config = config;
|
|
|
|
_bitrate = calculate_lora_bitrate_bps(
|
|
_config.bandwidth,
|
|
_config.spreading_factor,
|
|
_config.coding_rate
|
|
);
|
|
}
|
|
|
|
std::string SX1262Interface::toString() const {
|
|
return "SX1262Interface[" + _name + "]";
|
|
}
|
|
|
|
bool SX1262Interface::start() {
|
|
_online = false;
|
|
|
|
#ifdef ARDUINO
|
|
INFO("SX1262Interface: Initializing...");
|
|
INFO(" Frequency: " + std::to_string(_config.frequency) + " MHz");
|
|
INFO(" Bandwidth: " + std::to_string(_config.bandwidth) + " kHz");
|
|
INFO(" SF: " + std::to_string(_config.spreading_factor));
|
|
INFO(" CR: 4/" + std::to_string(_config.coding_rate));
|
|
INFO(" TX Power: " + std::to_string(_config.tx_power) + " dBm");
|
|
|
|
// Use external mutex if provided, otherwise create our own (fallback)
|
|
if (!_mutex_initialized) {
|
|
WARNING("SX1262Interface: No external SPI mutex set, creating own");
|
|
_spi_mutex = xSemaphoreCreateMutex();
|
|
if (_spi_mutex == nullptr) {
|
|
ERROR("SX1262Interface: Failed to create SPI mutex");
|
|
return false;
|
|
}
|
|
_mutex_initialized = true;
|
|
}
|
|
|
|
// Acquire SPI mutex
|
|
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
|
|
ERROR("SX1262Interface: Failed to acquire SPI mutex for init");
|
|
return false;
|
|
}
|
|
|
|
// Set radio CS high to avoid conflicts
|
|
pinMode(SX1262Pins::CS, OUTPUT);
|
|
digitalWrite(SX1262Pins::CS, HIGH);
|
|
|
|
// Use global SPI (FSPI) — all peripherals share same SPI peripheral
|
|
// to avoid GPIO matrix conflicts. SPI.begin() already called by SDAccess or Display.
|
|
_lora_spi = &SPI;
|
|
DEBUG("SX1262Interface: Using global SPI (FSPI) for LoRa");
|
|
|
|
// Create RadioLib module and radio with the shared SPI instance
|
|
_module = new Module(SX1262Pins::CS, SX1262Pins::DIO1, SX1262Pins::RST, SX1262Pins::BUSY, *_lora_spi);
|
|
_radio = new SX1262(_module);
|
|
|
|
// Initialize radio with configuration
|
|
int16_t state = _radio->begin(
|
|
_config.frequency,
|
|
_config.bandwidth,
|
|
_config.spreading_factor,
|
|
_config.coding_rate,
|
|
_config.sync_word,
|
|
_config.tx_power,
|
|
_config.preamble_length
|
|
);
|
|
|
|
if (state != RADIOLIB_ERR_NONE) {
|
|
ERROR("SX1262Interface: Radio init failed, code " + std::to_string(state));
|
|
xSemaphoreGive(_spi_mutex);
|
|
delete _radio;
|
|
delete _module;
|
|
_radio = nullptr;
|
|
_module = nullptr;
|
|
return false;
|
|
}
|
|
|
|
// Enable CRC for error detection
|
|
state = _radio->setCRC(true);
|
|
if (state != RADIOLIB_ERR_NONE) {
|
|
WARNING("SX1262Interface: Failed to enable CRC, code " + std::to_string(state));
|
|
}
|
|
|
|
// Use explicit header mode (includes length in LoRa header)
|
|
state = _radio->explicitHeader();
|
|
if (state != RADIOLIB_ERR_NONE) {
|
|
WARNING("SX1262Interface: Failed to set explicit header, code " + std::to_string(state));
|
|
}
|
|
|
|
xSemaphoreGive(_spi_mutex);
|
|
|
|
// Start listening for packets
|
|
start_receive();
|
|
|
|
_online = true;
|
|
INFO("SX1262Interface: Initialized successfully");
|
|
INFO(" Bitrate: " + std::to_string(Utilities::OS::round(_bitrate / 1000.0, 2)) + " kbps");
|
|
|
|
return true;
|
|
#else
|
|
ERROR("SX1262Interface: Not supported on this platform");
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void SX1262Interface::stop() {
|
|
#ifdef ARDUINO
|
|
if (_radio != nullptr) {
|
|
if (_spi_mutex != nullptr && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
_radio->standby();
|
|
xSemaphoreGive(_spi_mutex);
|
|
}
|
|
|
|
delete _radio;
|
|
delete _module;
|
|
_radio = nullptr;
|
|
_module = nullptr;
|
|
}
|
|
#endif
|
|
|
|
_online = false;
|
|
INFO("SX1262Interface: Stopped");
|
|
}
|
|
|
|
void SX1262Interface::start_receive() {
|
|
#ifdef ARDUINO
|
|
if (_radio == nullptr) return;
|
|
|
|
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
int16_t state = _radio->startReceive();
|
|
xSemaphoreGive(_spi_mutex);
|
|
|
|
if (state != RADIOLIB_ERR_NONE) {
|
|
ERROR("SX1262Interface: Failed to start receive, code " + std::to_string(state));
|
|
}
|
|
} else {
|
|
ERROR("SX1262Interface: Failed to acquire SPI mutex for start_receive!");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void SX1262Interface::sample_radio_activity(uint32_t now_ms) {
|
|
#ifdef ARDUINO
|
|
if (!_online || _radio == nullptr || _spi_mutex == nullptr) return;
|
|
|
|
uint32_t activity_generation = 0;
|
|
uint32_t last_activity_sample_ms = 0;
|
|
if (!lock_activity(pdMS_TO_TICKS(2))) return;
|
|
activity_generation = _activity_history.generation();
|
|
last_activity_sample_ms = _last_activity_sample_ms;
|
|
now_ms = millis();
|
|
unlock_activity();
|
|
|
|
const uint32_t elapsed = now_ms - last_activity_sample_ms;
|
|
if (elapsed < ACTIVITY_SAMPLE_INTERVAL_MS) return;
|
|
const uint32_t raw_due = elapsed / ACTIVITY_SAMPLE_INTERVAL_MS;
|
|
const std::size_t due = raw_due > RadioActivity::History::CAPACITY
|
|
? RadioActivity::History::CAPACITY
|
|
: static_cast<std::size_t>(raw_due);
|
|
|
|
auto advance_clock = [&]() {
|
|
if (raw_due > RadioActivity::History::CAPACITY) {
|
|
_last_activity_sample_ms = now_ms;
|
|
} else {
|
|
_last_activity_sample_ms = last_activity_sample_ms +
|
|
raw_due * ACTIVITY_SAMPLE_INTERVAL_MS;
|
|
}
|
|
};
|
|
auto record_gaps = [&](std::size_t count) {
|
|
if (!lock_activity(pdMS_TO_TICKS(2))) return;
|
|
if (_activity_history.generation() != activity_generation) {
|
|
unlock_activity();
|
|
return;
|
|
}
|
|
const std::size_t pending_span = _activity_history.pending_bucket_span();
|
|
const std::size_t active_span = pending_span > count ? count : pending_span;
|
|
const std::size_t inactive_count = count - active_span;
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
_activity_history.record_gap(i >= inactive_count);
|
|
}
|
|
advance_clock();
|
|
unlock_activity();
|
|
};
|
|
|
|
if (_transmitting) {
|
|
record_gaps(due);
|
|
return;
|
|
}
|
|
|
|
// Sampling is deliberately best-effort: display/SD/radio work always wins.
|
|
if (xSemaphoreTake(_spi_mutex, 0) != pdTRUE) {
|
|
record_gaps(due);
|
|
return;
|
|
}
|
|
if (_transmitting) {
|
|
xSemaphoreGive(_spi_mutex);
|
|
record_gaps(due);
|
|
return;
|
|
}
|
|
const float instantaneous_rssi = _radio->getRSSI(false);
|
|
xSemaphoreGive(_spi_mutex);
|
|
|
|
if (!lock_activity(pdMS_TO_TICKS(2))) return;
|
|
if (_activity_history.generation() != activity_generation) {
|
|
unlock_activity();
|
|
return;
|
|
}
|
|
const std::size_t pending_span = _activity_history.pending_bucket_span();
|
|
const std::size_t active_span = pending_span > due ? due : pending_span;
|
|
const std::size_t inactive_count = due - active_span;
|
|
for (std::size_t i = 0; i + 1 < due; ++i) {
|
|
_activity_history.record_gap(i >= inactive_count);
|
|
}
|
|
_activity_history.record(static_cast<int16_t>(instantaneous_rssi));
|
|
advance_clock();
|
|
unlock_activity();
|
|
#else
|
|
(void)now_ms;
|
|
#endif
|
|
}
|
|
|
|
RadioActivity::Snapshot SX1262Interface::radio_activity_snapshot() const {
|
|
#ifdef ARDUINO
|
|
if (!lock_activity(pdMS_TO_TICKS(2))) return {};
|
|
RadioActivity::Snapshot result = _activity_history.snapshot();
|
|
unlock_activity();
|
|
return result;
|
|
#else
|
|
return _activity_history.snapshot();
|
|
#endif
|
|
}
|
|
|
|
void SX1262Interface::loop() {
|
|
if (!_online) return;
|
|
|
|
#ifdef ARDUINO
|
|
if (_radio == nullptr) return;
|
|
|
|
uint32_t activity_generation = 0;
|
|
bool activity_generation_valid = false;
|
|
if (lock_activity(portMAX_DELAY)) {
|
|
activity_generation = _activity_history.generation();
|
|
activity_generation_valid = true;
|
|
unlock_activity();
|
|
}
|
|
|
|
// Try to acquire SPI mutex (non-blocking to avoid stalling display)
|
|
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5)) != pdTRUE) {
|
|
return; // Display is using SPI, try again later
|
|
}
|
|
|
|
// Check IRQ status to see if a packet was actually received
|
|
uint16_t irqStatus = _radio->getIrqStatus();
|
|
|
|
// Only process if RX_DONE flag is set (0x0002 for SX126x)
|
|
if (!(irqStatus & 0x0002)) {
|
|
xSemaphoreGive(_spi_mutex);
|
|
return; // No new packet
|
|
}
|
|
|
|
// Read the received packet (this also clears IRQ internally)
|
|
int16_t state = _radio->readData(_rx_buffer.writable(HW_MTU), HW_MTU);
|
|
|
|
// Immediately restart receive to clear IRQ flags and prepare for next packet
|
|
_radio->startReceive();
|
|
|
|
if (state == RADIOLIB_ERR_NONE) {
|
|
// Got a packet
|
|
size_t len = _radio->getPacketLength();
|
|
if (len > 1) { // Must have at least header + data
|
|
_rx_buffer.resize(len);
|
|
|
|
// Get signal quality
|
|
_last_rssi = _radio->getRSSI();
|
|
_last_snr = _radio->getSNR();
|
|
|
|
xSemaphoreGive(_spi_mutex);
|
|
|
|
// RNode packet format: [1-byte random header][payload]
|
|
// Skip header byte, pass payload to transport
|
|
Bytes payload = _rx_buffer.mid(1);
|
|
|
|
DEBUG("SX1262Interface: Received " + std::to_string(len) + " bytes, " +
|
|
"RSSI=" + std::to_string((int)_last_rssi) + " dBm, " +
|
|
"SNR=" + std::to_string((int)_last_snr) + " dB");
|
|
|
|
// SPI is already released. Wait for the bounded history mutex so a
|
|
// real receive marker cannot be dropped behind a snapshot/catch-up.
|
|
if (activity_generation_valid && lock_activity(portMAX_DELAY)) {
|
|
if (_activity_history.generation() != activity_generation) {
|
|
unlock_activity();
|
|
} else {
|
|
_activity_history.mark_event(RadioActivity::Event::Rx);
|
|
unlock_activity();
|
|
}
|
|
}
|
|
|
|
on_incoming(payload);
|
|
return;
|
|
}
|
|
} else if (state != RADIOLIB_ERR_RX_TIMEOUT) {
|
|
// An error occurred (not just timeout)
|
|
ERROR("SX1262Interface: Receive error, code " + std::to_string(state));
|
|
}
|
|
|
|
xSemaphoreGive(_spi_mutex);
|
|
#endif
|
|
}
|
|
|
|
bool SX1262Interface::send_outgoing(const Bytes& data) {
|
|
if (!_online) return false;
|
|
|
|
#ifdef ARDUINO
|
|
if (_radio == nullptr) return false;
|
|
|
|
uint32_t activity_generation = 0;
|
|
bool activity_generation_valid = false;
|
|
if (lock_activity(portMAX_DELAY)) {
|
|
activity_generation = _activity_history.generation();
|
|
activity_generation_valid = true;
|
|
unlock_activity();
|
|
}
|
|
|
|
DEBUG(toString() + ": Sending " + std::to_string(data.size()) + " bytes");
|
|
|
|
// Build packet with random header (RNode-compatible format)
|
|
// Header: upper 4 bits random, lower 4 bits reserved
|
|
uint8_t header = Cryptography::randomnum(256) & 0xF0;
|
|
|
|
size_t len = 1 + data.size();
|
|
if (len > HW_MTU) {
|
|
ERROR("SX1262Interface: Packet too large (" + std::to_string(len) + " > " + std::to_string(HW_MTU) + ")");
|
|
return false;
|
|
}
|
|
|
|
uint8_t* buf = new uint8_t[len];
|
|
buf[0] = header;
|
|
memcpy(buf + 1, data.data(), data.size());
|
|
|
|
// Acquire SPI mutex
|
|
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
|
|
ERROR("SX1262Interface: Failed to acquire SPI mutex for TX");
|
|
delete[] buf;
|
|
return false;
|
|
}
|
|
|
|
_transmitting = true;
|
|
const uint32_t tx_started_ms = millis();
|
|
|
|
// Transmit (blocking)
|
|
int16_t state = _radio->transmit(buf, len);
|
|
const uint32_t tx_duration_ms = millis() - tx_started_ms;
|
|
|
|
_transmitting = false;
|
|
|
|
// Return to receive mode immediately while still holding SPI mutex
|
|
// (no gap for display task to steal the bus and leave radio in STANDBY)
|
|
int16_t rxState = _radio->startReceive();
|
|
xSemaphoreGive(_spi_mutex);
|
|
delete[] buf;
|
|
|
|
if (rxState != RADIOLIB_ERR_NONE) {
|
|
ERROR("SX1262Interface: Failed to restart receive after TX, code " + std::to_string(rxState));
|
|
}
|
|
|
|
if (state == RADIOLIB_ERR_NONE) {
|
|
DEBUG("SX1262Interface: Sent " + std::to_string(len) + " bytes");
|
|
std::size_t tx_buckets = static_cast<std::size_t>(
|
|
(tx_duration_ms + ACTIVITY_SAMPLE_INTERVAL_MS - 1) /
|
|
ACTIVITY_SAMPLE_INTERVAL_MS);
|
|
if (tx_buckets == 0) tx_buckets = 1;
|
|
// SPI is already released. Wait for the bounded history mutex so a
|
|
// completed transmission duration cannot disappear from the timeline.
|
|
if (activity_generation_valid && lock_activity(portMAX_DELAY)) {
|
|
if (_activity_history.generation() != activity_generation) {
|
|
unlock_activity();
|
|
} else {
|
|
_activity_history.mark_event(RadioActivity::Event::Tx, tx_buckets);
|
|
unlock_activity();
|
|
}
|
|
}
|
|
// Perform post-send housekeeping
|
|
InterfaceImpl::handle_outgoing(data);
|
|
return true;
|
|
} else {
|
|
ERROR("SX1262Interface: Transmit failed, code " + std::to_string(state));
|
|
return false;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
void SX1262Interface::on_incoming(const Bytes& data) {
|
|
DEBUG(toString() + ": Incoming " + std::to_string(data.size()) + " bytes");
|
|
// Pass received data to transport
|
|
InterfaceImpl::handle_incoming(data);
|
|
}
|