mirror of
https://github.com/Genaker/LoraSA.git
synced 2026-03-28 17:42:59 +01:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,4 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
|
||||
out/
|
||||
out/*
|
||||
@@ -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))
|
||||
|
||||
295
lib/comms/comms.cpp
Normal file
295
lib/comms/comms.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "comms.h"
|
||||
#include <config.h>
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <USB.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
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;
|
||||
}
|
||||
88
lib/comms/comms.h
Normal file
88
lib/comms/comms.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef __COMMS_H
|
||||
#define __COMMS_H
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <config.h>
|
||||
|
||||
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
|
||||
211
lib/config/config.cpp
Normal file
211
lib/config/config.cpp
Normal file
@@ -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);
|
||||
}
|
||||
35
lib/config/config.h
Normal file
35
lib/config/config.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef _LORA_CONFIG_H
|
||||
#define _LORA_CONFIG_H
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
#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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
170
spectrum_scan.c
Normal file
170
spectrum_scan.c
Normal file
@@ -0,0 +1,170 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
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);
|
||||
}
|
||||
143
src/main.cpp
143
src/main.cpp
@@ -52,6 +52,8 @@
|
||||
#define RADIOLIB_CHECK_PARAMS (0)
|
||||
|
||||
#include <charts.h>
|
||||
#include <comms.h>
|
||||
#include <config.h>
|
||||
#include <events.h>
|
||||
#include <scan.h>
|
||||
#include <stdlib.h>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user