Christian Mueller e706b22f31 Add design specification for LoRaWAN Web Portal
Based on Pflichtenheft v2.2, documents the full system design
including daemon, API, frontend, and infrastructure with agreed
deviations (Axum 0.8, SQLx 0.8, Vite 6, Tailwind 4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:27:49 +01:00

515 lines
16 KiB
C++

/*******************************************************************************
* Copyright (c) 2015 Matthijs Kooijman
* Copyright (c) 2018-2019 MCCI Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* This the HAL to run LMIC on top of the Arduino environment.
*******************************************************************************/
#include <Arduino.h>
#include <SPI.h>
// include all the lmic header files, including ../lmic/hal.h
#include "../lmic.h"
// include the C++ hal.h
#include "hal.h"
// we may need some things from stdio.
#include <stdio.h>
// -----------------------------------------------------------------------------
// I/O
static const Arduino_LMIC::HalPinmap_t *plmic_pins;
static Arduino_LMIC::HalConfiguration_t *pHalConfig;
static Arduino_LMIC::HalConfiguration_t nullHalConig;
static hal_failure_handler_t* custom_hal_failure_handler = NULL;
static void hal_interrupt_init(); // Fwd declaration
static void hal_io_init () {
// NSS and DIO0 are required, DIO1 is required for LoRa, DIO2 for FSK
ASSERT(plmic_pins->nss != LMIC_UNUSED_PIN);
ASSERT(plmic_pins->dio[0] != LMIC_UNUSED_PIN);
ASSERT(plmic_pins->dio[1] != LMIC_UNUSED_PIN || plmic_pins->dio[2] != LMIC_UNUSED_PIN);
// Serial.print("nss: "); Serial.println(plmic_pins->nss);
// Serial.print("rst: "); Serial.println(plmic_pins->rst);
// Serial.print("dio[0]: "); Serial.println(plmic_pins->dio[0]);
// Serial.print("dio[1]: "); Serial.println(plmic_pins->dio[1]);
// Serial.print("dio[2]: "); Serial.println(plmic_pins->dio[2]);
// initialize SPI chip select to high (it's active low)
digitalWrite(plmic_pins->nss, HIGH);
pinMode(plmic_pins->nss, OUTPUT);
if (plmic_pins->rxtx != LMIC_UNUSED_PIN) {
// initialize to RX
digitalWrite(plmic_pins->rxtx, LOW != plmic_pins->rxtx_rx_active);
pinMode(plmic_pins->rxtx, OUTPUT);
}
if (plmic_pins->rst != LMIC_UNUSED_PIN) {
// initialize RST to floating
pinMode(plmic_pins->rst, INPUT);
}
hal_interrupt_init();
}
// val == 1 => tx
void hal_pin_rxtx (u1_t val) {
if (plmic_pins->rxtx != LMIC_UNUSED_PIN)
digitalWrite(plmic_pins->rxtx, val != plmic_pins->rxtx_rx_active);
}
// set radio RST pin to given value (or keep floating!)
void hal_pin_rst (u1_t val) {
if (plmic_pins->rst == LMIC_UNUSED_PIN)
return;
if(val == 0 || val == 1) { // drive pin
digitalWrite(plmic_pins->rst, val);
pinMode(plmic_pins->rst, OUTPUT);
} else { // keep pin floating
pinMode(plmic_pins->rst, INPUT);
}
}
s1_t hal_getRssiCal (void) {
return plmic_pins->rssi_cal;
}
//--------------------
// Interrupt handling
//--------------------
static constexpr unsigned NUM_DIO_INTERRUPT = 3;
static_assert(NUM_DIO_INTERRUPT <= NUM_DIO, "Number of interrupt-sensitive lines must be less than number of GPIOs");
static ostime_t interrupt_time[NUM_DIO_INTERRUPT] = {0};
#if !defined(LMIC_USE_INTERRUPTS)
static void hal_interrupt_init() {
pinMode(plmic_pins->dio[0], INPUT);
if (plmic_pins->dio[1] != LMIC_UNUSED_PIN)
pinMode(plmic_pins->dio[1], INPUT);
if (plmic_pins->dio[2] != LMIC_UNUSED_PIN)
pinMode(plmic_pins->dio[2], INPUT);
static_assert(NUM_DIO_INTERRUPT == 3, "Number of interrupt lines must be set to 3");
}
static bool dio_states[NUM_DIO_INTERRUPT] = {0};
void hal_pollPendingIRQs_helper() {
uint8_t i;
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
continue;
if (dio_states[i] != digitalRead(plmic_pins->dio[i])) {
dio_states[i] = !dio_states[i];
if (dio_states[i] && interrupt_time[i] == 0) {
ostime_t const now = os_getTime();
interrupt_time[i] = now ? now : 1;
}
}
}
}
#else
// Interrupt handlers
static void hal_isrPin0() {
if (interrupt_time[0] == 0) {
ostime_t now = os_getTime();
interrupt_time[0] = now ? now : 1;
}
}
static void hal_isrPin1() {
if (interrupt_time[1] == 0) {
ostime_t now = os_getTime();
interrupt_time[1] = now ? now : 1;
}
}
static void hal_isrPin2() {
if (interrupt_time[2] == 0) {
ostime_t now = os_getTime();
interrupt_time[2] = now ? now : 1;
}
}
typedef void (*isr_t)();
static const isr_t interrupt_fns[NUM_DIO_INTERRUPT] = {hal_isrPin0, hal_isrPin1, hal_isrPin2};
static_assert(NUM_DIO_INTERRUPT == 3, "number of interrupts must be 3 for initializing interrupt_fns[]");
static void hal_interrupt_init() {
for (uint8_t i = 0; i < NUM_DIO_INTERRUPT; ++i) {
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
continue;
pinMode(plmic_pins->dio[i], INPUT);
attachInterrupt(digitalPinToInterrupt(plmic_pins->dio[i]), interrupt_fns[i], RISING);
}
}
#endif // LMIC_USE_INTERRUPTS
void hal_processPendingIRQs() {
uint8_t i;
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
ostime_t iTime;
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
continue;
// NOTE(tmm@mcci.com): if using interrupts, this next step
// assumes uniprocessor and fairly strict memory ordering
// semantics relative to ISRs. It would be better to use
// interlocked-exchange, but that's really far beyond
// Arduino semantics. Because our ISRs use "first time
// stamp" semantics, we don't have a value-race. But if
// we were to disable ints here, we might observe a second
// edge that we'll otherwise miss. Not a problem in this
// use case, as the radio won't release IRQs until we
// explicitly clear them.
iTime = interrupt_time[i];
if (iTime) {
interrupt_time[i] = 0;
radio_irq_handler_v2(i, iTime);
}
}
}
// -----------------------------------------------------------------------------
// SPI
static void hal_spi_init () {
SPI.begin();
}
static void hal_spi_trx(u1_t cmd, u1_t* buf, size_t len, bit_t is_read) {
uint32_t spi_freq;
u1_t nss = plmic_pins->nss;
if ((spi_freq = plmic_pins->spi_freq) == 0)
spi_freq = LMIC_SPI_FREQ;
SPISettings settings(spi_freq, MSBFIRST, SPI_MODE0);
SPI.beginTransaction(settings);
digitalWrite(nss, 0);
SPI.transfer(cmd);
for (; len > 0; --len, ++buf) {
u1_t data = is_read ? 0x00 : *buf;
data = SPI.transfer(data);
if (is_read)
*buf = data;
}
digitalWrite(nss, 1);
SPI.endTransaction();
}
void hal_spi_write(u1_t cmd, const u1_t* buf, size_t len) {
hal_spi_trx(cmd, (u1_t*)buf, len, 0);
}
void hal_spi_read(u1_t cmd, u1_t* buf, size_t len) {
hal_spi_trx(cmd, buf, len, 1);
}
// -----------------------------------------------------------------------------
// TIME
static void hal_time_init () {
// Nothing to do
}
u4_t hal_ticks () {
// Because micros() is scaled down in this function, micros() will
// overflow before the tick timer should, causing the tick timer to
// miss a significant part of its values if not corrected. To fix
// this, the "overflow" serves as an overflow area for the micros()
// counter. It consists of three parts:
// - The US_PER_OSTICK upper bits are effectively an extension for
// the micros() counter and are added to the result of this
// function.
// - The next bit overlaps with the most significant bit of
// micros(). This is used to detect micros() overflows.
// - The remaining bits are always zero.
//
// By comparing the overlapping bit with the corresponding bit in
// the micros() return value, overflows can be detected and the
// upper bits are incremented. This is done using some clever
// bitwise operations, to remove the need for comparisons and a
// jumps, which should result in efficient code. By avoiding shifts
// other than by multiples of 8 as much as possible, this is also
// efficient on AVR (which only has 1-bit shifts).
static uint8_t overflow = 0;
// Scaled down timestamp. The top US_PER_OSTICK_EXPONENT bits are 0,
// the others will be the lower bits of our return value.
uint32_t scaled = micros() >> US_PER_OSTICK_EXPONENT;
// Most significant byte of scaled
uint8_t msb = scaled >> 24;
// Mask pointing to the overlapping bit in msb and overflow.
const uint8_t mask = (1 << (7 - US_PER_OSTICK_EXPONENT));
// Update overflow. If the overlapping bit is different
// between overflow and msb, it is added to the stored value,
// so the overlapping bit becomes equal again and, if it changed
// from 1 to 0, the upper bits are incremented.
overflow += (msb ^ overflow) & mask;
// Return the scaled value with the upper bits of stored added. The
// overlapping bit will be equal and the lower bits will be 0, so
// bitwise or is a no-op for them.
return scaled | ((uint32_t)overflow << 24);
// 0 leads to correct, but overly complex code (it could just return
// micros() unmodified), 8 leaves no room for the overlapping bit.
static_assert(US_PER_OSTICK_EXPONENT > 0 && US_PER_OSTICK_EXPONENT < 8, "Invalid US_PER_OSTICK_EXPONENT value");
}
// Returns the number of ticks until time. Negative values indicate that
// time has already passed.
static s4_t delta_time(u4_t time) {
return (s4_t)(time - hal_ticks());
}
// deal with boards that are stressed by no-interrupt delays #529, etc.
#if defined(ARDUINO_DISCO_L072CZ_LRWAN1)
# define HAL_WAITUNTIL_DOWNCOUNT_MS 16 // on this board, 16 ms works better
# define HAL_WAITUNTIL_DOWNCOUNT_THRESH ms2osticks(16) // as does this threashold.
#else
# define HAL_WAITUNTIL_DOWNCOUNT_MS 8 // on most boards, delay for 8 ms
# define HAL_WAITUNTIL_DOWNCOUNT_THRESH ms2osticks(9) // but try to leave a little slack for final timing.
#endif
u4_t hal_waitUntil (u4_t time) {
s4_t delta = delta_time(time);
// check for already too late.
if (delta < 0)
return -delta;
// From delayMicroseconds docs: Currently, the largest value that
// will produce an accurate delay is 16383. Also, STM32 does a better
// job with delay is less than 10,000 us; so reduce in steps.
// It's nice to use delay() for the longer times.
while (delta > HAL_WAITUNTIL_DOWNCOUNT_THRESH) {
// deliberately delay 8ms rather than 9ms, so we
// will exit loop with delta typically positive.
// Depends on BSP keeping time accurately even if interrupts
// are disabled.
delay(HAL_WAITUNTIL_DOWNCOUNT_MS);
// re-synchronize.
delta = delta_time(time);
}
// The radio driver runs with interrupt disabled, and this can
// mess up timing APIs on some platforms. If we know the BSP feature
// set, we can decide whether to use delta_time() [more exact,
// but not always possible with interrupts off], or fall back to
// delay_microseconds() [less exact, but more universal]
#if defined(_mcci_arduino_version)
// unluckily, delayMicroseconds() isn't very accurate.
// but delta_time() works with interrupts disabled.
// so spin using delta_time().
while (delta_time(time) > 0)
/* loop */;
#else // ! defined(_mcci_arduino_version)
// on other BSPs, we need to stick with the older way,
// until we fix the radio driver to run with interrupts
// enabled.
if (delta > 0)
delayMicroseconds(delta * US_PER_OSTICK);
#endif // ! defined(_mcci_arduino_version)
// we aren't "late". Callers are interested in gross delays, not
// necessarily delays due to poor timekeeping here.
return 0;
}
// check and rewind for target time
u1_t hal_checkTimer (u4_t time) {
// No need to schedule wakeup, since we're not sleeping
return delta_time(time) <= 0;
}
static uint8_t irqlevel = 0;
void hal_disableIRQs () {
noInterrupts();
irqlevel++;
}
void hal_enableIRQs () {
if(--irqlevel == 0) {
interrupts();
#if !defined(LMIC_USE_INTERRUPTS)
// Instead of using proper interrupts (which are a bit tricky
// and/or not available on all pins on AVR), just poll the pin
// values. Since os_runloop disables and re-enables interrupts,
// putting this here makes sure we check at least once every
// loop.
//
// As an additional bonus, this prevents the can of worms that
// we would otherwise get for running SPI transfers inside ISRs.
// We merely collect the edges and timestamps here; we wait for
// a call to hal_processPendingIRQs() before dispatching.
hal_pollPendingIRQs_helper();
#endif /* !defined(LMIC_USE_INTERRUPTS) */
}
}
uint8_t hal_getIrqLevel(void) {
return irqlevel;
}
void hal_sleep () {
// Not implemented
}
// -----------------------------------------------------------------------------
#if defined(LMIC_PRINTF_TO)
#if !defined(__AVR)
static ssize_t uart_putchar (void *, const char *buf, size_t len) {
return LMIC_PRINTF_TO.write((const uint8_t *)buf, len);
}
static cookie_io_functions_t functions =
{
.read = NULL,
.write = uart_putchar,
.seek = NULL,
.close = NULL
};
void hal_printf_init() {
stdout = fopencookie(NULL, "w", functions);
if (stdout != nullptr) {
setvbuf(stdout, NULL, _IONBF, 0);
}
}
#else // defined(__AVR)
static int uart_putchar (char c, FILE *)
{
LMIC_PRINTF_TO.write(c) ;
return 0 ;
}
void hal_printf_init() {
// create a FILE structure to reference our UART output function
static FILE uartout;
memset(&uartout, 0, sizeof(uartout));
// fill in the UART file descriptor with pointer to writer.
fdev_setup_stream (&uartout, uart_putchar, NULL, _FDEV_SETUP_WRITE);
// The uart is the standard output device STDOUT.
stdout = &uartout ;
}
#endif // !defined(ESP8266) || defined(ESP31B) || defined(ESP32)
#endif // defined(LMIC_PRINTF_TO)
//void hal_init (void) {
void hal_init_lmic() {
// use the global constant
Arduino_LMIC::hal_init_with_pinmap(&lmic_pins);
}
// hal_init_ex is a C API routine, written in C++, and it's called
// with a pointer to an lmic_pinmap.
void hal_init_ex (const void *pContext) {
const lmic_pinmap * const pHalPinmap = (const lmic_pinmap *) pContext;
if (! Arduino_LMIC::hal_init_with_pinmap(pHalPinmap)) {
hal_failed(__FILE__, __LINE__);
}
}
// C++ API: initialize the HAL properly with a configuration object
namespace Arduino_LMIC {
bool hal_init_with_pinmap(const HalPinmap_t *pPinmap)
{
if (pPinmap == nullptr)
return false;
// set the static pinmap pointer.
plmic_pins = pPinmap;
// set the static HalConfiguration pointer.
HalConfiguration_t * const pThisHalConfig = pPinmap->pConfig;
if (pThisHalConfig != nullptr)
pHalConfig = pThisHalConfig;
else
pHalConfig = &nullHalConig;
pHalConfig->begin();
// configure radio I/O and interrupt handler
hal_io_init();
// configure radio SPI
hal_spi_init();
// configure timer and interrupt handler
hal_time_init();
#if defined(LMIC_PRINTF_TO)
// printf support
hal_printf_init();
#endif
// declare success
return true;
}
}; // namespace Arduino_LMIC
void hal_failed (const char *file, u2_t line) {
if (custom_hal_failure_handler != NULL) {
(*custom_hal_failure_handler)(file, line);
}
#if defined(LMIC_FAILURE_TO)
LMIC_FAILURE_TO.println("FAILURE ");
LMIC_FAILURE_TO.print(file);
LMIC_FAILURE_TO.print(':');
LMIC_FAILURE_TO.println(line);
LMIC_FAILURE_TO.flush();
#endif
hal_disableIRQs();
// Infinite loop
while (1) {
;
}
}
void hal_set_failure_handler(const hal_failure_handler_t* const handler) {
custom_hal_failure_handler = handler;
}
ostime_t hal_setModuleActive (bit_t val) {
// setModuleActive() takes a c++ bool, so
// it effectively says "val != 0". We
// don't have to.
return pHalConfig->setModuleActive(val);
}
bit_t hal_queryUsingTcxo(void) {
return pHalConfig->queryUsingTcxo();
}
uint8_t hal_getTxPowerPolicy(
u1_t inputPolicy,
s1_t requestedPower,
u4_t frequency
) {
return (uint8_t) pHalConfig->getTxPowerPolicy(
Arduino_LMIC::HalConfiguration_t::TxPowerPolicy_t(inputPolicy),
requestedPower,
frequency
);
}