From 06a6c3ee2062efcf8281b74c54eef35146e3ea18 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 26 Apr 2026 22:07:07 -0500 Subject: [PATCH] Native MacOS hello world (#10309) * Native MacOS hello world * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/native/portduino/platformio.ini Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: ensure null-termination in getSerialString() and handle len==0 Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/e5647919-2255-48ad-bcaa-7a2c2fdbf212 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --------- Co-authored-by: Jonathan Bennett Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- bin/build-native.sh | 3 +- src/Power.cpp | 2 + src/RedirectablePrint.h | 2 +- src/input/InputBroker.cpp | 3 ++ src/input/LinuxInput.h | 5 ++- src/input/LinuxInputImpl.h | 3 +- src/mesh/HardwareRNG.cpp | 12 ++++- src/mesh/MeshRadio.h | 7 ++- src/mesh/RadioLibInterface.cpp | 2 +- src/mesh/RadioLibInterface.h | 2 +- src/mqtt/MQTT.cpp | 2 +- src/platform/portduino/PortduinoGlue.cpp | 18 +++++--- src/platform/portduino/USBHal.h | 10 +++-- variants/native/portduino.ini | 30 ++++++++----- variants/native/portduino/platformio.ini | 56 ++++++++++++++++++++++++ 15 files changed, 129 insertions(+), 28 deletions(-) diff --git a/bin/build-native.sh b/bin/build-native.sh index f35e46a87..e34b75580 100755 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -31,5 +31,6 @@ basename=meshtasticd-$1-$VERSION pio pkg install --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed -cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)" +os_name=$(uname -s | tr '[:upper:]' '[:lower:]') +cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)" cp bin/native-install.* $OUTDIR/ diff --git a/src/Power.cpp b/src/Power.cpp index 17715e848..bb9f554be 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -781,8 +781,10 @@ void Power::reboot() rp2040.reboot(); #elif defined(ARCH_PORTDUINO) deInitApiServer(); +#ifdef __linux__ if (aLinuxInputImpl) aLinuxInputImpl->deInit(); +#endif SPI.end(); Wire.end(); Serial1.end(); diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index c66226171..8535933fc 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -1,8 +1,8 @@ #pragma once #include "../freertosinc.h" +#include "Print.h" #include "mesh/generated/meshtastic/mesh.pb.h" -#include #include #include diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index b7c9b27a9..393cbc0ec 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -390,8 +390,11 @@ void InputBroker::Init() seesawRotary = nullptr; } } +#ifdef __linux__ + // Linux evdev keyboard input only — macOS has no . aLinuxInputImpl = new LinuxInputImpl(); aLinuxInputImpl->init(); +#endif } #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL diff --git a/src/input/LinuxInput.h b/src/input/LinuxInput.h index 43d08493c..673d29b3c 100644 --- a/src/input/LinuxInput.h +++ b/src/input/LinuxInput.h @@ -1,5 +1,8 @@ #pragma once -#if ARCH_PORTDUINO +// Linux evdev keyboard input. Only compiled on Linux portduino targets; +// macOS / non-Linux builds have no or epoll, and the +// headless build doesn't need real keyboards anyway. +#if ARCH_PORTDUINO && defined(__linux__) #include "InputBroker.h" #include "concurrency/OSThread.h" #include diff --git a/src/input/LinuxInputImpl.h b/src/input/LinuxInputImpl.h index e734b0294..716c6619a 100644 --- a/src/input/LinuxInputImpl.h +++ b/src/input/LinuxInputImpl.h @@ -1,4 +1,5 @@ -#ifdef ARCH_PORTDUINO +// Linux evdev impl. Same Linux-only gating as LinuxInput.h. +#if defined(ARCH_PORTDUINO) && defined(__linux__) #pragma once #include "LinuxInput.h" #include "main.h" diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index b79b0d012..a34a9477c 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -19,8 +19,12 @@ extern Adafruit_nRFCrypto nRFCrypto; #include #elif defined(ARCH_PORTDUINO) #include -#include #include +#ifdef __linux__ +#include // getrandom() +#else +#include // arc4random_buf() on Darwin/BSD +#endif #endif namespace HardwareRNG @@ -119,10 +123,16 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy) filled = true; #elif defined(ARCH_PORTDUINO) // Prefer the host OS RNG first when running under Portduino. +#ifdef __linux__ ssize_t generated = ::getrandom(buffer, length, 0); if (generated == static_cast(length)) { filled = true; } +#else + // arc4random_buf is available on Darwin/BSD and cannot fail. + ::arc4random_buf(buffer, length); + filled = true; +#endif if (!filled) { fillWithRandomDevice(buffer, length); diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index 089b4b189..fe4788bff 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -6,8 +6,11 @@ #include "configuration.h" #include "detect/LoRaRadioType.h" -// Sentinel marking the end of a modem preset array -static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END = +// Sentinel marking the end of a modem preset array. Declared `const` rather +// than `constexpr` because the cast from 0xFF to the enum is out-of-range and +// therefore not a valid constant expression on Clang 16+ (Apple Clang on +// macOS). The value is only ever compared at runtime, so static-init is fine. +static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END = static_cast(0xFF); // Region profile: bundles the preset list with regulatory parameters shared across regions diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 6024d06b6..de468cf97 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -119,7 +119,7 @@ bool RadioLibInterface::canSendImmediately() return true; } -bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag) +bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag) { bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); // Handle false detections diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 2859558ed..0740561f9 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -220,7 +220,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified protected: uint32_t activeReceiveStart = 0; - bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); + bool receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag); /** Do any hardware setup needed on entry into send configuration for the radio. * Subclasses can customize, but must also call this base method */ diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index aba06c210..283fcffb1 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -32,7 +32,7 @@ #include #include -#include +#include "IPAddress.h" #if defined(ARCH_PORTDUINO) #include #elif !defined(ntohl) diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 7833b3603..fd26926d9 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -9,13 +9,10 @@ #include "PortduinoGlue.h" #include "SHA256.h" #include "api/ServerAPI.h" -#include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" #include #include #include -#include -#include #include #include #include @@ -25,6 +22,12 @@ #include #include +#ifdef PORTDUINO_LINUX_HARDWARE +#include "linux/gpio/LinuxGPIOPin.h" +#include +#include +#endif + #ifdef PORTDUINO_LINUX_HARDWARE #include #endif @@ -130,9 +133,9 @@ void getMacAddr(uint8_t *dmac) } } else if (portduino_config.mac_address.length() > 11) { MAC_from_string(portduino_config.mac_address, dmac); - exit; + return; } else { - +#ifdef PORTDUINO_LINUX_HARDWARE struct hci_dev_info di = {0}; di.dev_id = 0; bdaddr_t bdaddr; @@ -152,6 +155,11 @@ void getMacAddr(uint8_t *dmac) dmac[3] = di.bdaddr.b[2]; dmac[4] = di.bdaddr.b[1]; dmac[5] = di.bdaddr.b[0]; +#else + // No BlueZ on non-Linux hosts (e.g. macOS). Leave dmac at its default; + // the caller can override via the --hwid CLI flag or the YAML config. + (void)dmac; +#endif } } diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index 1725763f2..9496b2ccb 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -5,6 +5,7 @@ #include "platform/portduino/PortduinoGlue.h" #include #include +#include #include #include #include @@ -34,7 +35,7 @@ class Ch341Hal : public RadioLibHal : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { if (serial != "") { - strncpy(pinedio.serial_number, serial.c_str(), 8); + std::strncpy(pinedio.serial_number, serial.c_str(), 8); pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); } // LOG_INFO("USB Serial: %s", pinedio.serial_number); @@ -59,8 +60,11 @@ class Ch341Hal : public RadioLibHal void getSerialString(char *_serial, size_t len) { - len = len > 8 ? 8 : len; - strncpy(_serial, pinedio.serial_number, len); + if (len == 0) + return; + size_t bytesCopied = (len - 1) < 8 ? (len - 1) : 8; + std::strncpy(_serial, pinedio.serial_number, bytesCopied); + _serial[bytesCopied] = '\0'; } void getProductString(char *_product_string, size_t len) diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index b276d2779..35c8c6697 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -2,7 +2,7 @@ [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/135b91e953db0b5f44d278f8ebd5b8d985fc03d8.zip + https://github.com/meshtastic/platform-native/archive/4ea5e09ac7d51a593e12ec7c1ebb6cd06745ce53.zip framework = arduino build_src_filter = @@ -37,25 +37,35 @@ lib_deps = # renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680 https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip -build_flags = +; Cross-platform build flags shared between native (Linux) and native-macos builds. +; Anything Linux-only (libbluetooth, libgpiod, libi2c, /sys/class/gpio, +; PORTDUINO_LINUX_HARDWARE, glibc-style _FORTIFY_SOURCE) goes in `build_flags` +; instead. UDP multicast also lives there because the firmware's +; UdpMulticastHandler.h unconditionally `#include ` on non-NRF52 +; targets, which requires the framework's bundled WiFi shim that we +; lib_ignore on macOS. +build_flags_common = ${arduino_base.build_flags} -D ARCH_PORTDUINO -fPIC - -D_FORTIFY_SOURCE=2 - -fstack-protector-all -Wstack-protector --param ssp-buffer-size=4 -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED - -DPORTDUINO_LINUX_HARDWARE - -DHAS_UDP_MULTICAST=1 -lpthread - -lstdc++fs - -lbluetooth - -lgpiod -lyaml-cpp - -li2c -luv -std=gnu17 -std=gnu++17 + +build_flags = + ${portduino_base.build_flags_common} + -DHAS_UDP_MULTICAST=1 + -D_FORTIFY_SOURCE=2 + -fstack-protector-all -Wstack-protector --param ssp-buffer-size=4 + -DPORTDUINO_LINUX_HARDWARE + -lstdc++fs + -lbluetooth + -lgpiod + -li2c lib_ignore = Adafruit NeoPixel Adafruit ST7735 and ST7789 Library diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 045e3edea..c497d0c17 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -117,3 +117,59 @@ build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:nati test_testing_command = ${platformio.build_dir}/${this.__env__}/meshtasticd -s + +; --------------------------------------------------------------------------- +; Native build for macOS (Darwin / arm64 + x86_64). Headless meshtasticd that +; runs in SimRadio mode (`-s`) or against real LoRa hardware via a CH341 +; USB-SPI bridge. No BlueZ, libgpiod, or Linux I2C — those require Linux. +; +; Prerequisites (Homebrew): +; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config +; +; The macOS-side patches now live upstream: +; * meshtastic/platform-native — `String.h`-shadow shim, `-Wno-enum-constexpr-conversion`, +; empty-variant-dir guard. Pulled via `portduino_base.platform` zip pin. +; * meshtastic/framework-portduino — LinuxHardwareI2C macOS stubs, AsyncUDP +; SOCK_NONBLOCK fallback, Common.h __APPLE__ guard, WiFiServer.cpp extern-C +; fix, package.json URL refresh. Pulled by platform-native at its pinned commit. +; This env therefore only carries the firmware-side build flags and src filter. +; --------------------------------------------------------------------------- +[env:native-macos] +extends = native_base +; Apple's ld doesn't accept GNU ld's `-Wl,-Map,` syntax (inherited from +; the top-level platformio.ini). Strip it; the linker map isn't useful for +; the macOS dev loop anyway, and Apple ld's equivalent (`-Wl,-map,`) +; uses different argument shape. +build_unflags = -Wl,-Map,"${platformio.build_dir}"/output.map +build_flags = ${portduino_base.build_flags_common} + -I variants/native/portduino + -I/opt/homebrew/include + -I/opt/homebrew/opt/argp-standalone/include + -I/opt/homebrew/opt/yaml-cpp/include + -L/opt/homebrew/lib + -L/opt/homebrew/opt/argp-standalone/lib + -L/opt/homebrew/opt/yaml-cpp/lib + -largp + -DPORTDUINO_DARWIN + ; Headless build — variants/native/portduino/variant.h would otherwise + ; default HAS_SCREEN to 1 and pull in screen-renderer source that uses + ; VLA-with-initializer (a GNU/GCC extension Apple Clang rejects). + ; MESHTASTIC_EXCLUDE_SCREEN gates the optional `screen->setHeading(...)`- + ; style screen-driver hooks scattered through sensor sources. + -DHAS_SCREEN=0 + -DMESHTASTIC_EXCLUDE_SCREEN=1 + !pkg-config --libs openssl --silence-errors || : +; src/input/Linux*.{cpp,h} drive evdev (``) which doesn't exist +; on macOS. graphics/Panel_sdl.* and graphics/TFTDisplay.cpp pull LovyanGFX +; (which we lib_ignore on macOS for the issue). Neither is needed +; for the headless build. +build_src_filter = ${native_base.build_src_filter} + - + - + - + - +; LovyanGFX includes (Linux-only) and is only needed by TFT +; variants — not relevant for the headless macOS build. +lib_ignore = + ${portduino_base.lib_ignore} + LovyanGFX