From 907bac301e651a66c8e403df0bbf5512713f8cab Mon Sep 17 00:00:00 2001 From: Grzegorz Godlewski Date: Sat, 14 Mar 2026 14:09:40 +0100 Subject: [PATCH 1/4] variants: add linux portduino native firmware support --- boards/linux.json | 21 ++++ src/helpers/radiolib/LinuxSX1262.h | 48 ++++++++ src/helpers/radiolib/LinuxSX1262Wrapper.h | 22 ++++ variants/linux/LinuxBoard.cpp | 137 ++++++++++++++++++++++ variants/linux/LinuxBoard.h | 92 +++++++++++++++ variants/linux/meshcored.ini | 28 +++++ variants/linux/meshcored.service | 29 +++++ variants/linux/platformio.ini | 47 ++++++++ variants/linux/target.cpp | 54 +++++++++ variants/linux/target.h | 32 +++++ variants/portduino/platformio.ini | 41 +++++++ 11 files changed, 551 insertions(+) create mode 100644 boards/linux.json create mode 100644 src/helpers/radiolib/LinuxSX1262.h create mode 100644 src/helpers/radiolib/LinuxSX1262Wrapper.h create mode 100644 variants/linux/LinuxBoard.cpp create mode 100644 variants/linux/LinuxBoard.h create mode 100644 variants/linux/meshcored.ini create mode 100644 variants/linux/meshcored.service create mode 100644 variants/linux/platformio.ini create mode 100644 variants/linux/target.cpp create mode 100644 variants/linux/target.h create mode 100644 variants/portduino/platformio.ini diff --git a/boards/linux.json b/boards/linux.json new file mode 100644 index 0000000000..21b5cdc33a --- /dev/null +++ b/boards/linux.json @@ -0,0 +1,21 @@ +{ + "build": { + "arduino": { + }, + "core": "linux", + "extra_flags": [ + ], + "hwids": [], + "mcu": "arm64", + "variant": "linux" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": {}, + "frameworks": ["portduino", "linux"], + "name": "Linux", + "upload": { + "maximum_ram_size": 0, + "maximum_size": 0 + }, + "vendor": "Linux" +} diff --git a/src/helpers/radiolib/LinuxSX1262.h b/src/helpers/radiolib/LinuxSX1262.h new file mode 100644 index 0000000000..8d5977e8b0 --- /dev/null +++ b/src/helpers/radiolib/LinuxSX1262.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received +#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 +#define SX126X_PREAMBLE_LENGTH 16 + +extern LinuxBoard board; + +class LinuxSX1262 : public SX1262 { + public: + LinuxSX1262(Module *mod) : SX1262(mod) { } + + bool std_init(SPIClass* spi = NULL) + { + LinuxConfig config = board.config; + + Serial.printf("Radio begin %f %f %d %d %f\n", config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, config.lora_tcxo); + int status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, config.lora_tcxo); + // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f + if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { + status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, 0.0f); + } + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + setCRC(1); + + setCurrentLimit(config.current_limit); + setDio2AsRfSwitch(config.dio2_as_rf_switch); + setRxBoostedGainMode(config.rx_boosted_gain); + if (config.lora_rxen_pin != RADIOLIB_NC || config.lora_txen_pin != RADIOLIB_NC) { + setRfSwitchPins(config.lora_rxen_pin, config.lora_txen_pin); + } + + return true; + } + + bool isReceiving() { + uint16_t irq = getIrqFlags(); + bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + return detected; + } +}; diff --git a/src/helpers/radiolib/LinuxSX1262Wrapper.h b/src/helpers/radiolib/LinuxSX1262Wrapper.h new file mode 100644 index 0000000000..fbbfd19c9b --- /dev/null +++ b/src/helpers/radiolib/LinuxSX1262Wrapper.h @@ -0,0 +1,22 @@ +#pragma once + +#include "LinuxSX1262.h" +#include "RadioLibWrappers.h" + +class LinuxSX1262Wrapper : public RadioLibWrapper { +public: + LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + bool isReceivingPacket() override { + return ((LinuxSX1262 *)_radio)->isReceiving(); + } + float getCurrentRSSI() override { + return ((LinuxSX1262 *)_radio)->getRSSI(false); + } + float getLastRSSI() const override { return ((LinuxSX1262 *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((LinuxSX1262 *)_radio)->getSNR(); } + + float packetScore(float snr, int packet_len) override { + int sf = ((LinuxSX1262 *)_radio)->spreadingFactor; + return packetScoreInt(snr, sf, packet_len); + } +}; diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp new file mode 100644 index 0000000000..0feb95d2bc --- /dev/null +++ b/variants/linux/LinuxBoard.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include "linux/gpio/LinuxGPIOPin.h" +#include "LinuxBoard.h" + +int initGPIOPin(uint8_t pinNum, const std::string gpioChipName, uint8_t line) +{ +#ifdef PORTDUINO_LINUX_HARDWARE + char gpio_name[32]; + snprintf(gpio_name, sizeof(gpio_name), "GPIO%d", pinNum); + + try { + GPIOPin *csPin; + csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name); + csPin->setSilent(); + gpioBind(csPin); + return 0; + } catch (...) { + MESH_DEBUG_PRINTLN("Warning, cannot claim pin %d", pinNum); + return 1; + } +#else + return 0; +#endif +} + +void portduinoSetup() { +} + +void LinuxBoard::begin() { + config.load("/etc/meshcored/meshcored.ini"); + + Serial.printf("SPI begin %s\n", config.spidev); + SPI.begin(config.spidev); + + Serial.printf("LoRa pins NSS=%d BUSY=%d IRQ=%d RESET=%d TX=%d RX=%d\n", + (int)config.lora_nss_pin, + (int)config.lora_busy_pin, + (int)config.lora_irq_pin, + (int)config.lora_reset_pin, + (int)config.lora_rxen_pin, + (int)config.lora_txen_pin); + + if (config.lora_nss_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_nss_pin, "gpiochip0", config.lora_nss_pin); + } + if (config.lora_busy_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_busy_pin, "gpiochip0", config.lora_busy_pin); + } + if (config.lora_irq_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_irq_pin, "gpiochip0", config.lora_irq_pin); + } + if (config.lora_reset_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_reset_pin, "gpiochip0", config.lora_reset_pin); + } + if (config.lora_rxen_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_rxen_pin, "gpiochip0", config.lora_rxen_pin); + } + if (config.lora_txen_pin != RADIOLIB_NC) { + initGPIOPin(config.lora_txen_pin, "gpiochip0", config.lora_txen_pin); + } +} + +void trim(char *str) { + char *end; + while (isspace((unsigned char)*str)) str++; + if (*str == 0) { *str = 0; return; } + end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) end--; + end[1] = '\0'; +} + +char *safe_copy(char *value, size_t maxlen) { + char *retval; + size_t length = strlen(value) + 1; + if (length > maxlen) length = maxlen; + + retval = (char *)malloc(length); + strncpy(retval, value, length - 1); + retval[length - 1] = '\0'; + return retval; +} + +int LinuxConfig::load(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) return -1; + + char line[512]; + while (fgets(line, sizeof(line), f)) { + char *p = line; + // skip whitespace + while (isspace(*p)) p++; + // skip empty lines and comments + if (*p == '\0' || *p == '#' || *p == ';') continue; + + char *key = p; + while (*p && !isspace(*p) && *p != '=') p++; + if (*p == '\0') continue; + *p++ = '\0'; + + while (*p && (isspace(*p) || *p == '=')) p++; + char *value = p; + p = value; + while (*p && *p != '\n' && *p != '\r' && *p != '#' && *p != ';') p++; + *p = '\0'; + + trim(key); + trim(value); + + if (strcmp(key, "spidev") == 0) spidev = safe_copy(value, 32); + else if (strcmp(key, "lora_freq") == 0) lora_freq = atof(value); + else if (strcmp(key, "lora_bw") == 0) lora_bw = atof(value); + else if (strcmp(key, "lora_sf") == 0) lora_sf = (uint8_t)atoi(value); + else if (strcmp(key, "lora_cr") == 0) lora_cr = (uint8_t)atoi(value); + else if (strcmp(key, "lora_tcxo") == 0) lora_tcxo = atof(value); + else if (strcmp(key, "lora_tx_power") == 0) lora_tx_power = atoi(value); + else if (strcmp(key, "current_limit") == 0) current_limit = atof(value); + else if (strcmp(key, "dio2_as_rf_switch") == 0) dio2_as_rf_switch = value != 0; + else if (strcmp(key, "rx_boosted_gain") == 0) rx_boosted_gain = value != 0; + + else if (strcmp(key, "lora_irq_pin") == 0) lora_irq_pin = atoi(value); + else if (strcmp(key, "lora_reset_pin") == 0) lora_reset_pin = atoi(value); + else if (strcmp(key, "lora_nss_pin") == 0) lora_nss_pin = atoi(value); + else if (strcmp(key, "lora_busy_pin") == 0) lora_busy_pin = atoi(value); + else if (strcmp(key, "lora_rxen_pin") == 0) lora_rxen_pin = atoi(value); + else if (strcmp(key, "lora_txen_pin") == 0) lora_txen_pin = atoi(value); + + else if (strcmp(key, "advert_name") == 0) advert_name = safe_copy(value, 100); + else if (strcmp(key, "admin_password") == 0) admin_password = safe_copy(value, 100); + else if (strcmp(key, "lat") == 0) lat = atof(value); + else if (strcmp(key, "lon") == 0) lon = atof(value); + } + fclose(f); + return 0; +} diff --git a/variants/linux/LinuxBoard.h b/variants/linux/LinuxBoard.h new file mode 100644 index 0000000000..c7ae7501c8 --- /dev/null +++ b/variants/linux/LinuxBoard.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include + +class LinuxConfig { +public: + float lora_freq = LORA_FREQ; + float lora_bw = LORA_BW; + uint8_t lora_sf = LORA_SF; +#ifdef LORA_CR + uint8_t lora_cr = LORA_CR; +#else + uint8_t lora_cr = 5; +#endif + + uint32_t lora_irq_pin = RADIOLIB_NC; + uint32_t lora_reset_pin = RADIOLIB_NC; + uint32_t lora_nss_pin = RADIOLIB_NC; + uint32_t lora_busy_pin = RADIOLIB_NC; + uint32_t lora_rxen_pin = RADIOLIB_NC; + uint32_t lora_txen_pin = RADIOLIB_NC; + + int8_t lora_tx_power = 22; + float current_limit = 140; + bool dio2_as_rf_switch = false; + bool rx_boosted_gain = true; + + char* spidev = "/dev/spidev0.0"; + + float lora_tcxo = 1.8f; + + char *advert_name = "Linux Repeater"; + char *admin_password = "password"; + float lat = 0.0f; + float lon = 0.0f; + + int load(const char *filename); +}; + +class LinuxBoard : public mesh::MainBoard { +protected: + uint8_t startup_reason; + uint8_t btn_prev_state; + +public: + void begin(); + + uint16_t getBattMilliVolts() override { + return 0; + } + + uint8_t getStartupReason() const override { return startup_reason; } + + const char* getManufacturerName() const override { + return "Linux"; + } + + int buttonStateChanged() { + return 0; + } + + void powerOff() override { + exit(0); + } + + void reboot() override { + exit(0); + } + + LinuxConfig config; +}; + +class LinuxRTCClock : public mesh::RTCClock { +public: + LinuxRTCClock() { } + void begin() { + } + uint32_t getCurrentTime() override { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec; + } + void setCurrentTime(uint32_t time) override { + struct timeval tv; + tv.tv_sec = time; + tv.tv_usec = 0; + settimeofday(&tv, NULL); + } +}; diff --git a/variants/linux/meshcored.ini b/variants/linux/meshcored.ini new file mode 100644 index 0000000000..6fb346fbbb --- /dev/null +++ b/variants/linux/meshcored.ini @@ -0,0 +1,28 @@ +advert_name = "Sample Router" +admin_password = "password" +lat = 0.0 +lon = 0.0 + +# Waveshare LoRa hat +#lora_irq_pin = 16 +#lora_reset_pin = 18 +#lora_nss_pin = 21 +#lora_busy_pin = 20 + +lora_irq_pin = 22 +lora_reset_pin = 13 +#lora_nss_pin = # SS pin handled by RPI +#lora_busy_pin = # Seems to be unused? +#lora_rxen_pin +#lora_txen_pin + +spidev = /dev/spidev0.0 +lora_freq = 869.618 +lora_bw = 62.5 +lora_sf = 8 +lora_cr = 8 +lora_tcxo = 1.8 +#lora_tx_power = 22 +#current_limit = 140 +#dio2_as_rf_switch = 1 +#rx_boosted_gain = 1 diff --git a/variants/linux/meshcored.service b/variants/linux/meshcored.service new file mode 100644 index 0000000000..345a10724b --- /dev/null +++ b/variants/linux/meshcored.service @@ -0,0 +1,29 @@ +# /var/lib/systemd/system/meshcored.service +[Unit] +Description=Meshcore Daemon (meshcored) +After=network.target +Wants=network.target + +[Service] +Type=simple +User=meshcore +Group=meshcore +ExecStart=/usr/bin/stdbuf -oL /usr/bin/meshcored --fsdir /var/lib/meshcore +WorkingDirectory=/var/lib/meshcore +Restart=on-failure +RestartSec=5 +LimitNOFILE=65535 + +# Security hardening +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +NoNewPrivileges=yes +ReadWritePaths=/var/lib/meshcore # allow writing only to its own data dir + +# Create data dir with correct ownership if it doesn't exist +ExecStartPre=/bin/mkdir -p /var/lib/meshcore +ExecStartPre=/bin/chown meshcore:meshcore /var/lib/meshcore + +[Install] +WantedBy=multi-user.target diff --git a/variants/linux/platformio.ini b/variants/linux/platformio.ini new file mode 100644 index 0000000000..d60a50fe71 --- /dev/null +++ b/variants/linux/platformio.ini @@ -0,0 +1,47 @@ +[linux_base] +extends = portduino_base +build_flags = ${portduino_base.build_flags} + -I variants/linux + -I /usr/include +board = cross_platform +board_level = extra +lib_deps = + ${portduino_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + +build_src_filter = ${portduino_base.build_src_filter} + +<../variants/linux> + - + - + - + - + - + +[env:linux] +extends = linux_base +; The pkg-config commands below optionally add link flags. +; the || : is just a "or run the null command" to avoid returning an error code +build_flags = ${linux_base.build_flags} + !pkg-config --cflags --libs libbsd-overlay --silence-errors || : + +[env:linux_repeater] +extends = linux_base +build_flags = + ${linux_base.build_flags} + -D RADIO_CLASS=LinuxSX1262 + -D WRAPPER_CLASS=LinuxSX1262Wrapper + -D USE_CUSTOM_SX1262_WRAPPER + -D SKIP_CONFIG_OVERWRITE=1 + -D ADVERT_NAME='"Linux Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=100 + -D LORA_TX_POWER=22 + -D MESH_DEBUG=1 + +build_src_filter = ${linux_base.build_src_filter} + +<../examples/simple_repeater> + +lib_deps = + ${linux_base.lib_deps} diff --git a/variants/linux/target.cpp b/variants/linux/target.cpp new file mode 100644 index 0000000000..0f5941f890 --- /dev/null +++ b/variants/linux/target.cpp @@ -0,0 +1,54 @@ +#include +#include "target.h" + +class PortduinoHal : public ArduinoHal +{ +public: + PortduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; + + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { + spi->transfer(out, in, len); + } +}; + +LinuxBoard board; + +SPISettings spiSettings = SPISettings(2000000, MSBFIRST, SPI_MODE0); +ArduinoHal *hal = new PortduinoHal(SPI, spiSettings); +RADIO_CLASS radio = new Module(hal, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC); +WRAPPER_CLASS radio_driver(radio, board); + +LinuxRTCClock rtc_clock; +EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + rtc_clock.begin(); + + radio = new Module(hal, board.config.lora_nss_pin, board.config.lora_irq_pin, board.config.lora_reset_pin, board.config.lora_busy_pin); + return radio.std_init(&SPI); +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/linux/target.h b/variants/linux/target.h new file mode 100644 index 0000000000..1f5539ca94 --- /dev/null +++ b/variants/linux/target.h @@ -0,0 +1,32 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#endif + +#if (USE_CUSTOM_SX1262_WRAPPER) +#include +#endif + +extern LinuxBoard board; +extern WRAPPER_CLASS radio_driver; +extern LinuxRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini new file mode 100644 index 0000000000..5785e965aa --- /dev/null +++ b/variants/portduino/platformio.ini @@ -0,0 +1,41 @@ +[portduino_base] +platform = + # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop + https://github.com/meshtastic/platform-native/archive/f566d364204416cdbf298e349213f7d551f793d9.zip +framework = arduino + +build_src_filter = + ${env.build_src_filter} + - + - + - + - + - + - + - + - + - + +lib_deps = + ${env.lib_deps} + rweather/Crypto@0.4.0 + adafruit/Adafruit seesaw Library@1.7.9 + electroniccats/CayenneLPP @ 1.6.1 + adafruit/RTClib @ ^2.1.3 + jgromes/RadioLib@7.4.0 + +build_flags = + ${arduino_base.build_flags} + -DARCH_PORTDUINO + -DPORTDUINO_PLATFORM + -DRADIOLIB_EEPROM_UNSUPPORTED + -DPORTDUINO_LINUX_HARDWARE + -fPIC + -lpthread + -lstdc++fs + -lbluetooth + -lgpiod + -li2c + -luv + -std=gnu17 + -std=c++17 From a5d6a9affc691876aac1b862edd3f25ca6d7f000 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:38:31 +0100 Subject: [PATCH 2/4] variants: add scaffolding for linux native (#1) * variants: add scaffolding for linux native * address review comments * address review comments --- examples/simple_repeater/MyMesh.cpp | 8 +-- examples/simple_repeater/MyMesh.h | 2 + examples/simple_repeater/main.cpp | 8 +++ src/helpers/ClientACL.cpp | 2 +- src/helpers/CommonCLI.cpp | 2 +- src/helpers/IdentityStore.cpp | 4 +- src/helpers/IdentityStore.h | 2 +- src/helpers/RegionMap.cpp | 2 +- src/helpers/TxtDataHelpers.cpp | 7 +++ variants/linux/LinuxBoard.cpp | 1 + variants/linux/LinuxBoard.h | 3 + variants/linux/README.md | 92 +++++++++++++++++++++++++++++ variants/linux/meshcored.ini | 1 + variants/linux/meshcored.service | 2 +- 14 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 variants/linux/README.md diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..7f26379b8e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -389,7 +389,7 @@ mesh::Packet *MyMesh::createSelfAdvert() { File MyMesh::openAppend(const char *fname) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) return _fs->open(fname, FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) +#elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) return _fs->open(fname, "a"); #else return _fs->open(fname, "a", true); @@ -950,6 +950,8 @@ bool MyMesh::formatFileSystem() { return LittleFS.format(); #elif defined(ESP32) return SPIFFS.format(); +#elif defined(ARCH_PORTDUINO) + return false; // not supported on Linux #else #error "need to implement file system erase" return false; @@ -1082,9 +1084,7 @@ void MyMesh::formatPacketStatsReply(char *reply) { void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); -#elif defined(ESP32) - IdentityStore store(*_fs, "/identity"); -#elif defined(RP2040_PLATFORM) +#elif defined(ESP32) || defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) IdentityStore store(*_fs, "/identity"); #else #error "need to define saveIdentity()" diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 47610a7006..6699f709dc 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -11,6 +11,8 @@ #include #elif defined(ESP32) #include +#elif defined(ARCH_PORTDUINO) + #include #endif #ifdef WITH_RS232_BRIDGE diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..fe6e7bb647 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -73,6 +73,14 @@ void setup() { fs = &LittleFS; IdentityStore store(LittleFS, "/identity"); store.begin(); +#elif defined(ARCH_PORTDUINO) + if (::mkdir(board.config.data_dir, 0755) != 0 && errno != EEXIST) { + Serial.printf("WARNING: could not create data_dir '%s': %s\n", board.config.data_dir, strerror(errno)); + } + portduinoVFS->mountpoint(board.config.data_dir); + fs = &PortduinoFS; + IdentityStore store(PortduinoFS, "/identity"); + store.begin(); #else #error "need to define filesystem" #endif diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1282382737..40a98a5b7a 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -4,7 +4,7 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) + #elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) return _fs->open(filename, "w"); #else return _fs->open(filename, "w", true); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2f7a0fffcb..687f4e42c3 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -126,7 +126,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) fs->remove("/com_prefs"); File file = fs->open("/com_prefs", FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) +#elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) File file = fs->open("/com_prefs", "w"); #else File file = fs->open("/com_prefs", "w", true); diff --git a/src/helpers/IdentityStore.cpp b/src/helpers/IdentityStore.cpp index dc85d69cdd..0a9700f0aa 100644 --- a/src/helpers/IdentityStore.cpp +++ b/src/helpers/IdentityStore.cpp @@ -49,7 +49,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) +#elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) File file = _fs->open(filename, "w"); #else File file = _fs->open(filename, "w", true); @@ -71,7 +71,7 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) +#elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) File file = _fs->open(filename, "w"); #else File file = _fs->open(filename, "w", true); diff --git a/src/helpers/IdentityStore.h b/src/helpers/IdentityStore.h index d0d7ee457e..a96aa536cc 100644 --- a/src/helpers/IdentityStore.h +++ b/src/helpers/IdentityStore.h @@ -1,6 +1,6 @@ #pragma once -#if defined(ESP32) || defined(RP2040_PLATFORM) +#if defined(ESP32) || defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) #include #define FILESYSTEM fs::FS #elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 2cc47e1d5a..704e2b1a37 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -61,7 +61,7 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) + #elif defined(RP2040_PLATFORM) || defined(ARCH_PORTDUINO) return _fs->open(filename, "w"); #else return _fs->open(filename, "w", true); diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index d327931fde..60832f6653 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -1,4 +1,7 @@ #include "TxtDataHelpers.h" +#if defined(ARCH_PORTDUINO) + #include +#endif void StrHelper::strncpy(char* dest, const char* src, size_t buf_sz) { while (buf_sz > 1 && *src) { @@ -102,7 +105,11 @@ static void _ftoa(float f, char *p, int *status) *p++ = '0'; else { +#if defined(ARCH_PORTDUINO) + sprintf(p, "%" PRId32, int_part); +#else ltoa(int_part, p, 10); +#endif while (*p) p++; } diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp index 0feb95d2bc..dee65baed1 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -131,6 +131,7 @@ int LinuxConfig::load(const char *filename) { else if (strcmp(key, "admin_password") == 0) admin_password = safe_copy(value, 100); else if (strcmp(key, "lat") == 0) lat = atof(value); else if (strcmp(key, "lon") == 0) lon = atof(value); + else if (strcmp(key, "data_dir") == 0) data_dir = safe_copy(value, 256); } fclose(f); return 0; diff --git a/variants/linux/LinuxBoard.h b/variants/linux/LinuxBoard.h index c7ae7501c8..584ef195f6 100644 --- a/variants/linux/LinuxBoard.h +++ b/variants/linux/LinuxBoard.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include class LinuxConfig { @@ -36,6 +38,7 @@ class LinuxConfig { char *admin_password = "password"; float lat = 0.0f; float lon = 0.0f; + char *data_dir = "/var/lib/meshcore"; int load(const char *filename); }; diff --git a/variants/linux/README.md b/variants/linux/README.md new file mode 100644 index 0000000000..68d9416a0c --- /dev/null +++ b/variants/linux/README.md @@ -0,0 +1,92 @@ +# MeshCore Linux Variant + +Native Linux support for MeshCore, targeting Raspberry Pi (Zero, 3, 4, 5) and similar SBCs with an SX1262 LoRa radio attached over SPI. Uses the [Portduino](https://github.com/meshtastic/platform-native) Arduino-compatibility layer to run the same firmware codebase on Linux without modification to the core library. + +## Hardware + +- Raspberry Pi (any model with SPI) +- SX1262-based LoRa module wired to the Pi's SPI bus (e.g. Waveshare SX1262 HAT, RAK2287, similar) +- SPI, IRQ, RESET, and optionally BUSY/RXEN/TXEN GPIO pins + +## Build + +**Dependencies** (install on the build machine and on the Pi): + +```sh +# Arch Linux +sudo pacman -S libgpiod i2c-tools + +# Debian/Raspberry Pi OS +sudo apt install libgpiod-dev libi2c-dev +``` + +**Build with PlatformIO:** + +```sh +FIRMWARE_VERSION=dev pio run -e linux_repeater +# binary: .pio/build/linux_repeater/program +``` + +## Configuration + +The binary reads `/etc/meshcored/meshcored.ini` on startup. Copy and edit the sample: + +```sh +sudo mkdir -p /etc/meshcored +sudo cp variants/linux/meshcored.ini /etc/meshcored/meshcored.ini +sudo nano /etc/meshcored/meshcored.ini +``` + +Key settings: + +| Key | Default | Notes | +|-----|---------|-------| +| `spidev` | `/dev/spidev0.0` | SPI device node | +| `lora_irq_pin` | (none) | GPIO pin number for IRQ | +| `lora_reset_pin` | (none) | GPIO pin number for RESET | +| `lora_nss_pin` | (none) | GPIO pin number for NSS/CS (if not handled by SPI driver) | +| `lora_busy_pin` | (none) | GPIO pin number for BUSY | +| `lora_freq` | `869.618` | Frequency in MHz | +| `lora_bw` | `62.5` | Bandwidth in kHz | +| `lora_sf` | `8` | Spreading factor | +| `lora_cr` | `8` | Coding rate | +| `lora_tcxo` | `1.8` | TCXO voltage (V); set to `0.0` if no TCXO | +| `lora_tx_power` | `22` | TX power in dBm | +| `advert_name` | `"Linux Repeater"` | Node name broadcast to the mesh | +| `admin_password` | `"password"` | Change this | +| `lat` / `lon` | `0.0` | GPS coordinates for advertisement | +| `data_dir` | `/var/lib/meshcore` | Where identity and prefs are stored | + +## Running + +Enable SPI and set GPIO permissions on the Pi: + +```sh +# Raspberry Pi OS +sudo raspi-config # Interface Options → SPI → Enable +sudo usermod -aG spi,gpio $USER +``` + +Run directly: + +```sh +sudo .pio/build/linux_repeater/program +``` + +## Systemd Service + +```sh +sudo cp variants/linux/meshcored.service /etc/systemd/system/ +sudo useradd -r -s /sbin/nologin meshcore +sudo install -d -o meshcore -g meshcore /var/lib/meshcore +# copy binary to /usr/bin/meshcored +sudo systemctl enable --now meshcored +sudo journalctl -u meshcored -f +``` + +## Known Gaps / TODO + +- **No CLI argument parsing** — config path is hardcoded to `/etc/meshcored/meshcored.ini`; `data_dir` is only configurable via the INI file. +- **Only repeater firmware** — there is no `linux_companion` target yet; companion radio support (BLE/serial interface to a phone app) is not implemented for Linux. +- **`formatFileSystem()`** returns `false` (not implemented) — the CLI `format` command will report failure on Linux. +- **No power management** — `board.sleep()` is a no-op; the power-saving loop in `main.cpp` never actually sleeps. diff --git a/variants/linux/meshcored.ini b/variants/linux/meshcored.ini index 6fb346fbbb..1a10bb147d 100644 --- a/variants/linux/meshcored.ini +++ b/variants/linux/meshcored.ini @@ -2,6 +2,7 @@ advert_name = "Sample Router" admin_password = "password" lat = 0.0 lon = 0.0 +#data_dir = /var/lib/meshcore # Waveshare LoRa hat #lora_irq_pin = 16 diff --git a/variants/linux/meshcored.service b/variants/linux/meshcored.service index 345a10724b..c81ca4776f 100644 --- a/variants/linux/meshcored.service +++ b/variants/linux/meshcored.service @@ -8,7 +8,7 @@ Wants=network.target Type=simple User=meshcore Group=meshcore -ExecStart=/usr/bin/stdbuf -oL /usr/bin/meshcored --fsdir /var/lib/meshcore +ExecStart=/usr/bin/stdbuf -oL /usr/bin/meshcored WorkingDirectory=/var/lib/meshcore Restart=on-failure RestartSec=5 From 4fe09d48bcedc5f041a2751ed31d7b7eeb4aaf4f Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:12:11 +0100 Subject: [PATCH 3/4] variants: allow linux repeater to be configured at runtime (#2) * variants: allow linux repeater to be configured at runtime * address review comments * address review comments * address review comments --- examples/simple_repeater/MyMesh.cpp | 13 +++ variants/linux/99-meshcore.rules | 6 ++ variants/linux/LinuxBoard.cpp | 9 ++ variants/linux/README.md | 108 ++++++++++++++++++------ variants/linux/meshcored.ini | 4 +- variants/linux/meshcored.ini.pow-sx1262 | 24 ++++++ variants/linux/meshcored.ini.waveshare | 24 ++++++ variants/linux/meshcored.service | 5 +- variants/linux/platformio.ini | 4 - 9 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 variants/linux/99-meshcore.rules create mode 100644 variants/linux/meshcored.ini.pow-sx1262 create mode 100644 variants/linux/meshcored.ini.waveshare diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7f26379b8e..f48b8b264a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -904,6 +904,19 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc void MyMesh::begin(FILESYSTEM *fs) { mesh::Mesh::begin(); _fs = fs; +#if defined(ARCH_PORTDUINO) + // Apply runtime INI config as first-run defaults before loading persisted prefs. + // If /com_prefs exists, loadPrefs() below will overwrite these with the saved values. + StrHelper::strncpy(_prefs.node_name, board.config.advert_name, sizeof(_prefs.node_name)); + _prefs.node_lat = board.config.lat; + _prefs.node_lon = board.config.lon; + StrHelper::strncpy(_prefs.password, board.config.admin_password, sizeof(_prefs.password)); + _prefs.freq = board.config.lora_freq; + _prefs.bw = board.config.lora_bw; + _prefs.sf = board.config.lora_sf; + _prefs.cr = board.config.lora_cr; + _prefs.tx_power_dbm = board.config.lora_tx_power; +#endif // load persisted prefs _cli.loadPrefs(_fs); acl.load(_fs, self_id); diff --git a/variants/linux/99-meshcore.rules b/variants/linux/99-meshcore.rules new file mode 100644 index 0000000000..4c3df92e47 --- /dev/null +++ b/variants/linux/99-meshcore.rules @@ -0,0 +1,6 @@ +# udev rules for meshcored — grant the meshcore group access to SPI and GPIO devices. +# Works on Arch Linux, Debian/Raspberry Pi OS, and other distributions. +# Install: sudo cp 99-meshcore.rules /etc/udev/rules.d/ + +SUBSYSTEM=="spidev", GROUP="meshcore", MODE="0660" +KERNEL=="gpiochip*", GROUP="meshcore", MODE="0660" diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp index dee65baed1..e975884c15 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -109,6 +109,15 @@ int LinuxConfig::load(const char *filename) { trim(key); trim(value); + // strip optional surrounding quotes from string values + { + size_t vlen = strlen(value); + if (vlen >= 2 && (value[0] == '"' || value[0] == '\'') && value[vlen-1] == value[0]) { + value[vlen-1] = '\0'; + value++; + } + } + if (strcmp(key, "spidev") == 0) spidev = safe_copy(value, 32); else if (strcmp(key, "lora_freq") == 0) lora_freq = atof(value); else if (strcmp(key, "lora_bw") == 0) lora_bw = atof(value); diff --git a/variants/linux/README.md b/variants/linux/README.md index 68d9416a0c..276fd8e6c7 100644 --- a/variants/linux/README.md +++ b/variants/linux/README.md @@ -5,7 +5,7 @@ Native Linux support for MeshCore, targeting Raspberry Pi (Zero, 3, 4, 5) and si ## Hardware - Raspberry Pi (any model with SPI) -- SX1262-based LoRa module wired to the Pi's SPI bus (e.g. Waveshare SX1262 HAT, RAK2287, similar) +- SX1262-based LoRa module wired to the Pi's SPI bus (e.g. Waveshare SX1262 HAT, PoW SX1262 HAT) - SPI, IRQ, RESET, and optionally BUSY/RXEN/TXEN GPIO pins ## Build @@ -20,73 +20,133 @@ sudo pacman -S libgpiod i2c-tools sudo apt install libgpiod-dev libi2c-dev ``` -**Build with PlatformIO:** +**Build with `build.sh`** (recommended — embeds version and commit hash): ```sh -FIRMWARE_VERSION=dev pio run -e linux_repeater +FIRMWARE_VERSION=dev ./build.sh build-firmware linux_repeater # binary: .pio/build/linux_repeater/program ``` -## Configuration +Alternatively, build directly with PlatformIO (no version metadata): + +```sh +FIRMWARE_VERSION=dev pio run -e linux_repeater +``` + +## Setup + +### 1. Install the binary + +```sh +sudo cp .pio/build/linux_repeater/program /usr/bin/meshcored +``` + +### 2. Create the config file + +Two ready-made templates are provided in `variants/linux/`: -The binary reads `/etc/meshcored/meshcored.ini` on startup. Copy and edit the sample: +| Template | Hardware | +|----------|----------| +| `meshcored.ini.pow-sx1262` | RPi Zero 2W + PoW SX1262 HAT | +| `meshcored.ini.waveshare` | RPi 3/4/5 + Waveshare SX1262 LoRa HAT | ```sh sudo mkdir -p /etc/meshcored -sudo cp variants/linux/meshcored.ini /etc/meshcored/meshcored.ini +# Pick the template that matches your hardware: +sudo cp variants/linux/meshcored.ini.waveshare /etc/meshcored/meshcored.ini sudo nano /etc/meshcored/meshcored.ini ``` +The config file has two roles: + +- **Hardware config** (always read on every startup): SPI device, GPIO pin numbers, LoRa radio parameters. +- **First-run node defaults**: `advert_name`, `admin_password`, `lat`, `lon`. On the first boot these are saved to `data_dir`. After that, use the serial CLI to change them (`set name`, `set password`, etc.) — the INI values are no longer consulted for these fields. + Key settings: | Key | Default | Notes | |-----|---------|-------| | `spidev` | `/dev/spidev0.0` | SPI device node | -| `lora_irq_pin` | (none) | GPIO pin number for IRQ | -| `lora_reset_pin` | (none) | GPIO pin number for RESET | -| `lora_nss_pin` | (none) | GPIO pin number for NSS/CS (if not handled by SPI driver) | -| `lora_busy_pin` | (none) | GPIO pin number for BUSY | +| `lora_irq_pin` | (none) | GPIO line number for IRQ | +| `lora_reset_pin` | (none) | GPIO line number for RESET | +| `lora_nss_pin` | (none) | GPIO line number for NSS/CS (if not handled by the SPI driver) | +| `lora_busy_pin` | (none) | GPIO line number for BUSY | | `lora_freq` | `869.618` | Frequency in MHz | | `lora_bw` | `62.5` | Bandwidth in kHz | | `lora_sf` | `8` | Spreading factor | | `lora_cr` | `8` | Coding rate | -| `lora_tcxo` | `1.8` | TCXO voltage (V); set to `0.0` if no TCXO | +| `lora_tcxo` | `1.8` | TCXO voltage (V); set to `0.0` if your module has no TCXO | | `lora_tx_power` | `22` | TX power in dBm | -| `advert_name` | `"Linux Repeater"` | Node name broadcast to the mesh | -| `admin_password` | `"password"` | Change this | -| `lat` / `lon` | `0.0` | GPS coordinates for advertisement | -| `data_dir` | `/var/lib/meshcore` | Where identity and prefs are stored | +| `advert_name` | `"Linux Repeater"` | Node name — first-run default only | +| `admin_password` | `"password"` | Admin password — **change this**, first-run default only | +| `lat` / `lon` | `0.0` | GPS coordinates for advertisement — first-run default only | +| `data_dir` | `/var/lib/meshcore` | Where identity and node prefs are persisted | -## Running - -Enable SPI and set GPIO permissions on the Pi: +### 3. Enable SPI and GPIO access ```sh # Raspberry Pi OS -sudo raspi-config # Interface Options → SPI → Enable +sudo raspi-config # Interface Options → SPI → Enable sudo usermod -aG spi,gpio $USER ``` -Run directly: +On Arch Linux and other distributions without `spi`/`gpio` groups, use the provided udev rules instead (also works on Raspberry Pi OS): ```sh -sudo .pio/build/linux_repeater/program +sudo cp variants/linux/99-meshcore.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger ``` -## Systemd Service +### 4. Run + +**Directly** (for testing): + +```sh +sudo /usr/bin/meshcored +``` + +`sudo` is needed on first run to create `data_dir` if it doesn't exist. Once the directory is created and owned appropriately, it can run as a non-root user. + +**As a systemd service** (recommended for production): ```sh sudo cp variants/linux/meshcored.service /etc/systemd/system/ +sudo cp variants/linux/99-meshcore.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger sudo useradd -r -s /sbin/nologin meshcore -sudo install -d -o meshcore -g meshcore /var/lib/meshcore -# copy binary to /usr/bin/meshcored +sudo mkdir -p /var/lib/meshcore +sudo chown meshcore:meshcore /var/lib/meshcore +sudo chmod 640 /etc/meshcored/meshcored.ini +sudo chown root:meshcore /etc/meshcored/meshcored.ini +sudo systemctl daemon-reload sudo systemctl enable --now meshcored sudo journalctl -u meshcored -f ``` +### 5. Reconfiguring after first run + +Node name, password, and location can be changed via the serial CLI after first boot: + +``` +set name +set password +set lat +set lon +``` + +To reset all node prefs and re-apply the INI file defaults, delete the saved prefs and restart: + +```sh +sudo rm /var/lib/meshcore/com_prefs +sudo systemctl restart meshcored +``` + +> **Note:** LoRa radio parameters (`lora_freq`, `lora_bw`, `lora_sf`, `lora_cr`, `lora_tx_power`) are also first-run defaults. After first boot they are saved in `com_prefs` and the INI values are no longer read for those fields. To apply a changed radio parameter, use the CLI (`set freq`, `set sf`, etc.) or reset prefs as above. + ## Known Gaps / TODO - **No CLI argument parsing** — config path is hardcoded to `/etc/meshcored/meshcored.ini`; `data_dir` is only configurable via the INI file. - **Only repeater firmware** — there is no `linux_companion` target yet; companion radio support (BLE/serial interface to a phone app) is not implemented for Linux. - **`formatFileSystem()`** returns `false` (not implemented) — the CLI `format` command will report failure on Linux. - **No power management** — `board.sleep()` is a no-op; the power-saving loop in `main.cpp` never actually sleeps. +- **Portduino branding** — on startup the binary identifies itself as "An application written with portduino" with a Meshtastic bug URL. This is hardcoded in the Portduino framework and cannot be changed without patching the framework. diff --git a/variants/linux/meshcored.ini b/variants/linux/meshcored.ini index 1a10bb147d..2c57adbbb9 100644 --- a/variants/linux/meshcored.ini +++ b/variants/linux/meshcored.ini @@ -1,5 +1,5 @@ -advert_name = "Sample Router" -admin_password = "password" +advert_name = Sample Router +admin_password = password lat = 0.0 lon = 0.0 #data_dir = /var/lib/meshcore diff --git a/variants/linux/meshcored.ini.pow-sx1262 b/variants/linux/meshcored.ini.pow-sx1262 new file mode 100644 index 0000000000..1f2e03000f --- /dev/null +++ b/variants/linux/meshcored.ini.pow-sx1262 @@ -0,0 +1,24 @@ +# meshcored.ini — PoW SX1262 HAT on Raspberry Pi Zero 2W +# GPIO numbering is BCM (the number after "GPIO", e.g. GPIO22 = 22). +# +# Hardware config (read on every startup): +spidev = /dev/spidev0.0 +lora_irq_pin = 22 +lora_reset_pin = 13 +# lora_nss_pin — SS is handled by the SPI driver; not needed +# lora_busy_pin — not wired on this HAT +lora_freq = 869.618 +lora_bw = 62.5 +lora_sf = 8 +lora_cr = 8 +lora_tcxo = 1.8 +lora_tx_power = 22 + +# First-run node defaults (ignored after first boot; use CLI to change): +advert_name = PoW Linux Repeater +admin_password = changeme +lat = 0.0 +lon = 0.0 + +# data_dir — where identity and node prefs are persisted (default shown): +#data_dir = /var/lib/meshcore diff --git a/variants/linux/meshcored.ini.waveshare b/variants/linux/meshcored.ini.waveshare new file mode 100644 index 0000000000..84df7f3acd --- /dev/null +++ b/variants/linux/meshcored.ini.waveshare @@ -0,0 +1,24 @@ +# meshcored.ini — Waveshare SX1262 LoRa HAT on Raspberry Pi 3/4/5 +# GPIO numbering is BCM (the number after "GPIO", e.g. GPIO16 = 16). +# +# Hardware config (read on every startup): +spidev = /dev/spidev0.0 +lora_irq_pin = 16 +lora_reset_pin = 18 +lora_nss_pin = 21 +lora_busy_pin = 20 +lora_freq = 869.618 +lora_bw = 62.5 +lora_sf = 8 +lora_cr = 8 +lora_tcxo = 1.8 +lora_tx_power = 22 + +# First-run node defaults (ignored after first boot; use CLI to change): +advert_name = Waveshare Linux Repeater +admin_password = changeme +lat = 0.0 +lon = 0.0 + +# data_dir — where identity and node prefs are persisted (default shown): +#data_dir = /var/lib/meshcore diff --git a/variants/linux/meshcored.service b/variants/linux/meshcored.service index c81ca4776f..b68ff2a62c 100644 --- a/variants/linux/meshcored.service +++ b/variants/linux/meshcored.service @@ -8,7 +8,7 @@ Wants=network.target Type=simple User=meshcore Group=meshcore -ExecStart=/usr/bin/stdbuf -oL /usr/bin/meshcored +ExecStart=/usr/bin/stdbuf -oL /usr/bin/meshcored --fsdir /var/lib/meshcore WorkingDirectory=/var/lib/meshcore Restart=on-failure RestartSec=5 @@ -19,7 +19,8 @@ ProtectSystem=strict ProtectHome=yes PrivateTmp=yes NoNewPrivileges=yes -ReadWritePaths=/var/lib/meshcore # allow writing only to its own data dir +# Allow writing only to its own data dir +ReadWritePaths=/var/lib/meshcore # Create data dir with correct ownership if it doesn't exist ExecStartPre=/bin/mkdir -p /var/lib/meshcore diff --git a/variants/linux/platformio.ini b/variants/linux/platformio.ini index d60a50fe71..3fc49b2cfd 100644 --- a/variants/linux/platformio.ini +++ b/variants/linux/platformio.ini @@ -32,10 +32,6 @@ build_flags = -D WRAPPER_CLASS=LinuxSX1262Wrapper -D USE_CUSTOM_SX1262_WRAPPER -D SKIP_CONFIG_OVERWRITE=1 - -D ADVERT_NAME='"Linux Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=100 -D LORA_TX_POWER=22 -D MESH_DEBUG=1 From 7a8615f2c3c85bf9f01c4f74d43807c0deea47f3 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:24:14 +0100 Subject: [PATCH 4/4] Fix boolean config parsing in LinuxBoard (#6) --- variants/linux/LinuxBoard.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp index e975884c15..4c25da0a11 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -126,8 +126,8 @@ int LinuxConfig::load(const char *filename) { else if (strcmp(key, "lora_tcxo") == 0) lora_tcxo = atof(value); else if (strcmp(key, "lora_tx_power") == 0) lora_tx_power = atoi(value); else if (strcmp(key, "current_limit") == 0) current_limit = atof(value); - else if (strcmp(key, "dio2_as_rf_switch") == 0) dio2_as_rf_switch = value != 0; - else if (strcmp(key, "rx_boosted_gain") == 0) rx_boosted_gain = value != 0; + else if (strcmp(key, "dio2_as_rf_switch") == 0) dio2_as_rf_switch = atoi(value) != 0; + else if (strcmp(key, "rx_boosted_gain") == 0) rx_boosted_gain = atoi(value) != 0; else if (strcmp(key, "lora_irq_pin") == 0) lora_irq_pin = atoi(value); else if (strcmp(key, "lora_reset_pin") == 0) lora_reset_pin = atoi(value);