#include "CryptoEngine.h"
#include "HardwareRNG.h"
#include "PortduinoGPIO.h"
#include "SPIChip.h"
#include "mesh/RF95Interface.h"
#include "sleep.h"
#include "target_specific.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 <sys/ioctl.h>
#include <unistd.h>

#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 __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;

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;

static error_t parse_opt(int key, char *arg, struct argp_state *state)
{
    switch (key) {
    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;
}

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"},
                                           {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);
        }
#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
    }
}

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();

    if (portduino_config.force_simradio == true) {
        portduino_config.lora_module = use_simradio;
    } else if (configPath != nullptr) {
        if (loadConfig(configPath)) {
            if (!yamlOnly)
                std::cout << "Using " << configPath << " as config file" << std::endl;
        } else {
            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)
                std::cout << "Using local config.yaml as config file" << std::endl;
        } else {
            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)
                std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
        } else {
            std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl;
            exit(EXIT_FAILURE);
        }
    } else {
        if (!yamlOnly)
            std::cout << "No 'config.yaml' found..." << std::endl;
        portduino_config.lora_module = use_simradio;
    }

    if (portduino_config.config_directory != "") {
        std::string filetype = ".yaml";
        for (const std::filesystem::directory_entry &entry :
             std::filesystem::directory_iterator{portduino_config.config_directory}) {
            if (ends_with(entry.path().string(), ".yaml")) {
                std::cout << "Also using " << entry << " as additional config file" << std::endl;
                loadConfig(entry.path().c_str());
            }
        }
    }

    if (yamlOnly) {
        std::cout << portduino_config.emit_yaml() << std::endl;
        exit(EXIT_SUCCESS);
    }

    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'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
}

bool loadConfig(const char *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"]) {
                for (const auto &loraModule : portduino_config.loraModules) {
                    if (yamlConfig["Lora"]["Module"].as<std::string>("") == loraModule.second) {
                        portduino_config.lora_module = loraModule.first;
                        break;
                    }
                }
            }
            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"]["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.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 (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>("");

            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>("") != "") {
                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) {
        std::cout << "*** Exception " << e.what() << std::endl;
        return false;
    }
    return true;
}

// 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
    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;
}

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;
    }
}
