Files
meshtastic_firmware/src/platform/portduino/PortduinoGlue.cpp
T
21e3a583bd Yaml check for Meshtasticd (#11224)
* feat(portduino): add `meshtasticd --check` config validator

Users hand-writing files in /etc/meshtasticd/config.d/ get no feedback when a
key is misplaced, misspelled or duplicated: meshtasticd silently ignores what
it does not read, so a broken config looks identical to a working one.

Add a --check mode that loads the configuration exactly as startup does, then
reports what it found and exits:

- Duplicate keys, via the yaml-cpp Parser/EventHandler stream. The Node API
  cannot see them because the map is already collapsed by the time it exists,
  and yaml-cpp keeps the FIRST occurrence, so a later override is discarded.
- Unknown or misnested keys, against a schema mirroring what loadConfig()
  reads, with a hint naming the section a stray key actually belongs to.
- rfswitch_table validation: unrecognised pins, mode rows whose length does not
  match the pin list, values that are not HIGH/LOW, and unknown modes.
- Cross-file overlap: every .yaml in the config directory merges into one
  portduino_config, so the file loaded LAST wins, the opposite of the
  within-file rule. Those files are read in filesystem order, not alphabetical.
- A warning when more than one file defines a Lora section: spidev, spiSpeed,
  gpiochip, DIO2_AS_RF_SWITCH, DIO3_TCXO_VOLTAGE and USB_PID/VID/Serialnum are
  assigned unconditionally with a default every time one is seen, so any of
  them not repeated in the last file loaded is silently reset.
- The resolved gpiochip/line for each pin, since a line that exists on the
  wrong chip is claimed successfully and then silently does nothing.

Exits non-zero when errors were found so it can also gate CI over
bin/config.d/**, keeping one implementation rather than a second schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(portduino): flag pins that resolve to -1 in --check

A pin key whose value will not convert to a number falls back to RADIOLIB_NC
(-1) while still being marked enabled, and initGPIOPin() then trips an
assertion inside LinuxGPIOPin rather than failing cleanly. YAML indentation
makes this easy to hit by accident: a stray line under "CS: 8" folds into the
value as a multi-line scalar, so the file parses, the daemon crashes with a
stack trace from a library file, and --check reported "Configuration looks
good" while printing "pin -1" two lines above.

Report it as an error naming the likely cause instead.

Also correct a comment claiming unparseable config.d files are skipped
silently. They are not: loadConfig() prints "*** Exception ..." with the line
and column. It is the discarded return value, not the diagnostic, that makes
the file's absence from the merged config easy to miss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(portduino): cover `meshtasticd --check` with fixtures and a fuzz suite

Adds the tests the config validator was missing, and the checks and fixes that
writing them turned up. The theme throughout is configuration that the YAML
parser accepts but that does not mean what it looks like it means.

Tests
-----

bin/test-config-check.sh - 57 assertions driving a built meshtasticd against
test/fixtures/portduino-config (50 fixtures plus two config.d trees). A shell
test rather than a Unity suite because both behaviours under test are properties
of the process: --check is judged by its exit status and printed report, and the
"a normal run rejects a bad config" path ends in exit() inside portduinoSetup(),
neither of which is reachable from a suite that links one translation unit.
Every fixture carries a comment header naming its planted fault and the expected
finding, so it can be read on its own. Coverage:

  * a clean config for each of the ten radio module families (RF95, sx1262,
    sx1268, LLCC68, sx1280, lr1110, lr1120, lr1121, sim, auto), asserted both
    findings-free and resolving to that module, so a silent fallback to sim
    cannot pass
  * LR11xx rfswitch tables: unrecognised pins, rows longer and shorter than the
    pin count, levels that are not exactly HIGH, a missing pins list, more than
    five pins, a scalar table, unknown MODE_ keys, a MODE_ row stranded one
    level out, and a legal partial table
  * the PA gain table in both accepted shapes, entries outside the uint16 range
    it is stored in, and more than the 22 points that are kept
  * values of the wrong type, split by consequence: the two settings read with
    no fallback stop meshtasticd starting, everything else is silently replaced
    by its default
  * out-of-range and unit mistakes: TCXO voltage written in millivolts, ports
    outside their usable range, an over-long StatusMessage
  * MAC sources: both keys set at once, a malformed address, an interface that
    does not exist
  * structural faults: duplicate keys, non-mapping and unknown sections, a key
    left at the top level, a sequence at the document root, an empty file,
    unreadable pins, unparseable YAML
  * cross-file behaviour over a config.d directory, including the switch tables
    that do not override each other
  * five configs run WITHOUT --check, each of which must still be refused, so
    check mode cannot quietly make the normal path permissive

test/test_fuzz_config - adversarial fuzzing of the checker itself, the "the tool
meant to diagnose your config crashes on it" failure mode. Scope is deliberately
narrow: yaml-cpp does the parsing and is fuzzed upstream, so what is exercised
here is our code above the parse, above all the duplicate-key detector, which is
the one hand-rolled piece and walks the raw parser event stream with its own
stack. Groups: the checked-in fixtures as a seed corpus, 3000 byte mutations of
them (flips, truncation, insertion, splicing, deletion), and structural torture
(nesting to 4096 in flow and block style, duplicate keys at depth, anchors,
aliases and merge keys, 64KB keys, 256KB scalars, multi-document files). A
fourth group of random bytes is present but disabled behind
FUZZ_CONFIG_RANDOM_BYTES: it was half the runtime for the least return, since
uniform noise is rejected on the first token. The contract is crash-freedom and
termination under AddressSanitizer, not any particular finding.

CI runs the shell test in the existing native simulator job; the fuzz suite is
picked up by the existing ^test_fuzz_ area rule. native-suite-count 40 -> 41.
The fixtures are exempt from trunk in .trunk/trunk.yaml, since prettier rejects
the duplicate keys and bad indentation that are the point of them.

Checker fixes found while writing the tests
-------------------------------------------

--check reported a clean exit 0 on configs meshtasticd then refuses to boot, the
worst failure a diagnostic tool can have. Four hard exits inside loadConfig()
killed the report before it printed: an unparseable file, an unknown Lora.Module,
MACAddress and MACAddressSource both set, and HUB75 on a build without it. All
are now reported as findings, and all are still refused on a normal run.

New validation: Lora.Module against the accepted spellings, which are matched
exactly and inconsistently cased, with a suggestion when only case differs; a
per-key value type table covering ~85 keys, tested by asking yaml-cpp to perform
the same conversion loadConfig() will so it cannot drift; the PA gain table;
DIO3_TCXO_VOLTAGE, which is in volts and multiplied by 1000, so the millivolt
value everything else uses silently asks for 1800V; APIPort and Webserver.Port
ranges; MaxNodes; StatusMessage truncation; MAC address and source; and an
unreadable ConfigDirectory.

Also fixes a crash: a ConfigDirectory that cannot be read threw an uncaught
filesystem_error from directory_iterator and aborted meshtasticd with SIGABRT,
taking --check down with it. It now fails cleanly.

Two smaller ones: cppcheck's uselessCallsSubstr on the ancestor walk, which was
failing every check job; and the duplicate-key detector's stack pop, which was
unguarded and relied on yaml-cpp emitting balanced events.

Switch tables are the one place "the file loaded last wins" is false. The loader
only ever writes HIGH and never writes LOW back, so a HIGH from an earlier file
survives a later file that clears it and the radio drives the OR of every table
loaded. Confirmed with --output-yaml. Reported as an error for now; the loader
itself is left alone, as that changes RF behaviour.

* fix(portduino): report CH341 pins as adapter indexes, not gpiochip lines

--check printed "Resolved GPIO lines (what meshtasticd will try to claim)" for
every config, listing a gpiochip and line for each Lora pin and advising they be
confirmed against gpiodetect and gpioinfo. For spidev: ch341 every part of that
is false. portduinoSetup() skips initGPIOPin() for every Lora pin when spidev is
ch341 and hands the raw numbers to Ch341Hal, so nothing is claimed from a
gpiochip -- and on Windows and macOS, where a USB adapter is the only way to
attach a radio, there is no gpiochip, gpiodetect or gpioinfo to check against in
the first place. The checker had no ch341 coverage at all: not one fixture used
it, so the whole USB-SPI path went unexercised.

The summary now splits on the transport. A ch341 device gets its pins listed as
adapter indexes with the gpiod advice dropped, and a gpiochip or line mapping
written alongside it is reported: those are read, stored, and never used.

Also: "RF switch table: not set" read as a gap on an SX126x, where there is
nothing to set. setRfSwitchTable() is only ever called for an LR11xx, so absence
is now "not needed for this module" everywhere else, and "not resolved yet" for
auto, which has no module to judge against.

Fixtures: usb-ch341.yaml (clean, the meshstick shape) and ch341-gpiochip.yaml.

CI fix
------

test-native was RED on "config.d overrides are reported", which wanted 2
warnings and got 1. The fixture's two config.d files name different modules, so
which one wins -- and whether the LR11xx-without-a-switch-table warning fires --
depends on the order the filesystem returns them in. That is the very thing the
fixture exists to demonstrate, so the count is no longer asserted; the report's
own order caveat is asserted instead.

Review fixes
------------

The unreadable-ConfigDirectory diagnostic was the one new print in
PortduinoGlue.cpp not gated behind !configCheck, so it landed ahead of the report
header and broke the clean output the rest of the change is careful to keep.

Docs: rfswitch-valid.yaml carries seven modes, not eight, and empty-file.yaml is
comments-only rather than zero bytes.

* style(portduino): trim --check comment blocks and reconcile suite count

Condense the multi-paragraph comment blocks in the --check validator to the
one-to-two-line convention, and bump test/native-suite-count to 42 for the
test_fuzz_config suite added here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 11:06:24 +00:00

1376 lines
66 KiB
C++

#include "CryptoEngine.h"
#include "HardwareRNG.h"
#include "PortduinoGPIO.h"
#include "SPIChip.h"
#include "mesh/RF95Interface.h"
#include "sleep.h"
#include "target_specific.h"
#include "ConfigCheck.h"
#include "PortduinoGlue.h"
#include "SHA256.h"
#include "api/ServerAPI.h"
#include "meshUtils.h"
#include <ErriezCRC32.h>
#include <Utility.h>
#include <assert.h>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <stdexcept>
#include <unistd.h>
#ifndef _WIN32
// Only the PORTDUINO_LINUX_HARDWARE block below calls ioctl() (HCIGETDEVINFO,
// for the BlueZ-derived MAC address); Windows has no <sys/ioctl.h>.
#include <sys/ioctl.h>
#endif
#ifdef PORTDUINO_LINUX_HARDWARE
#include "linux/gpio/LinuxGPIOPin.h"
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#endif
#ifdef PORTDUINO_LINUX_HARDWARE
#include <cxxabi.h>
#endif
#ifdef _WIN32
// Defined in WindowsMacAddr.cpp, which keeps <iphlpapi.h> out of this TU: it
// pulls in RPC/OLE headers that collide with the Arduino API.
bool portduinoWindowsPrimaryMac(uint8_t *dmac);
#endif
#ifdef __APPLE__
// Used by getMacAddr()'s macOS fallback to read the en0 link-layer address.
// `getifaddrs()` is the BSD-portable way; `<net/if_dl.h>` provides the
// `sockaddr_dl` cast and the `LLADDR()` macro that points at the 6-byte MAC.
#include <cstring> // strcmp, memcpy
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
#endif
#include "platform/portduino/USBHal.h"
portduino_config_struct portduino_config;
portduino_status_struct portduino_status;
std::ofstream traceFile;
std::ofstream JSONFile;
Ch341Hal *ch341Hal = nullptr;
char *configPath = nullptr;
char *optionMac = nullptr;
bool verboseEnabled = false;
bool yamlOnly = false;
bool configCheck = false;
// Every config file we attempted to load, in load order, for --check to report on.
std::vector<std::string> attemptedConfigFiles;
const char *argp_program_version = optstr(APP_VERSION);
char stdoutBuffer[512];
// FIXME - move setBluetoothEnable into a HALPlatform class
void setBluetoothEnable(bool enable)
{
// not needed
}
void cpuDeepSleep(uint32_t msecs)
{
notImplemented("cpuDeepSleep");
}
void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel");
int TCPPort = SERVER_API_DEFAULT_PORT;
bool checkConfigPort = true;
// Long-only option: argp treats any key above the printable ASCII range as having no
// single-character equivalent.
#define OPT_CONFIG_CHECK 1001
static error_t parse_opt(int key, char *arg, struct argp_state *state)
{
switch (key) {
case OPT_CONFIG_CHECK:
configCheck = true;
break;
case 'p':
if (sscanf(arg, "%d", &TCPPort) < 1) {
return ARGP_ERR_UNKNOWN;
} else {
checkConfigPort = false;
printf("Using config file %d\n", TCPPort);
}
break;
case 'c':
configPath = arg;
break;
case 's':
portduino_config.force_simradio = true;
break;
case 'h':
optionMac = arg;
break;
case 'v':
verboseEnabled = true;
break;
case 'y':
yamlOnly = true;
break;
case ARGP_KEY_ARG:
return 0;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
// A kernel SPI transfer is capped by the spidev module's `bufsiz` parameter (4096 by default).
// LovyanGFX pushes the framebuffer in large chunks, so a display bigger than that budget fails
// deep inside the driver with a bare -EMSGSIZE. Check up front so the user gets told what to fix.
static void checkSpidevBufsiz()
{
if (portduino_config.display_spi_dev == "" || portduino_config.displayWidth == 0 || portduino_config.displayHeight == 0) {
return;
}
switch (portduino_config.displayPanel) {
case no_screen:
case x11:
case fb:
case hub75:
return; // not driven over spidev
default:
break;
}
const long required = (long)portduino_config.displayWidth * portduino_config.displayHeight / 2 * 3;
std::ifstream bufsizFile("/sys/module/spidev/parameters/bufsiz");
long bufsiz = 0;
if (!bufsizFile.is_open() || !(bufsizFile >> bufsiz)) {
// spidev may be built into the kernel without exposing the parameter; nothing to check.
return;
}
if (bufsiz < required) {
std::cerr << "SPI display " << portduino_config.displayWidth << "x" << portduino_config.displayHeight
<< " needs a spidev buffer of at least " << required << " bytes, but "
<< "/sys/module/spidev/parameters/bufsiz is " << bufsiz << "." << std::endl;
std::cerr << "Add 'spidev.bufsiz=" << required << "' to your kernel command line "
<< "(/boot/firmware/cmdline.txt on Raspberry Pi OS) and reboot." << std::endl;
std::cerr << "Or echo that value into /etc/modprobe.d/spidev.conf and reload the spidev module" << std::endl;
exit(EXIT_FAILURE);
}
}
void portduinoCustomInit()
{
static struct argp_option options[] = {
{"port", 'p', "PORT", 0, "The TCP port to use."},
{"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."},
{"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"},
{"sim", 's', 0, 0, "Run in Simulated radio mode"},
{"verbose", 'v', 0, 0, "Set log level to full debug"},
{"output-yaml", 'y', 0, 0, "Output config yaml and exit"},
{"check", OPT_CONFIG_CHECK, 0, 0, "Check the configuration for problems, print a report, and exit"},
{0}};
static void *childArguments;
static char doc[] = "Meshtastic native build.";
static char args_doc[] = "...";
static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0};
const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0};
portduinoAddArguments(child, childArguments);
}
void getMacAddr(uint8_t *dmac)
{
// We should store this value, and short-circuit all this if it's already been set.
if (optionMac != nullptr && strlen(optionMac) > 0) {
if (strlen(optionMac) >= 12) {
MAC_from_string(optionMac, dmac);
} else {
uint32_t hwId = {0};
sscanf(optionMac, "%u", &hwId);
dmac[0] = 0x80;
dmac[1] = 0;
dmac[2] = hwId >> 24;
dmac[3] = hwId >> 16;
dmac[4] = hwId >> 8;
dmac[5] = hwId & 0xff;
}
} else if (portduino_config.mac_address.length() > 11) {
MAC_from_string(portduino_config.mac_address, dmac);
return;
} else {
#ifdef PORTDUINO_LINUX_HARDWARE
struct hci_dev_info di = {0};
di.dev_id = 0;
bdaddr_t bdaddr;
int btsock;
btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1);
if (btsock < 0) { // If anything fails, just return with the default value
return;
}
if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) {
return;
}
dmac[0] = di.bdaddr.b[5];
dmac[1] = di.bdaddr.b[4];
dmac[2] = di.bdaddr.b[3];
dmac[3] = di.bdaddr.b[2];
dmac[4] = di.bdaddr.b[1];
dmac[5] = di.bdaddr.b[0];
#elif defined(__APPLE__)
// No BlueZ on macOS, but we can fall back to the host's primary
// network interface MAC. `en0` is Wi-Fi on every shipping Mac
// (Ethernet, when present, is en1 or higher), which gives the user
// the same kind of stable, host-derived identifier that the BlueZ
// path provides on Linux. If en0 isn't found or has no MAC, dmac is
// left untouched and the caller's "Blank MAC Address not allowed!"
// check will still fire - preserving existing behavior for users
// who deliberately rely on --hwid or YAML override.
struct ifaddrs *ifap = nullptr;
if (getifaddrs(&ifap) == 0) {
for (struct ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) {
if (p->ifa_addr == nullptr || p->ifa_addr->sa_family != AF_LINK) {
continue;
}
if (strcmp(p->ifa_name, "en0") != 0) {
continue;
}
auto *sdl = reinterpret_cast<struct sockaddr_dl *>(p->ifa_addr);
if (sdl->sdl_alen == 6) {
memcpy(dmac, LLADDR(sdl), 6);
break;
}
}
freeifaddrs(ifap);
}
#elif defined(_WIN32)
// No BlueZ on Windows; the host's primary adapter MAC is the equivalent
// stable identifier. On failure dmac is untouched and the blank-MAC check fires.
portduinoWindowsPrimaryMac(dmac);
#else
// No platform-specific MAC source; leave dmac at its default. Caller
// can override via the --hwid CLI flag or the YAML config.
(void)dmac;
#endif
}
}
bool getDeviceId(uint8_t *deviceId)
{
if (portduino_config.has_device_id) {
memcpy(deviceId, portduino_config.device_id, sizeof(portduino_config.device_id));
return true;
}
// Config-supplied id stays preferred: host NIC/BT MACs can be unstable (docker, multi-NIC).
return getMacAddrDeviceId(deviceId);
}
std::string cleanupNameForAutoconf(std::string name)
{
// Convert spaces -> dashes, lowercase
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) {
if (c == ' ') {
return '-';
}
return (char)std::tolower(c);
});
return name;
}
/** apps run under portduino can optionally define a portduinoSetup() to
* use portduino specific init code (such as gpioBind) to setup portduino on their host machine,
* before running 'arduino' code.
*/
void portduinoSetup()
{
int max_GPIO = 0;
std::string gpioChipName = "gpiochip";
portduino_config.displayPanel = no_screen;
// Force stdout to be line buffered
setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer));
// We do this super early so that we can log from the rest of the init code
concurrency::hasBeenSetup = true;
consoleInit();
#ifdef ARCH_PORTDUINO_WASM
// Browser build: no YAML/filesystem config. Apply a hardcoded SX1262/CH341
// setup and create the WebUSB-backed Ch341Hal, then skip the Linux config path.
{
extern void wasm_config_apply();
wasm_config_apply();
ch341Hal =
new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid);
}
return;
#endif
if (portduino_config.force_simradio == true) {
portduino_config.lora_module = use_simradio;
} else if (configPath != nullptr) {
if (loadConfig(configPath)) {
if (!yamlOnly && !configCheck)
std::cout << "Using " << configPath << " as config file" << std::endl;
} else if (!configCheck) {
// In check mode the path is already in attemptedConfigFiles, so fall through
// to runConfigCheck() and let it report the parse error with a file and line.
std::cout << "Unable to use " << configPath << " as config file" << std::endl;
exit(EXIT_FAILURE);
}
} else if (access("config.yaml", R_OK) == 0) {
if (loadConfig("config.yaml")) {
if (!yamlOnly && !configCheck)
std::cout << "Using local config.yaml as config file" << std::endl;
} else if (!configCheck) {
std::cout << "Unable to use local config.yaml as config file" << std::endl;
exit(EXIT_FAILURE);
}
} else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) {
if (loadConfig("/etc/meshtasticd/config.yaml")) {
if (!yamlOnly && !configCheck)
std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
} else if (!configCheck) {
std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl;
exit(EXIT_FAILURE);
}
} else {
if (!yamlOnly && !configCheck)
std::cout << "No 'config.yaml' found..." << std::endl;
portduino_config.lora_module = use_simradio;
}
if (portduino_config.config_directory != "") {
// The throwing form of directory_iterator turns an unreadable ConfigDirectory into an
// uncaught filesystem_error and a SIGABRT, so take the error_code overload instead.
std::error_code dirError;
std::filesystem::directory_iterator entries{portduino_config.config_directory, dirError};
if (dirError) {
// Half a configuration is worse than none. --check continues so the report can say
// so with the rest of the findings.
if (!configCheck) {
std::cout << "Unable to read ConfigDirectory " << portduino_config.config_directory << ": " << dirError.message()
<< std::endl;
exit(EXIT_FAILURE);
}
}
for (const std::filesystem::directory_entry &entry : entries) {
if (ends_with(entry.path().string(), ".yaml")) {
if (!configCheck)
std::cout << "Also using " << entry << " as additional config file" << std::endl;
// .string() rather than .c_str(): path::value_type is wchar_t on
// Windows, and loadConfig() takes a const char *.
loadConfig(entry.path().string().c_str());
}
}
}
#ifndef ARCH_PORTDUINO_WASM
// --check wins over --output-yaml: asking for validation and getting a config dump
// with no report at all would be the more surprising of the two outcomes.
if (configCheck)
exit(runConfigCheck(attemptedConfigFiles));
if (yamlOnly) {
std::cout << portduino_config.emit_yaml() << std::endl;
exit(EXIT_SUCCESS);
}
#endif
if (portduino_config.force_simradio) {
std::cout << "Running in simulated mode." << std::endl;
portduino_config.MaxNodes = 200; // Default to 200 nodes
// Set the random seed equal to TCPPort to have a different seed per instance
uint32_t seed = TCPPort;
HardwareRNG::seed(seed);
randomSeed(seed);
return;
}
// If LoRa `Module: auto` (default in config.yaml),
// attempt to auto config based on Product Strings
if (portduino_config.lora_module == use_autoconf) {
bool found_hat = false;
bool found_rak_eeprom = false;
bool found_ch341 = false;
char hat_vendor[96] = {0};
char autoconf_product[96] = {0};
// Try CH341
try {
std::cout << "autoconf: Looking for CH341 device..." << std::endl;
auto probe = std::unique_ptr<Ch341Hal>(new Ch341Hal(0, portduino_config.lora_usb_serial_num,
portduino_config.lora_usb_vid, portduino_config.lora_usb_pid));
probe->getProductString(autoconf_product, 95);
std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl;
found_ch341 = true;
} catch (...) {
std::cout << "autoconf: Could not locate CH341 device" << std::endl;
}
// Try Pi HAT+
if (strlen(autoconf_product) < 6) {
std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
if (access("/proc/device-tree/hat/vendor", R_OK) == 0) {
std::ifstream hatVendorFile("/proc/device-tree/hat/vendor");
if (hatVendorFile.is_open()) {
hatVendorFile.read(hat_vendor, 95);
hatVendorFile.close();
}
}
if (access("/proc/device-tree/hat/product", R_OK) == 0) {
std::ifstream hatProductFile("/proc/device-tree/hat/product");
if (hatProductFile.is_open()) {
hatProductFile.read(autoconf_product, 95);
hatProductFile.close();
}
std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat"
<< std::endl;
// check for custom data fields
int i = 0;
while (access(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str(), R_OK) == 0) {
std::ifstream customFieldFile(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str());
if (customFieldFile.is_open()) {
std::string customFieldName;
std::string customFieldValue;
getline(customFieldFile, customFieldName, ' ');
getline(customFieldFile, customFieldValue, ' ');
customFieldFile.close();
printf("autoconf: Found hat+ custom field %s: %s\n", customFieldName.c_str(), customFieldValue.c_str());
portduino_config.hat_plus_custom_fields[customFieldName] = customFieldValue;
}
i++;
}
// potential TODO: Validate that this is a real UUID
std::ifstream hatUUID("/proc/device-tree/hat/uuid");
char uuid[38] = {0};
if (hatUUID.is_open()) {
hatUUID.read(uuid, 37);
hatUUID.close();
std::cout << "autoconf: UUID " << uuid << std::endl;
SHA256 uuid_hash;
uint8_t uuid_hash_bytes[32] = {0};
uuid_hash.reset();
uuid_hash.update(uuid, 37);
uuid_hash.finalize(uuid_hash_bytes, 32);
for (int j = 0; j < 16; j++) {
portduino_config.device_id[j] = uuid_hash_bytes[j];
}
portduino_config.has_device_id = true;
uint8_t dmac[6] = {0};
dmac[0] = (uuid_hash_bytes[17] << 4) | 2;
dmac[1] = uuid_hash_bytes[18];
dmac[2] = uuid_hash_bytes[19];
dmac[3] = uuid_hash_bytes[20];
dmac[4] = uuid_hash_bytes[21];
dmac[5] = uuid_hash_bytes[22];
char macBuf[13] = {0};
snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4],
dmac[5]);
portduino_config.mac_address = macBuf;
found_hat = true;
}
} else {
std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl;
}
}
// attempt to load autoconf data from an EEPROM on 0x50
// RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8
// <model string>:mac address :<16 random unique bytes in hexidecimal> : crc32
// crc32 is calculated on the eeprom string up to but not including the final colon
if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") {
try {
char *mac_start = nullptr;
char *devID_start = nullptr;
char *crc32_start = nullptr;
Wire.begin();
Wire.beginTransmission(0x50);
Wire.write(0x0);
Wire.write(0x0);
Wire.endTransmission();
Wire.requestFrom((uint8_t)0x50, (uint8_t)75);
uint8_t i = 0;
delay(100);
std::string autoconf_raw;
while (Wire.available() && i < sizeof(autoconf_product)) {
autoconf_product[i] = Wire.read();
if (autoconf_product[i] == 0xff) {
autoconf_product[i] = 0x0;
break;
}
autoconf_raw += autoconf_product[i];
if (autoconf_product[i] == ':') {
autoconf_product[i] = 0x0;
if (mac_start == nullptr) {
mac_start = autoconf_product + i + 1;
} else if (devID_start == nullptr) {
devID_start = autoconf_product + i + 1;
} else if (crc32_start == nullptr) {
crc32_start = autoconf_product + i + 1;
}
}
i++;
}
if (crc32_start != nullptr && strlen(crc32_start) == 8) {
std::string crc32_str(crc32_start);
uint32_t crc32_value = 0;
// convert crc32 ascii to raw uint32
for (int j = 0; j < 4; j++) {
crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8;
}
std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl;
// set the autoconf string to blank and short circuit
if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) {
std::cout << "autoconf: crc32 mismatch, dropping " << std::endl;
autoconf_product[0] = 0x0;
} else {
std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl;
found_rak_eeprom = true;
if (mac_start != nullptr) {
std::cout << "autoconf: Found mac data " << mac_start << std::endl;
if (strlen(mac_start) == 12)
portduino_config.mac_address = std::string(mac_start);
}
if (devID_start != nullptr) {
std::cout << "autoconf: Found deviceid data " << devID_start << std::endl;
if (strlen(devID_start) == 32) {
std::string devID_str(devID_start);
for (int j = 0; j < 16; j++) {
portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16);
}
portduino_config.has_device_id = true;
}
}
}
} else {
std::cout << "autoconf: crc32 missing " << std::endl;
autoconf_product[0] = 0x0;
}
} catch (...) {
std::cout << "autoconf: Could not locate EEPROM" << std::endl;
}
}
// Load the config file based on the product string
if (strlen(autoconf_product) > 0) {
// From configProducts map in PortduinoGlue.h
std::string product_config = "";
if (configProducts.find(autoconf_product) != configProducts.end()) {
product_config = configProducts.at(autoconf_product);
} else {
if (found_hat) {
product_config =
cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml");
if (strncmp(hat_vendor, "RAK", strlen("RAK")) == 0 &&
strncmp(autoconf_product, "6421 Pi Hat", strlen("6421 Pi Hat")) == 0) {
std::cout << "autoconf: Setting hardwareModel to RAK6421" << std::endl;
portduino_status.hardwareModel = meshtastic_HardwareModel_RAK6421;
}
} else if (found_ch341) {
product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml");
// look for more data after the null terminator
size_t len = strlen(autoconf_product);
if (len < 74) {
memcpy(portduino_config.device_id, autoconf_product + len + 1, 16);
if (!memfll(portduino_config.device_id, '\0', 16) && !memfll(portduino_config.device_id, 0xff, 16)) {
portduino_config.has_device_id = true;
if (strncmp(autoconf_product, "MESHSTICK 1262", strlen("MESHSTICK 1262")) == 0) {
std::cout << "autoconf: Setting hardwareModel to Meshstick 1262" << std::endl;
portduino_status.hardwareModel = meshtastic_HardwareModel_MESHSTICK_1262;
}
}
}
}
// Don't try to automatically find config for a device with RAK eeprom.
if (found_rak_eeprom) {
std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl;
exit(EXIT_FAILURE);
}
if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) {
std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")"
<< std::endl;
exit(EXIT_FAILURE);
}
}
if (loadConfig((portduino_config.available_directory + product_config).c_str())) {
std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl;
} else {
std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product
<< std::endl;
exit(EXIT_FAILURE);
}
} else {
std::cerr << "autoconf: Could not locate any devices" << std::endl;
exit(EXIT_FAILURE);
}
}
// if we have s SPI display, check /sys/module/spidev/parameters/bufsiz
// It needs to be at least width * height / 2 * 3
// fail with a more useful error message.
checkSpidevBufsiz();
// if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address
uint8_t dmac[6] = {0};
if (portduino_config.lora_spi_dev == "ch341") {
try {
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
portduino_config.lora_usb_pid);
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
std::cerr << "Could not initialize CH341 device!" << std::endl;
exit(EXIT_FAILURE);
}
char serial[9] = {0};
// Pass the full buffer size (9 = 8 chars + null) to getSerialString,
// not 8. The function treats `len` as buffer size and reserves one
// slot for the null terminator, so passing 8 produced a 7-char serial
// and broke the `strlen(serial) == 8` check below - masked on Linux
// by the BlueZ HCI MAC fallback in getMacAddr(), but on macOS (where
// the BlueZ path is __linux__-guarded) it left mac_address empty and
// meshtasticd refused to start.
ch341Hal->getSerialString(serial, sizeof(serial));
std::cout << "CH341 Serial " << serial << std::endl;
char product_string[96] = {0};
ch341Hal->getProductString(product_string, sizeof(product_string));
std::cout << "CH341 Product " << product_string << std::endl;
if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) {
std::cout << "Deriving MAC address from Serial and Product String" << std::endl;
uint8_t hash[104] = {0};
memcpy(hash, serial, 8);
memcpy(hash + 8, product_string, strlen(product_string));
crypto->hash(hash, 8 + strlen(product_string));
dmac[0] = (hash[0] << 4) | 2;
dmac[1] = hash[1];
dmac[2] = hash[2];
dmac[3] = hash[3];
dmac[4] = hash[4];
dmac[5] = hash[5];
char macBuf[13] = {0};
sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
portduino_config.mac_address = macBuf;
}
}
getMacAddr(dmac);
#ifndef PIO_UNIT_TESTING
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
exit(EXIT_FAILURE);
}
#endif
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
// Rather important to set this, if not running simulated.
uint32_t seed = static_cast<uint32_t>(time(NULL));
HardwareRNG::seed(seed);
randomSeed(seed);
std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip);
std::set<int> used_pins;
for (const auto *i : portduino_config.all_pins) {
if (i->enabled && i->pin > max_GPIO) {
max_GPIO = i->pin;
}
}
for (auto i : portduino_config.extra_pins) {
if (i.enabled && i.pin > max_GPIO) {
max_GPIO = i.pin;
}
}
gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need.
// Need to bind all the configured GPIO pins so they're not simulated
// TODO: If one of these fails, we should log and terminate
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") {
continue;
}
if (i->enabled) {
if (used_pins.find(i->pin) != used_pins.end()) {
printf("Pin %d is in use for multiple purposes\n", i->pin);
} else {
if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) {
printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line);
exit(EXIT_FAILURE);
}
used_pins.insert(i->pin);
}
}
}
printf("Initializing extra pins\n");
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") {
continue;
}
if (i.enabled) {
if (used_pins.find(i.pin) != used_pins.end()) {
printf("Pin %d is in use for multiple purposes\n", i.pin);
} else {
if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) {
printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line);
exit(EXIT_FAILURE);
}
used_pins.insert(i.pin);
}
}
}
// In one test, this dance seemed necessary to trigger the pin to detect properly.
if (portduino_config.lora_pa_detect_pin.enabled) {
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLDOWN);
sleep(1);
if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == LOW) {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLDOWN is LOW" << std::endl;
}
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLUP);
sleep(1);
if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == HIGH) {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is HIGH, dropping PA curve" << std::endl;
portduino_config.num_pa_points = 1;
portduino_config.tx_gain_lora[0] = 0;
} else {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is LOW, using PA curve" << std::endl;
}
// disable bias once finished
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT);
} else if (portduino_config.hat_plus_custom_fields.find("io_slot1") != portduino_config.hat_plus_custom_fields.end()) {
printf("Hat+ io_slot1 is %s\n", portduino_config.hat_plus_custom_fields["io_slot1"].c_str());
if (portduino_config.hat_plus_custom_fields["io_slot1"] != "RAK13302") {
std::cout << "Hat+ io_slot1 is not RAK13302, skipping PA curve" << std::endl;
portduino_config.num_pa_points = 1;
portduino_config.tx_gain_lora[0] = 0;
}
}
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") {
continue;
}
if (i.enabled && i.default_high) {
pinMode(i.pin, OUTPUT);
digitalWrite(i.pin, HIGH);
}
}
// 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") {
SPI.begin(portduino_config.lora_spi_dev.c_str());
}
if (portduino_config.traceFilename != "") {
try {
traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app);
} catch (std::ofstream::failure &e) {
std::cout << "*** traceFile Exception " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
if (!traceFile.is_open()) {
std::cout << "*** traceFile open failure" << std::endl;
exit(EXIT_FAILURE);
}
} else if (portduino_config.JSONFilename != "") {
try {
if (portduino_config.JSONFileRotate == 0) {
JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app);
}
} catch (std::ofstream::failure &e) {
std::cout << "*** JSONFile Exception " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
if (!JSONFile.is_open()) {
std::cout << "*** JSONFile open failure" << std::endl;
exit(EXIT_FAILURE);
}
}
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
portduino_config.logoutputlevel = level_debug;
}
if (portduino_config.lora_spi_dev != "") {
portduinoSetOptions({.realHardware = true});
}
return;
}
int initGPIOPin(int pinNum, const std::string &gpioChipName, int line)
{
#ifdef PORTDUINO_LINUX_HARDWARE
std::string gpio_name = "GPIO" + std::to_string(pinNum);
std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl;
try {
GPIOPin *csPin;
csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str());
csPin->setSilent();
gpioBind(csPin);
return ERRNO_OK;
} catch (...) {
const std::type_info *t = abi::__cxa_current_exception_type();
std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl;
return ERRNO_DISABLED;
}
#else
return ERRNO_OK;
#endif
}
#ifdef ARCH_PORTDUINO_WASM
// Browser node: configuration comes from the wasm_set_lora_* setters, not a YAML
// file. Reached only as dead code after portduinoSetup()'s early return; kept
// defined (and yaml-free) so those references still link.
bool loadConfig(const char *configPath)
{
(void)configPath;
return false;
}
#else
bool loadConfig(const char *configPath)
{
// Recorded even when the load below fails: an unparseable config.d entry is skipped and its
// return value discarded by the caller, so --check needs to know it was attempted.
attemptedConfigFiles.push_back(configPath);
YAML::Node yamlConfig;
try {
yamlConfig = YAML::LoadFile(configPath);
if (yamlConfig["Logging"]) {
if (yamlConfig["Logging"]["LogLevel"].as<std::string>("info") == "trace") {
portduino_config.logoutputlevel = level_trace;
} else if (yamlConfig["Logging"]["LogLevel"].as<std::string>("info") == "debug") {
portduino_config.logoutputlevel = level_debug;
} else if (yamlConfig["Logging"]["LogLevel"].as<std::string>("info") == "info") {
portduino_config.logoutputlevel = level_info;
} else if (yamlConfig["Logging"]["LogLevel"].as<std::string>("info") == "warn") {
portduino_config.logoutputlevel = level_warn;
} else if (yamlConfig["Logging"]["LogLevel"].as<std::string>("info") == "error") {
portduino_config.logoutputlevel = level_error;
}
portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as<std::string>("");
portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as<std::string>("");
portduino_config.JSONFileRotate = yamlConfig["Logging"]["JSONFileRotate"].as<int>(0);
portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as<int>(0);
if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "textmessage")
portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "telemetry")
portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "nodeinfo")
portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "position")
portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "waypoint")
portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "neighborinfo")
portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "traceroute")
portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "detection")
portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "paxcounter")
portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP;
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "remotehardware")
portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP;
if (yamlConfig["Logging"]["AsciiLogs"]) {
// Default is !isatty(1) but can be set explicitly in config.yaml
portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as<bool>();
portduino_config.ascii_logs_explicit = true;
}
}
if (yamlConfig["Lora"]) {
if (yamlConfig["Lora"]["Module"]) {
const std::string moduleName = yamlConfig["Lora"]["Module"].as<std::string>("");
bool found = false;
for (const auto &loraModule : portduino_config.loraModules) {
if (moduleName == loraModule.second) {
portduino_config.lora_module = loraModule.first;
found = true;
break;
}
}
if (!found && !configCheck) {
// --check names the valid modules in its report; exiting here would
// replace that with a bare one-liner.
std::cerr << "Unknown Lora.Module: " << moduleName << std::endl;
exit(EXIT_FAILURE);
}
}
if (yamlConfig["Lora"]["SX126X_MAX_POWER"])
portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as<int>(22);
if (yamlConfig["Lora"]["SX128X_MAX_POWER"])
portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as<int>(13);
if (yamlConfig["Lora"]["LR1110_MAX_POWER"])
portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as<int>(22);
if (yamlConfig["Lora"]["LR1120_MAX_POWER"])
portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as<int>(13);
if (yamlConfig["Lora"]["LR2021_MAX_POWER"])
portduino_config.lr2021_max_power = yamlConfig["Lora"]["LR2021_MAX_POWER"].as<int>(22);
if (yamlConfig["Lora"]["LR2021_MAX_POWER_HF"])
portduino_config.lr2021_max_power_hf = yamlConfig["Lora"]["LR2021_MAX_POWER_HF"].as<int>(12);
if (yamlConfig["Lora"]["RF95_MAX_POWER"])
portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as<int>(20);
if (yamlConfig["Lora"]["TX_GAIN_LORA"]) {
YAML::Node tx_gain_node = yamlConfig["Lora"]["TX_GAIN_LORA"];
if (tx_gain_node.IsSequence() && tx_gain_node.size() != 0) {
portduino_config.num_pa_points = min(tx_gain_node.size(), std::size(portduino_config.tx_gain_lora));
for (int i = 0; i < portduino_config.num_pa_points; i++) {
portduino_config.tx_gain_lora[i] = tx_gain_node[i].as<int>();
}
} else {
portduino_config.num_pa_points = 1;
portduino_config.tx_gain_lora[0] = tx_gain_node.as<int>(0);
}
}
if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio &&
!portduino_config.force_simradio) {
portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as<bool>(false);
portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as<float>(0) * 1000;
if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as<bool>(false)) {
portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true"
}
// backwards API compatibility and to globally set gpiochip once
portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as<int>(0);
for (auto this_pin : portduino_config.all_pins) {
if (this_pin->config_section == "Lora") {
readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin);
}
}
}
if (yamlConfig["Lora"]["Enable_Pins"]) {
for (auto extra_pin : yamlConfig["Lora"]["Enable_Pins"]) {
portduino_config.extra_pins.push_back(pinMapping());
portduino_config.extra_pins.back().config_section = "Lora";
portduino_config.extra_pins.back().config_name = "Enable_Pins";
portduino_config.extra_pins.back().enabled = true;
portduino_config.extra_pins.back().default_high = true;
readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back());
}
}
portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as<int>(2000000);
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 = "/dev/" + portduino_config.lora_spi_dev;
if (portduino_config.lora_spi_dev.length() == 14) {
int x = portduino_config.lora_spi_dev.at(11) - '0';
int y = portduino_config.lora_spi_dev.at(13) - '0';
// Pretty sure this is always true
if (x >= 0 && x < 10 && y >= 0 && y < 10) {
// I believe this bit of weirdness is specifically for the new GUI
portduino_config.lora_spi_dev_int = x + y << 4;
portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int;
portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int;
}
}
}
if (yamlConfig["Lora"]["rfswitch_table"]) {
portduino_config.has_rfswitch_table = true;
portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY;
portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX;
portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX;
portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP;
portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF;
portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS;
portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI;
portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE;
for (int i = 0; i < 5; i++) {
// set up the pin array first
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO5")
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5;
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO6")
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6;
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO7")
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7;
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO8")
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8;
if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as<std::string>("") == "DIO10")
portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10;
// now fill in the table
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[0].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[1].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[2].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[3].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[4].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[5].values[i] = HIGH;
if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as<std::string>("") == "HIGH")
portduino_config.rfswitch_table[6].values[i] = HIGH;
}
}
}
readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin);
if (yamlConfig["GPS"]) {
std::string serialPath = yamlConfig["GPS"]["SerialPath"].as<std::string>("");
if (serialPath != "") {
Serial1.setPath(serialPath);
portduino_config.gps_serial_path = serialPath;
portduino_config.has_gps = 1;
}
std::string gpsdHost = yamlConfig["GPS"]["GpsdHost"].as<std::string>("");
if (!gpsdHost.empty()) {
if (portduino_config.has_gps) {
LOG_WARN("GPS config: both SerialPath and GpsdHost are set; GpsdHost takes priority");
}
int gpsdPort = yamlConfig["GPS"]["GpsdPort"].as<int>(2947);
if (gpsdPort < 1 || gpsdPort > 65535) {
LOG_ERROR("GPS config: GpsdPort %d is out of range [1, 65535]; ignoring GPS config", gpsdPort);
} else {
portduino_config.gpsd_host = gpsdHost;
portduino_config.gpsd_port = gpsdPort;
portduino_config.has_gps = 1;
}
}
}
if (yamlConfig["GPIO"]["ExtraPins"]) {
for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) {
portduino_config.extra_pins.push_back(pinMapping());
portduino_config.extra_pins.back().config_section = "GPIO";
portduino_config.extra_pins.back().config_name = "ExtraPins";
portduino_config.extra_pins.back().enabled = true;
readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back());
}
}
if (yamlConfig["I2C"]) {
portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as<std::string>("");
}
if (yamlConfig["Display"]) {
for (const auto &screen_name : portduino_config.screen_names) {
if (yamlConfig["Display"]["Panel"].as<std::string>("") == screen_name.second)
portduino_config.displayPanel = screen_name.first;
}
portduino_config.displayHeight = yamlConfig["Display"]["Height"].as<int>(0);
portduino_config.displayWidth = yamlConfig["Display"]["Width"].as<int>(0);
readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1);
readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1);
readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1);
readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1);
readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1);
portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as<bool>(false);
portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as<bool>(false);
portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as<int>(0);
portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as<int>(0);
portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as<bool>(false);
portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as<int>(1);
portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as<bool>(false);
portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as<int>(40000000);
if (yamlConfig["Display"]["spidev"]) {
portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as<std::string>("spidev0.1");
if (portduino_config.display_spi_dev.length() == 14) {
int x = portduino_config.display_spi_dev.at(11) - '0';
int y = portduino_config.display_spi_dev.at(13) - '0';
if (x >= 0 && x < 10 && y >= 0 && y < 10) {
portduino_config.display_spi_dev_int = x + y << 4;
portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int;
}
}
}
#if !defined(HAS_HUB75_NATIVE)
if (portduino_config.displayPanel == hub75 && !configCheck) {
// --check still validates the rest of the file and reports this as a
// finding, so it must not exit from inside the load.
std::cerr << "HUB75 display panel selected, but this build does not support HUB75" << std::endl;
exit(EXIT_FAILURE);
}
#endif
// HUB75 RGB matrix (Raspberry Pi). Options map onto rgb_matrix::RGBMatrix::Options +
// RuntimeOptions; the library owns its GPIO pins so nothing is read via readGPIOFromYaml.
if (portduino_config.displayPanel == hub75 && yamlConfig["Display"]["HUB75"]) {
YAML::Node hub75 = yamlConfig["Display"]["HUB75"];
portduino_config.hub75_hardware_mapping = hub75["HardwareMapping"].as<std::string>("regular");
portduino_config.hub75_rows = hub75["Rows"].as<int>(64);
portduino_config.hub75_cols = hub75["Cols"].as<int>(64);
portduino_config.hub75_chain_length = hub75["ChainLength"].as<int>(1);
portduino_config.hub75_parallel = hub75["Parallel"].as<int>(1);
portduino_config.hub75_pwm_bits = hub75["PWMBits"].as<int>(11);
portduino_config.hub75_pwm_lsb_nanoseconds = hub75["PWMLSBNanoseconds"].as<int>(130);
portduino_config.hub75_brightness = hub75["Brightness"].as<int>(100);
portduino_config.hub75_scan_mode = hub75["ScanMode"].as<int>(0);
portduino_config.hub75_row_address_type = hub75["RowAddressType"].as<int>(0);
portduino_config.hub75_multiplexing = hub75["Multiplexing"].as<int>(0);
portduino_config.hub75_disable_hardware_pulsing = hub75["DisableHardwarePulsing"].as<bool>(false);
portduino_config.hub75_show_refresh_rate = hub75["ShowRefreshRate"].as<bool>(false);
portduino_config.hub75_inverse_colors = hub75["InverseColors"].as<bool>(false);
portduino_config.hub75_led_rgb_sequence = hub75["RGBSequence"].as<std::string>("RGB");
portduino_config.hub75_pixel_mapper_config = hub75["PixelMapper"].as<std::string>("");
portduino_config.hub75_panel_type = hub75["PanelType"].as<std::string>("");
portduino_config.hub75_limit_refresh_rate_hz = hub75["LimitRefreshRateHz"].as<int>(0);
portduino_config.hub75_gpio_slowdown = hub75["GPIOSlowdown"].as<int>(1);
// The BaseUI framebuffer geometry is the full panel size in pixels.
portduino_config.displayWidth = portduino_config.hub75_cols * portduino_config.hub75_chain_length;
portduino_config.displayHeight = portduino_config.hub75_rows * portduino_config.hub75_parallel;
}
}
if (yamlConfig["Touchscreen"]) {
if (yamlConfig["Touchscreen"]["Module"].as<std::string>("") == "XPT2046")
portduino_config.touchscreenModule = xpt2046;
else if (yamlConfig["Touchscreen"]["Module"].as<std::string>("") == "STMPE610")
portduino_config.touchscreenModule = stmpe610;
else if (yamlConfig["Touchscreen"]["Module"].as<std::string>("") == "GT911")
portduino_config.touchscreenModule = gt911;
else if (yamlConfig["Touchscreen"]["Module"].as<std::string>("") == "FT5x06")
portduino_config.touchscreenModule = ft5x06;
readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1);
readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1);
portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as<int>(1000000);
portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as<int>(-1);
portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as<int>(-1);
if (yamlConfig["Touchscreen"]["spidev"]) {
portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as<std::string>("");
if (portduino_config.touchscreen_spi_dev.length() == 14) {
int x = portduino_config.touchscreen_spi_dev.at(11) - '0';
int y = portduino_config.touchscreen_spi_dev.at(13) - '0';
if (x >= 0 && x < 10 && y >= 0 && y < 10) {
portduino_config.touchscreen_spi_dev_int = x + y << 4;
}
}
}
}
if (yamlConfig["Input"]) {
portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as<std::string>("");
portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as<std::string>("");
portduino_config.joystickDevice = (yamlConfig["Input"]["JoystickDevice"]).as<std::string>("");
if (yamlConfig["Input"]["JoystickButtons"]) {
// action name -> evdev button code (hex like 0x122 or decimal); stored inverted
// as code -> lowercase action name for the driver to look up per keypress.
for (const auto &button : yamlConfig["Input"]["JoystickButtons"]) {
std::string action = button.first.as<std::string>("");
for (auto &c : action)
c = tolower(c);
int code = 0;
try {
// base 0 accepts hex (0x122) or decimal; a malformed value just skips this entry.
code = std::stoi(button.second.as<std::string>(""), nullptr, 0);
} catch (const std::exception &) {
code = 0;
}
if (code != 0 && action != "")
portduino_config.joystickButtons[code] = action;
}
}
readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin);
readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin);
if (yamlConfig["Input"]["TrackballDirection"].as<std::string>("RISING") == "RISING") {
portduino_config.tbDirection = 4;
} else if (yamlConfig["Input"]["TrackballDirection"].as<std::string>("RISING") == "FALLING") {
portduino_config.tbDirection = 3;
}
}
if (yamlConfig["Webserver"]) {
portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as<int>(-1);
portduino_config.webserver_root_path =
(yamlConfig["Webserver"]["RootPath"]).as<std::string>("/usr/share/meshtasticd/web");
portduino_config.webserver_ssl_key_path =
(yamlConfig["Webserver"]["SSLKey"]).as<std::string>("/etc/meshtasticd/ssl/private_key.pem");
portduino_config.webserver_ssl_cert_path =
(yamlConfig["Webserver"]["SSLCert"]).as<std::string>("/etc/meshtasticd/ssl/certificate.pem");
}
if (yamlConfig["HostMetrics"]) {
portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as<int>(0);
portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as<int>(0);
portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as<std::string>("");
}
if (yamlConfig["Config"]) {
portduino_config.has_config_overrides = true;
if (yamlConfig["Config"]["DisplayMode"]) {
portduino_config.has_configDisplayMode = true;
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR;
} else if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "INVERTED") {
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED;
} else if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "COLOR") {
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
} else {
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
}
}
if (yamlConfig["Config"]["StatusMessage"]) {
portduino_config.has_statusMessage = true;
portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as<std::string>("");
}
if ((yamlConfig["Config"]["EnableUDP"]).as<bool>(false)) {
portduino_config.enable_UDP = true;
}
}
if (yamlConfig["General"]) {
portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as<int>(200);
portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as<int>(100);
portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as<std::string>("");
portduino_config.available_directory =
(yamlConfig["General"]["AvailableDirectory"]).as<std::string>("/etc/meshtasticd/available.d/");
if ((yamlConfig["General"]["MACAddress"]).as<std::string>("") != "" &&
(yamlConfig["General"]["MACAddressSource"]).as<std::string>("") != "") {
// --check reports this as a finding against the file it came from, so
// exiting here would kill the report before it is printed.
if (!configCheck) {
std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl;
exit(EXIT_FAILURE);
}
}
if (checkConfigPort) {
portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as<int>(-1);
if (portduino_config.api_port > 1023 && portduino_config.api_port < 65536) {
TCPPort = (portduino_config.api_port);
}
}
portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as<std::string>("");
if (portduino_config.mac_address != "") {
portduino_config.mac_address_explicit = true;
} else if ((yamlConfig["General"]["MACAddressSource"]).as<std::string>("") != "") {
portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as<std::string>("");
std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address");
std::getline(infile, portduino_config.mac_address);
}
// https://stackoverflow.com/a/20326454
portduino_config.mac_address.erase(
std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'),
portduino_config.mac_address.end());
}
} catch (YAML::Exception &e) {
// The check report repeats this against the file it came from, so printing it
// here too would only put a stray line above the report.
if (!configCheck)
std::cout << "*** Exception " << e.what() << std::endl;
return false;
}
return true;
}
#endif // !ARCH_PORTDUINO_WASM
// https://stackoverflow.com/questions/874134/find-out-if-string-ends-with-another-string-in-c
static bool ends_with(std::string_view str, std::string_view suffix)
{
return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}
bool MAC_from_string(std::string mac_str, uint8_t *dmac)
{
mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end());
if (mac_str.length() != 12) {
return false;
}
// Validate every character is a hex digit before parsing. std::stoi
// would otherwise skip leading whitespace and silently truncate at the
// first non-digit, which is too lenient for a MAC address.
for (char c : mac_str) {
if (!isxdigit(static_cast<unsigned char>(c))) {
return false;
}
}
// Parse into a temporary so dmac is not partially modified if a later
// byte fails. At least one caller in getMacAddr() ignores the bool
// return, so leaving stale bytes in dmac on failure would silently
// produce a wrong MAC.
uint8_t tmp[6];
try {
for (int i = 0; i < 6; i++) {
tmp[i] = static_cast<uint8_t>(std::stoi(mac_str.substr(i * 2, 2), nullptr, 16));
}
} catch (const std::exception &) {
return false;
}
memcpy(dmac, tmp, 6);
return true;
}
std::string exec(const char *cmd)
{ // https://stackoverflow.com/a/478960
#ifdef ARCH_PORTDUINO_WASM
(void)cmd; // no shell/popen in the browser - shell-outs degrade to empty
return "";
#endif
std::array<char, 128> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
if (!pipe) {
throw std::runtime_error("popen() failed!");
}
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe.get()) != nullptr) {
result += buffer.data();
}
return result;
}
#ifndef ARCH_PORTDUINO_WASM
void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault)
{
if (sourceNode.IsMap()) {
destPin.enabled = true;
destPin.pin = sourceNode["pin"].as<int>(pinDefault);
destPin.line = sourceNode["line"].as<int>(destPin.pin);
destPin.gpiochip = sourceNode["gpiochip"].as<int>(portduino_config.lora_default_gpiochip);
} else if (sourceNode) { // backwards API compatibility
destPin.enabled = true;
destPin.pin = sourceNode.as<int>(pinDefault);
destPin.line = destPin.pin;
destPin.gpiochip = portduino_config.lora_default_gpiochip;
}
}
#endif // !ARCH_PORTDUINO_WASM