diff --git a/SpectrumScan.py b/SpectrumScan.py index fb17802..1bc0513 100644 --- a/SpectrumScan.py +++ b/SpectrumScan.py @@ -50,6 +50,7 @@ def parse_line(line): POLY = 0x1021 def crc16(s, c): + c = c ^ 0xffff for ch in s: c = c ^ (ord(ch) << 8) for i in range(8): @@ -58,7 +59,7 @@ def crc16(s, c): else: c = (c << 1) & 0xffff - return c + return c ^ 0xffff def main(): parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''\ diff --git a/lib/comms/comms.cpp b/lib/comms/comms.cpp index 9fd3c4e..53781b3 100644 --- a/lib/comms/comms.cpp +++ b/lib/comms/comms.cpp @@ -1,4 +1,3 @@ -#ifdef SERIAL_OUT #include "comms.h" #include @@ -7,7 +6,22 @@ #include #include -Comms *Comms0; +Comms *HostComms; +Comms *Comms0 = NULL; +Comms *Comms1 = NULL; + +RadioComms *RxComms = NULL; +RadioComms *TxComms = NULL; + +void _onReceiveUsb(size_t len) +{ + if (HostComms == NULL) + { + return; + } + + HostComms->_onReceive(); +} void _onReceive0() { @@ -19,54 +33,104 @@ void _onReceive0() Comms0->_onReceive(); } +void _onReceive1() +{ + if (Comms1 == NULL) + { + return; + } + + Comms1->_onReceive(); +} + +#if ARDUINO_USB_MODE +#define IF_CDC_EVENT(e, data) \ + arduino_hw_cdc_event_data_t *data = (arduino_hw_cdc_event_data_t *)event_data; \ + if (event_base == ARDUINO_HW_CDC_EVENTS && event_id == ARDUINO_HW_CDC_##e) +#else +#define IF_CDC_EVENT(e, data) \ + arduino_usb_cdc_event_data_t *data = (arduino_usb_cdc_event_data_t *)event_data; \ + if (event_base == ARDUINO_USB_CDC_EVENTS && event_id == ARDUINO_USB_CDC_##e) +#endif + void _onUsbEvent0(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { - if (event_base == ARDUINO_HW_CDC_EVENTS) - { - // arduino_hw_cdc_event_data_t *data = (arduino_hw_cdc_event_data_t *)event_data; - if (event_id == ARDUINO_HW_CDC_RX_EVENT) - { - _onReceive0(/*data->rx.len*/); - } - } + IF_CDC_EVENT(RX_EVENT, data) { _onReceiveUsb(data->rx.len); } } bool Comms::initComms(Config &c) { + bool fine = false; + +#ifdef ARDUINO_USB_CDC_ON_BOOT if (c.listen_on_usb.equalsIgnoreCase("readline")) { // comms using readline plaintext protocol - Comms0 = new ReadlineComms(Serial); + HostComms = new ReadlineComms("Host", Serial); +#if ARDUINO_USB_MODE + // if Serial is HWCDC... Serial.onEvent(ARDUINO_HW_CDC_RX_EVENT, _onUsbEvent0); +#else + // if Serial is USBCDC... + Serial.onEvent(ARDUINO_USB_CDC_RX_EVENT, _onUsbEvent0); +#endif Serial.begin(); Serial.println("Initialized communications on Serial using readline protocol"); - return true; + fine = true; } - else if (c.listen_on_serial0.equalsIgnoreCase("readline")) +#endif + + if (c.listen_on_serial0.equalsIgnoreCase("readline")) { // comms using readline plaintext protocol - Comms0 = new ReadlineComms(Serial0); - Serial0.onReceive(_onReceive0, false); + Comms0 = new ReadlineComms("UART0", SERIAL0); + SERIAL0.onReceive(_onReceive0, false); + SERIAL0.begin(115200); Serial.println("Initialized communications on Serial0 using readline protocol"); - - return true; } - - if (c.listen_on_serial0.equalsIgnoreCase("none")) + else { Comms0 = new NoopComms(); - Serial.println("Configured none - Initialized no communications"); - return false; + Serial.println("Configured none - Initialized no communications on Serial0"); } - Comms0 = new NoopComms(); - Serial.println("Nothing is configured - initialized no communications"); - return false; + if (c.listen_on_serial1.equalsIgnoreCase("readline")) + { + // comms using readline plaintext protocol + Comms1 = new ReadlineComms("UART1", Serial1); + Serial1.onReceive(_onReceive1, false); + Serial1.begin(115200); + + Serial.println("Initialized communications on Serial1 using readline protocol"); + } + else + { + Comms1 = new NoopComms(); + + Serial.println("Configured none - Initialized no communications on Serial1"); + } + + if (c.rx_lora != NULL) + { + RxComms = new RadioComms("RxComms", radio, *c.rx_lora); + } + + if (c.tx_lora != NULL) + { + TxComms = new RadioComms("TxComms", radio, *c.tx_lora); + } + + if (!fine) + { + HostComms = new NoopComms(); + Serial.println("Nothing is configured - initialized no communications"); + } + return fine; } size_t Comms::available() { return received_pos; } @@ -123,6 +187,7 @@ String _wrap_str(String); #define POLY 0x1021 uint16_t crc16(String v, uint16_t c) { + c ^= 0xffff; for (int i = 0; i < v.length(); i++) { uint16_t ch = v.charAt(i); @@ -140,7 +205,7 @@ uint16_t crc16(String v, uint16_t c) } } - return c; + return c ^ 0xffff; } void ReadlineComms::_onReceive() @@ -176,6 +241,10 @@ void ReadlineComms::_onReceive() delete m; } } + else + { + Serial.println(name + ": discarding > " + pack); + } partialPacket = partialPacket.substring(i + 1); i = partialPacket.indexOf('\n'); } @@ -190,16 +259,44 @@ bool ReadlineComms::send(Message &m) { case MessageType::SCAN: p = _scan_str(m.payload.scan); + Serial.println(name + ": the message is: " + p); break; case MessageType::SCAN_RESULT: p = _scan_result_str(m.payload.dump); break; + case MessageType::CONFIG_TASK: + p = m.payload.config.is_set ? "SET " : "GET "; + p += *m.payload.config.key; + if (m.payload.config.is_set) + { + p += " " + *m.payload.config.value; + } + break; } serial.print(_wrap_str(p)); return true; } +String _stringParam(String &p, String default_v) +{ + p.trim(); + int i = p.indexOf(' '); + if (i < 0) + { + i = p.length(); + } + + String v = p.substring(0, i); + p = p.substring(i + 1); + + if (i == 0) + { + v = default_v; + } + return v; +} + int64_t _intParam(String &p, int64_t default_v) { p.trim(); @@ -267,7 +364,27 @@ Message *_parsePacket(String p) return m; } - Serial.println("ignoring unknown message " + p); + if (cmd.equalsIgnoreCase("get")) + { + Message *m = new Message(); + m->type = MessageType::CONFIG_TASK; + m->payload.config.is_set = false; + m->payload.config.key = new String(_stringParam(p, "")); + m->payload.config.value = NULL; + return m; + } + + if (cmd.equalsIgnoreCase("set")) + { + Message *m = new Message(); + m->type = MessageType::CONFIG_TASK; + m->payload.config.is_set = true; + m->payload.config.key = new String(_stringParam(p, "")); + m->payload.config.value = new String(_stringParam(p, "")); + + return m; + } + return NULL; } @@ -294,4 +411,30 @@ String _wrap_str(String v) String r = String(v.length()) + "\n" + v; return "WRAP " + String(crc16(r, 0), 16) + " " + r; } -#endif + +Message::~Message() +{ + if (type == SCAN_RESULT) + { + if (payload.dump.sz > 0) + { + delete[] payload.dump.freqs_khz; + delete[] payload.dump.rssis; + payload.dump.sz = 0; + } + + return; + } + + if (type == CONFIG_TASK) + { + delete payload.config.key; + + if (payload.config.is_set) + { + delete payload.config.value; + } + + return; + } +} diff --git a/lib/comms/comms.h b/lib/comms/comms.h index 0cf61da..b852a06 100644 --- a/lib/comms/comms.h +++ b/lib/comms/comms.h @@ -1,16 +1,25 @@ #ifndef __COMMS_H #define __COMMS_H -#ifdef SERIAL_OUT #include +#include + +#include #include +#ifndef ARDUINO_USB_CDC_ON_BOOT +#define SERIAL0 Serial +#else +#define SERIAL0 Serial0 +#endif + enum MessageType { WRAP = 0, SCAN, SCAN_RESULT, - _MAX_MESSAGE_TYPE = SCAN_RESULT + CONFIG_TASK, + _MAX_MESSAGE_TYPE = CONFIG_TASK }; struct Wrapper @@ -32,19 +41,30 @@ struct ScanTaskResult int16_t *rssis; }; +struct ConfigTask +{ + String *key; + String *value; + bool is_set; +}; + struct Message { MessageType type; union { Wrapper wrap; + ConfigTask config; ScanTask scan; ScanTaskResult dump; } payload; + + ~Message(); }; struct Comms { + String name; Stream &serial; Message **received; size_t received_sz; @@ -52,8 +72,9 @@ struct Comms Message *wrap; - Comms(Stream &serial) - : serial(serial), received(NULL), received_sz(0), received_pos(0), wrap(NULL) {}; + Comms(String name, Stream &serial) + : name(name), serial(serial), received(NULL), received_sz(0), received_pos(0), + wrap(NULL) {}; virtual size_t available(); virtual bool send(Message &) = 0; @@ -67,7 +88,7 @@ struct Comms struct NoopComms : Comms { - NoopComms() : Comms(Serial0) {}; + NoopComms() : Comms("no-op", SERIAL0) {}; virtual bool send(Message &) { return true; }; virtual void _onReceive() {}; @@ -77,14 +98,39 @@ struct ReadlineComms : Comms { String partialPacket; - ReadlineComms(Stream &serial) : Comms(serial), partialPacket("") {}; + ReadlineComms(String name, Stream &serial) + : Comms(name, serial), partialPacket("") {}; virtual bool send(Message &) override; virtual void _onReceive() override; }; +extern Comms *HostComms; + extern Comms *Comms0; -#endif +extern Comms *Comms1; + +struct RadioComms +{ + String name; + RADIO_TYPE &radio; + LoRaConfig &loraCfg; + + RadioComms(String name, RADIO_TYPE &radio, LoRaConfig &cfg) + : name(name), radio(radio), loraCfg(cfg) + { + } + + Message **received; + + int16_t configureRadio(); + int16_t send(Message &); + Message *receive(uint16_t timeout_ms); +}; + +extern RadioComms *RxComms; +extern RadioComms *TxComms; + #endif diff --git a/lib/comms/radio_comms.cpp b/lib/comms/radio_comms.cpp new file mode 100644 index 0000000..fa269b6 --- /dev/null +++ b/lib/comms/radio_comms.cpp @@ -0,0 +1,238 @@ +#include "comms.h" + +int16_t RadioComms::configureRadio() +{ + int16_t status = + radio.begin(loraCfg.freq, loraCfg.bw, loraCfg.sf, loraCfg.cr, loraCfg.sync_word, + loraCfg.tx_power, loraCfg.preamble_len); + if (status != RADIOLIB_ERR_NONE) + { + Serial.printf("could not configure Lora: %d\n", status); + return status; + } + + if (!loraCfg.crc) + { + status = radio.setCRC(0); + if (status != RADIOLIB_ERR_NONE) + { + Serial.printf("could not configure CRC for Lora: %d\n", status); + return status; + } + } + + return status; +} + +size_t _write(uint8_t *m, size_t sz, size_t p, uint8_t v) +{ + if (p >= sz) + return sz; + m[p] = v; + return p + 1; +} + +size_t _write(uint8_t *m, size_t sz, size_t p, uint8_t *v, size_t v_sz) +{ + while (v_sz > 0) + { + p = _write(m, sz, p, *v); + v++; + v_sz--; + } + + return p; +} + +#define MAX_MSG 128 +#define RSSI_HI -80 +#define DETAIL_RSSIS 8 +#define RSSI_LO (RSSI_HI - 1 - DETAIL_RSSIS) +int16_t RadioComms::send(Message &m) +{ + if (m.type != SCAN_RESULT) + { + return RADIOLIB_ERR_INVALID_FUNCTION; + } + + uint8_t msg[MAX_MSG]; + size_t dump_sz = m.payload.dump.sz; + size_t p = _write(msg, MAX_MSG, 0, (uint8_t)m.type); + + // first cut: dump the RSSI as-is + // optimize the message size later + p = _write(msg, MAX_MSG, p, (uint8_t *)&m.payload.dump.freqs_khz[0], 4); + p = _write(msg, MAX_MSG, p, (uint8_t *)&m.payload.dump.freqs_khz[dump_sz - 1], 4); + p = _write(msg, MAX_MSG, p, (uint8_t *)&dump_sz, 2); + + size_t rem = MAX_MSG - p; + if (rem > dump_sz) + rem = dump_sz; + + uint8_t bits = 0; + size_t pp = p; + for (int i = 0; i < dump_sz; i++) + { + if (i * rem / dump_sz > p - pp) + { + p = _write(msg, MAX_MSG, p, bits); + bits = 0; + } + int16_t v = m.payload.dump.rssis[i]; + if (v >= 0) + { + v = 255; + } + else + { + v += 255; + if (v < 0) + { + v = 0; + } + } + + bits = max(bits, (uint8_t)v); + } + + if (dump_sz > 0) + { + p = _write(msg, MAX_MSG, p, bits); + } + + return radio.transmit(msg, p); +} + +size_t _read(uint8_t *buf, size_t sz, size_t p, uint8_t *v, size_t len) +{ + for (; p < sz && len > 0; v++, p++, len--) + { + *v = buf[p]; + } + + return p; +} + +size_t _read(uint8_t *buf, size_t sz, size_t p, uint8_t *v) +{ + return _read(buf, sz, p, v, 1); +} + +volatile bool _received = false; +void _rcv() { _received = true; } + +Message *RadioComms::receive(uint16_t timeout_ms) +{ + uint8_t msg[MAX_MSG]; + +#ifdef USING_LR1121 + Message *message = NULL; +#warning Radio Comms not fully supported for LR1121 +#else + // because of this, receive is single-threaded, single-device + _received = false; + radio.setDio1Action(_rcv); + uint32_t timeout_ticks = (uint32_t)timeout_ms * (1000000 / 15625); + + int16_t status = radio.startReceive(timeout_ticks); + if (status != RADIOLIB_ERR_NONE) + { + radio.clearDio1Action(); + Serial.printf("Failed to start receive: %d\n", status); + return NULL; + } + + // wait on a semaphore + while (!_received) + { + yield(); + } + radio.clearDio1Action(); + + size_t len = radio.getPacketLength(true); + uint8_t *packet = msg; + + if (len > MAX_MSG) + { + packet = new uint8_t[len]; + } + status = radio.readData(packet, len); + + if (status == RADIOLIB_ERR_RX_TIMEOUT) + { + return NULL; + } + + if (status != RADIOLIB_ERR_NONE || len == 0) + { + if (packet != msg) + delete[] packet; + + Serial.printf("Failed to read data: E(%d), len == %d\n", status, len); + return NULL; + } + + if (len > MAX_MSG) + { + Serial.printf("Packet is longer than expected: %d\n", len); + } + + size_t p; + uint8_t b; + p = _read(packet, len, 0, &b); + + Message *message = NULL; + if (b == SCAN_RESULT) + { + message = new Message(); + message->type = SCAN_RESULT; + + uint32_t s, e; + size_t dump_sz = 0; + p = _read(packet, len, p, (uint8_t *)&s, 4); + p = _read(packet, len, p, (uint8_t *)&e, 4); + p = _read(packet, len, p, (uint8_t *)&dump_sz, 2); + size_t rem = len - p; + + message->payload.dump.sz = dump_sz; + if (dump_sz > 0) + { + message->payload.dump.rssis = new int16_t[dump_sz]; + message->payload.dump.freqs_khz = new uint32_t[dump_sz]; + message->payload.dump.freqs_khz[0] = s; + message->payload.dump.freqs_khz[dump_sz - 1] = e; + + for (int i = 1; i < dump_sz - 1; i++) + { + uint32_t incr = (e - s) / (dump_sz - i); + s += incr; + message->payload.dump.freqs_khz[i] = s; + } + + for (int i = 0, k = 0; i < rem; i++) + { + int j = (i + 1) * dump_sz / rem; + int16_t rssi = 0; + p = _read(packet, len, p, (uint8_t *)&rssi); + rssi -= 255; + for (; k < j; k++) + { + message->payload.dump.rssis[k] = rssi; + } + } + } + } + else + { + Serial.printf("Received message type %" PRIu8 ", length %d, but expecting %" PRIu8 + " - ignoring\n", + b, len, (uint8_t)MessageType::SCAN_RESULT); + } + + if (packet != msg) + { + delete[] packet; + } +#endif + return message; +} diff --git a/lib/config/config.cpp b/lib/config/config.cpp index 879174d..2b2ec69 100644 --- a/lib/config/config.cpp +++ b/lib/config/config.cpp @@ -83,38 +83,20 @@ Config Config::init() } // do something with known keys and values - - if (r.key.equalsIgnoreCase("print_profile_time")) + if (c.updateConfig(r.key, r.value)) { - 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")) + if (r.key.equalsIgnoreCase("rx_lora")) { - c.log_data_json_interval = r.value.toInt(); + c.rx_lora = configureLora(r.value); continue; } - if (r.key.equalsIgnoreCase("listen_on_serial0")) + if (r.key.equalsIgnoreCase("tx_lora")) { - c.listen_on_serial0 = r.value; - continue; - } - - if (r.key.equalsIgnoreCase("listen_on_usb")) - { - c.listen_on_serial0 = r.value; + c.tx_lora = configureLora(r.value); continue; } @@ -125,6 +107,391 @@ Config Config::init() return c; } +bool Config::updateConfig(String key, String value) +{ + if (key.equalsIgnoreCase("print_profile_time")) + { + 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 + { + print_profile_time = p; + } + return true; + } + + 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("is_host")) + { + 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 + { + is_host = p; + } + return true; + } + + 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(",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 : 7, + cr : 5, + tx_power : 1, + preamble_len : 8, + sync_word : 0x1e, + crc : false, + 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("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); + } +} + bool Config::write_config(const char *path) { File f = SD.open(path, FILE_WRITE, /*create = */ true); @@ -133,15 +500,72 @@ bool Config::write_config(const char *path) 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_usb = " + listen_on_usb); + 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.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; + } + + 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"); + } + + return ""; +} + ParseResult parse_config_line(String ln) { ln.trim(); diff --git a/lib/config/config.h b/lib/config/config.h index 1b057f3..87136e8 100644 --- a/lib/config/config.h +++ b/lib/config/config.h @@ -3,22 +3,63 @@ #include +struct ScanRange +{ + uint64_t start_khz; + uint64_t end_khz; + uint64_t step_khz; +}; + +struct LoRaConfig +{ + float freq; + uint16_t bw; + uint8_t sf; + uint8_t cr; + uint8_t tx_power; + uint16_t preamble_len; + uint8_t sync_word; + bool crc; + uint8_t implicit_header; +}; + +LoRaConfig *configureLora(String cfg); + #define CREATE_MISSING_CONFIG true struct Config { bool create_missing_config; bool print_profile_time; + String detection_strategy; + int samples; + size_t scan_ranges_sz; + ScanRange *scan_ranges; int log_data_json_interval; String listen_on_serial0; + String listen_on_serial1; String listen_on_usb; + LoRaConfig *rx_lora; + LoRaConfig *tx_lora; + + bool is_host; Config() : create_missing_config(CREATE_MISSING_CONFIG), print_profile_time(false), - log_data_json_interval(1000), listen_on_serial0(String("none")), - listen_on_usb("readline") {}; + detection_strategy(String("RSSI")), samples(0), scan_ranges_sz(0), + scan_ranges(NULL), log_data_json_interval(1000), + listen_on_serial0(String("none")), listen_on_serial1(String("readline")), + listen_on_usb(String("readline")), rx_lora(NULL), tx_lora(NULL), + // Enable Lora Send: + // rx_lora(configureLora("freq:920")),tx_lora(configureLora("freq:916")) + is_host(false) {}; + bool write_config(const char *path); static Config init(); + bool updateConfig(String key, String value); + String getConfig(String key); + + void configureDetectionStrategy(String cfg); }; struct ParseResult diff --git a/lib/events/event_types.h b/lib/events/event_types.h new file mode 100644 index 0000000..725fdbc --- /dev/null +++ b/lib/events/event_types.h @@ -0,0 +1,13 @@ +#ifndef LORASA_EVENT_TYPES_H +#define LORASA_EVENT_TYPES_H + +struct Event; +enum EventType +{ + ALL_EVENTS = 0, // used only at registration time + DETECTED, + SCAN_TASK_COMPLETE, + _MAX_EVENT_TYPE = SCAN_TASK_COMPLETE // unused as event type +}; +struct Listener; +#endif diff --git a/lib/events/events.h b/lib/events/events.h index 329dea5..6f1dea0 100644 --- a/lib/events/events.h +++ b/lib/events/events.h @@ -1,17 +1,8 @@ #ifndef LORASA_EVENTS_H #define LORASA_EVENTS_H -struct Event; -enum EventType -{ - ALL_EVENTS = 0, // used only at registration time - DETECTED, - SCAN_TASK_COMPLETE, - _MAX_EVENT_TYPE = SCAN_TASK_COMPLETE // unused as event type -}; -struct Listener; - #include +#include #include struct Event diff --git a/include/LiLyGo.h b/lib/loraboards/LiLyGo.h similarity index 53% rename from include/LiLyGo.h rename to lib/loraboards/LiLyGo.h index 1d44603..103240b 100644 --- a/include/LiLyGo.h +++ b/lib/loraboards/LiLyGo.h @@ -1,4 +1,52 @@ +#ifndef __LILY_GO_H +#define __LILY_GO_H +#include "RadioLib.h" + +#ifdef HELTEC + +#ifdef HELTEC_NO_DISPLAY +#define HELTEC_NO_DISPLAY_INSTANCE +#else +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#include "SSD1306Wire.h" +#endif + +#define RADIO_TYPE SX1262 + +#ifndef HELTEC_NO_RADIO_INSTANCE +#ifndef ARDUINO_heltec_wifi_32_lora_V3 +// Assume MISO and MOSI being wrong when not using Heltec's board definition +// and use hspi to make it work anyway. See heltec_setup() for the actual SPI setup. +#include +extern SPIClass *hspi; +#define RADIO_MODULE_INIT() new Module(SS, DIO1, RST_LoRa, BUSY_LoRa, *hspi); +#else // ARDUINO_heltec_wifi_32_lora_V3 +#endif // end ARDUINO_heltec_wifi_32_lora_V3 +#endif // end HELTEC_NO_RADIO_INSTANCE + +extern RADIO_TYPE radio; + +#ifndef HELTEC_NO_DISPLAY_INSTANCE +#ifdef HELTEC_WIRELESS_STICK +#define DISPLAY_GEOMETRY GEOMETRY_64_32 +#else +#define DISPLAY_GEOMETRY GEOMETRY_128_64 +#endif +#define SCREEN_ADDRESS 0x3C + +#define DISPLAY_TYPE SSD1306Wire +#define DISPLAY_INIT() SSD1306Wire(SCREEN_ADDRESS, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY) +// SH1106Wire display(0x3c, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY); +#else +#define DISPLAY_TYPE void * +#endif + +// This file contains a binary patch for the SX1262 +#include "modules/SX126x/patches/SX126x_patch_scan.h" + +#else // Define for our code #define RST_OLED UNUSED_PIN #define LED BOARD_LED @@ -8,9 +56,7 @@ // (See RadioLib_convenience.h) #define RADIOLIB_DO_DURING_HALT heltec_delay(10) #include "RadioLib_convenience.h" -#ifdef HELTEC_NO_DISPLAY -#define HELTEC_NO_DISPLAY_INSTANCE -#else + #define DISPLAY_WIDTH 128 #define DISPLAY_HEIGHT 64 // #include "OLEDDisplayUi.h" @@ -18,41 +64,38 @@ // #include "SSD1306Brzo.h" #include "SSD1306Wire.h" -#endif -#define ARDUINO_heltec_wifi_32_lora_V3 -#ifndef HELTEC_NO_RADIO_INSTANCE -#ifndef ARDUINO_heltec_wifi_32_lora_V3 -// Assume MISO and MOSI being wrong when not using Heltec's board definition -// and use hspi to make it work anyway. See heltec_setup() for the actual SPI setup. -#include -SPIClass *hspi = new SPIClass(2); -SX1262 radio = new Module(SS, DIO1, RST_LoRa, BUSY_LoRa, *hspi); -#else // ARDUINO_heltec_wifi_32_lora_V3 #ifdef USING_SX1280PA -SX1280 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#define RADIO_TYPE SX1280 +#define RADIO_MODULE_INIT() \ + new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); #endif // end USING_SX1280PA #ifdef USING_SX1262 // Default SPI on pins from pins_arduino.h -SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#define RADIO_TYPE SX1262 +#define RADIO_MODULE_INIT() \ + new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN) #endif // end USING_SX1262 #ifdef USING_LR1121 // Default SPI on pins from pins_arduino.h -LR1121 radio = new Module(RADIO_CS_PIN, RADIO_DIO9_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#define RADIO_TYPE LR1121 +#define RADIO_MODULE_INIT() \ + new Module(RADIO_CS_PIN, RADIO_DIO9_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); #endif // end USING_LR1121 #ifdef USING_SX1276 // Default SPI on pins from pins_arduino.h -SX1276 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#define RADIO_TYPE SX1276 +#define RADIO_MODULE_INIT() \ + new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); #endif // end USING_SX1276 -#endif // end ARDUINO_heltec_wifi_32_lora_V3 -#endif // end HELTEC_NO_RADIO_INSTANCE -void heltec_led(int led) {} +extern RADIO_TYPE radio; -void heltec_deep_sleep() {} +void heltec_led(int led); -void heltec_delay(int millisec) { delay(millisec); } +void heltec_deep_sleep(int sleep_seconds = 0); + +void heltec_delay(int millisec); -#ifndef HELTEC_NO_DISPLAY_INSTANCE /** * @class PrintSplitter * @brief A class that splits the output of the Print class to two different @@ -82,85 +125,35 @@ class PrintSplitter : public Print Print &b; }; -#ifdef HELTEC_WIRELESS_STICK -#define DISPLAY_GEOMETRY GEOMETRY_64_32 -#else #define DISPLAY_GEOMETRY GEOMETRY_128_64 -#endif #define SCREEN_ADDRESS 0x3C -SSD1306Wire display(SCREEN_ADDRESS, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY); +#define DISPLAY_TYPE SSD1306Wire +#define DISPLAY_INIT() SSD1306Wire(SCREEN_ADDRESS, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY) // SH1106Wire display(0x3c, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY); -PrintSplitter both(Serial, display); -#else -Print &both = Serial; -#endif + +#define BOTH_TYPE PrintSplitter +#define BOTH_INIT() PrintSplitter(Serial, display) + +extern DISPLAY_TYPE display; +extern BOTH_TYPE both; + // some fake pin #ifdef T3_V1_6_SX1276 #define BUTTON_PIN 22 #endif -#define BUTTON BUTTON_PIN -#include "HotButton.h" -HotButton button(BUTTON); - -void heltec_loop() -{ -#ifndef DT3_V1_6_SX1276 - button.update(); -#endif -} // This file contains a binary patch for the SX1262 #include "modules/SX126x/patches/SX126x_patch_scan.h" -void heltec_display_power(bool on) -{ -#ifndef HELTEC_NO_DISPLAY_INSTANCE - if (on) - { -#ifdef HELTEC_WIRELESS_STICK - // They hooked the display to "external" power, and didn't tell anyone - heltec_ve(true); - delay(5); -#endif - pinMode(RST_OLED, OUTPUT); - digitalWrite(RST_OLED, HIGH); - delay(1); - digitalWrite(RST_OLED, LOW); - delay(20); - digitalWrite(RST_OLED, HIGH); - } - else - { -#ifdef HELTEC_WIRELESS_STICK - heltec_ve(false); -#else - display.displayOff(); -#endif - } -#endif -} -void heltec_setup() -{ - Serial.begin(115200); - Serial.println("LILYGO BOARD"); +#define BUTTON BUTTON_PIN +#include "HotButton.h" +extern HotButton button; -#if defined(ARDUINO_ARCH_ESP32) - SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN); -#elif defined(ARDUINO_ARCH_STM32) - SPI.setMISO(RADIO_MISO_PIN); - SPI.setMOSI(RADIO_MOSI_PIN); - SPI.setSCLK(RADIO_SCLK_PIN); - SPI.begin(); -#endif +void heltec_loop(); -#ifndef ARDUINO_heltec_wifi_32_lora_V3 - hspi->begin(SCK, MISO, MOSI, SS); +void heltec_display_power(bool on); + +void heltec_setup(); #endif -#ifndef HELTEC_NO_DISPLAY_INSTANCE - heltec_display_power(true); - display.init(); - // display.setContrast(200); - display.flipScreenVertically(); #endif -} diff --git a/lib/loraboards/LoRaBoards.cpp b/lib/loraboards/LoRaBoards.cpp index 3689a93..7353c99 100644 --- a/lib/loraboards/LoRaBoards.cpp +++ b/lib/loraboards/LoRaBoards.cpp @@ -12,6 +12,90 @@ #include "LoRaBoards.h" +#include "LiLyGo.h" + +// Implement stubs for functions that exist on Heltec, but not on LilyGo +void heltec_led(int led) {} + +void heltec_deep_sleep(int sleep_seconds) {} + +void heltec_delay(int millisec) { delay(millisec); } + +DISPLAY_TYPE display = DISPLAY_INIT(); +BOTH_TYPE both = BOTH_INIT(); + +HotButton button(BUTTON); + +void heltec_loop() +{ +#ifndef DT3_V1_6_SX1276 + button.update(); +#endif +} + +void heltec_display_power(bool on) +{ +#ifndef HELTEC_NO_DISPLAY_INSTANCE + if (on) + { +#ifdef HELTEC_WIRELESS_STICK + // They hooked the display to "external" power, and didn't tell anyone + heltec_ve(true); + delay(5); +#endif + pinMode(RST_OLED, OUTPUT); + digitalWrite(RST_OLED, HIGH); + delay(1); + digitalWrite(RST_OLED, LOW); + delay(20); + digitalWrite(RST_OLED, HIGH); + } + else + { +#ifdef HELTEC_WIRELESS_STICK + heltec_ve(false); +#else + display.displayOff(); +#endif + } +#endif +} + +void heltec_setup() +{ + Serial.begin(115200); + Serial.println("LILYGO BOARD"); + +#if defined(ARDUINO_ARCH_ESP32) + SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN); +#elif defined(ARDUINO_ARCH_STM32) + SPI.setMISO(RADIO_MISO_PIN); + SPI.setMOSI(RADIO_MOSI_PIN); + SPI.setSCLK(RADIO_SCLK_PIN); + SPI.begin(); +#endif + +#ifdef HELTEC +#ifndef ARDUINO_heltec_wifi_32_lora_V3 + hspi->begin(SCK, MISO, MOSI, SS); +#endif +#endif +#ifndef HELTEC_NO_DISPLAY_INSTANCE + heltec_display_power(true); + display.init(); + // display.setContrast(200); + display.flipScreenVertically(); +#endif +} + +#ifdef HELTEC +#ifndef ARDUINO_heltec_wifi_32_lora_V3 +SPIClass hspi = new SPIClass(2); +#endif +#endif + +RADIO_TYPE radio = RADIO_MODULE_INIT(); + #if defined(HAS_SDCARD) SPIClass SDCardSPI(HSPI); #endif diff --git a/lib/loraboards/LoRaBoards.h b/lib/loraboards/LoRaBoards.h index 655376e..811f209 100644 --- a/lib/loraboards/LoRaBoards.h +++ b/lib/loraboards/LoRaBoards.h @@ -7,6 +7,7 @@ * @last-update 2024-08-07 */ +#ifdef LILYGO #pragma once #include "utilities.h" @@ -95,3 +96,4 @@ extern SPIClass SDCardSPI; #elif defined(ARDUINO_ARCH_STM32) extern HardwareSerial SerialGPS; #endif +#endif diff --git a/lib/models/WaterfallModel.cpp b/lib/models/WaterfallModel.cpp index 99f7816..5f02972 100644 --- a/lib/models/WaterfallModel.cpp +++ b/lib/models/WaterfallModel.cpp @@ -1,4 +1,7 @@ #include "models.h" +#include +using namespace std; + #include WaterfallModel::WaterfallModel(size_t w, uint64_t base_dt, size_t m_sz, @@ -67,13 +70,13 @@ void WaterfallModel::reset(uint64_t t0, size_t w) * and it gets added to incomplete minute. This gets repeated for incomplete * minutes, etc. */ -size_t WaterfallModel::updateModel(uint16_t t, size_t x, uint16_t y) +size_t WaterfallModel::updateModel(uint64_t t, size_t x, uint16_t y) { size_t changed = 1; while (t > times[0]) { - changed = push(); + changed = max(changed, push()); } counts[0][x]++; diff --git a/lib/models/models.h b/lib/models/models.h index ce8de8f..0a09abd 100644 --- a/lib/models/models.h +++ b/lib/models/models.h @@ -18,7 +18,7 @@ struct WaterfallModel WaterfallModel(size_t w, uint64_t base_dt, size_t m_sz, const size_t *multiples); void reset(uint64_t t0, size_t width); - size_t updateModel(uint16_t t, size_t x, uint16_t y); + size_t updateModel(uint64_t t, size_t x, uint16_t y); size_t push(); char *toString(); diff --git a/lib/scan/scan.cpp b/lib/scan/scan.cpp index 89ff530..ea2f02d 100644 --- a/lib/scan/scan.cpp +++ b/lib/scan/scan.cpp @@ -4,9 +4,11 @@ #include "scan.h" #include #include +#include #include -uint16_t Scan::rssiMethod(size_t samples, uint16_t *result, size_t res_size) +uint16_t Scan::rssiMethod(float (*getRSSI)(void *), void *param, size_t samples, + uint16_t *result, size_t res_size) { float scale((float)res_size / (HI_RSSI_THRESHOLD - LO_RSSI_THRESHOLD + 0.1)); @@ -18,7 +20,7 @@ uint16_t Scan::rssiMethod(size_t samples, uint16_t *result, size_t res_size) // N of samples for (int r = 0; r < samples; r++) { - float rssi = getRSSI(); + float rssi = getRSSI(param); if (rssi < -65535) rssi = -65535; diff --git a/lib/scan/scan.h b/lib/scan/scan.h index c826609..6b8c28c 100644 --- a/lib/scan/scan.h +++ b/lib/scan/scan.h @@ -1,11 +1,11 @@ +#ifndef LORASA_SCAN_H +#define LORASA_SCAN_H + +#include #include -#include +#include #include -#ifndef LORASA_CORE_H - -#define LORASA_CORE_H - #ifdef PRINT_DEBUG #define LOG(args...) Serial.printf(args...) #define LOG_IF(cond, args...) \ @@ -34,6 +34,22 @@ constexpr float LO_RSSI_THRESHOLD = HI_RSSI_THRESHOLD - 66; #define SAMPLES_RSSI 20 #endif +struct ScanPage +{ + uint64_t start_mhz; + uint64_t end_mhz; + size_t page_sz; + ScanRange *scan_ranges; + + ~ScanPage() + { + if (page_sz > 0) + { + delete[] scan_ranges; + } + } +}; + struct Scan { uint64_t epoch; @@ -61,11 +77,10 @@ struct Scan }, comms_initialized(false) {}; - virtual float getRSSI() = 0; - // rssiMethod gets the data similar to the scan method, // but uses getRSSI directly. - uint16_t rssiMethod(size_t samples, uint16_t *result, size_t res_size); + uint16_t rssiMethod(float (*getRSSI)(void *), void *param, size_t samples, + uint16_t *result, size_t res_size); // detect method analyses result, and produces filtered_result, marking // those values that represent a detection event. diff --git a/platformio.ini b/platformio.ini index 5723634..6696f87 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,6 +34,8 @@ lib_deps = build_flags = -DHELTEC_POWER_BUTTON -DHELTEC + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 [env:heltec_wifi_lora_32_V3-OSD] platform = espressif32 diff --git a/spectrum_scan.c b/spectrum_scan.c index aa8c9c9..9c10771 100644 --- a/spectrum_scan.c +++ b/spectrum_scan.c @@ -13,6 +13,7 @@ typedef struct { #define POLY 0x1021 uint16_t crc16(char *p, char *end, uint16_t c) { + c ^= 0xffff; if (end == NULL) { end = strchr(p, 0); } @@ -29,7 +30,7 @@ uint16_t crc16(char *p, char *end, uint16_t c) { } } - return c; + return c ^ 0xffff; } #define BUFSIZE 102400 @@ -132,6 +133,8 @@ int main(int argc, char** argv) lines--; write(1, buffer, pos); } else if (!is_wrap) { + write(1, "> ", 2); + write(1, buffer, pos); errors++; } diff --git a/src/main.cpp b/src/main.cpp index 8ae8a1e..771d45d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,10 +55,8 @@ #define RADIOLIB_CHECK_PARAMS (0) #include -#ifdef SERIAL_OUT #include #include -#endif #include #include #include @@ -79,7 +77,7 @@ // #include "utilities.h" // Our Code -#include "LiLyGo.h" +#include #endif // end LILYGO #define BT_SCAN_DELAY 60 * 1 * 1000 @@ -88,6 +86,8 @@ long noDevicesMillis = 0, cycleCnt = 0; bool present = false; bool scanFinished = true; +bool radioIsScan = false; + // time to scan BT #define BT_SCAN_TIME 10 @@ -153,8 +153,12 @@ typedef enum // constexpr int RSSI_OUTPUT_FORMULA = 2; // Feature to scan diapasones. Other frequency settings will be ignored. -// int SCAN_RANGES[] = {850890, 920950}; -int SCAN_RANGES[] = {}; +// String SCAN_RANGES = String("850..890,920..950"); +String SCAN_RANGES = ""; + +size_t scan_pages_sz = 0; +ScanPage *scan_pages; +size_t scan_page = 0; // MHZ per page // to put everything into one page set RANGE_PER_PAGE = FREQ_END - 800 @@ -213,8 +217,6 @@ bool detected_y[STEPS]; // 20 - ??? steps // Used as a Led Light and Buzzer/count trigger bool first_run, new_pixel, detected_x = false; -// drone detection flag -bool detected = false; uint64_t drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL; #define TRIGGER_LEVEL -80.0 uint64_t drone_detected_frequency_start = 0; @@ -243,9 +245,8 @@ HardwareSerial SerialPort(SERIAL_PORT); // #define WEB_SERVER true -uint64_t x, y, range_item, w = WATERFALL_START, i = 0; +uint64_t x, y, w = WATERFALL_START, i = 0; int osd_x = 1, osd_y = 2, col = 0, max_bin = 32; -uint64_t ranges_count = 0; int rssi = 0; int state = 0; @@ -482,17 +483,11 @@ void osdProcess() } #endif -#ifdef SERIAL_OUT Config config; -#endif -struct RadioScan : Scan -{ - float getRSSI() override; -}; - -float RadioScan::getRSSI() +float getRSSI(void *param) { + Scan *r = (Scan *)param; #if defined(USING_SX1280PA) // radio.startReceive(); // get instantaneous RSSI value @@ -512,7 +507,26 @@ float RadioScan::getRSSI() #endif } -RadioScan r; +float getCAD(void *param) +{ + Scan *r = (Scan *)param; + + int16_t err = radio.scanChannel(); + if (err != RADIOLIB_ERR_NONE) + { + return -999; + } + +#ifdef USING_LR1121 + // LR1121 doesn't implement getRSSI(bool), getRSSI always + // returns RSSI of the last packet + return radio.getRSSI(); +#else + return radio.getRSSI(true); +#endif +} + +Scan r; #define WATERFALL_SENSITIVITY 0.05 DecoratedBarChart *bar; @@ -521,19 +535,68 @@ StackedChart stacked(display, 0, 0, 0, 0); UptimeClock *uptime; +int16_t initForScan(float freq) +{ + int16_t state; + +#if defined(USING_SX1280PA) + state = radio.beginGFSK(freq); +#elif defined(USING_LR1121) + state = radio.beginGFSK(freq, 4.8F, 5.0F, 156.2F, 10, 16U, 1.7F); +#else + state = radio.beginFSK(freq); +#endif + + return state; +} + +bool setFrequency(float curr_freq) +{ + r.current_frequency = curr_freq; + LOG("setFrequency:%f\n", r.current_frequency); + + int16_t state; +#ifdef USING_SX1280PA + int16_t state1 = + radio.setFrequency(r.current_frequency); // 1280 doesn't have calibration + + state = radio.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF); + if (state != RADIOLIB_ERR_NONE) + { + Serial.println("Error:startReceive:" + String(state)); + } + + state = state1; +#elif USING_SX1276 + state = radio.setFrequency(freq); +#else + state = radio.setFrequency(r.current_frequency, + true); // false = calibration is needed here +#endif + if (state != RADIOLIB_ERR_NONE) + { + display.drawString(0, 64 - 10, + "E(" + String(state) + + "):setFrequency:" + String(r.current_frequency)); + Serial.println("E(" + String(state) + + "):setFrequency:" + String(r.current_frequency)); + display.display(); + delay(2); + return false; + } + + return true; +} + void init_radio() { // initialize SX1262 FSK modem at the initial frequency both.println("Init radio"); -#if defined(USING_SX1280PA) - state = radio.beginGFSK(CONF_FREQ_BEGIN); -#elif defined(USING_LR1121) - state = radio.beginGFSK(CONF_FREQ_BEGIN, 4.8F, 5.0F, 156.2F, 10, 16U, 1.7F); -#else - state = radio.beginFSK(CONF_FREQ_BEGIN); -#endif + state = initForScan(CONF_FREQ_BEGIN); + if (state == RADIOLIB_ERR_NONE) { + radioIsScan = true; Serial.println(F("success!")); } else @@ -579,30 +642,13 @@ void init_radio() } both.println("Starting scanning..."); -// calibrate only once ,,, at startup -// TODO: check documentation (9.2.1) if we must calibrate in certain ranges -#ifdef USING_SX1280PA - state = radio.setFrequency(CONF_FREQ_BEGIN); - if (state != RADIOLIB_ERR_NONE) - { - Serial.println("Error:setFrequency:" + String(state)); - } - state = radio.startReceive(); - if (state != RADIOLIB_ERR_NONE) - { - Serial.println("Error:startReceive:" + String(state)); - } -#elif USING_SX1276 - // Sets carrier frequency. Allowed values range from 137.0 MHz to 1020.0 MHz. - radio.setFrequency(CONF_FREQ_BEGIN); -#else - radio.setFrequency(CONF_FREQ_BEGIN, true); -#endif + // calibrate only once ,,, at startup + // TODO: check documentation (9.2.1) if we must calibrate in certain ranges + setFrequency(CONF_FREQ_BEGIN); delay(50); } -#ifdef SERIAL_OUT struct frequency_scan_result { uint64_t begin; @@ -613,12 +659,10 @@ struct frequency_scan_result ScanTaskResult dump; size_t readings_sz; } frequency_scan_result; -#endif TaskHandle_t logToSerial = NULL; TaskHandle_t dumpToComms = NULL; -#ifdef SERIAL_OUT void eventListenerForReport(void *arg, Event &e) { if (e.type == EventType::DETECTED) @@ -686,6 +730,8 @@ ScanTask report_scans = ScanTask{ delay : 0 // 0 => as and when it happens; > 0 => at least once that many ms }; +bool requested_host = true; + void dumpToCommsTask(void *parameter) { uint64_t last_epoch = frequency_scan_result.last_epoch; @@ -712,10 +758,22 @@ void dumpToCommsTask(void *parameter) Message m; m.type = MessageType::SCAN_RESULT; m.payload.dump = frequency_scan_result.dump; - Comms0->send(m); + if (requested_host) + { + HostComms->send(m); + } + else + { + if (Comms0 != NULL) + Comms0->send(m); + if (Comms1 != NULL) + Comms1->send(m); + } + + m.payload.dump.sz = + 0; // dump is shared, so should not delete arrays in destructor } } -#endif #ifdef LOG_DATA_JSON void logToSerialTask(void *parameter) @@ -752,6 +810,119 @@ void logToSerialTask(void *parameter) void drone_sound_alarm(void *arg, Event &e); +void configurePages() +{ + if (scan_pages_sz > 0) + delete[] scan_pages; + + if (single_page_scan) + { + scan_pages_sz = 1; + ScanPage scan_page = { + start_mhz : CONF_FREQ_BEGIN, + end_mhz : CONF_FREQ_END, + page_sz : config.scan_ranges_sz + }; + if (scan_page.page_sz > 0) + { + scan_page.scan_ranges = new ScanRange[scan_page.page_sz]; + } + for (int i = 0; i < scan_page.page_sz; i++) + { + scan_page.scan_ranges[i] = config.scan_ranges[i]; + } + scan_pages = new ScanPage[1]{scan_page}; + scan_page.page_sz = + 0; // make sure it doesn't free up the Scanranges that were just constructed + } + else + { + scan_pages_sz = + (CONF_FREQ_END - CONF_FREQ_BEGIN + RANGE_PER_PAGE - 1) / RANGE_PER_PAGE; + scan_pages = new ScanPage[scan_pages_sz]; + for (int j = 0; j < scan_pages_sz; j++) + { + ScanPage scan_page = { + start_mhz : CONF_FREQ_BEGIN + j * RANGE_PER_PAGE, + end_mhz : CONF_FREQ_BEGIN + (j + 1) * RANGE_PER_PAGE, + page_sz : 0 + }; + for (int i = 0; i < config.scan_ranges_sz; i++) + { + if (config.scan_ranges[i].start_khz > scan_page.end_mhz * 1000 || + config.scan_ranges[i].end_khz < scan_page.start_mhz * 1000) + { + continue; + } + scan_page.page_sz++; + } + + if (scan_page.page_sz > 0) + { + scan_page.scan_ranges = new ScanRange[scan_page.page_sz]; + for (int i = 0, r = 0; i < config.scan_ranges_sz; i++) + { + if (config.scan_ranges[i].start_khz > scan_page.end_mhz * 1000 || + config.scan_ranges[i].end_khz < scan_page.start_mhz * 1000) + { + continue; + } + + scan_page.scan_ranges[r] = { + start_khz : max(config.scan_ranges[i].start_khz, + scan_page.start_mhz * 1000), + end_khz : + min(config.scan_ranges[i].end_khz, scan_page.end_mhz * 1000), + step_khz : config.scan_ranges[i].step_khz + }; + r++; + } + } + scan_pages[j] = scan_page; + scan_page.page_sz = + 0; // we copied over the values, make sure the array doesn't get freed + } + } +} + +void configureDetection() +{ + if (config.scan_ranges_sz == 0) + { + config.scan_ranges_sz = 1; + config.scan_ranges = new ScanRange[1]; + config.scan_ranges[0].start_khz = FREQ_BEGIN * 1000; + config.scan_ranges[0].end_khz = FREQ_END * 1000; + config.scan_ranges[0].step_khz = + (float)(FREQ_END - FREQ_BEGIN) * 1000 / (STEPS * SCAN_RBW_FACTOR); + } + + if (config.samples <= 0) + { + config.samples = SAMPLES_RSSI; + } + + CONF_SAMPLES = config.samples; + + CONF_FREQ_BEGIN = config.scan_ranges[0].start_khz / 1000; + CONF_FREQ_END = config.scan_ranges[0].end_khz / 1000; + for (int i = 0; i < config.scan_ranges_sz; i++) + { + CONF_FREQ_BEGIN = min(CONF_FREQ_BEGIN, config.scan_ranges[i].start_khz / 1000); + CONF_FREQ_END = max(CONF_FREQ_END, config.scan_ranges[i].end_khz / 1000); + } + + median_frequency = (CONF_FREQ_BEGIN + CONF_FREQ_END) / 2; + + samples = CONF_SAMPLES; + + RANGE_PER_PAGE = CONF_FREQ_END - CONF_FREQ_BEGIN; // FREQ_END - CONF_FREQ_BEGIN + RANGE = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); + range = RANGE; + + configurePages(); +} + void readConfigFile() { // writeFile(LittleFS, "/text.txt", "{WIFI:{name:\"sdfsdf\", @@ -777,27 +948,22 @@ void readConfigFile() smpls = readParameterFromParameterFile("samples"); Serial.println("SAMPLES: " + smpls); - CONF_SAMPLES = (smpls == "") ? samples : atoi(smpls.c_str()); - samples = CONF_SAMPLES; - CONF_FREQ_BEGIN = (fstart == "") ? FREQ_BEGIN : atoi(fstart.c_str()); - CONF_FREQ_END = (fend == "") ? FREQ_END : atoi(fend.c_str()); + String detection = String("RSSI"); + if (smpls.length() > 0) + detection += "," + smpls; + if (fstart.length() == 0) + fstart = String(FREQ_BEGIN * 1000); + if (fend.length() == 0) + fend = String(FREQ_END * 1000); + + detection += ":" + fstart + ".." + fend + "/" + String(STEPS * SCAN_RBW_FACTOR); + + config.configureDetectionStrategy(detection); + configureDetection(); both.println("C FREQ BEGIN:" + String(CONF_FREQ_BEGIN)); both.println("C FREQ END:" + String(CONF_FREQ_END)); both.println("C SAMPLES:" + String(CONF_SAMPLES)); - - RANGE_PER_PAGE = CONF_FREQ_END - CONF_FREQ_BEGIN; // FREQ_END - CONF_FREQ_BEGIN - - RANGE = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); - - SINGLE_STEP = (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)); - - range = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); - - iterations = RANGE / RANGE_PER_PAGE; - - // uint64_t range_frequency = FREQ_END - CONF_FREQ_BEGIN; - median_frequency = (CONF_FREQ_BEGIN + CONF_FREQ_END) / 2; } void setup(void) @@ -842,7 +1008,6 @@ void setup(void) bt_start = millis(); wf_start = millis(); -#ifdef SERIAL_OUT config = Config::init(); r.comms_initialized = Comms::initComms(config); if (r.comms_initialized) @@ -853,7 +1018,6 @@ void setup(void) { Serial.println("Comms did not initialize"); } -#endif pinMode(LED, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); @@ -913,24 +1077,19 @@ void setup(void) initLittleFS(); readConfigFile(); - #endif #ifndef WEB_SERVER - CONF_SAMPLES = samples; - CONF_FREQ_BEGIN = FREQ_BEGIN; - CONF_FREQ_END = FREQ_END; + if (config.scan_ranges_sz == 0 && SCAN_RANGES.length() > 0) + { + config.configureDetectionStrategy(config.detection_strategy + ":" + SCAN_RANGES); + } + + configureDetection(); both.println("FREQ BEGIN:" + String(CONF_FREQ_BEGIN)); both.println("FREQ END:" + String(CONF_FREQ_END)); both.println("SAMPLES:" + String(CONF_SAMPLES)); - - RANGE_PER_PAGE = CONF_FREQ_END - CONF_FREQ_BEGIN; // FREQ_END - CONF_FREQ_BEGIN - RANGE = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); - SINGLE_STEP = (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)); - range = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); - iterations = RANGE / RANGE_PER_PAGE; - median_frequency = (CONF_FREQ_BEGIN + CONF_FREQ_END) / 2; #endif init_radio(); @@ -1007,6 +1166,8 @@ void setup(void) } } } + + configurePages(); display.clear(); Serial.println(); @@ -1036,9 +1197,7 @@ void setup(void) #ifdef LOG_DATA_JSON xTaskCreate(logToSerialTask, "LOG_DATA_JSON", 2048, NULL, 1, &logToSerial); #endif -#ifdef SERIAL_OUT xTaskCreate(dumpToCommsTask, "DUMP_RESPONSE_PROCESS", 2048, NULL, 1, &dumpToComms); -#endif r.trigger_level = TRIGGER_LEVEL; stacked.reset(0, 0, display.width(), display.height()); @@ -1076,12 +1235,10 @@ void setup(void) r.addEventListener(DETECTED, drone_sound_alarm, &r); r.addEventListener(SCAN_TASK_COMPLETE, stacked); -#ifdef SERIAL_OUT frequency_scan_result.readings_sz = 0; frequency_scan_result.dump.sz = 0; r.addEventListener(ALL_EVENTS, eventListenerForReport, NULL); -#endif #ifdef UPTIME_CLOCK uptime = new UptimeClock(display, millis()); @@ -1248,35 +1405,11 @@ bool is_new_x_pixel(int x) return false; } -void check_ranges() -{ - if (RANGE_PER_PAGE == range) - { - single_page_scan = true; - } - else - { - single_page_scan = false; - } - - for (int range : SCAN_RANGES) - { - ranges_count++; - } - - if (ranges_count > 0) - { - iterations = ranges_count; - single_page_scan = false; - } -} - -#ifdef SERIAL_OUT void checkComms() { - while (Comms0->available() > 0) + while (HostComms->available() > 0) { - Message *m = Comms0->receive(); + Message *m = HostComms->receive(); if (m == NULL) continue; @@ -1284,27 +1417,124 @@ void checkComms() { case MessageType::SCAN: report_scans = m->payload.scan; + requested_host = true; + Serial.println("Host: forwarding message SCAN to peer"); + Comms0->send(*m); // forward to peer + Comms1->send(*m); // forward to peer + break; + case MessageType::CONFIG_TASK: + if (m->payload.config.is_set) + { + String v = config.getConfig(*m->payload.config.key); + bool r = + config.updateConfig(*m->payload.config.key, *m->payload.config.value); + Serial.printf("SET config (%s): %s = %s (was: %s)\n", r ? "OK" : "failed", + m->payload.config.key->c_str(), + m->payload.config.value->c_str(), v.c_str()); + } + else + { + Serial.printf("GET config: %s = %s\n", m->payload.config.key->c_str(), + config.getConfig(*m->payload.config.key).c_str()); + } + break; + } + delete m; + } + + while (Comms0->available() > 0) + { + Message *m = Comms0->receive(); + Serial.println("Comms0: was available, but didn't receive"); + if (m == NULL) + continue; + + switch (m->type) + { + case MessageType::SCAN: + report_scans = m->payload.scan; // receive from peer + requested_host = false; + break; + + case MessageType::SCAN_RESULT: + HostComms->send(*m); // forward from peer + break; + } + delete m; + } + + while (Comms1->available() > 0) + { + Message *m = Comms1->receive(); + Serial.println("Comms1: was available, but didn't receive"); + if (m == NULL) + continue; + + switch (m->type) + { + case MessageType::SCAN: + report_scans = m->payload.scan; // receive from peer + requested_host = false; + break; + + case MessageType::SCAN_RESULT: + HostComms->send(*m); // forward from peer break; } delete m; } } -#endif // MAX Frequency RSSI BIN value of the samples int max_rssi_x = 999; +void doScan(); + +void reportScan(RadioComms &c); + +int16_t checkRadio(RadioComms &c); + void loop(void) { r.led_flag = false; r.detection_count = 0; drone_detected_frequency_start = 0; - ranges_count = 0; -#ifdef SERIAL_OUT checkComms(); + if (config.is_host) + { + if (TxComms != NULL) + { + // NB: swapping the use of Tx and Rx comms, so a pair of modules + // with identical rx/tx_lora config can talk + int16_t status = checkRadio(*TxComms); + if (status != RADIOLIB_ERR_NONE) + { + Serial.printf("Error getting a message: %d\n", status); + } + } + } + else + { + doScan(); + if (TxComms != NULL) + reportScan(*TxComms); + if (RxComms != NULL) + checkRadio(*RxComms); + } +} + +void doScan() +{ + if (!radioIsScan) + { + radioIsScan = true; + initForScan(CONF_FREQ_BEGIN); + state = radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_NONE); + } + // reset scan time if (config.print_profile_time) { @@ -1312,7 +1542,6 @@ void loop(void) loop_start = millis(); } r.epoch++; -#endif if (!ANIMATED_RELOAD || !single_page_scan) { @@ -1331,35 +1560,13 @@ void loop(void) r.fr_begin = CONF_FREQ_BEGIN; r.fr_end = r.fr_begin; - // 50 is a single-screen range - // TODO: Make 50 a variable with the option to show the full range - iterations = range / RANGE_PER_PAGE; - -#if 0 // disabled code - if (range % RANGE_PER_PAGE != 0) + for (scan_page = 0; scan_page < scan_pages_sz; scan_page++) { - // add more scan - //++; - } -#endif - - check_ranges(); - - // Iterating by small ranges by 50 Mhz each pixel is 0.4 Mhz - for (range_item = 0; range_item < iterations; range_item++) - { - range = RANGE_PER_PAGE; - if (ranges_count == 0) - { - r.fr_begin = (range_item == 0) ? r.fr_begin : r.fr_begin + range; - r.fr_end = r.fr_begin + RANGE_PER_PAGE; - } - else - { - r.fr_begin = SCAN_RANGES[range_item] / 1000; - r.fr_end = SCAN_RANGES[range_item] % 1000; - range = r.fr_end - r.fr_begin; - } + ScanPage &page = scan_pages[scan_page]; + r.fr_begin = page.start_mhz; + r.fr_end = page.end_mhz; + range = r.fr_end - r.fr_begin; + median_frequency = (page.start_mhz + page.end_mhz) / 2; #ifdef DISABLED_CODE if (!ANIMATED_RELOAD || !single_page_scan) @@ -1379,10 +1586,38 @@ void loop(void) // horizontal (x axis) Frequency loop osd_x = 1, osd_y = 2, col = 0, max_bin = 0; + // x loop - for (x = 0; x < STEPS * SCAN_RBW_FACTOR; x++) + for (int range = 0, step = 0; range < page.page_sz; range += (step == 0)) { - new_pixel = is_new_x_pixel(x); + // the logic is: + // 1. go through each scan_range in the order that they are declared + // in the page + // 2. start with scan_range.start and always end with scan_range.end + // if adding step lands us a little short of end, there will be + // extra iteration to scan actual end frequency + // 3. the next iteration after scanning end frequency will be next range + // 4. x is derived from the frequency we are going to scan with respect to + // the page size + ScanRange scan_range = page.scan_ranges[range]; + uint64_t curr_freq = scan_range.start_khz + scan_range.step_khz * step; + if (curr_freq > scan_range.end_khz) + curr_freq = scan_range.end_khz; + + // for now support legacy calculation of x that relies on SCAN_RBW_FACTOR + x = (curr_freq - page.start_mhz * 1000) * STEPS * SCAN_RBW_FACTOR / + ((page.end_mhz - page.start_mhz) * 1000); + + new_pixel = step == 0 || is_new_x_pixel(x); + if (curr_freq == scan_range.end_khz) + { + step = 0; // trigger switch to the next range on the next iteration + } + else + { + step++; + } + if (ANIMATED_RELOAD && SCAN_RBW_FACTOR == 1) { UI_drawCursor(x); @@ -1399,35 +1634,8 @@ void loop(void) // Because of the SCAN_RBW_FACTOR x is not a display coordinate anymore // x > STEPS on SCAN_RBW_FACTOR int display_x = x / SCAN_RBW_FACTOR; - float step = (range * ((float)x / (STEPS * SCAN_RBW_FACTOR))); - r.current_frequency = r.fr_begin + step; - LOG("setFrequency:%f\n", r.current_frequency); - -#ifdef USING_SX1280PA - state = - radio.setFrequency(r.current_frequency); // 1280 doesn't have calibration - radio.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF); -#elif USING_SX1276 - state = radio.setFrequency(freq); -#else - state = radio.setFrequency(r.current_frequency, - true); // true = no calibration need here -#endif - int radio_error_count = 0; - if (state != RADIOLIB_ERR_NONE) - { - display.drawString(0, 64 - 10, - "E(" + String(state) + - "):setFrequency:" + String(r.current_frequency)); - Serial.println("E(" + String(state) + - "):setFrequency:" + String(r.current_frequency)); - display.display(); - delay(2); - radio_error_count++; - if (radio_error_count > 10) - continue; - } + setFrequency(curr_freq / 1000.0); LOG("Step:%d Freq: %f\n", x, r.current_frequency); // SpectralScan Method @@ -1461,7 +1669,23 @@ void loop(void) // Spectrum analyzer using getRSSI { LOG("METHOD RSSI"); - uint16_t max_rssi = r.rssiMethod(CONF_SAMPLES, result, + + float (*g)(void *); + samples = CONF_SAMPLES; + + if (config.detection_strategy.equalsIgnoreCase("RSSI")) + g = &getRSSI; + else if (config.detection_strategy.equalsIgnoreCase("CAD")) + { + g = &getCAD; + samples = min( + 1, + CONF_SAMPLES); // TODO: do we need to support values other than 1 + } + else + g = &getRSSI; + + uint16_t max_rssi = r.rssiMethod(g, &r, samples, result, RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE); if (max_x_rssi[display_x] > max_rssi) @@ -1499,10 +1723,8 @@ void loop(void) max_rssi_x = detected_at; } - detected = event.detected.detected; detected_y[display_x] = false; - float rr = event.detected.rssi; r.drone_detection_level = drone_detection_level; if (event.detected.trigger) @@ -1706,17 +1928,13 @@ void loop(void) joy_btn_clicked = false; -#ifdef SERIAL_OUT if (config.print_profile_time) { -#endif #ifdef PRINT_PROFILE_TIME loop_time = millis() - loop_start; Serial.printf("LOOP: %lld ms; SCAN: %lld ms;\n ", loop_time, scan_time); #endif -#ifdef SERIAL_OUT } -#endif // No WiFi and BT Scan Without OSD #ifdef OSD_ENABLED @@ -1740,3 +1958,57 @@ void loop(void) sideBarCol = SIDEBAR_START_COL; #endif } + +int16_t checkRadio(RadioComms &comms) +{ + radioIsScan = false; + int16_t status = comms.configureRadio(); + if (status != RADIOLIB_ERR_NONE) + return status; + + Message *msg = comms.receive( + config.is_host + ? 2000 + : 200); // 200ms should be enough to receive 500 bytes at SF 7 and BW 500 + if (msg == NULL) + { + return status; + } + + if (msg->type == SCAN_RESULT) + { + HostComms->send(*msg); + } + else + { + Serial.printf("Received a message of unsupported type: %d\n", msg->type); + } + + delete msg; + + return status; +} + +void reportScan(RadioComms &comms) +{ + radioIsScan = false; + int16_t status = comms.configureRadio(); + if (status != RADIOLIB_ERR_NONE) + { + Serial.printf("Failed to configure Radio: %d\n", status); + return; + } + + Message m; + m.type = SCAN_RESULT; + m.payload.dump = frequency_scan_result.dump; + status = comms.send(m); + + m.payload.dump.sz = 0; // dump is shared, so should not delete underlying arrays + + if (status != RADIOLIB_ERR_NONE) + { + Serial.printf("Failed to send scan result: %d\n", status); + return; + } +} diff --git a/src/ui.cpp b/src/ui.cpp index 77bdc58..4349d85 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -15,17 +15,14 @@ #define SCALE_TEXT_TOP (HEIGHT + X_AXIS_WEIGHT + MAJOR_TICK_LENGTH) // temporary dirty import ... to be solved durring upcoming refactoring -extern unsigned int RANGE_PER_PAGE; extern uint64_t CONF_FREQ_BEGIN; extern uint64_t CONF_FREQ_END; extern unsigned int median_frequency; extern unsigned int drone_detected_frequency_start; extern unsigned int drone_detected_frequency_end; -extern unsigned int ranges_count; -extern int SCAN_RANGES[]; -extern unsigned int ranges_count; -extern unsigned int iterations; -extern unsigned int range_item; +extern size_t scan_pages_sz; +extern ScanPage *scan_pages; +extern size_t scan_page; extern uint64_t loop_time; @@ -166,7 +163,7 @@ void StatusBar::draw() digitalWrite(LED, LOW); } - if (ranges_count == 0) + if (scan_pages_sz == 1) { #ifdef DEBUG display.setTextAlignment(TEXT_ALIGN_LEFT); @@ -179,18 +176,18 @@ void StatusBar::draw() display.setTextAlignment(TEXT_ALIGN_RIGHT); display.drawString(pos_x + width, text_y, String(CONF_FREQ_END)); } - else if (ranges_count > 0) + else if (scan_pages_sz > 1) { display.setTextAlignment(TEXT_ALIGN_LEFT); display.drawString(pos_x, text_y, - String(SCAN_RANGES[range_item] / 1000) + "-" + - String(SCAN_RANGES[range_item] % 1000)); - if (range_item + 1 < iterations) + String(scan_pages[scan_page].start_mhz) + "-" + + String(scan_pages[scan_page].end_mhz)); + if (scan_page + 1 < scan_pages_sz) { display.setTextAlignment(TEXT_ALIGN_RIGHT); display.drawString(pos_x + width, text_y, - String(SCAN_RANGES[range_item + 1] / 1000) + "-" + - String(SCAN_RANGES[range_item + 1] % 1000)); + String(scan_pages[scan_page + 1].start_mhz) + "-" + + String(scan_pages[scan_page + 1].end_mhz)); } } ui_initialized = true; diff --git a/test/test_rssi.cpp b/test/test_rssi.cpp index a38d166..737caea 100644 --- a/test/test_rssi.cpp +++ b/test/test_rssi.cpp @@ -7,21 +7,21 @@ struct TestScan : Scan { TestScan(float *ctx, int sz) : ctx(ctx), sz(sz), idx(0) {} - float getRSSI() override; - float *ctx; int sz; int idx; }; -float TestScan::getRSSI() +float getRSSI(void *param) { - if (idx >= sz) + TestScan *r = (TestScan *)param; + + if (r->idx >= r->sz) { return -1000000; } - return ctx[idx++]; + return r->ctx[r->idx++]; } constexpr int test_sz = 13; @@ -35,7 +35,7 @@ void test_rssi(void) TestScan t = TestScan(inputs, inputs_sz); - uint16_t r = t.rssiMethod(inputs_sz, samples, test_sz); + uint16_t r = t.rssiMethod(getRSSI, &t, inputs_sz, samples, test_sz); uint16_t expect[test_sz] = {20, 50, 55, 60, 0, 70, 75, 80, 0, 90, 0, 100, 110}; diff --git a/test/test_waterfall.cpp b/test/test_waterfall.cpp index 1875065..69cb3bc 100644 --- a/test/test_waterfall.cpp +++ b/test/test_waterfall.cpp @@ -50,8 +50,11 @@ void test_push() m.reset(0, 1); uint64_t i = 0; + size_t update_to = 0; for (; i < 10; i++) - m.updateModel(i, 0, 1); + update_to = max(update_to, m.updateModel(i, 0, 1)); + + TEST_ASSERT_EQUAL_INT(6, update_to); r = m.toString(); TEST_ASSERT_EQUAL_STRING("w:1 b:34 " @@ -92,8 +95,11 @@ void test_push() r); delete r; + update_to = 0; for (; i < 100; i += 10) - m.updateModel(i, 0, 1); + update_to = max(update_to, m.updateModel(i, 0, 1)); + + TEST_ASSERT_EQUAL_INT(13, update_to); r = m.toString(); TEST_ASSERT_EQUAL_STRING("w:1 b:34 " @@ -134,8 +140,11 @@ void test_push() r); delete r; + update_to = 0; for (; i < 10000; i++) - m.updateModel(i, 0, 1); + update_to = max(update_to, m.updateModel(i, 0, 1)); + + TEST_ASSERT_EQUAL_INT(34, update_to); r = m.toString(); TEST_ASSERT_EQUAL_STRING("w:1 b:34 " @@ -176,45 +185,48 @@ void test_push() r); delete r; - for (; i < 5000; i++) - m.updateModel(i, 0, 1); + update_to = 0; + for (; i < 15000; i++) + update_to = max(update_to, m.updateModel(i, 0, 1)); + + TEST_ASSERT_EQUAL_INT(34, update_to); r = m.toString(); TEST_ASSERT_EQUAL_STRING("w:1 b:34 " - "[dt:1 t:9999 [ c:1 e:1 ]" - "dt:1 t:9998 [ c:1 e:1 ]" - "dt:1 t:9997 [ c:1 e:1 ]" - "dt:1 t:9996 [ c:1 e:1 ]" - "dt:1 t:9995 [ c:1 e:1 ]" - "dt:5 t:9995 [ c:4 e:4 ]" - "dt:5 t:9990 [ c:5 e:5 ]" - "dt:5 t:9985 [ c:5 e:5 ]" - "dt:15 t:9990 [ c:5 e:5 ]" - "dt:15 t:9975 [ c:15 e:15 ]" - "dt:15 t:9960 [ c:15 e:15 ]" - "dt:15 t:9945 [ c:15 e:15 ]" - "dt:60 t:9960 [ c:30 e:30 ]" - "dt:60 t:9900 [ c:60 e:60 ]" - "dt:60 t:9840 [ c:60 e:60 ]" - "dt:60 t:9780 [ c:60 e:60 ]" - "dt:60 t:9720 [ c:60 e:60 ]" - "dt:60 t:9660 [ c:60 e:60 ]" - "dt:60 t:9600 [ c:60 e:60 ]" - "dt:60 t:9540 [ c:60 e:60 ]" - "dt:60 t:9480 [ c:60 e:60 ]" - "dt:60 t:9420 [ c:60 e:60 ]" - "dt:60 t:9360 [ c:60 e:60 ]" - "dt:60 t:9300 [ c:60 e:60 ]" - "dt:60 t:9240 [ c:60 e:60 ]" - "dt:60 t:9180 [ c:60 e:60 ]" - "dt:60 t:9120 [ c:60 e:60 ]" - "dt:900 t:9900 [ c:60 e:60 ]" - "dt:900 t:9000 [ c:900 e:900 ]" - "dt:900 t:8100 [ c:900 e:900 ]" - "dt:900 t:7200 [ c:900 e:900 ]" - "dt:3600 t:7200 [ c:2700 e:2700 ]" - "dt:3600 t:3600 [ c:3520 e:3520 ]" - "dt:3600 t:3600 [ c:0 e:0 ] ]", + "[dt:1 t:14999 [ c:1 e:1 ]" + "dt:1 t:14998 [ c:1 e:1 ]" + "dt:1 t:14997 [ c:1 e:1 ]" + "dt:1 t:14996 [ c:1 e:1 ]" + "dt:1 t:14995 [ c:1 e:1 ]" + "dt:5 t:14995 [ c:4 e:4 ]" + "dt:5 t:14990 [ c:5 e:5 ]" + "dt:5 t:14985 [ c:5 e:5 ]" + "dt:15 t:14985 [ c:10 e:10 ]" + "dt:15 t:14970 [ c:15 e:15 ]" + "dt:15 t:14955 [ c:15 e:15 ]" + "dt:15 t:14940 [ c:15 e:15 ]" + "dt:60 t:14940 [ c:45 e:45 ]" + "dt:60 t:14880 [ c:60 e:60 ]" + "dt:60 t:14820 [ c:60 e:60 ]" + "dt:60 t:14760 [ c:60 e:60 ]" + "dt:60 t:14700 [ c:60 e:60 ]" + "dt:60 t:14640 [ c:60 e:60 ]" + "dt:60 t:14580 [ c:60 e:60 ]" + "dt:60 t:14520 [ c:60 e:60 ]" + "dt:60 t:14460 [ c:60 e:60 ]" + "dt:60 t:14400 [ c:60 e:60 ]" + "dt:60 t:14340 [ c:60 e:60 ]" + "dt:60 t:14280 [ c:60 e:60 ]" + "dt:60 t:14220 [ c:60 e:60 ]" + "dt:60 t:14160 [ c:60 e:60 ]" + "dt:60 t:14100 [ c:60 e:60 ]" + "dt:900 t:14400 [ c:540 e:540 ]" + "dt:900 t:13500 [ c:900 e:900 ]" + "dt:900 t:12600 [ c:900 e:900 ]" + "dt:900 t:11700 [ c:900 e:900 ]" + "dt:3600 t:10800 [ c:3600 e:3600 ]" + "dt:3600 t:7200 [ c:3600 e:3600 ]" + "dt:3600 t:3600 [ c:3520 e:3520 ] ]", r); delete r; }