Waterfall chart to capture history of detections

This commit is contained in:
Sassa NF
2024-09-30 07:36:36 +01:00
parent 30883386c4
commit ad7ad3d8af
8 changed files with 524 additions and 19 deletions
+57
View File
@@ -0,0 +1,57 @@
#include "charts.h"
#include <cstdint>
void WaterfallChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
Chart::reset(x, y, w, h);
model->reset(model->times[0], w);
update_to = model->buckets;
}
void WaterfallChart::updatePoint(uint64_t t, float x, float y)
{
if (x < min_x || x >= max_x)
{
return;
}
update_to = max(update_to, model->updateModel(t, x2pos(x), y >= level_y));
}
void WaterfallChart::draw()
{
size_t h = min(update_to, (size_t)height);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < width; x++)
{
bool b = model->counts[y][x] > 0 &&
(model->events[y][x] >= model->counts[y][x] * threshold);
if (b)
{
display.setColor(WHITE);
}
else
{
display.setColor(BLACK);
}
display.setPixel(pos_x + x, pos_y + y);
}
}
update_to = 0;
}
int WaterfallChart::x2pos(float x)
{
if (x < min_x)
x = min_x;
if (x > max_x)
x = max_x;
return width * (x - min_x) / (max_x - min_x);
}
+25
View File
@@ -3,6 +3,7 @@
#include <OLEDDisplay.h>
#include <cstdint>
#include <models.h>
#include <stdlib.h>
struct Chart
@@ -128,4 +129,28 @@ struct StackedChart : Chart
void draw() override;
};
struct WaterfallChart : Chart
{
float min_x, max_x;
float level_y, threshold;
size_t update_to;
WaterfallModel *model;
WaterfallChart(OLEDDisplay &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
float min_x, float max_x, float level_y, float threshold,
WaterfallModel *m)
: Chart(d, x, y, w, h), model(m), min_x(min_x), max_x(max_x), level_y(level_y),
threshold(threshold), update_to(m->buckets) {};
void updatePoint(uint64_t t, float x, float y);
void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override;
void draw() override;
int x2pos(float x);
};
#endif
+153
View File
@@ -0,0 +1,153 @@
#include "models.h"
#include <cstring>
WaterfallModel::WaterfallModel(size_t w, uint64_t base_dt, size_t m_sz,
const size_t *multiples)
{
width = w;
size_t dt_sz = 0;
for (int i = 0; i < m_sz; i++)
dt_sz += multiples[i];
buckets = dt_sz;
dt = new uint64_t[buckets];
events = new uint32_t *[buckets];
counts = new uint32_t *[buckets];
times = new uint64_t[buckets];
uint64_t m = base_dt;
for (int i = 0, j = 0; i < m_sz; i++)
{
for (int k = 0; k < multiples[i]; k++, j++)
{
dt[j] = m;
events[j] = new uint32_t[width];
counts[j] = new uint32_t[width];
}
m *= multiples[i];
}
}
void WaterfallModel::reset(uint64_t t0, size_t w)
{
if (w != width)
{
width = w;
for (int i = 0; i < buckets; i++)
{
delete[] counts[i];
delete[] events[i];
counts[i] = new uint32_t[w];
events[i] = new uint32_t[w];
}
}
for (int i = 0; i < buckets; i++)
{
memset(counts[i], 0, width * sizeof(uint32_t));
memset(events[i], 0, width * sizeof(uint32_t));
times[i] = t0 + dt[i];
}
}
/*
* The model is literally a stack of counters:
* - incomplete second
* - n complete seconds
* - incomplete minute
* - n complete minutes
* - ...
*
* updateModel updates incomplete second. When the second becomes complete, it
* gets pushed to complete seconds, and the last complete second is rotated out
* 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 changed = 1;
while (t > times[0])
{
changed = push();
}
counts[0][x]++;
events[0][x] += y;
return changed;
}
size_t WaterfallModel::push()
{
size_t i = 1;
for (; i < buckets; i++)
{
if (dt[i - 1] == dt[i])
continue;
if (times[i - 1] <= times[i])
break;
times[i - 1] = times[i] + dt[i];
}
uint64_t t0 = times[0];
uint32_t *cc = counts[i - 1];
uint32_t *ee = events[i - 1];
memmove(times + 1, times, (i - 1) * sizeof(uint64_t));
memmove(counts + 1, counts, (i - 1) * sizeof(uint32_t *));
memmove(events + 1, events, (i - 1) * sizeof(uint32_t *));
if (i < buckets)
{
for (int j = 0; j < width; j++)
{
counts[i][j] += cc[j];
events[i][j] += ee[j];
}
i++;
}
memset(cc, 0, width * sizeof(uint32_t));
memset(ee, 0, width * sizeof(uint32_t));
counts[0] = cc;
events[0] = ee;
times[0] = t0 + dt[0];
return i;
}
#ifdef TO_STRING
#include <sstream>
#include <string>
#endif
char *WaterfallModel::toString()
{
#ifdef TO_STRING
std::stringstream r;
r << "w:" << width << " b:" << buckets << " [";
for (int i = 0; i < buckets; i++)
{
r << "dt:" << dt[i] << " t:" << times[i] << " [";
for (int j = 0; j < width; j++)
r << " c:" << counts[i][j] << " e:" << events[i][j];
r << " ]";
}
r << " ]";
char *ret = new char[r.str().length() + 1];
strncpy(ret, r.str().c_str(), r.str().length());
#else
char *ret = NULL;
#endif
return ret;
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef CHARTS_MODELS_H
#define CHARTS_MODELS_H
#include <cstdint>
#include <cstring>
#include <stdlib.h>
struct WaterfallModel
{
uint32_t **events;
uint32_t **counts;
uint64_t *times;
uint64_t *dt;
size_t buckets;
size_t width;
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 push();
char *toString();
};
#endif
+21 -5
View File
@@ -366,7 +366,9 @@ void osdProcess()
}
#endif
#define WATERFALL_SENSITIVITY 0.05
DecoratedBarChart *bar;
WaterfallChart *waterChart;
StackedChart stacked(display, 0, 0, 0, 0);
void init_radio()
@@ -664,9 +666,21 @@ void setup(void)
stacked.reset(0, 0, display.width(), display.height() - 6);
size_t *multiples = new size_t[6]{5, 3, 4, 15, 4, 3};
WaterfallModel *model =
new WaterfallModel((size_t)display.width(), 1000, 6, multiples);
model->reset(millis(), display.width());
delete[] multiples;
waterChart =
new WaterfallChart(display, 0, WATERFALL_START, display.width(), 0, FREQ_BEGIN,
FREQ_END, -(float)show_db_after, WATERFALL_SENSITIVITY, model);
stacked.reset(0, 0, display.width(), display.height() - 6);
size_t b = stacked.addChart(bar);
size_t c =
stacked.addChart(new Chart(display, 0, 0, display.width(), 0));
size_t c = stacked.addChart(waterChart);
stacked.setHeight(c, stacked.height - WATERFALL_START);
stacked.setHeight(b, stacked.height);
@@ -1086,6 +1100,8 @@ void loop(void)
rr = LO_RSSI_THRESHOLD;
}
waterChart->updatePoint(millis(), freq, rr);
int updated = bar->bar.updatePoint(freq, rr);
if (first_run || ANIMATED_RELOAD)
@@ -1108,7 +1124,7 @@ void loop(void)
{
waterfall[display_x] = true;
display.setColor(WHITE);
display.setPixel(display_x, w);
// display.setPixel(display_x, w);
}
}
#endif
@@ -1150,7 +1166,7 @@ void loop(void)
// TODO: make something like scrolling up if possible
waterfall[display_x] = false;
display.setColor(BLACK);
display.setPixel(display_x, w);
// display.setPixel(display_x, w);
display.setColor(WHITE);
}
#endif
@@ -1248,7 +1264,7 @@ void loop(void)
if (single_page_scan)
{
display.setColor(BLACK);
display.drawHorizontalLine(0, w, STEPS);
// display.drawHorizontalLine(0, w, STEPS);
display.setColor(WHITE);
}
#endif
-14
View File
@@ -3,10 +3,6 @@
#include "../lib/scan/scan.cpp"
#include <unity.h>
void setUp(void) {}
void tearDown(void) {}
struct TestScan : Scan
{
TestScan(float *ctx, int sz) : ctx(ctx), sz(sz), idx(0) {}
@@ -66,13 +62,3 @@ void test_detect()
TEST_ASSERT_EQUAL_INT16(1, r);
TEST_ASSERT_EQUAL_INT8_ARRAY(expect2, result, test_sz);
}
int main(int argc, char **argv)
{
UNITY_BEGIN();
RUN_TEST(test_rssi);
RUN_TEST(test_detect);
UNITY_END();
}
+220
View File
@@ -0,0 +1,220 @@
#define TO_STRING
#include "../lib/models/WaterfallModel.cpp"
#include <stdio.h>
#include <unity.h>
void test_push()
{
size_t *ms = new size_t[6]{5, 3, 4, 15, 4, 3};
WaterfallModel m(1, 1, 6, ms);
delete ms;
char *r = m.toString();
TEST_ASSERT_EQUAL_STRING("w:1 b:34 "
"[dt:1 t:0 [ c:0 e:0 ]"
"dt:1 t:0 [ c:0 e:0 ]"
"dt:1 t:0 [ c:0 e:0 ]"
"dt:1 t:0 [ c:0 e:0 ]"
"dt:1 t:0 [ c:0 e:0 ]"
"dt:5 t:0 [ c:0 e:0 ]"
"dt:5 t:0 [ c:0 e:0 ]"
"dt:5 t:0 [ c:0 e:0 ]"
"dt:15 t:0 [ c:0 e:0 ]"
"dt:15 t:0 [ c:0 e:0 ]"
"dt:15 t:0 [ c:0 e:0 ]"
"dt:15 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:60 t:0 [ c:0 e:0 ]"
"dt:900 t:0 [ c:0 e:0 ]"
"dt:900 t:0 [ c:0 e:0 ]"
"dt:900 t:0 [ c:0 e:0 ]"
"dt:900 t:0 [ c:0 e:0 ]"
"dt:3600 t:0 [ c:0 e:0 ]"
"dt:3600 t:0 [ c:0 e:0 ]"
"dt:3600 t:0 [ c:0 e:0 ] ]",
r);
delete r;
m.reset(0, 1);
uint64_t i = 0;
for (; i < 10; i++)
m.updateModel(i, 0, 1);
r = m.toString();
TEST_ASSERT_EQUAL_STRING("w:1 b:34 "
"[dt:1 t:9 [ c:1 e:1 ]"
"dt:1 t:8 [ c:1 e:1 ]"
"dt:1 t:7 [ c:1 e:1 ]"
"dt:1 t:6 [ c:1 e:1 ]"
"dt:1 t:5 [ c:1 e:1 ]"
"dt:5 t:5 [ c:5 e:5 ]"
"dt:5 t:5 [ c:0 e:0 ]"
"dt:5 t:5 [ c:0 e:0 ]"
"dt:15 t:15 [ c:0 e:0 ]"
"dt:15 t:15 [ c:0 e:0 ]"
"dt:15 t:15 [ c:0 e:0 ]"
"dt:15 t:15 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ] ]",
r);
delete r;
for (; i < 100; i += 10)
m.updateModel(i, 0, 1);
r = m.toString();
TEST_ASSERT_EQUAL_STRING("w:1 b:34 "
"[dt:1 t:90 [ c:1 e:1 ]"
"dt:1 t:89 [ c:0 e:0 ]"
"dt:1 t:88 [ c:0 e:0 ]"
"dt:1 t:87 [ c:0 e:0 ]"
"dt:1 t:86 [ c:0 e:0 ]"
"dt:5 t:85 [ c:0 e:0 ]"
"dt:5 t:80 [ c:1 e:1 ]"
"dt:5 t:75 [ c:0 e:0 ]"
"dt:15 t:75 [ c:1 e:1 ]"
"dt:15 t:60 [ c:2 e:2 ]"
"dt:15 t:45 [ c:1 e:1 ]"
"dt:15 t:30 [ c:2 e:2 ]"
"dt:60 t:60 [ c:11 e:11 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:60 t:60 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:900 t:900 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ]"
"dt:3600 t:3600 [ c:0 e:0 ] ]",
r);
delete r;
for (; i < 10000; i++)
m.updateModel(i, 0, 1);
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 ] ]",
r);
delete r;
for (; i < 5000; i++)
m.updateModel(i, 0, 1);
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 ] ]",
r);
delete r;
}
+21
View File
@@ -0,0 +1,21 @@
#include <unity.h>
void test_rssi();
void test_detect();
void test_push();
void setUp(void) {}
void tearDown(void) {}
int main(int argc, char **argv)
{
UNITY_BEGIN();
RUN_TEST(test_rssi);
RUN_TEST(test_detect);
RUN_TEST(test_push);
UNITY_END();
}