#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 (r.key.equalsIgnoreCase("print_profile_time")) { String v = r.value; bool p = v.equalsIgnoreCase("true"); if (!p && !v.equalsIgnoreCase("false")) { Serial.printf("Expected bool for '%s', found '%s' - ignoring\n", r.key.c_str(), r.value.c_str()); } else { c.print_profile_time = p; } continue; } if (r.key.equalsIgnoreCase("log_data_json_interval")) { c.log_data_json_interval = r.value.toInt(); continue; } if (r.key.equalsIgnoreCase("listen_on_serial0")) { c.listen_on_serial0 = r.value; continue; } if (r.key.equalsIgnoreCase("listen_on_serial1")) { c.listen_on_serial1 = r.value; continue; } if (r.key.equalsIgnoreCase("listen_on_usb")) { c.listen_on_serial0 = r.value; continue; } if (r.key.equalsIgnoreCase("detection_strategy")) { c.configureDetectionStrategy(r.value); continue; } Serial.printf("Unknown key '%s' will be ignored\n", r.key); } f.close(); return c; } 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 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; } 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); } } 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 = " + String(print_profile_time ? "true" : "false")); f.println("log_data_json_interval = " + String(log_data_json_interval)); f.println("listen_on_serial0 = " + listen_on_serial0); f.println("listen_on_serial1 = " + listen_on_serial1); f.println("listen_on_usb = " + listen_on_usb); f.println("detection_strategy = " + detectionStrategyToStr(*this)); f.close(); return true; } 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); }