diff --git a/lib/charts/WaterfallChart.cpp b/lib/charts/WaterfallChart.cpp new file mode 100644 index 0000000..af83090 --- /dev/null +++ b/lib/charts/WaterfallChart.cpp @@ -0,0 +1,57 @@ +#include "charts.h" +#include + +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); +} diff --git a/lib/charts/charts.h b/lib/charts/charts.h index f50df37..c2c7dfb 100644 --- a/lib/charts/charts.h +++ b/lib/charts/charts.h @@ -3,6 +3,7 @@ #include #include +#include #include 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 diff --git a/lib/models/WaterfallModel.cpp b/lib/models/WaterfallModel.cpp new file mode 100644 index 0000000..99f7816 --- /dev/null +++ b/lib/models/WaterfallModel.cpp @@ -0,0 +1,153 @@ +#include "models.h" +#include + +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 +#include + +#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; +} diff --git a/lib/models/models.h b/lib/models/models.h new file mode 100644 index 0000000..ce8de8f --- /dev/null +++ b/lib/models/models.h @@ -0,0 +1,27 @@ +#ifndef CHARTS_MODELS_H +#define CHARTS_MODELS_H + +#include +#include +#include + +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 diff --git a/src/main.cpp b/src/main.cpp index 639f350..d04b6ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 diff --git a/test/test_rssi.cpp b/test/test_rssi.cpp index 8a75b60..3d82f29 100644 --- a/test/test_rssi.cpp +++ b/test/test_rssi.cpp @@ -3,10 +3,6 @@ #include "../lib/scan/scan.cpp" #include -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(); -} diff --git a/test/test_waterfall.cpp b/test/test_waterfall.cpp new file mode 100644 index 0000000..1875065 --- /dev/null +++ b/test/test_waterfall.cpp @@ -0,0 +1,220 @@ +#define TO_STRING +#include "../lib/models/WaterfallModel.cpp" +#include +#include + +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; +} diff --git a/test/tests.cpp b/test/tests.cpp new file mode 100644 index 0000000..46fa133 --- /dev/null +++ b/test/tests.cpp @@ -0,0 +1,21 @@ +#include + +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(); +}