9#include "cdc_msg/MessageTransfer.h"
21using cdc::msg::MessageTransfer;
22using cdc::msg::PeerInfo;
23using cdc::msg::SendState;
25constexpr uint8_t kMaxPeers = 12;
26constexpr uint32_t kPeerRefreshMs = 1500;
27constexpr uint32_t kPickerScanMs = 2500;
31char s_consentText[160];
32bool s_msgConsentWasLocked =
false;
36PeerInfo s_peers[kMaxPeers];
37uint8_t s_peerCount = 0;
38bool s_peerReadonly =
false;
39uint32_t s_peerLastRefreshMs = 0;
40uint32_t s_peerFingerprint = 0;
44bool s_progressIsSend =
false;
45bool s_pickerPending =
false;
46uint32_t s_pickerStartMs = 0;
50void openPeerView(
bool readonly);
51void pushProgressView(
bool isSend);
52void onPeerSelect(uint16_t index,
void* userData);
58class MsgProgressView :
public ViewBase {
60 void setSend(
bool isSend) { isSend_ = isSend; lastPct_ = 255; markDirty(); }
62 void render(
bool partial)
override {
66 auto* gfx =
static_cast<Gdey029T94*
>(
display->getNativeHandle());
69 const uint16_t width =
display->getWidth();
70 const uint16_t height =
display->getHeight();
72 gfx->fillScreen(EPD_WHITE);
73 gfx->setFont(
nullptr);
74 gfx->setTextColor(EPD_BLACK);
77 const char* title = isSend_ ?
ui::tr(
"core.msg_sending") : ui::
tr(
"core.msg_receiving");
81 uint32_t done = isSend_ ? MessageTransfer::instance().sendProgress(&total)
82 : MessageTransfer::instance().recvProgress(&total);
83 uint8_t pct = (total > 0) ?
static_cast<uint8_t
>((done * 100ULL) / total) : 0;
87 const int barW = width - 20;
90 int fill = (barW - 4) * pct / 100;
91 if (fill > 0) gfx->fillRect(barX + 2, barY + 2, fill, barH - 4, EPD_BLACK);
94 snprintf(line,
sizeof(line),
"%u%% (%lu / %lu B)", pct,
95 static_cast<unsigned long>(done),
static_cast<unsigned long>(total));
96 gfx->setCursor(barX, barY + barH + 10);
103 bool needsRender()
const override {
return dirty_; }
105 void onTick(uint32_t )
override {
107 uint32_t done = isSend_ ? MessageTransfer::instance().sendProgress(&total)
108 : MessageTransfer::instance().recvProgress(&total);
109 uint8_t pct = (total > 0) ?
static_cast<uint8_t
>((done * 100ULL) / total) : 0;
110 if (pct != lastPct_) markDirty();
115 if (isSend_) MessageTransfer::instance().cancelSend();
121 const char* getName()
const override {
return "MsgProgressView"; }
124 bool isSend_ =
false;
125 uint8_t lastPct_ = 255;
128MsgProgressView* s_progressView =
nullptr;
135bool renderPeerRow(Gdey029T94* gfx,
const ListItem& item, uint16_t index,
136 int x,
int y,
int w,
int h,
bool selected,
void* userCtx) {
137 (void)index; (void)h; (void)userCtx;
138 if (!gfx || !item.userData)
return false;
139 const auto* p =
reinterpret_cast<const PeerInfo*
>(item.userData);
140 gfx->setFont(
nullptr);
142 int baseline = y + 6;
144 gfx->setCursor(x + 22, baseline);
147 snprintf(rssiBuf,
sizeof(rssiBuf),
"%d", p->rssi);
148 int16_t rx1, ry1; uint16_t rw, rh;
149 gfx->getTextBounds(rssiBuf, 0, 0, &rx1, &ry1, &rw, &rh);
150 gfx->setCursor(x + w - rw - 4, baseline);
155class MsgPeerView :
public ListView {
157 void onEnter(
void* context)
override {
160 s_peerLastRefreshMs = 0;
162 void onResume()
override {
166 void onPause()
override {
169 if (s_peerReadonly) MessageTransfer::instance().stopDiscovery();
171 void onExit()
override {
172 if (s_peerReadonly) {
173 MessageTransfer::instance().stopDiscovery();
174 }
else if (MessageTransfer::instance().sendState() == SendState::PickingPeer) {
176 MessageTransfer::instance().cancelSend();
180 void onTick(uint32_t nowMs)
override {
181 if (nowMs - s_peerLastRefreshMs < kPeerRefreshMs)
return;
182 s_peerLastRefreshMs = nowMs;
183 if (s_peerReadonly) {
186 if (MessageTransfer::instance().discoveryDone()) {
187 MessageTransfer::instance().startDiscovery(0,
true);
190 }
else if (MessageTransfer::instance().discoveryDone()) {
193 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
196 const char* getName()
const override {
return "MsgPeerView"; }
201 static void startScanForMode() {
202 if (s_peerReadonly) {
203 MessageTransfer::instance().startDiscovery(0,
true);
205 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
210MsgPeerView* s_peerView =
nullptr;
215uint32_t computePeerFingerprint() {
216 uint32_t fp = s_peerCount;
217 for (uint8_t i = 0; i < s_peerCount; ++i) {
218 for (uint8_t b = 0; b < 6; ++b) fp = fp * 31u + s_peers[i].addr[b];
224 if (!s_peerView)
return;
225 s_peerCount = MessageTransfer::instance().getPeers(s_peers, kMaxPeers);
226 uint32_t fp = computePeerFingerprint();
227 if (fp == s_peerFingerprint)
return;
228 s_peerFingerprint = fp;
229 if (s_peerCount == 0) {
230 s_peerItems[0] = {
ui::tr(
"core.msg_searching"), 0,
true,
nullptr};
231 s_peerView->init(s_peerTitle, s_peerItems, 1);
234 for (uint8_t i = 0; i < s_peerCount; ++i) {
235 s_peerItems[i] = {s_peers[i].name[0] ? s_peers[i].name :
"?", 0,
false, &s_peers[i]};
237 s_peerView->init(s_peerTitle, s_peerItems, s_peerCount);
240void onPeerSelect(uint16_t index,
void* ) {
241 if (index >= s_peerCount)
return;
242 const PeerInfo& p = s_peers[index];
243 if (s_peerReadonly) {
245 snprintf(info,
sizeof(info),
"%s\n\nRSSI: %d dBm", p.name[0] ? p.name :
"?", p.rssi);
249 if (MessageTransfer::instance().confirmInteractiveTarget(p.addr, p.addrType)) {
253 pushProgressView(
true);
260void openPeerView(
bool readonly) {
261 s_peerReadonly = readonly;
264 s_peerView =
new MsgPeerView();
265 s_peerView->setItemRenderer(renderPeerRow,
nullptr);
266 s_peerView->setOnSelect(onPeerSelect);
268 snprintf(s_peerTitle,
sizeof(s_peerTitle),
"%s",
269 readonly ?
ui::tr(
"core.msg_beacon_scan") :
ui::tr(
"core.msg_pick_peer"));
270 s_peerItems[0] = {
ui::tr(
"core.msg_searching"), 0,
true,
nullptr};
271 s_peerView->init(s_peerTitle, s_peerItems, 1);
272 s_peerFingerprint = computePeerFingerprint();
276void pushProgressView(
bool isSend) {
277 if (!s_progressView) s_progressView =
new MsgProgressView();
278 s_progressView->setSend(isSend);
279 s_progressIsSend = isSend;
288void onMsgConsentUnlocked(
void* ) {
289 MessageTransfer::instance().respondConsent(
true);
290 pushProgressView(
false);
294void onMsgConsentYes(
void* ) {
296 if (s_msgConsentWasLocked) {
299 s_msgConsentWasLocked =
false;
303 MessageTransfer::instance().respondConsent(
true);
304 pushProgressView(
false);
308void onMsgConsentNo(
void* ) {
310 s_msgConsentWasLocked =
false;
311 MessageTransfer::instance().respondConsent(
false);
314void onMsgConsentRequestEvent(
const core::Event& ) {
315 char peerName[cdc::msg::kNameBufSize] = {};
316 char mime[cdc::msg::kMimeBufSize] = {};
317 const char* descKey =
nullptr;
319 if (!MessageTransfer::instance().getPendingConsent(peerName,
sizeof(peerName), mime,
320 sizeof(mime), &descKey, &size)) {
323 const char* what = (descKey && descKey[0]) ?
ui::tr(descKey) : mime;
325 snprintf(fromLine,
sizeof(fromLine),
ui::tr(
"core.msg_offer_from"),
326 peerName[0] ? peerName :
"?");
327 snprintf(s_consentText,
sizeof(s_consentText),
"%s\n\n%s\n%lu B",
328 fromLine, what,
static_cast<unsigned long>(size));
333 if (s_msgConsentWasLocked) {
338 if (!s_consentView) s_consentView =
new InfoView();
339 s_consentView->init(
ui::tr(
"core.msg_offer_title"), s_consentText);
340 s_consentView->setYesNoCallbacks(onMsgConsentYes, onMsgConsentNo,
nullptr);
344void onMsgExchangeCompleteEvent(
const core::Event& ) {
345 MessageTransfer::TransferResult res;
346 bool have = MessageTransfer::instance().consumeResult(&res);
352 if (have && res.wasSend && s_peerView &&
360 }
else if (res.reason == cdc::msg::Reason::UserDeclined) {
362 }
else if (res.reason == cdc::msg::Reason::NoHandler) {
377enum BeaconMenuIdx { BM_TOGGLE = 0, BM_NAME, BM_SCAN, BM_COUNT };
379void rebuildBeaconMenu() {
380 auto& m = MessageTransfer::instance();
381 s_beaconItems[BM_TOGGLE] = {
382 m.isBeaconEnabled() ?
ui::tr(
"core.msg_beacon_on") : ui::
tr(
"core.msg_beacon_off"),
383 static_cast<uint8_t>(m.isBeaconActive() ?
'*' : 0), false, nullptr};
384 s_beaconItems[BM_NAME] = {
ui::tr(
"core.msg_beacon_name"), 0,
false,
nullptr};
385 s_beaconItems[BM_SCAN] = {
ui::tr(
"core.msg_beacon_scan"), 0,
false,
nullptr};
386 if (s_beaconMenu) s_beaconMenu->init(
ui::tr(
"core.msg_beacon"), s_beaconItems, BM_COUNT);
389void showBeaconNameSetup() {
391 MessageTransfer::instance().getBeaconName(),
392 [](
const char* text) { MessageTransfer::instance().setBeaconName(text); },
393 cdc::msg::kMaxNameLen);
396void onBeaconMenuSelect(uint16_t index,
void* ) {
399 auto& m = MessageTransfer::instance();
400 m.setBeaconEnabled(!m.isBeaconEnabled());
405 showBeaconNameSetup();
422 s_beaconMenu->setOnSelect(onBeaconMenuSelect);
430 onMsgConsentRequestEvent,
433 onMsgExchangeCompleteEvent,
438 if (MessageTransfer::instance().takeInteractiveRequest()) {
439 s_pickerPending =
true;
440 s_pickerStartMs = nowMs;
443 if (s_pickerPending && (nowMs - s_pickerStartMs >= kPickerScanMs ||
444 MessageTransfer::instance().discoveryDone())) {
445 s_pickerPending =
false;
453 if (!ble || !ble->isEnabled()) {
CDC Log: logging over TinyUSB CDC and UART.
static EventBus & instance()
Returns singleton event-bus instance.
static constexpr uint32_t eventMask(EventType type)
uint8_t subscribe(EventHandler handler, uint32_t mask=0)
Subscribes an event handler with optional type mask.
virtual void onResume()=0
virtual void onEnter(void *context=nullptr)=0
static ViewStack & instance()
Returns singleton view-stack instance.
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.
int render(const char *data, uint8_t max_version, uint8_t ecc, uint8_t scale, uint8_t quiet_modules, uint8_t *out, size_t out_size, uint16_t *out_stride_bytes, uint16_t *out_height_px)
Encodes data and packs it into a 1-bpp raster.
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(Adafruit_GFX *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
Centralized key-code constants for cdc_views.
const char * tr(const char *key)
Look up a translation by string key.
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.
void requestUnlockForTransfer(void(*onUnlocked)(void *), void *userData)
Wake the screen and start the PIN-unlock flow for a transfer accepted on the lock screen.
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.
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).