CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
MsgTransferUi.cpp
Go to the documentation of this file.
1
7
8#include "AppUiInternal.h"
9#include "cdc_msg/MessageTransfer.h"
10#include "cdc_hal/IDisplay.h"
12#include "cdc_log.h"
13
14#include <cstdio>
15#include <cstring>
16
17namespace cdc::ui {
18
19namespace {
20
21using cdc::msg::MessageTransfer;
22using cdc::msg::PeerInfo;
23using cdc::msg::SendState;
24
25constexpr uint8_t kMaxPeers = 12;
26constexpr uint32_t kPeerRefreshMs = 1500;
27constexpr uint32_t kPickerScanMs = 2500;
28
29// ---- consent ----
30InfoView* s_consentView = nullptr;
31char s_consentText[160];
32
33// ---- peer picker / beacon scan ----
34ListItem s_peerItems[kMaxPeers];
35PeerInfo s_peers[kMaxPeers];
36uint8_t s_peerCount = 0;
37bool s_peerReadonly = false;
38uint32_t s_peerLastRefreshMs = 0;
39uint32_t s_peerFingerprint = 0;
40char s_peerTitle[32];
41
42// ---- progress / picker request state ----
43bool s_progressIsSend = false;
44bool s_pickerPending = false;
45uint32_t s_pickerStartMs = 0;
46
47// Forward declarations for free functions used before their definitions.
48void refreshPeers();
49void openPeerView(bool readonly);
50void pushProgressView(bool isSend);
51void onPeerSelect(uint16_t index, void* userData);
52
53// ===========================================================================
54// Progress view
55// ===========================================================================
56
57class MsgProgressView : public ViewBase {
58public:
59 void setSend(bool isSend) { isSend_ = isSend; lastPct_ = 255; markDirty(); }
60
61 void render(bool partial) override {
62 (void)partial;
63 hal::IDisplay* display = hal::getDisplayInstance();
64 if (!display) return;
65 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
66 if (!gfx) return;
67
68 const uint16_t width = display->getWidth();
69 const uint16_t height = display->getHeight();
70
71 gfx->fillScreen(EPD_WHITE);
72 gfx->setFont(nullptr);
73 gfx->setTextColor(EPD_BLACK);
74 gfx->setTextSize(1);
75
76 const char* title = isSend_ ? ui::tr("core.msg_sending") : ui::tr("core.msg_receiving");
77 render::drawHeaderLeft(gfx, title, 6, 14, width);
78
79 uint32_t total = 0;
80 uint32_t done = isSend_ ? MessageTransfer::instance().sendProgress(&total)
81 : MessageTransfer::instance().recvProgress(&total);
82 uint8_t pct = (total > 0) ? static_cast<uint8_t>((done * 100ULL) / total) : 0;
83
84 const int barX = 10;
85 const int barY = 55;
86 const int barW = width - 20;
87 const int barH = 18;
88 render::drawDialogFrame(gfx, barX, barY, barW, barH);
89 int fill = (barW - 4) * pct / 100;
90 if (fill > 0) gfx->fillRect(barX + 2, barY + 2, fill, barH - 4, EPD_BLACK);
91
92 char line[48];
93 snprintf(line, sizeof(line), "%u%% (%lu / %lu B)", pct,
94 static_cast<unsigned long>(done), static_cast<unsigned long>(total));
95 gfx->setCursor(barX, barY + barH + 10);
96 render::printText(gfx, line);
97
98 render::drawFooterBar(gfx, width, height, nullptr, ui::tr("core.cancel"), true);
99 lastPct_ = pct;
100 }
101
102 bool needsRender() const override { return dirty_; }
103
104 void onTick(uint32_t /*nowMs*/) override {
105 uint32_t total = 0;
106 uint32_t done = isSend_ ? MessageTransfer::instance().sendProgress(&total)
107 : MessageTransfer::instance().recvProgress(&total);
108 uint8_t pct = (total > 0) ? static_cast<uint8_t>((done * 100ULL) / total) : 0;
109 if (pct != lastPct_) markDirty();
110 }
111
112 InputResult onKey(char key) override {
113 if (key == 'N') {
114 if (isSend_) MessageTransfer::instance().cancelSend();
116 }
118 }
119
120 const char* getName() const override { return "MsgProgressView"; }
121
122private:
123 bool isSend_ = false;
124 uint8_t lastPct_ = 255;
125};
126
127MsgProgressView* s_progressView = nullptr;
128
129// ===========================================================================
130// Peer picker / beacon scan view
131// ===========================================================================
132
134bool renderPeerRow(Gdey029T94* gfx, const ListItem& item, uint16_t index,
135 int x, int y, int w, int h, bool selected, void* userCtx) {
136 (void)index; (void)h; (void)userCtx;
137 if (!gfx || !item.userData) return false;
138 const auto* p = reinterpret_cast<const PeerInfo*>(item.userData);
139 gfx->setFont(nullptr);
140 gfx->setTextSize(1);
141 int baseline = y + 6;
142 drawSignalBars(gfx, x + 4, baseline - 6, p->rssi, selected);
143 gfx->setCursor(x + 22, baseline);
144 render::printText(gfx, p->name[0] ? p->name : "?");
145 char rssiBuf[8];
146 snprintf(rssiBuf, sizeof(rssiBuf), "%d", p->rssi);
147 int16_t rx1, ry1; uint16_t rw, rh;
148 gfx->getTextBounds(rssiBuf, 0, 0, &rx1, &ry1, &rw, &rh);
149 gfx->setCursor(x + w - rw - 4, baseline);
150 gfx->print(rssiBuf);
151 return true;
152}
153
154class MsgPeerView : public ListView {
155public:
156 void onEnter(void* context) override {
157 ListView::onEnter(context);
158 startScanForMode();
159 s_peerLastRefreshMs = 0;
160 }
161 void onResume() override {
163 startScanForMode();
164 }
165 void onPause() override {
166 // Covered by another view/modal: stop the continuous scan so it only runs
167 // while the beacon-scan is actually visible. onResume() restarts it.
168 if (s_peerReadonly) MessageTransfer::instance().stopDiscovery();
169 }
170 void onExit() override {
171 if (s_peerReadonly) {
172 MessageTransfer::instance().stopDiscovery(); // end the continuous scan
173 } else if (MessageTransfer::instance().sendState() == SendState::PickingPeer) {
174 // User backed out of an interactive send before picking; cancel it.
175 MessageTransfer::instance().cancelSend();
176 }
178 }
179 void onTick(uint32_t nowMs) override {
180 if (nowMs - s_peerLastRefreshMs < kPeerRefreshMs) return;
181 s_peerLastRefreshMs = nowMs;
182 if (s_peerReadonly) {
183 // Beacon scan: continuous multi-role scan. Poll results, and re-arm
184 // the scan if it ever stopped (cancel race, controller hiccup).
185 if (MessageTransfer::instance().discoveryDone()) {
186 MessageTransfer::instance().startDiscovery(0, true);
187 }
188 refreshPeers();
189 } else if (MessageTransfer::instance().discoveryDone()) {
190 // Send picker: burst scan, restart each cycle.
191 refreshPeers();
192 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
193 }
194 }
195 const char* getName() const override { return "MsgPeerView"; }
196
197private:
198 // Beacon scan runs a continuous scan while the beacon keeps advertising
199 // (so two scanning badges see each other); the send picker uses burst scans.
200 static void startScanForMode() {
201 if (s_peerReadonly) {
202 MessageTransfer::instance().startDiscovery(0, true);
203 } else {
204 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
205 }
206 }
207};
208
209MsgPeerView* s_peerView = nullptr;
210
214uint32_t computePeerFingerprint() {
215 uint32_t fp = s_peerCount;
216 for (uint8_t i = 0; i < s_peerCount; ++i) {
217 for (uint8_t b = 0; b < 6; ++b) fp = fp * 31u + s_peers[i].addr[b];
218 }
219 return fp;
220}
221
222void refreshPeers() {
223 if (!s_peerView) return;
224 s_peerCount = MessageTransfer::instance().getPeers(s_peers, kMaxPeers);
225 uint32_t fp = computePeerFingerprint();
226 if (fp == s_peerFingerprint) return; // unchanged set, spare the e-paper
227 s_peerFingerprint = fp;
228 if (s_peerCount == 0) {
229 s_peerItems[0] = {ui::tr("core.msg_searching"), 0, true, nullptr};
230 s_peerView->init(s_peerTitle, s_peerItems, 1);
231 return;
232 }
233 for (uint8_t i = 0; i < s_peerCount; ++i) {
234 s_peerItems[i] = {s_peers[i].name[0] ? s_peers[i].name : "?", 0, false, &s_peers[i]};
235 }
236 s_peerView->init(s_peerTitle, s_peerItems, s_peerCount);
237}
238
239void onPeerSelect(uint16_t index, void* /*userData*/) {
240 if (index >= s_peerCount) return;
241 const PeerInfo& p = s_peers[index];
242 if (s_peerReadonly) {
243 char info[96];
244 snprintf(info, sizeof(info), "%s\n\nRSSI: %d dBm", p.name[0] ? p.name : "?", p.rssi);
245 showInfo(ui::tr("core.msg_beacon_scan"), info);
246 return;
247 }
248 if (MessageTransfer::instance().confirmInteractiveTarget(p.addr, p.addrType)) {
249 // Push (not replace) over the picker: ViewStack ticks only the top stack
250 // view (+ top modal), so the picker beneath stops scanning until progress
251 // pops. On success the completion handler pops the picker too.
252 pushProgressView(true);
253 ViewStack::instance().push(s_progressView);
254 } else {
255 showToastError(ui::tr("core.msg_transfer_fail"));
256 }
257}
258
259void openPeerView(bool readonly) {
260 s_peerReadonly = readonly;
261 s_peerCount = 0;
262 if (!s_peerView) {
263 s_peerView = new MsgPeerView();
264 s_peerView->setItemRenderer(renderPeerRow, nullptr);
265 s_peerView->setOnSelect(onPeerSelect);
266 }
267 snprintf(s_peerTitle, sizeof(s_peerTitle), "%s",
268 readonly ? ui::tr("core.msg_beacon_scan") : ui::tr("core.msg_pick_peer"));
269 s_peerItems[0] = {ui::tr("core.msg_searching"), 0, true, nullptr};
270 s_peerView->init(s_peerTitle, s_peerItems, 1);
271 s_peerFingerprint = computePeerFingerprint(); // matches the empty placeholder
272 ViewStack::instance().push(s_peerView);
273}
274
275void pushProgressView(bool isSend) {
276 if (!s_progressView) s_progressView = new MsgProgressView();
277 s_progressView->setSend(isSend);
278 s_progressIsSend = isSend;
279}
280
281// ===========================================================================
282// Consent prompt
283// ===========================================================================
284
285void onMsgConsentYes(void* /*ud*/) {
287 MessageTransfer::instance().respondConsent(true);
288 pushProgressView(false);
289 ViewStack::instance().push(s_progressView);
290}
291
292void onMsgConsentNo(void* /*ud*/) {
294 MessageTransfer::instance().respondConsent(false);
295}
296
297void onMsgConsentRequestEvent(const core::Event& /*evt*/) {
298 // Receiving requires an ephemeral pairing, which the lock screen rejects
299 // anyway; decline up front (mirrors the numeric-comparison pairing prompt)
300 // so no consent modal or peer name surfaces over the lock screen.
301 if (isBadgeLocked()) {
302 MessageTransfer::instance().respondConsent(false);
303 return;
304 }
305 char peerName[cdc::msg::kNameBufSize] = {};
306 char mime[cdc::msg::kMimeBufSize] = {};
307 const char* descKey = nullptr;
308 uint32_t size = 0;
309 if (!MessageTransfer::instance().getPendingConsent(peerName, sizeof(peerName), mime,
310 sizeof(mime), &descKey, &size)) {
311 return;
312 }
313 const char* what = (descKey && descKey[0]) ? ui::tr(descKey) : mime;
314 char fromLine[64];
315 snprintf(fromLine, sizeof(fromLine), ui::tr("core.msg_offer_from"),
316 peerName[0] ? peerName : "?");
317 snprintf(s_consentText, sizeof(s_consentText), "%s\n\n%s\n%lu B",
318 fromLine, what, static_cast<unsigned long>(size));
319
320 if (!s_consentView) s_consentView = new InfoView();
321 s_consentView->init(ui::tr("core.msg_offer_title"), s_consentText);
322 s_consentView->setYesNoCallbacks(onMsgConsentYes, onMsgConsentNo, nullptr);
323 ViewStack::instance().showModal(s_consentView);
324}
325
326void onMsgExchangeCompleteEvent(const core::Event& /*evt*/) {
327 MessageTransfer::TransferResult res;
328 bool have = MessageTransfer::instance().consumeResult(&res);
329
330 if (s_progressView && ViewStack::instance().current() == s_progressView) {
332 // For an interactive send, also drop the peer picker beneath so the user
333 // returns to the module/plugin view instead of a re-scanning picker.
334 if (have && res.wasSend && s_peerView &&
335 ViewStack::instance().current() == static_cast<IView*>(s_peerView)) {
337 }
338 }
339 if (have) {
340 if (res.ok) {
341 showToastSuccess(ui::tr("core.msg_transfer_ok"));
342 } else if (res.reason == cdc::msg::Reason::UserDeclined) {
343 showToastInfo(ui::tr("core.msg_declined"));
344 } else if (res.reason == cdc::msg::Reason::NoHandler) {
345 showToastError(ui::tr("core.msg_unsupported"));
346 } else {
347 showToastError(ui::tr("core.msg_transfer_fail"));
348 }
349 }
350}
351
352// ===========================================================================
353// Beacon menu (Tools): on/off toggle, display name, scan
354// ===========================================================================
355
356ListView* s_beaconMenu = nullptr;
357ListItem s_beaconItems[3];
358
359enum BeaconMenuIdx { BM_TOGGLE = 0, BM_NAME, BM_SCAN, BM_COUNT };
360
361void rebuildBeaconMenu() {
362 auto& m = MessageTransfer::instance();
363 s_beaconItems[BM_TOGGLE] = {
364 m.isBeaconEnabled() ? ui::tr("core.msg_beacon_on") : ui::tr("core.msg_beacon_off"),
365 static_cast<uint8_t>(m.isBeaconActive() ? '*' : 0), false, nullptr};
366 s_beaconItems[BM_NAME] = {ui::tr("core.msg_beacon_name"), 0, false, nullptr};
367 s_beaconItems[BM_SCAN] = {ui::tr("core.msg_beacon_scan"), 0, false, nullptr};
368 if (s_beaconMenu) s_beaconMenu->init(ui::tr("core.msg_beacon"), s_beaconItems, BM_COUNT);
369}
370
371void showBeaconNameSetup() {
372 showT9Input(ui::tr("core.msg_beacon_name"),
373 MessageTransfer::instance().getBeaconName(),
374 [](const char* text) { MessageTransfer::instance().setBeaconName(text); },
375 cdc::msg::kMaxNameLen);
376}
377
378void onBeaconMenuSelect(uint16_t index, void* /*userData*/) {
379 switch (index) {
380 case BM_TOGGLE: {
381 auto& m = MessageTransfer::instance();
382 m.setBeaconEnabled(!m.isBeaconEnabled());
383 rebuildBeaconMenu();
384 return;
385 }
386 case BM_NAME:
387 showBeaconNameSetup();
388 return;
389 case BM_SCAN:
391 return;
392 }
393}
394
395} // namespace
396
397// ===========================================================================
398// Public entry points (declared in AppUiInternal.h)
399// ===========================================================================
400
402 if (!s_beaconMenu) {
403 s_beaconMenu = new ListView();
404 s_beaconMenu->setOnSelect(onBeaconMenuSelect);
405 }
406 rebuildBeaconMenu();
407 ViewStack::instance().push(s_beaconMenu);
408}
409
418
419void msgTransferUiProcess(uint32_t nowMs) {
420 if (MessageTransfer::instance().takeInteractiveRequest()) {
421 s_pickerPending = true;
422 s_pickerStartMs = nowMs;
423 showToastInfo(ui::tr("core.ble_scanning"), 0);
424 }
425 if (s_pickerPending && (nowMs - s_pickerStartMs >= kPickerScanMs ||
426 MessageTransfer::instance().discoveryDone())) {
427 s_pickerPending = false;
428 ViewStack::instance().hideModal(); // dismiss the scanning toast
429 openPeerView(false);
430 }
431}
432
435 if (!ble || !ble->isEnabled()) {
436 showToastError(ui::tr("core.hw_not_available"));
437 return;
438 }
439 openPeerView(true);
440}
441
442} // namespace cdc::ui
CDC Log: logging over TinyUSB CDC and UART.
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
static constexpr uint32_t eventMask(EventType type)
Definition EventBus.h:127
uint8_t subscribe(EventHandler handler, uint32_t mask=0)
Subscribes an event handler with optional type mask.
Definition EventBus.cpp:52
virtual void onResume()=0
virtual void onExit()=0
virtual void onEnter(void *context=nullptr)=0
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void showModal(IView *modal)
void push(IView *view, void *context=nullptr)
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
IBluetoothController * getBluetoothControllerInstance()
Returns singleton Bluetooth stub when NimBLE is unavailable.
void drawFooterBar(Gdey029T94 *gfx, uint16_t width, uint16_t height, const char *prefix, const char *hint, bool force=false)
Draws footer bar with optional prefix and hint text.
void drawDialogFrame(Gdey029T94 *gfx, int x, int y, int w, int h)
Draws a framed dialog box with double border.
void drawHeaderLeft(Gdey029T94 *gfx, const char *title, int x, int y, uint16_t width, int underlineOffset=18)
void printText(Gdey029T94 *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
void showMsgBeaconScan()
void showBeaconMenu()
InfoView * showInfo(const char *title, const char *text, const char *hint=nullptr)
Shows a shared info view instance and pushes it onto the view stack.
Definition InfoView.cpp:310
Gdey029T94 * display
void msgTransferUiInit()
InputResult
Definition IView.h:10
T9InputView * showT9Input(const char *title, const char *initialText, T9InputView::SaveCallback onSave, uint16_t maxLen=128)
Shows a shared T9 input view instance.
void msgTransferUiProcess(uint32_t nowMs)
void drawSignalBars(Gdey029T94 *gfx, int x, int y, int8_t rssi, bool inverted)
Draws RSSI signal bars using the shared lock-screen visual style.
Definition AppUi.cpp:199
void showToastSuccess(const char *message, uint16_t durationMs=1500)
Shows a success toast message.
void showToastInfo(const char *message, uint16_t durationMs=1500)
Shows an informational toast message.
void showToastError(const char *message, uint16_t durationMs=1500)
Shows an error toast message.
bool isBadgeLocked()
Returns whether the badge is currently locked (showing lock screen with no menu above).
Definition AppUi.cpp:689