#include "config.h" void listDir(fs::FS &fs, const char *dirname, uint8_t levels) { Serial.printf("Listing directory: %s\n", dirname); File root = fs.open(dirname); if (!root) { Serial.println("Failed to open directory"); return; } if (!root.isDirectory()) { Serial.println("Not a directory"); return; } File file = root.openNextFile(); while (file) { if (file.isDirectory()) { Serial.print(" DIR : "); Serial.println(file.name()); if (levels) { listDir(fs, file.path(), levels - 1); } } else { Serial.print(" FILE: "); Serial.print(file.name()); Serial.print(" SIZE: "); Serial.println(file.size()); } file = root.openNextFile(); } } #define LORA_CONFIG "/lora_config.txt" Config Config::init() { if (SD.cardType() == sdcard_type_t::CARD_NONE) { Serial.println("No SD card found, will assume defaults"); return Config(); } File f = SD.open(LORA_CONFIG, FILE_READ); if (!f) { Serial.println("Listing root directory:"); listDir(SD, "/", 0); Serial.println("Config file " LORA_CONFIG " not found, will assume defaults"); Config cfg; if (cfg.create_missing_config) { cfg.write_config(LORA_CONFIG); } return cfg; } Config c = Config(); while (f.available() > 0) { String ln = f.readStringUntil('\n'); ParseResult r = parse_config_line(ln); if (r.error.length() > 0) { Serial.printf("%s in '%s', will assume defaults\n", r.error, ln); return Config(); } if (r.key.length() == 0) { // blank line or comment - skip continue; } // do something with known keys and values if (c.updateConfig(r.key, r.value)) { continue; } Serial.printf("Unknown key '%s' will be ignored\n", r.key); } f.close(); return c; } #define UPDATE_BOOL(val, key, value) \ if (key.equalsIgnoreCase(#val)) \ { \ String v = value; \ bool p = v.equalsIgnoreCase("true"); \ if (!p && !v.equalsIgnoreCase("false")) \ { \ Serial.printf("Expected bool for '%s', found '%s' - ignoring\n", \ key.c_str(), value.c_str()); \ } \ else \ { \ val = p; \ } \ return true; \ } bool Config::updateConfig(String key, String value) { UPDATE_BOOL(print_profile_time, key, value); if (key.equalsIgnoreCase("log_data_json_interval")) { log_data_json_interval = value.toInt(); return true; } if (key.equalsIgnoreCase("listen_on_serial0")) { listen_on_serial0 = value; return true; } if (key.equalsIgnoreCase("listen_on_serial1")) { listen_on_serial1 = value; return true; } if (key.equalsIgnoreCase("listen_on_usb")) { listen_on_serial0 = value; return true; } if (key.equalsIgnoreCase("detection_strategy")) { configureDetectionStrategy(value); return true; } if (key.equalsIgnoreCase("rx_lora")) { rx_lora = configureLora(value); return true; } if (key.equalsIgnoreCase("tx_lora")) { tx_lora = configureLora(value); return true; } if (key.equalsIgnoreCase("uart0")) { uart0 = BusConfig::configure(value); return true; } if (key.equalsIgnoreCase("uart1")) { uart1 = BusConfig::configure(value); return true; } if (key.equalsIgnoreCase("spi1")) { spi1 = BusConfig::configure(value); return true; } if (key.equalsIgnoreCase("wire1")) { wire1 = BusConfig::configure(value); return true; } if (key.equalsIgnoreCase("radio2")) { radio2 = RadioModuleSPIConfig::configure(value); return true; } UPDATE_BOOL(is_host, key, value); UPDATE_BOOL(lora_enabled, key, value); return false; } String loraConfigToStr(LoRaConfig *cfg) { if (cfg == NULL) { return String("none"); } return String("freq:") + String(cfg->freq) + String(",bw:") + String(cfg->bw) + String(",sf:") + String(cfg->sf) + String(",cr:") + String(cfg->cr) + String(",tx_power:") + String(cfg->tx_power) + String(",preamble_len:") + String(cfg->preamble_len) + String(",sync_word:") + String(cfg->sync_word, 16) + String(",crc:") + String(cfg->crc ? "1" : "0") + String(",crc_seed:") + String(cfg->crc_seed, 16) + String(",crc_poly:") + String(cfg->crc_poly, 16) + String(",implicit_header:") + String(cfg->implicit_header); } String detectionStrategyToStr(Config &c) { String res = c.detection_strategy; if (c.scan_ranges_sz > 0) { res += ":"; for (int i = 0; i < c.scan_ranges_sz; i++) { if (i > 0) { res += ","; } res += String(c.scan_ranges[i].start_khz); int s = c.scan_ranges[i].end_khz - c.scan_ranges[i].start_khz; if (s > 0) { res += ".." + String(c.scan_ranges[i].end_khz); if (c.scan_ranges[i].step_khz < s) { res += "+" + String(c.scan_ranges[i].step_khz); } } } } return res; } // findSepa looks for a sepa in s from position begin, and does two things: // updates end with the index of the end of the string between begin and // separator or end of string, and returns index of the next search position // or -1 to stop the search. // // So you can do something like this: // for (int i = 0, j = 0, k = 0; (i = findSepa(s, sepa, j, k)) >= 0; j = i) // ...s.substring(j, k) int findSepa(String s, String sepa, int begin, int &end) { int i = s.indexOf(sepa, begin); if (i < 0) { end = s.length(); return begin == end ? -1 : end; } end = i; return i + sepa.length(); } uint64_t fromHex(String s) { uint64_t r = 0; for (char c : s) { uint64_t v; if (c >= 'A' && c <= 'F') { v = c - 'A' + 10; } else if (c >= 'a' && c <= 'f') { v = c - 'a' + 10; } else { v = c - '0'; } r = (r << 4) + v; } return r; } uint64_t toUint64(String s) { String v = s.substring(0, s.length() % 15); s = s.substring(v.length()); uint64_t r = v.toInt(); while (s.length() > 0) { r = r * ((uint64_t)1000000000000000ll) + s.substring(0, 15).toInt(); s = s.substring(15); } return r; } ScanRange parseScanRange(String &cfg, int &begin) { ScanRange res; int end; int i = findSepa(cfg, ",", begin, end); String r = cfg.substring(begin, end); begin = i; if (i < 0) { res.start_khz = -1; res.end_khz = -1; res.step_khz = -1; return res; } i = r.indexOf(".."); if (i < 0) { i = r.length(); } res.start_khz = toUint64(r.substring(0, i)); if (i == r.length()) { res.end_khz = res.start_khz; res.step_khz = 0; return res; } r = r.substring(i + 2); i = r.indexOf("+"); if (i < 0) { i = r.indexOf("/"); if (i < 0) i = r.length(); } res.end_khz = toUint64(r.substring(0, i)); if (i == r.length()) { res.step_khz = res.end_khz - res.start_khz; } else { res.step_khz = toUint64(r.substring(i + 1)); if (r.charAt(i) == '/') { // then it is not a literal increment, it is the number of steps if (res.step_khz == 0) res.step_khz = 1; res.step_khz = round(((double)(res.end_khz - res.start_khz)) / res.step_khz); } } return res; } LoRaConfig *configureLora(String cfg) { if (cfg.equalsIgnoreCase("none")) { return NULL; } LoRaConfig *lora = new LoRaConfig({ freq : 0, bw : 500, sf : DEFAULT_LORA_SF, cr : 5, tx_power : DEFAULT_LORA_TX_POWER, preamble_len : 8, sync_word : 0x1e, crc : false, crc_seed : 0, crc_poly : 0x1021, implicit_header : 0 }); int begin = 0; int end, i; while ((i = findSepa(cfg, ",", begin, end)) >= 0) { String param = cfg.substring(begin, end); begin = i; int j = param.indexOf(":"); if (j < 0) { Serial.printf("Expected ':' to be present in '%s' - ignoring config\n", param); continue; } String k = param.substring(0, j); param = param.substring(j + 1); if (k.equalsIgnoreCase("sync_word")) { lora->sync_word = (uint8_t)fromHex(param); continue; } if (k.equalsIgnoreCase("crc_seed")) { lora->crc_seed = (uint16_t)fromHex(param); continue; } if (k.equalsIgnoreCase("crc_poly")) { lora->crc_poly = (uint16_t)fromHex(param); continue; } if (k.equalsIgnoreCase("freq")) { lora->freq = param.toFloat(); continue; } int v = param.toInt(); if (k.equalsIgnoreCase("bw")) { lora->bw = (uint16_t)v; continue; } if (k.equalsIgnoreCase("sf")) { lora->sf = (uint8_t)v; continue; } if (k.equalsIgnoreCase("cr")) { lora->cr = (uint8_t)v; continue; } if (k.equalsIgnoreCase("tx_power")) { lora->tx_power = (uint8_t)v; continue; } if (k.equalsIgnoreCase("preamble_len")) { lora->preamble_len = (uint8_t)v; continue; } if (k.equalsIgnoreCase("crc")) { lora->crc = v != 0; continue; } if (k.equalsIgnoreCase("implicit_header")) { lora->implicit_header = (uint8_t)v; continue; } Serial.printf("Unknown key '%s' will be ignored\n", k); } return lora; } void Config::configureDetectionStrategy(String cfg) { if (scan_ranges_sz > 0) delete[] scan_ranges; scan_ranges = NULL; String method = cfg; int i = cfg.indexOf(":"); if (i >= 0) { method = cfg.substring(0, i); cfg = cfg.substring(i + 1); } else { cfg = ""; } samples = 0; i = method.indexOf(","); if (i >= 0) { samples = method.substring(i + 1).toInt(); method = method.substring(0, i); } detection_strategy = method; scan_ranges_sz = 0; for (int i = 0, k = 0; (i = findSepa(cfg, ",", i, k)) >= 0; scan_ranges_sz++) ; if (scan_ranges_sz == 0) { return; } scan_ranges = new ScanRange[scan_ranges_sz]; for (int i = 0, j = 0; i < scan_ranges_sz; i++) { scan_ranges[i] = parseScanRange(cfg, j); } } RadioModuleSPIConfig RadioModuleSPIConfig::configure(String cfg) { RadioModuleSPIConfig c; c.bus_num = 1; c.cs = RADIO_MODULE_CS_PIN; c.rst = RADIO_MODULE_RST_PIN; c.dio1 = RADIO_MODULE_DIO1_PIN; c.busy = RADIO_MODULE_BUSY_PIN; c.clock_freq = RADIO_MODULE_CLOCK_FREQ; c.msb_first = RADIO_MODULE_MSB_FIRST; c.spi_mode = RADIO_MODULE_SPI_MODE; int begin = 0; int end, i; while ((i = findSepa(cfg, ",", begin, end)) >= 0) { String param = cfg.substring(begin, end); begin = i; int j = param.indexOf(":"); if (j < 0) { c.module = param; c.enabled = !param.equalsIgnoreCase("none"); continue; } String k = param.substring(0, j); param = param.substring(j + 1); k.toLowerCase(); if (k.equals("bus")) { c.bus_num = param.toInt(); continue; } if (k.equals("rst")) { c.rst = param.toInt(); continue; } if (k.equals("dio1")) { c.dio1 = param.toInt(); continue; } if (k.equals("busy")) { c.busy = param.toInt(); continue; } if (k.equals("freq")) { c.clock_freq = param.toInt(); continue; } if (k.equals("msb")) { c.msb_first = !!param.toInt(); continue; } if (k.equals("spi_mode")) { c.spi_mode = param.toInt(); continue; } c.module = k; c.cs = param.toInt(); c.enabled = true; } return c; } String RadioModuleSPIConfig::toStr() { if (!enabled) { return "none"; } return module + ":" + String(cs) + ",bus:" + String(bus_num) + ",rst:" + String(rst) + ",dio1:" + String(dio1) + ",busy:" + String(busy) + ",freq:" + String(clock_freq) + ",msb:" + String(msb_first ? 1 : 0) + ",spi_mode:" + String(spi_mode); } bool Config::write_config(const char *path) { File f = SD.open(path, FILE_WRITE, /*create = */ true); if (!f) { return false; } f.println("print_profile_time = " + getConfig("print_profile_time")); f.println("log_data_json_interval = " + getConfig("log_data_json_interval")); f.println("listen_on_serial0 = " + getConfig("listen_on_serial0")); f.println("listen_on_serial1 = " + getConfig("listen_on_serial1")); f.println("listen_on_usb = " + getConfig("listen_on_usb")); f.println("detection_strategy = " + getConfig("detection_strategy")); f.println("rx_lora = " + getConfig("rx_lora")); f.println("tx_lora = " + getConfig("tx_lora")); f.println("is_host = " + getConfig("is_host")); f.println("lora_enabled = " + getConfig("lora_enabled")); f.println("uart0 = " + getConfig("uart0")); f.println("uart1 = " + getConfig("uart1")); f.println("spi1 = " + getConfig("spi1")); f.println("wire1 = " + getConfig("wire1")); f.println("radio2 = " + getConfig("radio2")); f.close(); return true; } String Config::getConfig(String key) { if (key.equalsIgnoreCase("print_profile_time")) { return String(print_profile_time ? "true" : "false"); } if (key.equalsIgnoreCase("log_data_json_interval")) { return String(log_data_json_interval); } if (key.equalsIgnoreCase("listen_on_serial0")) { return listen_on_serial0; } if (key.equalsIgnoreCase("listen_on_serial1")) { return listen_on_serial1; } if (key.equalsIgnoreCase("listen_on_usb")) { return listen_on_usb; } // Example: SET detection_strategy RSSI_MAX 900000..950000+390 if (key.equalsIgnoreCase("detection_strategy")) { return detectionStrategyToStr(*this); } if (key.equalsIgnoreCase("rx_lora")) { return loraConfigToStr(rx_lora); } if (key.equalsIgnoreCase("tx_lora")) { return loraConfigToStr(tx_lora); } if (key.equalsIgnoreCase("is_host")) { return String(is_host ? "true" : "false"); } if (key.equalsIgnoreCase("uart0")) { return uart0.toStr(); } if (key.equalsIgnoreCase("uart1")) { return uart1.toStr(); } if (key.equalsIgnoreCase("spi1")) { return spi1.toStr(); } if (key.equalsIgnoreCase("wire1")) { return wire1.toStr(); } if (key.equalsIgnoreCase("radio2")) { return radio2.toStr(); } return ""; } ParseResult parse_config_line(String ln) { ln.trim(); if (ln.length() == 0 || ln.charAt(0) == '#') { // blank line or comment - skip return ParseResult(String(), String()); } int i = ln.indexOf("="); // ok, this must exist if (i < 0) { return ParseResult(String("Broken config: expected '='")); } String k = ln.substring(0, i); k.trim(); String v = ln.substring(i + 1); v.trim(); if (v.length() == 0 || v.charAt(0) == '#') { return ParseResult(String("Broken config: expected non-empty value")); } if (v.charAt(0) == '"') { // quoted strings get special treatment int i = v.indexOf('"', 1); while (i > 0) { if (v.length() == i + 1 || v.charAt(i + 1) != '"') break; v = v.substring(0, i + 1) + v.substring(i + 2); i = v.indexOf('"', i + 1); } if (i < 0) { return ParseResult(String("Broken config: expected closing quotes")); } String c = v.substring(i + 1); c.trim(); if (c.length() > 0 && c.charAt(0) != '#') { return ParseResult( String("Broken config: expected nothing but whitespace and " "comments after value")); } v = v.substring(1, i); } else { int i = v.indexOf('#'); if (i > 0) { v = v.substring(0, i); v.trim(); } } return ParseResult(k, v); } BusConfig BusConfig::configure(String cfg) { BusConfig c; if (cfg.equalsIgnoreCase("none")) { return c; } int begin = 0; int end, i; while ((i = findSepa(cfg, ",", begin, end)) >= 0) { String param = cfg.substring(begin, end); begin = i; int j = param.indexOf(":"); if (j < 0) { Serial.printf("Expected ':' to be present in '%s' - ignoring config\n", param); continue; } String k = param.substring(0, j); param = param.substring(j + 1); k.toLowerCase(); if (k.equals("enabled")) { c.enabled = !param.equalsIgnoreCase("false"); continue; } if (c.bus_type == SPI && k.equals("clk") || c.bus_type == WIRE && k.equals("scl")) { c.clk = param.toInt(); continue; } if (c.bus_type == SERIAL && k.equals("tx") || c.bus_type == SPI && k.equals("mosi") || c.bus_type == WIRE && k.equals("sda")) { c.tx = param.toInt(); continue; } if (c.bus_type == SERIAL && k.equals("rx") || c.bus_type == SPI && k.equals("miso")) { c.rx = param.toInt(); continue; } if (c.bus_type != NONE) { Serial.printf("Unknown key: '%s'; ignoring config\n", k.c_str()); continue; } if (k.equals("none") || k.length() == 0) { continue; } c.enabled = true; char bus_type = k.charAt(0); switch (bus_type) { case 'u': c.bus_type = UART; break; case 's': c.bus_type = SPI; break; case 'w': c.bus_type = WIRE; break; default: c.bus_type = NONE; c.enabled = false; } c.bus_num = k.substring(1).toInt(); c.clock_freq = param.toInt(); } return c; } String BusConfig::toStr() { if (bus_type == NONE) { return "none"; } String ret = String(bus_type == UART ? "u" : bus_type == SPI ? "s" : bus_type == WIRE ? "w" : "none") + String(bus_num); ret += ":" + String(clock_freq) + (enabled ? "" : ",enabled:false"); switch (bus_type) { case UART: ret += ",rx:" + String(rx) + ",tx:" + String(tx); break; case SPI: ret += ",clk:" + String(clk) + ",mosi:" + String(mosi) + ",miso:" + String(miso); break; case WIRE: ret += ",scl:" + String(scl) + ",sda:" + String(sda); break; default: ret = "none"; } return ret; }