diff --git a/.vscode/settings.json b/.vscode/settings.json index c2f89c3..b5ca201 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,4 +12,7 @@ }, "files.insertFinalNewline": true, "files.autoSave": "onFocusChange", + "files.associations": { + "cstdint": "cpp" + }, } diff --git a/include/ui.h b/include/ui.h index 2f18a7b..ddf7a73 100644 --- a/include/ui.h +++ b/include/ui.h @@ -8,6 +8,9 @@ #include #endif +#include +#include + // #include // (optional) major and minor tick-marks at x MHz @@ -31,14 +34,21 @@ #define SCREEN_HEIGHT 64 // ???? not used -// publish functions -#ifdef Vision_Master_E290 -extern void UI_Init(DEPG0290BxS800FxX_BW *); -#else -extern void UI_Init(SSD1306Wire *); -#endif -extern void UI_displayDecorate(int, int, bool); -extern void UI_setLedFlag(bool); +extern void UI_Init(Display_t *); extern void UI_clearPlotter(void); extern void UI_clearTopStatus(void); extern void UI_drawCursor(int16_t); + +struct StatusBar : Chart +{ + Scan &r; + bool ui_initialized; + uint16_t scan_progress_count; + + StatusBar(Display_t &d, uint16_t x, uint16_t y, uint16_t w, Scan &r) + : Chart(d, x, y, w, LABEL_HEIGHT), r(r), ui_initialized(false), + scan_progress_count(0) {}; + + virtual void clearStatus(); + virtual void draw() override; +}; diff --git a/lib/charts/BarChart.cpp b/lib/charts/BarChart.cpp new file mode 100644 index 0000000..305cdbd --- /dev/null +++ b/lib/charts/BarChart.cpp @@ -0,0 +1,194 @@ +#include "charts.h" + +void BarChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + if (w != width) + { + delete[] ys; + delete[] changed; + + ys = new float[w]; + changed = new bool[w]; + } + + memset(ys, 0, w * sizeof(float)); + memset(changed, false, w * sizeof(bool)); + redraw_all = true; + + Chart::reset(x, y, w, h); +} + +int BarChart::updatePoint(float x, float y) +{ + if (x < min_x || x >= max_x) + { + return -1; + } + + size_t idx = width * (x - min_x) / (max_x - min_x); + if (idx >= width) + { + idx = width - 1; + } + + if (!changed[idx] || ys[idx] < y) + { + ys[idx] = y; + changed[idx] = true; + } + + return idx; +} + +void BarChart::draw() +{ + for (int x = 0; x < width; x++) + { + if (!changed[x] && !redraw_all) + continue; + + drawOne(x); + } + + redraw_all = false; +} + +void BarChart::drawOne(int x) +{ + if (x < 0) + return; + + int y = y2pos(ys[x]); + + if (y < height) + { + display.setColor(BLACK); + display.drawVerticalLine(pos_x + x, pos_y, y); + display.setColor(WHITE); + display.drawVerticalLine(pos_x + x, pos_y + y, height - y); + } + else + { + display.setColor(BLACK); + display.drawVerticalLine(pos_x + x, pos_y, height); + } + + if (x % 2 == 0) + { + display.setColor(INVERSE); + display.setPixel(pos_x + x, pos_y + y2pos(level_y)); + } + + changed[x] = false; +} + +int BarChart::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); +} + +int BarChart::y2pos(float y) +{ + if (y < min_y) + y = min_y; + if (y > max_y) + y = max_y; + + return height - height * (y - min_y) / (max_y - min_y); +} + +void BarChart::onEvent(Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + level_y = e.emitter.trigger_level; + + int u = updatePoint(e.emitter.current_frequency, e.detected.rssi); + if (e.emitter.animated) + { + drawOne(u); + } +} + +void DecoratedBarChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + Chart::reset(x, y, w, h); + bar.reset(x, y + LABEL_HEIGHT, w, h - LABEL_HEIGHT - AXIS_HEIGHT); +} + +void DecoratedBarChart::draw() +{ + bool draw_axis = bar.redraw_all; + + bar.draw(); + + display.setColor(BLACK); + display.fillRect(pos_x, pos_y, width, bar.pos_y - pos_y); + + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + uint16_t first_untouched = 0; + + for (uint16_t x = 0; x < width; x++) + { + float y = bar.ys[x]; + if (y >= bar.level_y) + { + String s = String(bar.ys[x], 0); + uint16_t w = display.getStringWidth(s); + uint16_t x1 = x; + for (; x < x1 + w && x < width; x++) + { + if (bar.ys[x] > y) + { + y = bar.ys[x]; + s = String(y, 0); + w = max(w, display.getStringWidth(s)); + } + } + + if (x > width && first_untouched <= width - w) + { + x1 = width - w; + } + + first_untouched = x; + + if (x1 + w <= width) + display.drawString(pos_x + x1, pos_y, s); + } + } + + if (draw_axis) + { + display.setColor(WHITE); + + uint16_t y = pos_y + height - AXIS_HEIGHT + 2; + display.fillRect(pos_x, y - 1, width, X_AXIS_WEIGHT); + + // Start and end ticks + display.fillRect(pos_x, y - 1, 2, AXIS_HEIGHT); + display.fillRect(pos_x + width - 2, y - 1, 2, AXIS_HEIGHT); + + for (float step = 0; bar.min_x + step * MAJOR_TICKS < bar.max_x; step += 1) + { + int tick_pos = bar.x2pos(bar.min_x + step * MAJOR_TICKS); + display.drawVerticalLine(pos_x + tick_pos, y, MAJOR_TICK_LENGTH); + } + + for (float step = 0; bar.min_x + step * MINOR_TICKS < bar.max_x; step += 1) + { + int tick_pos = bar.x2pos(bar.min_x + step * MINOR_TICKS); + display.drawVerticalLine(pos_x + tick_pos, y, MINOR_TICK_LENGTH); + } + } +} diff --git a/lib/charts/StackedChart.cpp b/lib/charts/StackedChart.cpp new file mode 100644 index 0000000..2c6ca73 --- /dev/null +++ b/lib/charts/StackedChart.cpp @@ -0,0 +1,95 @@ +#include "charts.h" + +uint16_t trim_w(uint16_t pos, uint16_t width, uint16_t w) +{ + return min(width, (uint16_t)(max(w, pos) - pos)); +} + +size_t StackedChart::addChart(Chart *c) +{ + Chart **cc = new Chart *[charts_sz + 1]; + memcpy(cc, charts, charts_sz * sizeof(Chart *)); + cc[charts_sz] = c; + free(charts); + + c->reset(pos_x + c->pos_x, pos_y + c->pos_y, trim_w(c->pos_x, c->width, width), + c->height); + charts = cc; + return charts_sz++; +} + +uint16_t StackedChart::setHeight(size_t c, uint16_t h) +{ + if (h < height) + { + charts[c]->reset(charts[c]->pos_x, charts[c]->pos_y, charts[c]->width, h); + + uint16_t used_space = 0; + for (int i = 0; i < charts_sz; i++) + { + used_space += charts[i]->height; + } + + return used_space; + } + // this chart gets special treatment - pack all other charts, + // and make this one as big as possible + uint16_t used_space = 0; + for (int i = 0; i < c; i++) + { + charts[i]->reset(charts[i]->pos_x, pos_y + used_space, charts[i]->width, + charts[i]->height); + used_space += charts[i]->height; + } + + uint16_t more_used_space = used_space; + for (int i = c + 1; i < charts_sz; i++) + { + more_used_space += charts[i]->height; + } + + if (more_used_space < height) + { + charts[c]->reset(charts[c]->pos_x, pos_y + used_space, charts[c]->width, + height - more_used_space); + used_space += charts[c]->height; + } + + for (int i = c + 1; i < charts_sz; i++) + { + charts[i]->reset(charts[i]->pos_x, pos_y + used_space, charts[i]->width, + charts[i]->height); + used_space += charts[i]->height; + } + + return used_space; +} + +void StackedChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + for (int i = 0; i < charts_sz; i++) + { + uint16_t rel_x = charts[i]->pos_x - pos_x; + uint16_t rel_y = charts[i]->pos_y - pos_y; + charts[i]->reset(x + rel_x, y + rel_y, trim_w(rel_x, charts[i]->width, w), + charts[i]->height); + } + + Chart::reset(x, y, w, h); +} + +void StackedChart::draw() +{ + for (int i = 0; i < charts_sz; i++) + charts[i]->draw(); +} + +void StackedChart::onEvent(Event &e) +{ + if (e.type != SCAN_TASK_COMPLETE) + { + return; + } + + draw(); +} diff --git a/lib/charts/UptimeClock.cpp b/lib/charts/UptimeClock.cpp new file mode 100644 index 0000000..f7e229e --- /dev/null +++ b/lib/charts/UptimeClock.cpp @@ -0,0 +1,28 @@ +#include "charts.h" + +void UptimeClock::draw(uint64_t t) +{ + t1 = t; + draw(); +} + +void UptimeClock::draw() +{ + uint64_t uptime = t1 - t0; + int mils = uptime % 1000; + int seconds = (uptime / 1000) % 60; + int minutes = (uptime / 60000) % 60; + int hours = uptime / 3600000; + String s = String(hours) + (minutes < 10 ? ":0" : ":") + String(minutes) + + (seconds < 10 ? ":0" : ":") + String(seconds) + + (mils < 10 ? ".00" + : mils < 100 ? ".0" + : ".") + + String(mils); + int w = display.getStringWidth(s); + display.setColor(BLACK); + display.fillRect((display.width() - w) / 2, display.height() / 2 - 3, w, 7); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH); + display.drawString(display.width() / 2, display.height() / 2, s); +} diff --git a/lib/charts/WaterfallChart.cpp b/lib/charts/WaterfallChart.cpp new file mode 100644 index 0000000..f3b6553 --- /dev/null +++ b/lib/charts/WaterfallChart.cpp @@ -0,0 +1,69 @@ +#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); +} + +void WaterfallChart::onEvent(Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + level_y = e.emitter.trigger_level; + + updatePoint(e.time_ms, e.detected.freq, e.detected.rssi); +} diff --git a/lib/charts/charts.h b/lib/charts/charts.h new file mode 100644 index 0000000..d159cc0 --- /dev/null +++ b/lib/charts/charts.h @@ -0,0 +1,178 @@ +#ifndef CHARTS_H +#define CHARTS_H + +#ifdef Vision_Master_E290 +#include "HT_DEPG0290BxS800FxX_BW.h" +typedef DEPG0290BxS800FxX_BW Display_t; +#else +#include +typedef OLEDDisplay Display_t; +#endif + +#include +#include +#include +#include + +struct Chart +{ + uint16_t pos_x, pos_y; + uint16_t width, height; + Display_t &display; + + Chart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : display(d), pos_x(x), pos_y(y), width(w), height(h) {}; + + /* + * This method resets the state and sets the reference time. + */ + virtual void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + { + pos_x = x; + pos_y = y; + width = w; + height = h; + } + + /* + * Redraw everything that needs redrawing. + */ + virtual void draw() {}; +}; + +/* + * ProgressChart supports updates with progressive redraw of just the affected area. + */ +struct ProgressChart : Chart +{ + ProgressChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : Chart(d, x, y, w, h) {}; + + /* + * Update one data point, and return what column needs redrawing. + */ + virtual int updatePoint(float x, float y) = 0; + + /* + * If you fancy animated progress, then pass the output of updatePoint to here. + */ + virtual void drawOne(int x) = 0; +}; + +struct BarChart : ProgressChart, Listener +{ + float min_x, max_x, min_y, max_y; + float level_y; + + float *ys; + bool *changed; + bool redraw_all; + + BarChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, float min_x, + float max_x, float min_y, float max_y, float level_y) + : ProgressChart(d, x, y, w, h), min_x(min_x), max_x(max_x), min_y(min_y), + max_y(max_y), level_y(level_y), redraw_all(true) + { + ys = new float[w]; + changed = new bool[w]; + + memset(ys, 0, w * sizeof(float)); + memset(changed, 0, w * sizeof(bool)); + }; + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + int updatePoint(float x, float y) override; + void drawOne(int x) override; + void draw() override; + void onEvent(Event &) override; + + int x2pos(float x); + int y2pos(float y); +}; + +#define LABEL_HEIGHT 7 +#define X_AXIS_WEIGHT 1 +#define MAJOR_TICK_LENGTH 2 +#define MAJOR_TICKS 10 +#define MINOR_TICK_LENGTH 1 +#define MINOR_TICKS 5 +#define AXIS_HEIGHT (X_AXIS_WEIGHT + MAJOR_TICK_LENGTH + 2) +struct DecoratedBarChart : Chart +{ + BarChart bar; + + DecoratedBarChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + float min_x, float max_x, float min_y, float max_y, float level_y) + : Chart(d, x, y, w, h), + bar(d, x, y + LABEL_HEIGHT, w, h - LABEL_HEIGHT - AXIS_HEIGHT, min_x, max_x, + min_y, max_y, level_y) {}; + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + void draw() override; +}; + +struct StackedChart : Chart, Listener +{ + Chart **charts; + size_t charts_sz; + + StackedChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : Chart(d, x, y, w, h), charts(NULL), charts_sz(0) {}; + + /* + * addChart adds c to the StackedChart, treats pos_x and pos_y of the chart + * as relative to this chart's origin, and trims width to fit. Adjust the + * height and pack charts using setHeight. + */ + size_t addChart(Chart *c); + + /* + * Adjust the height of the chart and return the resulting required height. + * If h is >= height, the chart gets a special treatment: packs all other + * charts, and uses up the rest of space. + */ + uint16_t setHeight(size_t c, uint16_t h); + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + + void draw() override; + + void onEvent(Event &e) override; +}; + +struct WaterfallChart : Chart, Listener +{ + float min_x, max_x; + float level_y, threshold; + + size_t update_to; + + WaterfallModel *model; + + WaterfallChart(Display_t &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; + void onEvent(Event &e) override; + + int x2pos(float x); +}; + +struct UptimeClock : Chart +{ + uint64_t t0; + uint64_t t1; + UptimeClock(Display_t &d, uint64_t t0) : Chart(d, 0, 0, 0, 0), t0(t0), t1(t0) {}; + + void draw(uint64_t t); + virtual void draw() override; +}; + +#endif diff --git a/lib/events/events.h b/lib/events/events.h new file mode 100644 index 0000000..539277c --- /dev/null +++ b/lib/events/events.h @@ -0,0 +1,45 @@ +#ifndef LORASA_EVENTS_H +#define LORASA_EVENTS_H + +struct Event; +enum EventType +{ + DETECTED = 0, + SCAN_TASK_COMPLETE, + _MAX_EVENT_TYPE // unused as event type +}; +struct Listener; + +#include +#include + +struct Event +{ + EventType type; + uint64_t epoch; + uint64_t time_ms; + + Scan &emitter; + + union + { + struct + { + float rssi; + float freq; + bool trigger; + bool detected; + size_t detected_at; + } detected; + }; + + Event(Scan &emitter, EventType type, uint64_t time_ms) + : emitter(emitter), type(type), epoch(emitter.epoch), time_ms(time_ms) {}; +}; + +struct Listener +{ + virtual void onEvent(Event &event) = 0; +}; + +#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/lib/scan/scan.cpp b/lib/scan/scan.cpp index bc58902..8c4fbed 100644 --- a/lib/scan/scan.cpp +++ b/lib/scan/scan.cpp @@ -66,10 +66,10 @@ uint16_t Scan::rssiMethod(size_t samples, uint16_t *result, size_t res_size) return max_signal; } -size_t Scan::detect(uint16_t *result, bool *filtered_result, size_t result_size, - int samples) +Event Scan::detect(uint16_t *result, bool *filtered_result, size_t result_size, + int samples) { - size_t max_rssi_x = 999; + size_t max_rssi_x = result_size; for (int y = 0; y < result_size; y++) { @@ -122,7 +122,61 @@ size_t Scan::detect(uint16_t *result, bool *filtered_result, size_t result_size, } } - return max_rssi_x; + Event event(*this, EventType::DETECTED, 0); + event.epoch = epoch; + event.detected.detected = max_rssi_x < result_size; + event.detected.freq = current_frequency; + event.detected.rssi = + event.detected.detected ? -(float)result[max_rssi_x] : LO_RSSI_THRESHOLD; + event.detected.detected_at = max_rssi_x; + event.detected.trigger = + event.detected.detected && event.detected.rssi >= trigger_level; + detection_count++; + + return event; +} + +size_t Scan::addEventListener(EventType t, Listener &l) +{ + size_t c = listener_count[(size_t)t]; + Listener **new_list = new Listener *[c + 1]; + new_list[c] = &l; + listener_count[(size_t)t] = c + 1; + + if (c > 0) + { + Listener **old_list = eventListeners[(size_t)t]; + memcpy(new_list, old_list, c * sizeof(Listener *)); + delete[] old_list; + } + + eventListeners[(size_t)t] = new_list; + return c; +} + +struct CallbackFunction : Listener +{ + void (*cb)(void *arg, Event &e); + void *arg; + + CallbackFunction(void cb(void *arg, Event &e), void *arg) : cb(cb), arg(arg) {} + + void onEvent(Event &e) { cb(arg, e); } +}; + +size_t Scan::addEventListener(EventType t, void cb(void *arg, Event &e), void *arg) +{ + return addEventListener(t, *(new CallbackFunction(cb, arg))); +} + +void Scan::fireEvent(Event &event) +{ + Listener **list = eventListeners[(size_t)event.type]; + size_t c = listener_count[(size_t)event.type]; + for (int i = 0; i < c; i++) + { + list[i]->onEvent(event); + } } #endif diff --git a/lib/scan/scan.h b/lib/scan/scan.h index f5af2c6..b46ef83 100644 --- a/lib/scan/scan.h +++ b/lib/scan/scan.h @@ -1,4 +1,5 @@ #include +#include #include #ifndef LORASA_CORE_H @@ -33,6 +34,27 @@ constexpr float LO_RSSI_THRESHOLD = HI_RSSI_THRESHOLD - 66; struct Scan { + uint64_t epoch; + float current_frequency; + uint64_t fr_begin; + uint64_t fr_end; + uint64_t drone_detection_level; + bool sound_on; + bool led_flag; + uint64_t detection_count; + bool animated; + float trigger_level; + + Listener **eventListeners[(size_t)EventType::_MAX_EVENT_TYPE]; + size_t listener_count[(size_t)EventType::_MAX_EVENT_TYPE]; + + 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, + } {}; + virtual float getRSSI() = 0; // rssiMethod gets the data similar to the scan method, @@ -43,8 +65,12 @@ struct Scan // those values that represent a detection event. // It returns index that represents strongest signal at which a detection event // occurred. - static size_t detect(uint16_t *result, bool *filtered_result, size_t result_size, - int samples); + Event detect(uint16_t *result, bool *filtered_result, size_t result_size, + int samples); + + size_t addEventListener(EventType t, Listener &l); + size_t addEventListener(EventType t, void cb(void *, Event &), void *arg); + void fireEvent(Event &e); }; // Remove reading without neighbors diff --git a/platformio.ini b/platformio.ini index 4995061..32ddad0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -59,6 +59,8 @@ board_build.f_cpu = 240000000 lib_deps = ropg/Heltec_ESP32_LoRa_v3@^0.9.1 RadioLib + U8g2 + XPowersLib build_flags = -DLILYGO -DT3_S3_V1_2_SX1262 @@ -72,7 +74,7 @@ build_flags = -DARDUINO_USB_MODE=1 -[env:lilygo-T3S3-v1-2-xs1280] +[env:lilygo-T3S3-v1-2-sx1280] platform = espressif32 board = t3_s3_v1_x framework = arduino diff --git a/src/main.cpp b/src/main.cpp index 0e91b25..bda4cde 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,7 +24,9 @@ // #define HELTEC_NO_DISPLAY #include +#ifdef HELTEC #include +#endif #include #include @@ -39,6 +41,8 @@ // library internals. #define RADIOLIB_GODMODE (1) +#include +#include #include #ifndef LILYGO @@ -169,13 +173,11 @@ constexpr int WINDOW_SIZE = 15; #define SINGLE_STEP (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)) uint64_t range = (int)(FREQ_END - FREQ_BEGIN); -uint64_t fr_begin = FREQ_BEGIN; -uint64_t fr_end = FREQ_BEGIN; uint64_t iterations = RANGE / RANGE_PER_PAGE; // uint64_t range_frequency = FREQ_END - FREQ_BEGIN; -uint64_t median_frequency = FREQ_BEGIN + FREQ_END - FREQ_BEGIN / 2; +uint64_t median_frequency = (FREQ_BEGIN + FREQ_END) / 2; // #define DISABLE_PLOT_CHART false // unused @@ -187,8 +189,7 @@ bool filtered_result[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; int max_bins_array_value[MAX_POWER_LEVELS]; int max_step_range = 32; -// Waterfall array -bool waterfall[STEPS], detected_y[STEPS]; // 20 - ??? steps of the waterfall +bool detected_y[STEPS]; // 20 - ??? steps // global variable @@ -197,12 +198,10 @@ bool first_run, new_pixel, detected_x = false; // drone detection flag bool detected = false; uint64_t drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL; -uint64_t show_db_after = 80; +#define TRIGGER_LEVEL -80.0 uint64_t drone_detected_frequency_start = 0; uint64_t drone_detected_frequency_end = 0; -uint64_t detection_count = 0; bool single_page_scan = false; -bool SOUND_ON = false; // #define PRINT_DEBUG #define PRINT_PROFILE_TIME @@ -222,7 +221,6 @@ uint64_t x, y, range_item, w = WATERFALL_START, i = 0; int osd_x = 1, osd_y = 2, col = 0, max_bin = 32; uint64_t ranges_count = 0; -float freq = 0; int rssi = 0; int state = 0; @@ -235,7 +233,6 @@ constexpr int samples = SAMPLES_RSSI; uint8_t result_index = 0; uint8_t button_pressed_counter = 0; -uint64_t loop_cnt = 0; #ifndef LILYGO // #define JOYSTICK_ENABLED @@ -363,6 +360,34 @@ void osdProcess() } #endif +struct RadioScan : Scan +{ + float getRSSI() override; +}; + +float RadioScan::getRSSI() +{ +#ifdef USING_SX1280PA + // radio.startReceive(); + // get instantaneous RSSI value + // When PR will be merged we can use radi.getRSSI(false); + uint8_t data[3] = {0, 0, 0}; // RssiInst, Status, RFU + radio.mod->SPIreadStream(RADIOLIB_SX128X_CMD_GET_RSSI_INST, data, 3); + return ((float)data[0] / (-2.0)); +#else + return radio.getRSSI(false); +#endif +} + +RadioScan r; + +#define WATERFALL_SENSITIVITY 0.05 +DecoratedBarChart *bar; +WaterfallChart *waterChart; +StackedChart stacked(display, 0, 0, 0, 0); + +UptimeClock *uptime; + void init_radio() { // initialize SX1262 FSK modem at the initial frequency @@ -450,7 +475,9 @@ struct frequency_scan_result void logToSerialTask(void *parameter) { +#ifdef HELTEC JsonDocument doc; +#endif char jsonOutput[200]; for (;;) @@ -475,17 +502,26 @@ void logToSerialTask(void *parameter) continue; } +#ifdef HELTEC doc["low_range_freq"] = frequency_scan_result.begin; doc["high_range_freq"] = frequency_scan_result.end; doc["value"] = max_result; serializeJson(doc, jsonOutput); Serial.println(jsonOutput); +#else + Serial.printf("{\"low_range_freq\": %ull, \"high_range_freq\": %ull, " + "\"value\": \"%s\"}\n", + frequency_scan_result.begin, frequency_scan_result.end, + max_result); +#endif } vTaskDelay(LOG_DATA_JSON_INTERVAL / portTICK_PERIOD_MS); } } +void drone_sound_alarm(void *arg, Event &e); + void setup(void) { #ifdef LILYGO @@ -513,7 +549,6 @@ void setup(void) #endif float vbat; float resolution; - loop_cnt = 0; bt_start = millis(); wf_start = millis(); @@ -532,7 +567,7 @@ void setup(void) delay(10); if (button.pressed()) { - SOUND_ON = !SOUND_ON; + r.sound_on = !r.sound_on; tone(BUZZER_PIN, 205, 100); delay(50); tone(BUZZER_PIN, 205, 100); @@ -642,6 +677,45 @@ void setup(void) #ifdef LOG_DATA_JSON xTaskCreate(logToSerialTask, "LOG_DATA_JSON", 2048, NULL, 1, NULL); #endif + + r.trigger_level = TRIGGER_LEVEL; + stacked.reset(0, 0, display.width(), display.height()); + + bar = new DecoratedBarChart(display, 0, 0, display.width(), 0, FREQ_BEGIN, FREQ_END, + LO_RSSI_THRESHOLD, HI_RSSI_THRESHOLD, r.trigger_level); + + size_t b = stacked.addChart(bar); + + Chart *statusBar = new StatusBar(display, 0, 0, display.width(), r); + +#if (WATERFALL_ENABLED == true) + 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, r.trigger_level, WATERFALL_SENSITIVITY, model); + + size_t c = stacked.addChart(waterChart); + stacked.setHeight(c, stacked.height - WATERFALL_START - statusBar->height); + + r.addEventListener(DETECTED, *waterChart); +#endif + + size_t d = stacked.addChart(statusBar); + stacked.setHeight(b, stacked.height); + + r.addEventListener(DETECTED, bar->bar); + r.addEventListener(DETECTED, drone_sound_alarm, &r); + r.addEventListener(SCAN_TASK_COMPLETE, stacked); + +#ifdef UPTIME_CLOCK + uptime = new UptimeClock(display, millis()); +#endif } // Formula to translate 33 bin to approximate RSSI value @@ -651,10 +725,9 @@ int binToRSSI(int bin) return 11 + (bin * 4); } -// return true if continue the code is false break the loop -bool buttonPressHandler(float freq) +// is there an input using Hot Button or joystick +bool buttonInputRequested() { - // Detection level button short press if (button.pressedFor(100) #ifdef JOYSTICK_ENABLED || joy_btn_click() @@ -662,73 +735,80 @@ bool buttonPressHandler(float freq) ) { button.update(); - button_pressed_counter = 0; - // if long press stop - while (button.pressedNow() + if (button.pressedNow() #ifdef JOYSTICK_ENABLED - || joy_btn_click() + || joy_btn_click() #endif ) { - // Print Curent frequency once - if (button_pressed_counter == 0) - { - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.drawString(128 / 2, 0, String(freq)); - display.display(); - } - delay(10); - button_pressed_counter++; - if (button_pressed_counter > 150) - { - digitalWrite(LED, HIGH); - delay(150); - digitalWrite(LED, LOW); - } - } - if (button_pressed_counter > 150) - { - // Remove Curent Frequency Text - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setColor(BLACK); - display.drawString(128 / 2, 0, String(freq)); - display.setColor(WHITE); - display.display(); - return false; - } - if (button_pressed_counter > 50 && button_pressed_counter < 150) - { - if (!joy_btn_clicked) - { - // Visually confirm it's off so user releases button - display.displayOff(); - // Deep sleep (has wait for release so we don't wake up - // immediately) - heltec_deep_sleep(); - } - return false; - } - button.update(); - display.setTextAlignment(TEXT_ALIGN_RIGHT); - // erase old drone detection level value - display.setColor(BLACK); - display.fillRect(128 - 13, 0, 13, 13); - display.setColor(WHITE); - drone_detection_level++; - // print new value - display.drawString(128, 0, String(drone_detection_level)); - tone(BUZZER_PIN, 104, 150); - if (drone_detection_level > 30) - { - drone_detection_level = 1; + return true; } } - return true; + + return false; } -void drone_sound_alarm(int drone_detection_level, int detection_count, - int tone_freq_db = 205) +enum ButtonEvent { + NONE = 0, + LONG_PRESS, + SHORT_PRESS, + TOO_SHORT, + SUSPEND +}; + +ButtonEvent buttonPressEvent() +{ + button_pressed_counter = 0; + // if long press stop + while (button.pressedNow() +#ifdef JOYSTICK_ENABLED + || joy_btn_click() +#endif + ) + { + delay(10); + button_pressed_counter++; + if (button_pressed_counter > 150) + { + digitalWrite(LED, HIGH); + delay(150); + digitalWrite(LED, LOW); + } + } + if (button_pressed_counter > 150) + { + return LONG_PRESS; + } + + if (button_pressed_counter > 50) + { + if (!joy_btn_clicked) + { + return SUSPEND; + } + return SHORT_PRESS; + } + button.update(); + + return TOO_SHORT; +} + +void drone_sound_alarm(void *arg, Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + Scan &r = *((Scan *)arg); + if (!r.sound_on) + return; + + int tone_freq_db = e.detected.detected_at * 2; + int drone_detection_level = r.drone_detection_level; + int detection_count = r.detection_count; + // If level is set to sensitive, // start beeping every 10th frequency and shorter // it improves performance less short beep delays... @@ -740,12 +820,12 @@ void drone_sound_alarm(int drone_detection_level, int detection_count, tone_freq_db = 285 - tone_freq_db; } - if (detection_count == 1 && SOUND_ON) + if (r.detection_count == 1 && r.sound_on) { tone(BUZZER_PIN, tone_freq_db, 10); // same action ??? but first time } - if (detection_count % 5 == 0 && SOUND_ON) + if (r.detection_count % 5 == 0 && r.sound_on) { tone(BUZZER_PIN, tone_freq_db, 10); // same action ??? but every 5th time @@ -753,7 +833,7 @@ void drone_sound_alarm(int drone_detection_level, int detection_count, } else { - if (detection_count % 20 == 0 && SOUND_ON) + if (r.detection_count % 20 == 0 && r.sound_on) { tone(BUZZER_PIN, 205, 10); // same action ??? but every 20th detection @@ -767,7 +847,7 @@ void joystickMoveCursor(int joy_x_pressed) if (joy_x_pressed > 0) { cursor_x_position--; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -775,7 +855,7 @@ void joystickMoveCursor(int joy_x_pressed) else if (joy_x_pressed < 0) { cursor_x_position++; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -783,7 +863,7 @@ void joystickMoveCursor(int joy_x_pressed) if (cursor_x_position > DISPLAY_WIDTH || cursor_x_position < 0) { cursor_x_position = 0; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -821,45 +901,23 @@ void check_ranges() } } -struct RadioScan : Scan -{ - float getRSSI() override; -}; - -float RadioScan::getRSSI() -{ -#ifdef USING_SX1280PA - // radio.startReceive(); - // get instantaneous RSSI value - // When PR will be merged we can use radi.getRSSI(false); - uint8_t data[3] = {0, 0, 0}; // RssiInst, Status, RFU - radio.mod->SPIreadStream(RADIOLIB_SX128X_CMD_GET_RSSI_INST, data, 3); - return ((float)data[0] / (-2.0)); -#else - return radio.getRSSI(false); -#endif -} - // MAX Frequency RSSI BIN value of the samples int max_rssi_x = 999; -RadioScan r; - void loop(void) { - UI_displayDecorate(0, 0, false); // some default values + r.led_flag = false; - detection_count = 0; + r.detection_count = 0; drone_detected_frequency_start = 0; ranges_count = 0; // reset scan time #ifdef PRINT_PROFILE_TIME scan_time = 0; - // general purpose loop counter - loop_cnt++; loop_start = millis(); #endif + r.epoch++; if (!ANIMATED_RELOAD || !single_page_scan) { @@ -875,8 +933,8 @@ void loop(void) RANGE_PER_PAGE = range; } - fr_begin = FREQ_BEGIN; - fr_end = fr_begin; + r.fr_begin = 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 @@ -898,14 +956,14 @@ void loop(void) range = RANGE_PER_PAGE; if (ranges_count == 0) { - fr_begin = (range_item == 0) ? fr_begin : fr_begin += range; - fr_end = fr_begin + RANGE_PER_PAGE; + r.fr_begin = (range_item == 0) ? r.fr_begin : r.fr_begin + range; + r.fr_end = r.fr_begin + RANGE_PER_PAGE; } else { - fr_begin = SCAN_RANGES[range_item] / 1000; - fr_end = SCAN_RANGES[range_item] % 1000; - range = fr_end - fr_begin; + r.fr_begin = SCAN_RANGES[range_item] / 1000; + r.fr_end = SCAN_RANGES[range_item] % 1000; + range = r.fr_end - r.fr_begin; } #ifdef DISABLED_CODE @@ -916,11 +974,6 @@ void loop(void) } #endif - if (single_page_scan == false) - { - UI_displayDecorate(fr_begin, fr_end, true); - } - drone_detected_frequency_start = 0; display.setTextAlignment(TEXT_ALIGN_RIGHT); @@ -951,26 +1004,29 @@ 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; - waterfall[display_x] = false; float step = (range * ((float)x / (STEPS * SCAN_RBW_FACTOR))); - freq = fr_begin + step; - LOG("setFrequency:%f\n", freq); + r.current_frequency = r.fr_begin + step; + LOG("setFrequency:%f\n", r.current_frequency); #ifdef USING_SX1280PA - state = radio.setFrequency(freq); // 1280 doesn't have calibration + 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(freq, false); // false = no calibration need here + state = radio.setFrequency(r.current_frequency, + false); // false = 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(freq)); - Serial.println("E(" + String(state) + "):setFrequency:" + String(freq)); + 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++; @@ -978,7 +1034,7 @@ void loop(void) continue; } - LOG("Step:%d Freq: %f\n", x, freq); + LOG("Step:%d Freq: %f\n", x, r.current_frequency); // SpectralScan Method #ifdef METHOD_SPECTRAL { @@ -1012,6 +1068,7 @@ void loop(void) LOG("METHOD RSSI"); uint16_t max_rssi = r.rssiMethod(SAMPLES_RSSI, result, RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE); + if (max_x_rssi[display_x] > max_rssi) { max_x_rssi[display_x] = max_rssi; @@ -1036,65 +1093,38 @@ void loop(void) display.setColor(WHITE); } #endif - size_t detected_at = r.detect( - result, filtered_result, RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE, samples); + Event event = r.detect(result, filtered_result, + RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE, samples); + event.time_ms = millis(); + size_t detected_at = event.detected.detected_at; if (max_rssi_x > detected_at) { // MAx bin Value not RSSI max_rssi_x = detected_at; } - detected = detected_at < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE; + detected = event.detected.detected; detected_y[display_x] = false; -#if FILTER_SPECTRUM_RESULTS - for (int y = 0; y < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE; y++) - { - // calculating max window x RSSI after filters - x_window = (int)(display_x / WINDOW_SIZE); - int abs_result = abs(result[y]); - if (filtered_result[y] == 1 && result[y] != 0 && result[y] != 1 && - max_x_window[x_window] > abs_result) - { - max_x_window[x_window] = abs_result; - LOG("MAX x window: %i %i\n", x_window, abs_result); - } - } -#endif + float rr = event.detected.rssi; + r.drone_detection_level = drone_detection_level; - if (detected_at <= drone_detection_level) + if (event.detected.trigger) { // check if we should alarm about a drone presence if (detected_y[display_x] == false) // detection threshold match { // Set LED to ON (filtered in UI component) - UI_setLedFlag(true); -#if (WATERFALL_ENABLED == true) - if (single_page_scan) - { - // Drone detection true for waterfall - if (!waterfall[display_x]) - { - waterfall[display_x] = true; - display.setColor(WHITE); - display.setPixel(display_x, w); - } - } -#endif + r.led_flag = true; if (drone_detected_frequency_start == 0) { // mark freq start - drone_detected_frequency_start = freq; + drone_detected_frequency_start = r.current_frequency; } // mark freq end ... will shift right to last detected range - drone_detected_frequency_end = freq; - if (SOUND_ON == true) - { - drone_sound_alarm(drone_detection_level, detection_count, - max_rssi_x * 2); - } + drone_detected_frequency_end = r.current_frequency; #ifdef LOG_DATA_JSON frequency_scan_result.begin = drone_detected_frequency_start; @@ -1113,64 +1143,15 @@ void loop(void) #endif } } -#if (WATERFALL_ENABLED == true) - if ((single_page_scan) && (waterfall[display_x] != true) && new_pixel) - { - // If drone not found set dark pixel on the waterfall - // TODO: make something like scrolling up if possible - waterfall[display_x] = false; - display.setColor(BLACK); - display.setPixel(display_x, w); - display.setColor(WHITE); - } -#endif } -#ifdef PRINT_DEBUG - for (int y = 0; y < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE; y++) - { - if (filtered_result[y] == 1) - { - LOG("Pixel:%i(%i):%i,", display_x, x, y); - } - } -#endif - - for (int y = 0; y < min(RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE, - MAX_POWER_LEVELS - START_LOW); - y++) - { - if (filtered_result[y] == 1) - { - // Set MAIN signal level pixel - display.setPixelColor(display_x, y + START_LOW, WHITE); - } - } - - // ------------------------------------------------------------- - // Draw "Detection Level line" every 2 pixel - // ------------------------------------------------------------- - if (display_x % 2 == 0) - { - if (filtered_result[drone_detection_level] == 1) - { - display.setColor(INVERSE); - } - else - { - display.setColor(WHITE); - } - display.setPixel(display_x, drone_detection_level + START_LOW); - // display.setPixel(display_x, y + START_LOW - 1); // 2 px wide - - display.setColor(WHITE); - } + r.fireEvent(event); #ifdef JOYSTICK_ENABLED // Draw joystick cursor and Frequency RSSI value if (display_x == cursor_x_position) { - display.drawString(display_x - 1, 0, String((int)freq)); + display.drawString(display_x - 1, 0, String((int)r.current_frequency)); display.drawLine(display_x, 1, display_x, 12); // if method scan RSSI we can get exact RSSI value display.drawString(display_x + 17, 0, "-" + String((int)max_rssi_x * 4)); @@ -1180,22 +1161,76 @@ void loop(void) #ifdef PRINT_PROFILE_TIME scan_time += (millis() - scan_start_time); #endif - // count detected - if (detected) - { - detection_count++; - } - #ifdef PRINT_DEBUG Serial.println("....\n"); #endif - if (first_run || ANIMATED_RELOAD) + if (r.animated) { display.display(); } - if (buttonPressHandler(freq) == false) - break; + if (buttonInputRequested()) + { + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(display.width() / 2, 0, String(r.current_frequency)); + display.display(); + + ButtonEvent e = buttonPressEvent(); + + if (e == LONG_PRESS) + { + // Remove Curent Frequency Text + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setColor(BLACK); + display.drawString(display.width() / 2, 0, + String(r.current_frequency)); + display.setColor(WHITE); + display.display(); + + break; + } + + if (e == SUSPEND) + { + // Visually confirm it's off so user releases button + display.displayOff(); + // Deep sleep (has wait for release so we don't wake up + // immediately) + heltec_deep_sleep(); + break; + } + + if (e == SHORT_PRESS) + break; + + if (e == TOO_SHORT) + { + String v = String(r.trigger_level) + " dB"; + uint16_t w = display.getStringWidth(v); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + // erase old drone detection level value + display.setColor(BLACK); + display.fillRect(display.width() - w, 0, 13, w); + display.setColor(WHITE); + + // dt is roughly single-pixel increment + float dt = + bar->bar.height == 0 + ? 0.0 + : (LO_RSSI_THRESHOLD - HI_RSSI_THRESHOLD) / bar->bar.height; + r.trigger_level += dt; + if (r.trigger_level <= LO_RSSI_THRESHOLD) + { + r.trigger_level = HI_RSSI_THRESHOLD; + } + + // print new value + display.drawString(display.width(), 0, v); + tone(BUZZER_PIN, 104, 150); + + bar->bar.redraw_all = true; + } + } // wait a little bit before the next scan, // otherwise the SX1262 hangs @@ -1243,29 +1278,16 @@ void loop(void) { w = WATERFALL_START; } -#if (WATERFALL_ENABLED == true) - // Draw waterfall position cursor - if (single_page_scan) - { - display.setColor(BLACK); - display.drawHorizontalLine(0, w, STEPS); - display.setColor(WHITE); - } -#endif -#ifdef METHOD_RSSI - // Printing Max Window DB. - for (int x2 = 0; x2 < STEPS / WINDOW_SIZE; x2++) { - if (max_x_window[x2] < show_db_after && max_x_window[x2] != 0) - { - display.drawString(x2 * WINDOW_SIZE + WINDOW_SIZE, 0, - "-" + String(max_x_window[x2])); - } - max_x_window[x2] = 999; + Event event(r, SCAN_TASK_COMPLETE, millis()); + r.fireEvent(event); } -#endif // Render display data here + +#ifdef UPTIME_CLOCK + uptime->draw(millis()); +#endif display.display(); #ifdef OSD_ENABLED // Sometimes OSD prints entire screen with the digits. diff --git a/src/ui.cpp b/src/ui.cpp index ee8bc92..e488060 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -2,6 +2,8 @@ #include "RadioLib.h" #include "global_config.h" #include "images.h" +#include +#include // ------------------------------------------------- // LOCAL DEFINES @@ -12,24 +14,9 @@ // #define SCALE_TEXT_TOP (HEIGHT + X_AXIS_WEIGHT + MAJOR_TICK_LENGTH) -static unsigned int start_scan_text = (128 / 2) - 3; -// initialized flag -static bool ui_initialized = false; -static bool led_flag = false; -static unsigned short int scan_progress_count = 0; - -#ifdef Vision_Master_E290 -static DEPG0290BxS800FxX_BW *display_instance; -#else -//(0x3c, SDA_OLED, SCL_OLED, DISPLAY_GEOMETRY); -static SSD1306Wire *display_instance; -#endif // temporary dirty import ... to be solved durring upcoming refactoring -extern unsigned int drone_detection_level; extern unsigned int RANGE_PER_PAGE; extern unsigned int median_frequency; -extern unsigned int detection_count; -extern bool SOUND_ON; extern unsigned int drone_detected_frequency_start; extern unsigned int drone_detected_frequency_end; extern unsigned int ranges_count; @@ -40,233 +27,138 @@ extern unsigned int range_item; extern uint64_t loop_time; -#ifndef Vision_Master_E290 -void UI_Init(SSD1306Wire *display_ptr) +void UI_Init(Display_t *display_ptr) { - // init pointer to display instance. - display_instance = display_ptr; // check for null ??? - display_instance->clear(); + display_ptr->clear(); // draw the UCOG welcome logo - display_instance->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); - display_instance->display(); + display_ptr->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); + display_ptr->display(); } -#endif -#ifdef Vision_Master_E290 -void UI_Init(DEPG0290BxS800FxX_BW *display_ptr) -{ - // init pointer to display instance. - display_instance = display_ptr; - // check for null ??? - display_instance->clear(); - // draw the UCOG welcome logo - display_instance->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); - display_instance->display(); -} -#endif - -void UI_setLedFlag(bool new_status) { led_flag = new_status; } - -void clearStatus(void) +void StatusBar::clearStatus(void) { // clear status line - display_instance->setColor(BLACK); - display_instance->fillRect(0, ROW_STATUS_TEXT + 2, 128, 13); - display_instance->setColor(WHITE); + display.setColor(BLACK); + display.fillRect(pos_x, pos_y, width, height); } void UI_clearPlotter(void) { // clear the scan plot rectangle (top part) - display_instance->setColor(BLACK); - display_instance->fillRect(0, 10, STEPS, HEIGHT - 10); - display_instance->setColor(WHITE); + // display_instance->setColor(BLACK); + // display_instance->fillRect(0, 10, STEPS, HEIGHT - 10); + // display_instance->setColor(WHITE); } void UI_clearTopStatus(void) { // clear the scan plot rectangle (top part) - display_instance->setColor(BLACK); - display_instance->fillRect(0, 0, STEPS, 10); - display_instance->setColor(WHITE); -} - -/** - * @brief Draws ticks on the display at regular whole intervals. - * - * @param every The interval between ticks in MHz. - * @param length The length of each tick in pixels. - */ -void drawTicks(float every, int length) -{ - int first_tick; - bool correction; - int pixels_per_step; - int correction_number; - int tick; - int tick_minor; - int median; - - first_tick = 0; - //+ (every - (fr_begin - (int)(fr_begin / every) * every)); - /*if (first_tick < fr_begin) - { - first_tick += every; - }*/ - correction = false; - pixels_per_step = STEPS / (RANGE_PER_PAGE / every); - if (STEPS / RANGE_PER_PAGE != 0) - { - correction = true; - } - correction_number = STEPS - (int)(pixels_per_step * (RANGE_PER_PAGE / every)); - tick = 0; - tick_minor = 0; - median = (RANGE_PER_PAGE / every) / 2; - // TODO: (RANGE_PER_PAGE / every) - // * 2 has twice extra steps we need to figureout correct logic or minor - // ticks is not showing to the end - for (int t = 0; t <= (RANGE_PER_PAGE / every) * 2; t++) - { - // fix if pixels per step is not int and we have shift - if (correction && t % 2 != 0 && correction_number > 1) - { - // pixels_per_step++; - correction_number--; - } - tick += pixels_per_step; - tick_minor = tick / 2; - if (tick <= 128 - 3) - { - display_instance->drawLine(tick, HEIGHT + X_AXIS_WEIGHT, tick, - HEIGHT + X_AXIS_WEIGHT + length); - // Central tick - if (tick > (128 / 2) - 3 && tick < (128 / 2) + 3) - { - display_instance->drawLine(tick + 1, HEIGHT + X_AXIS_WEIGHT, tick + 1, - HEIGHT + X_AXIS_WEIGHT + length); - } - } -#ifdef MINOR_TICKS - // Fix two ticks together - if ((tick_minor + 1 != tick) && (tick_minor - 1 != tick) && - (tick_minor + 2 != tick) && (tick_minor - 2 != tick)) - { - display_instance->drawLine(tick_minor, HEIGHT + X_AXIS_WEIGHT, tick_minor, - HEIGHT + X_AXIS_WEIGHT + MINOR_TICK_LENGTH); - } - // Central tick - if (tick_minor > (128 / 2) - 3 && tick_minor < (128 / 2) + 3) - { - display_instance->drawLine(tick_minor + 1, HEIGHT + X_AXIS_WEIGHT, - tick_minor + 1, - HEIGHT + X_AXIS_WEIGHT + MINOR_TICK_LENGTH); - } -#endif - } + // display_instance->setColor(BLACK); + // display_instance->fillRect(0, 0, STEPS, 10); + // display_instance->setColor(WHITE); } void UI_drawCursor(int16_t possition) { // Draw animated vertical cursor on reload process - display_instance->setColor(BLACK); - display_instance->drawVerticalLine(possition, 0, HEIGHT); - display_instance->drawVerticalLine(possition + 1, 0, HEIGHT); - display_instance->drawVerticalLine(possition + 2, 0, HEIGHT); - display_instance->setColor(WHITE); + // display_instance->setColor(BLACK); + // display_instance->drawVerticalLine(possition, 0, HEIGHT); + // display_instance->drawVerticalLine(possition + 1, 0, HEIGHT); + // display_instance->drawVerticalLine(possition + 2, 0, HEIGHT); + // display_instance->setColor(WHITE); } /** * @brief Decorates the display: everything but the plot itself. */ -void UI_displayDecorate(int begin = 0, int end = 0, bool redraw = false) +void StatusBar::draw() { + uint16_t text_y = pos_y + height - 10; + if (!ui_initialized) { - // Start and end ticks - display_instance->fillRect(0, HEIGHT + X_AXIS_WEIGHT, 2, MAJOR_TICK_LENGTH + 1); - display_instance->fillRect(126, HEIGHT + X_AXIS_WEIGHT, 2, MAJOR_TICK_LENGTH + 1); // Drone detection level - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, 0, String(drone_detection_level)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(width, 0, String(r.drone_detection_level)); } - if (!ui_initialized || redraw) + if (!ui_initialized) { // Clear something - display_instance->setColor(BLACK); - display_instance->fillRect(0, SCALE_TEXT_TOP + 1, 128, 12); - display_instance->setColor(WHITE); - + /* display_instance->setColor(BLACK); + display_instance->fillRect(0, SCALE_TEXT_TOP + 1, 128, 12); + display_instance->setColor(WHITE); + */ // Drone detection level - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, 0, String(drone_detection_level)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, 0, String(r.drone_detection_level)); // Frequency start - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, - (begin == 0) ? String(FREQ_BEGIN) : String(begin)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, + (r.fr_begin == 0) ? String(FREQ_BEGIN) : String(r.fr_begin)); // Frequency detected - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); - display_instance->drawString(128 / 2, ROW_STATUS_TEXT, - (begin == 0) ? String(median_frequency) - : String(begin + ((end - begin) / 2))); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(pos_x + width / 2, text_y, + (r.fr_begin == 0) + ? String(median_frequency) + : String(r.fr_begin + ((r.fr_end - r.fr_begin) / 2))); // Frequency end - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, - (end == 0) ? String(FREQ_END) : String(end)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, text_y, + (r.fr_end == 0) ? String(FREQ_END) : String(r.fr_end)); } // Status text block - if (led_flag) // 'drone' detected + if (r.led_flag) // 'drone' detected { - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); + display.setTextAlignment(TEXT_ALIGN_CENTER); // clear status line clearStatus(); - display_instance->drawString(start_scan_text + 2, ROW_STATUS_TEXT, - String(drone_detected_frequency_start) + ">RF<" + - String(drone_detected_frequency_end)); + display.setColor(WHITE); + display.drawString(pos_x + width / 2, text_y, + String(drone_detected_frequency_start) + ">RF<" + + String(drone_detected_frequency_end)); } else { // "Scanning" - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); + display.setTextAlignment(TEXT_ALIGN_CENTER); // clear status line clearStatus(); - if (scan_progress_count == 0) + String s = "Scan \\"; + if (scan_progress_count == 1) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan \\"); - } - else if (scan_progress_count == 1) - { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan |"); + s = "Scan |"; } else if (scan_progress_count == 2) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan /"); + s = "Scan /"; } else if (scan_progress_count == 3) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan -"); + s = "Scan -"; } scan_progress_count++; if (scan_progress_count >= 4) { scan_progress_count = 0; } + display.setColor(WHITE); + display.drawString(pos_x + width / 2 - 3, text_y, s); } - if (led_flag == true && detection_count >= 5) + if (r.led_flag && r.detection_count >= 5) { digitalWrite(LED, HIGH); - if (SOUND_ON) + if (r.sound_on) { tone(BUZZER_PIN, 104, 100); } digitalWrite(REB_PIN, HIGH); - led_flag = false; + r.led_flag = false; } - else if (!redraw) + else if (!r.led_flag) { digitalWrite(LED, LOW); } @@ -274,39 +166,29 @@ void UI_displayDecorate(int begin = 0, int end = 0, bool redraw = false) if (ranges_count == 0) { #ifdef DEBUG - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, String(loop_time)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, String(loop_time)); #else - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, String(FREQ_BEGIN)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, String(FREQ_BEGIN)); #endif - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, String(FREQ_END)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, text_y, String(FREQ_END)); } else if (ranges_count > 0) { - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, - String(SCAN_RANGES[range_item] / 1000) + "-" + - String(SCAN_RANGES[range_item] % 1000)); + 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) { - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, - String(SCAN_RANGES[range_item + 1] / 1000) + - "-" + - String(SCAN_RANGES[range_item + 1] % 1000)); + 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)); } } - if (ui_initialized == false) - { - // X-axis - display_instance->fillRect(0, HEIGHT, STEPS, X_AXIS_WEIGHT); -// ticks -#ifdef MAJOR_TICKS - drawTicks(MAJOR_TICKS, MAJOR_TICK_LENGTH); -#endif - } ui_initialized = true; } diff --git a/test/test_rssi.cpp b/test/test_rssi.cpp index 8a75b60..a38d166 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) {} @@ -52,27 +48,20 @@ void test_detect() uint16_t samples[test_sz] = {20, 50, 55, 60, 0, 70, 75, 80, 0, 90, 0, 100, 110}; bool result[test_sz]; - size_t r = Scan::detect(samples, result, test_sz, 1); + TestScan test_scan({}, 0); + Event e = test_scan.detect(samples, result, test_sz, 1); + size_t r = e.detected.detected_at; bool expect[test_sz] = {1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1}; TEST_ASSERT_EQUAL_INT16(0, r); TEST_ASSERT_EQUAL_INT8_ARRAY(expect, result, test_sz); - r = Scan::detect(samples, result, test_sz, 2); + Event e2 = test_scan.detect(samples, result, test_sz, 2); + r = e2.detected.detected_at; bool expect2[test_sz] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0}; 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(); +}