diff --git a/.gitignore b/.gitignore index 3711360..74685b8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch - -out/ +out/* \ No newline at end of file diff --git a/SpectrumScan.py b/SpectrumScan.py index ee713a3..fb17802 100644 --- a/SpectrumScan.py +++ b/SpectrumScan.py @@ -43,7 +43,22 @@ def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, lengt def parse_line(line): """Parse a JSON line from the serial input.""" - return json.loads(line) + + line = line[line.index('SCAN_RESULT '):] # support garbage interleaving with the string + _, count, rest = line.split(' ', 2) + return int(count), json.loads(rest.replace('(', '[').replace(')', ']')) + +POLY = 0x1021 +def crc16(s, c): + for ch in s: + c = c ^ (ord(ch) << 8) + for i in range(8): + if c & 0x8000: + c = ((c << 1) ^ POLY) & 0xffff + else: + c = (c << 1) & 0xffff + + return c def main(): parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''\ @@ -62,13 +77,14 @@ def main(): help=f'Number of scanlines to record (defaults to {DEFAULT_SCAN_LEN})') parser.add_argument('--offset', default=DEFAULT_RSSI_OFFSET, type=int, help=f'Default RSSI offset in dBm (defaults to {DEFAULT_RSSI_OFFSET})') - parser.add_argument('--freq', default=-1, type=float, - help='Default starting frequency in MHz') + parser.add_argument('--buckets', default=-1, type=int, + help='Default number of buckets to group frequencies into; if < 1, will autodetect') + args = parser.parse_args() # Create the result array scan_len = args.len - arr = np.zeros((scan_len, SCAN_WIDTH)) + arr = None # Scanline counter row = 0 @@ -76,42 +92,81 @@ def main(): # List of frequencies freq_list = [] + checksum = -1 + so_far = 0 + # Open the COM port with serial.Serial(args.port, args.speed, timeout=None) as com: - while True: + + com.write(bytes('SCAN -1 -1\n', 'ascii')) + + lines = 0 + errors = 0 + while row < scan_len: # Update the progress bar print_progress_bar(row, scan_len) # Read a single line try: - line = com.readline().decode('utf-8').strip() + line = com.readline().decode('utf-8') except UnicodeDecodeError: + errors += 1 continue - if line.startswith("{"): + if 'WRAP ' in line: try: - data = parse_line(line) - except json.JSONDecodeError: + _, c, rest = line.split(' ', 2) + checksum = int(c, 16) + so_far = crc16(rest, 0) + except Exception as e: + errors += 1 + continue + + if 'SCAN_RESULT ' in line: + if checksum == -1: + errors += 1 continue - # get the lowest frequency for now, could be averaged too. - freq = data["low_range_freq"] + c16 = crc16(line, so_far) + if checksum != c16: + errors += 1 + checksum = -1 + continue - # value in negative, eg: -70 - rssi = int(data["value"]) + checksum = -1 + + lines += 1 + try: + count, data = parse_line(line) + data.sort() + except json.JSONDecodeError: + errors += 1 + continue + + r = list(zip(*data)) + if len(r) != 2 or len(data) != count: + errors += 1 + continue + + freqs, rssis = r + + if arr is None: + w = count if args.buckets < 1 else args.buckets + arr = np.zeros((scan_len, w)) + freq_list = freqs + + for col in range(len(rssis)): + arr[row][col] = rssis[col] - if freq not in freq_list: - freq_list.append(freq) - - col = freq_list.index(freq) - arr[row][col] = rssi - # Increment the row counter row += 1 - # Check if we're done - if row >= scan_len: - break + # tell it to stop producing SCAN_RESULTS + com.write(bytes('SCAN 0 -1\n', 'ascii')) + + print("Read %d lines, encountered %d errors. Success rate: %.2f" % + (lines, errors, (100 - 100 * errors / lines) if lines > 0 else 0)) + arr[arr == 0] = arr.min() - 20 # Create the figure fig, ax = plt.subplots(figsize=(12, 8)) diff --git a/lib/comms/comms.cpp b/lib/comms/comms.cpp new file mode 100644 index 0000000..cbf95df --- /dev/null +++ b/lib/comms/comms.cpp @@ -0,0 +1,295 @@ +#include "comms.h" +#include + +#include +#include +#include +#include + +Comms *Comms0; + +void _onReceive0() +{ + if (Comms0 == NULL) + { + return; + } + + Comms0->_onReceive(); +} + +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*/); + } + } +} + +bool Comms::initComms(Config &c) +{ + if (c.listen_on_usb.equalsIgnoreCase("readline")) + { + // comms using readline plaintext protocol + Comms0 = new ReadlineComms(Serial); + Serial.onEvent(ARDUINO_HW_CDC_RX_EVENT, _onUsbEvent0); + Serial.begin(); + + Serial.println("Initialized communications on Serial using readline protocol"); + + return true; + } + else if (c.listen_on_serial0.equalsIgnoreCase("readline")) + { + // comms using readline plaintext protocol + Comms0 = new ReadlineComms(Serial0); + Serial0.onReceive(_onReceive0, false); + + Serial.println("Initialized communications on Serial0 using readline protocol"); + + return true; + } + + if (c.listen_on_serial0.equalsIgnoreCase("none")) + { + Comms0 = new NoopComms(); + + Serial.println("Configured none - Initialized no communications"); + return false; + } + + Comms0 = new NoopComms(); + Serial.println("Nothing is configured - initialized no communications"); + return false; +} + +size_t Comms::available() { return received_pos; } + +#define _RECEIVED_BUF_INCREMENT 10 +#define _MAX_RECEIVED_SZ 100 +bool Comms::_messageArrived(Message &m) +{ + if (received_pos == received_sz) + { + // TODO: if received_sz exceeds a configurable bound, complain and drop the + // message on the floor + if (received_sz >= _MAX_RECEIVED_SZ) + { + Serial.println("Receive buffer backlog too large; dropping the message"); + return false; + } + Message **m = new Message *[received_sz + _RECEIVED_BUF_INCREMENT]; + if (received_sz > 0) + { + memcpy(m, received, received_sz * sizeof(Message *)); + delete[] received; + } + received = m; + received_sz += _RECEIVED_BUF_INCREMENT; + } + + received[received_pos] = &m; + received_pos++; + + return true; +} + +Message *Comms::receive() +{ + if (received_pos == 0) + { + return NULL; + } + + Message *m = received[0]; + received_pos--; + + memmove(received, received + 1, received_pos * sizeof(Message *)); + + return m; +} + +Message *_parsePacket(String); +String _scan_str(ScanTask &); +String _scan_result_str(ScanTaskResult &); +String _wrap_str(String); + +#define POLY 0x1021 +uint16_t crc16(String v, uint16_t c) +{ + for (int i = 0; i < v.length(); i++) + { + uint16_t ch = v.charAt(i); + c = c ^ (ch << 8); + for (int j = 0; j < 8; j++) + { + if (c & 0x8000) + { + c = (c << 1) ^ POLY; + } + else + { + c <<= 1; + } + } + } + + return c; +} + +void ReadlineComms::_onReceive() +{ + while (serial.available() > 0) + { + partialPacket = partialPacket + serial.readString(); + int i = partialPacket.indexOf('\n'); + while (i >= 0) + { + String pack = partialPacket.substring(0, i); + bool messageOk = true; + + if (wrap != NULL) + { + messageOk = pack.length() == wrap->payload.wrap.length; + if (messageOk) + { + messageOk = crc16(pack, 0) == wrap->payload.wrap.crc; + } + delete wrap; + wrap = NULL; + } + Message *m = messageOk ? _parsePacket(pack) : NULL; + if (m != NULL) + { + if (m->type == WRAP) + { + wrap = m; + } + else if (!_messageArrived(*m)) + { + delete m; + } + } + partialPacket = partialPacket.substring(i + 1); + i = partialPacket.indexOf('\n'); + } + } +} + +bool ReadlineComms::send(Message &m) +{ + String p; + + switch (m.type) + { + case MessageType::SCAN: + p = _scan_str(m.payload.scan); + break; + case MessageType::SCAN_RESULT: + p = _scan_result_str(m.payload.dump); + break; + } + + serial.print(_wrap_str(p)); + return true; +} + +int64_t _intParam(String &p, int64_t default_v) +{ + p.trim(); + int i = p.indexOf(' '); + if (i < 0) + { + i = p.length(); + } + + int64_t v = p.substring(0, i).toInt(); + p = p.substring(i + 1); + return v; +} + +int64_t _hexParam(String &p, int64_t default_v) +{ + p.trim(); + int i = p.indexOf(' '); + if (i < 0) + { + i = p.length(); + } + + int64_t v = strtol(p.substring(0, i).c_str(), 0, 16); + p = p.substring(i + 1); + return v; +} + +Message *_parsePacket(String p) +{ + p.trim(); + if (p.length() == 0) + { + return NULL; + } + + String cmd = p; + int i = p.indexOf(' '); + if (i < 0) + { + p = ""; + } + else + { + cmd = p.substring(0, i); + p = p.substring(i + 1); + p.trim(); + } + + if (cmd.equalsIgnoreCase("wrap")) + { + Message *m = new Message(); + m->type = MessageType::WRAP; + m->payload.wrap.crc = _hexParam(p, -1); + m->payload.wrap.length = _intParam(p, -1); + return m; + } + + if (cmd.equalsIgnoreCase("scan")) + { + Message *m = new Message(); + m->type = MessageType::SCAN; + m->payload.scan.count = _intParam(p, 1); + m->payload.scan.delay = _intParam(p, -1); + return m; + } + + Serial.println("ignoring unknown message " + p); + return NULL; +} + +String _scan_str(ScanTask &t) +{ + return "SCAN " + String(t.count) + " " + String(t.delay) + "\n"; +} + +String _scan_result_str(ScanTaskResult &r) +{ + String p = "SCAN_RESULT " + String(r.sz) + " [ "; + + for (int i = 0; i < r.sz; i++) + { + p += (i == 0 ? "(" : ", (") + String(r.freqs_khz[i]) + ", " + String(r.rssis[i]) + + ")"; + } + + return p + " ]\n"; +} + +String _wrap_str(String v) +{ + String r = String(v.length()) + "\n" + v; + return "WRAP " + String(crc16(r, 0), 16) + " " + r; +} diff --git a/lib/comms/comms.h b/lib/comms/comms.h new file mode 100644 index 0000000..a37fe12 --- /dev/null +++ b/lib/comms/comms.h @@ -0,0 +1,88 @@ +#ifndef __COMMS_H +#define __COMMS_H + +#include +#include + +enum MessageType +{ + WRAP = 0, + SCAN, + SCAN_RESULT, + _MAX_MESSAGE_TYPE = SCAN_RESULT +}; + +struct Wrapper +{ + int32_t length; + uint16_t crc; +}; + +struct ScanTask +{ + int64_t count; + int64_t delay; +}; + +struct ScanTaskResult +{ + size_t sz; + uint32_t *freqs_khz; + int16_t *rssis; +}; + +struct Message +{ + MessageType type; + union + { + Wrapper wrap; + ScanTask scan; + ScanTaskResult dump; + } payload; +}; + +struct Comms +{ + Stream &serial; + Message **received; + size_t received_sz; + size_t received_pos; + + Message *wrap; + + Comms(Stream &serial) + : serial(serial), received(NULL), received_sz(0), received_pos(0), wrap(NULL) {}; + + virtual size_t available(); + virtual bool send(Message &) = 0; + virtual Message *receive(); + + virtual void _onReceive() = 0; + virtual bool _messageArrived(Message &); + + static bool initComms(Config &); +}; + +struct NoopComms : Comms +{ + NoopComms() : Comms(Serial0) {}; + + virtual bool send(Message &) { return true; }; + virtual void _onReceive() {}; +}; + +struct ReadlineComms : Comms +{ + String partialPacket; + + ReadlineComms(Stream &serial) : Comms(serial), partialPacket("") {}; + + virtual bool send(Message &) override; + + virtual void _onReceive() override; +}; + +extern Comms *Comms0; + +#endif diff --git a/lib/config/config.cpp b/lib/config/config.cpp new file mode 100644 index 0000000..879174d --- /dev/null +++ b/lib/config/config.cpp @@ -0,0 +1,211 @@ +#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_usb")) + { + c.listen_on_serial0 = r.value; + continue; + } + + Serial.printf("Unknown key '%s' will be ignored\n", r.key); + } + + f.close(); + return c; +} + +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_usb = " + listen_on_usb); + + 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); +} diff --git a/lib/config/config.h b/lib/config/config.h new file mode 100644 index 0000000..1b057f3 --- /dev/null +++ b/lib/config/config.h @@ -0,0 +1,35 @@ +#ifndef _LORA_CONFIG_H +#define _LORA_CONFIG_H + +#include + +#define CREATE_MISSING_CONFIG true +struct Config +{ + bool create_missing_config; + bool print_profile_time; + int log_data_json_interval; + String listen_on_serial0; + String listen_on_usb; + + 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") {}; + bool write_config(const char *path); + + static Config init(); +}; + +struct ParseResult +{ + String key; + String value; + String error; + + ParseResult(String e) : key(String()), value(String()), error(e) {}; + ParseResult(String k, String v) : key(k), value(v), error(String()) {}; +}; + +ParseResult parse_config_line(String ln); +#endif diff --git a/lib/loraboards/LoRaBoards.cpp b/lib/loraboards/LoRaBoards.cpp index 0331489..5afdb70 100644 --- a/lib/loraboards/LoRaBoards.cpp +++ b/lib/loraboards/LoRaBoards.cpp @@ -705,7 +705,33 @@ void setupBoards(bool disable_u8g2) beginPower(); - beginSDCard(); + bool sdReady; + for (int i = 0; i < 5 && !(sdReady = beginSDCard()); i++) + { + Serial.println("SD card failed or not found"); + delay(1000); + } + + if (sdReady) + { + char *card_type = "UNKNOWN"; + sdcard_type_t t = SD.cardType(); + + if (t == sdcard_type_t::CARD_MMC) + { + card_type = "MMC"; + } + else if (t == sdcard_type_t::CARD_SD) + { + card_type = "SD"; + } + else if (t == sdcard_type_t::CARD_SDHC) + { + card_type = "SDHC"; + } + + Serial.printf("SD card %s is ready.\n", card_type); + } if (!disable_u8g2) { diff --git a/lib/scan/scan.h b/lib/scan/scan.h index 38b610e..c826609 100644 --- a/lib/scan/scan.h +++ b/lib/scan/scan.h @@ -47,15 +47,19 @@ struct Scan bool animated; float trigger_level; + bool comms_initialized; + Listener **eventListeners[(size_t)EventType::_MAX_EVENT_TYPE + 1]; size_t listener_count[(size_t)EventType::_MAX_EVENT_TYPE + 1]; Scan() : epoch(0), current_frequency(0), fr_begin(0), fr_end(0), drone_detection_level(0), sound_on(false), led_flag(false), detection_count(0), - animated(false), trigger_level(0), listener_count{ - 0, - } {}; + animated(false), trigger_level(0), + listener_count{ + 0, + }, + comms_initialized(false) {}; virtual float getRSSI() = 0; diff --git a/spectrum_scan.c b/spectrum_scan.c new file mode 100644 index 0000000..aa8c9c9 --- /dev/null +++ b/spectrum_scan.c @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include +#include + +typedef struct { + uint16_t checksum; + uint16_t so_far; + size_t len; +} wrap_t; + +#define POLY 0x1021 +uint16_t crc16(char *p, char *end, uint16_t c) { + if (end == NULL) { + end = strchr(p, 0); + } + + for (; p != end; p++) { + uint16_t ch = *p; + c = c ^ (ch << 8); + for (int j = 0; j < 8; j++) { + if (c & 0x8000) { + c = (c << 1) ^ POLY; + } else { + c <<= 1; + } + } + } + + return c; +} + +#define BUFSIZE 102400 +#define LOOPS 200 +int main(int argc, char** argv) +{ + char *port = "/dev/ttyACM0"; + + int device = open(port, O_RDWR | O_NOCTTY | O_SYNC); + + char *scan_ln = "SCAN -1 -1\n"; + char *buffer = malloc(BUFSIZE + 1); + + write(device, scan_ln, strlen(scan_ln)); + + int lines = 0; + int errors = 0; + + int pos = 0; + int progress = 0; + int progress_ln = 0; + + wrap_t wrapper; + wrapper.len = 0; + + while(lines - errors < LOOPS) + { + if (pos == BUFSIZE) + { + buffer[pos] = 0; + printf("Something went completely wrong: %s\n", buffer); + break; + } + + int n = read(device, buffer + pos, BUFSIZE - pos); + + if (n > 0) + { + n += pos; + buffer[n] = 0; + + char *nl = strchr(buffer + pos, '\n'); + while (nl != NULL) + { + lines++; + + // assert: n points at the first unused byte in buffer + pos = (nl - buffer) + 1; + + char *wrap = "WRAP "; + char is_wrap = strncmp(buffer, wrap, strlen(wrap)) == 0; + if (is_wrap) { + char *end; + char *p = buffer + strlen(wrap); + wrapper.checksum = strtol(p, &end, 16); + wrapper.len = 0; + if (end == p) { + is_wrap = 0; + } else { + p = end; + wrapper.len = strtol(p, &end, 10); + if (end == p) { + is_wrap = 0; + } else { + wrapper.so_far = crc16(p+1, end+1, 0); + } + } + } + + char *scan_result = "SCAN_RESULT "; + char is_scan_result = strncmp(buffer, scan_result, strlen(scan_result)) == 0; + + if (is_scan_result) { + uint16_t c16 = crc16(buffer, buffer + pos, wrapper.so_far); + if (wrapper.len == 0 || wrapper.checksum != c16) + { + errors++; + if (wrapper.len == 0) { + int n = snprintf(buffer, pos, "E: no wrap"); // string definitely fits + buffer[n] = ' '; + } else { + int n = snprintf(buffer, pos, "E: bad crc"); // string definitely fits + buffer[n] = ' '; + int n1 = snprintf(buffer + n + 1, pos - n - 1, "want: %x, got: %x", wrapper.checksum, c16); + n1 += n + 1; + if (n1 <= pos - n - 1) { + buffer[n1] = ' '; + } + } + } + wrapper.len = 0; // ok, used up the wrapper + + // assert: pos points at the first byte of the next line + write(1, buffer, pos); + // printf("Last char is: %c\n", buffer[pos-3]); - wow, HWCDC.println ends up producing \r\n + } else if (strncmp(buffer, "Wrote stuff in ", 15) == 0) { + lines--; + write(1, buffer, pos); + } else if (strncmp(buffer, "Unable to make ", 15) == 0) { + lines--; + write(1, buffer, pos); + } else if (!is_wrap) { + errors++; + } + + if (lines > progress_ln) + { + progress_ln = lines + 10; + if (lines - errors > progress) + { + progress = lines - errors; + } + else + { + printf("D'oh! %d lines read, %d errors - no progress\n", lines, errors); + } + } + + n -= pos; + memmove(buffer, nl + 1, n + 1); + // assert: n points at the first unused byte after moving out the parsed line + nl = strchr(buffer, '\n'); + } + + pos = n; + } + } + + char *stop_scan_ln = "SCAN 0 -1\n"; + write(device, stop_scan_ln, strlen(stop_scan_ln)); + close(device); + + free(buffer); + + printf("Read %d lines, got %d errors. Success rate: %.2f\n", + lines, errors, + lines == 0 ? 0.0: 100 - 100.0 * errors / lines); +} diff --git a/src/main.cpp b/src/main.cpp index 76803a8..ca7dc3b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,8 @@ #define RADIOLIB_CHECK_PARAMS (0) #include +#include +#include #include #include #include @@ -207,8 +209,8 @@ uint64_t drone_detected_frequency_end = 0; bool single_page_scan = false; // #define PRINT_DEBUG -#define PRINT_PROFILE_TIME +#define PRINT_PROFILE_TIME #ifdef PRINT_PROFILE_TIME uint64_t loop_start = 0; uint64_t loop_time = 0; @@ -218,7 +220,6 @@ uint64_t scan_start_time = 0; // log data via serial console, JSON format: // #define LOG_DATA_JSON true -int LOG_DATA_JSON_INTERVAL = 1000; // Log at least every second uint64_t x, y, range_item, w = WATERFALL_START, i = 0; int osd_x = 1, osd_y = 2, col = 0, max_bin = 32; @@ -364,6 +365,8 @@ void osdProcess() } #endif +Config config; + struct RadioScan : Scan { float getRSSI() override; @@ -486,14 +489,50 @@ struct frequency_scan_result uint64_t end; uint64_t last_epoch; int16_t rssi; // deliberately not a float; floats can pin task to wrong core forever + + ScanTaskResult dump; + size_t readings_sz; } frequency_scan_result; TaskHandle_t logToSerial = NULL; +TaskHandle_t dumpToComms = NULL; -void eventListenerForMSP(void *arg, Event &e) +void eventListenerForReport(void *arg, Event &e) { if (e.type == EventType::DETECTED) { + if (e.epoch != frequency_scan_result.last_epoch) + { + frequency_scan_result.dump.sz = 0; + } + + if (frequency_scan_result.dump.sz >= frequency_scan_result.readings_sz) + { + size_t old_sz = frequency_scan_result.readings_sz; + frequency_scan_result.readings_sz = frequency_scan_result.dump.sz + 1; + uint32_t *f = new uint32_t[frequency_scan_result.readings_sz]; + int16_t *r = new int16_t[frequency_scan_result.readings_sz]; + + if (old_sz > 0) + { + memcpy(f, frequency_scan_result.dump.freqs_khz, + old_sz * sizeof(uint32_t)); + memcpy(r, frequency_scan_result.dump.rssis, old_sz * sizeof(int16_t)); + + delete[] frequency_scan_result.dump.freqs_khz; + delete[] frequency_scan_result.dump.rssis; + } + + frequency_scan_result.dump.freqs_khz = f; + frequency_scan_result.dump.rssis = r; + } + + frequency_scan_result.dump.freqs_khz[frequency_scan_result.dump.sz] = + e.detected.freq * 1000; // convert to kHz + frequency_scan_result.dump.rssis[frequency_scan_result.dump.sz] = + max(e.detected.rssi, -999.0f); + frequency_scan_result.dump.sz++; + if (e.epoch != frequency_scan_result.last_epoch || e.detected.rssi > frequency_scan_result.rssi) { @@ -511,10 +550,50 @@ void eventListenerForMSP(void *arg, Event &e) { xTaskNotifyGive(logToSerial); } + + if (dumpToComms != NULL) + { + xTaskNotifyGive(dumpToComms); + } return; } } +ScanTask report_scans = ScanTask{ + count : 0, // 0 => report none; < 0 => report forever; > 0 => report that many + delay : 0 // 0 => as and when it happens; > 0 => at least once that many ms +}; + +void dumpToCommsTask(void *parameter) +{ + uint64_t last_epoch = frequency_scan_result.last_epoch; + + for (;;) + { + int64_t delay = report_scans.delay; + if (delay == 0) + { + delay = (1ull << 63) - 1; + } + + ulTaskNotifyTake(true, pdMS_TO_TICKS(delay)); + if (report_scans.count == 0 || frequency_scan_result.last_epoch == last_epoch) + { + continue; + } + + if (report_scans.count > 0) + { + report_scans.count--; + } + + Message m; + m.type = MessageType::SCAN_RESULT; + m.payload.dump = frequency_scan_result.dump; + Comms0->send(m); + } +} + void logToSerialTask(void *parameter) { #ifdef HELTEC @@ -527,7 +606,7 @@ void logToSerialTask(void *parameter) for (;;) { - ulTaskNotifyTake(true, pdMS_TO_TICKS(LOG_DATA_JSON_INTERVAL)); + ulTaskNotifyTake(true, pdMS_TO_TICKS(config.log_data_json_interval)); if (frequency_scan_result.begin != frequency_scan_result.end || frequency_scan_result.last_epoch != last_epoch) { @@ -612,7 +691,7 @@ void setup(void) #ifdef LILYGO setupBoards(); // true for disable U8g2 display library delay(500); - Serial.println("Setup LiLyGO board is done"); + Serial.println("Setup LiLybeginSDCardGO board is done"); #endif // LED brightness @@ -637,6 +716,17 @@ void setup(void) bt_start = millis(); wf_start = millis(); + config = Config::init(); + r.comms_initialized = Comms::initComms(config); + if (r.comms_initialized) + { + Serial.println("Comms initialized fine"); + } + else + { + Serial.println("Comms did not initialize"); + } + pinMode(LED, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); pinMode(REB_PIN, OUTPUT); @@ -799,6 +889,7 @@ void setup(void) #ifdef LOG_DATA_JSON xTaskCreate(logToSerialTask, "LOG_DATA_JSON", 2048, NULL, 1, &logToSerial); #endif + xTaskCreate(dumpToCommsTask, "DUMP_RESPONSE_PROCESS", 2048, NULL, 1, &dumpToComms); r.trigger_level = TRIGGER_LEVEL; stacked.reset(0, 0, display.width(), display.height()); @@ -836,7 +927,9 @@ void setup(void) r.addEventListener(DETECTED, drone_sound_alarm, &r); r.addEventListener(SCAN_TASK_COMPLETE, stacked); - r.addEventListener(ALL_EVENTS, eventListenerForMSP, NULL); + frequency_scan_result.readings_sz = 0; + frequency_scan_result.dump.sz = 0; + r.addEventListener(ALL_EVENTS, eventListenerForReport, NULL); #ifdef UPTIME_CLOCK uptime = new UptimeClock(display, millis()); @@ -1026,6 +1119,24 @@ void check_ranges() } } +void checkComms() +{ + while (Comms0->available() > 0) + { + Message *m = Comms0->receive(); + if (m == NULL) + continue; + + switch (m->type) + { + case MessageType::SCAN: + report_scans = m->payload.scan; + break; + } + delete m; + } +} + // MAX Frequency RSSI BIN value of the samples int max_rssi_x = 999; @@ -1037,11 +1148,14 @@ void loop(void) drone_detected_frequency_start = 0; ranges_count = 0; -// reset scan time -#ifdef PRINT_PROFILE_TIME - scan_time = 0; - loop_start = millis(); -#endif + checkComms(); + + // reset scan time + if (config.print_profile_time) + { + scan_time = 0; + loop_start = millis(); + } r.epoch++; if (!ANIMATED_RELOAD || !single_page_scan) @@ -1436,10 +1550,13 @@ void loop(void) joy_btn_clicked = false; + if (config.print_profile_time) + { #ifdef PRINT_PROFILE_TIME - loop_time = millis() - loop_start; - Serial.printf("LOOP: %lld ms; SCAN: %lld ms;\n ", loop_time, scan_time); + loop_time = millis() - loop_start; + Serial.printf("LOOP: %lld ms; SCAN: %lld ms;\n ", loop_time, scan_time); #endif + } // No WiFi and BT Scan Without OSD #ifdef OSD_ENABLED #ifdef WIFI_SCANNING_ENABLED