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];
35PeerInfo s_peers[kMaxPeers];
36uint8_t s_peerCount = 0;
37bool s_peerReadonly =
false;
38uint32_t s_peerLastRefreshMs = 0;
39uint32_t s_peerFingerprint = 0;
43bool s_progressIsSend =
false;
44bool s_pickerPending =
false;
45uint32_t s_pickerStartMs = 0;
49void openPeerView(
bool readonly);
50void pushProgressView(
bool isSend);
51void onPeerSelect(uint16_t index,
void* userData);
57class MsgProgressView :
public ViewBase {
59 void setSend(
bool isSend) { isSend_ = isSend; lastPct_ = 255; markDirty(); }
61 void render(
bool partial)
override {
65 auto* gfx =
static_cast<Gdey029T94*
>(
display->getNativeHandle());
68 const uint16_t width =
display->getWidth();
69 const uint16_t height =
display->getHeight();
71 gfx->fillScreen(EPD_WHITE);
72 gfx->setFont(
nullptr);
73 gfx->setTextColor(EPD_BLACK);
76 const char* title = isSend_ ?
ui::tr(
"core.msg_sending") : ui::
tr(
"core.msg_receiving");
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;
86 const int barW = width - 20;
89 int fill = (barW - 4) * pct / 100;
90 if (fill > 0) gfx->fillRect(barX + 2, barY + 2, fill, barH - 4, EPD_BLACK);
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);
102 bool needsRender()
const override {
return dirty_; }
104 void onTick(uint32_t )
override {
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();
114 if (isSend_) MessageTransfer::instance().cancelSend();
120 const char* getName()
const override {
return "MsgProgressView"; }
123 bool isSend_ =
false;
124 uint8_t lastPct_ = 255;
127MsgProgressView* s_progressView =
nullptr;
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);
141 int baseline = y + 6;
143 gfx->setCursor(x + 22, baseline);
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);
154class MsgPeerView :
public ListView {
156 void onEnter(
void* context)
override {
159 s_peerLastRefreshMs = 0;
161 void onResume()
override {
165 void onPause()
override {
168 if (s_peerReadonly) MessageTransfer::instance().stopDiscovery();
170 void onExit()
override {
171 if (s_peerReadonly) {
172 MessageTransfer::instance().stopDiscovery();
173 }
else if (MessageTransfer::instance().sendState() == SendState::PickingPeer) {
175 MessageTransfer::instance().cancelSend();
179 void onTick(uint32_t nowMs)
override {
180 if (nowMs - s_peerLastRefreshMs < kPeerRefreshMs)
return;
181 s_peerLastRefreshMs = nowMs;
182 if (s_peerReadonly) {
185 if (MessageTransfer::instance().discoveryDone()) {
186 MessageTransfer::instance().startDiscovery(0,
true);
189 }
else if (MessageTransfer::instance().discoveryDone()) {
192 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
195 const char* getName()
const override {
return "MsgPeerView"; }
200 static void startScanForMode() {
201 if (s_peerReadonly) {
202 MessageTransfer::instance().startDiscovery(0,
true);
204 MessageTransfer::instance().startDiscovery(kPeerRefreshMs * 4);
209MsgPeerView* s_peerView =
nullptr;
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];
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;
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);
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]};
236 s_peerView->init(s_peerTitle, s_peerItems, s_peerCount);
239void onPeerSelect(uint16_t index,
void* ) {
240 if (index >= s_peerCount)
return;
241 const PeerInfo& p = s_peers[index];
242 if (s_peerReadonly) {
244 snprintf(info,
sizeof(info),
"%s\n\nRSSI: %d dBm", p.name[0] ? p.name :
"?", p.rssi);
248 if (MessageTransfer::instance().confirmInteractiveTarget(p.addr, p.addrType)) {
252 pushProgressView(
true);
259void openPeerView(
bool readonly) {
260 s_peerReadonly = readonly;
263 s_peerView =
new MsgPeerView();
264 s_peerView->setItemRenderer(renderPeerRow,
nullptr);
265 s_peerView->setOnSelect(onPeerSelect);
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();
275void pushProgressView(
bool isSend) {
276 if (!s_progressView) s_progressView =
new MsgProgressView();
277 s_progressView->setSend(isSend);
278 s_progressIsSend = isSend;
285void onMsgConsentYes(
void* ) {
287 MessageTransfer::instance().respondConsent(
true);
288 pushProgressView(
false);
292void onMsgConsentNo(
void* ) {
294 MessageTransfer::instance().respondConsent(
false);
297void onMsgConsentRequestEvent(
const core::Event& ) {
302 MessageTransfer::instance().respondConsent(
false);
305 char peerName[cdc::msg::kNameBufSize] = {};
306 char mime[cdc::msg::kMimeBufSize] = {};
307 const char* descKey =
nullptr;
309 if (!MessageTransfer::instance().getPendingConsent(peerName,
sizeof(peerName), mime,
310 sizeof(mime), &descKey, &size)) {
313 const char* what = (descKey && descKey[0]) ?
ui::tr(descKey) : mime;
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));
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);
326void onMsgExchangeCompleteEvent(
const core::Event& ) {
327 MessageTransfer::TransferResult res;
328 bool have = MessageTransfer::instance().consumeResult(&res);
334 if (have && res.wasSend && s_peerView &&
342 }
else if (res.reason == cdc::msg::Reason::UserDeclined) {
344 }
else if (res.reason == cdc::msg::Reason::NoHandler) {
359enum BeaconMenuIdx { BM_TOGGLE = 0, BM_NAME, BM_SCAN, BM_COUNT };
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);
371void showBeaconNameSetup() {
373 MessageTransfer::instance().getBeaconName(),
374 [](
const char* text) { MessageTransfer::instance().setBeaconName(text); },
375 cdc::msg::kMaxNameLen);
378void onBeaconMenuSelect(uint16_t index,
void* ) {
381 auto& m = MessageTransfer::instance();
382 m.setBeaconEnabled(!m.isBeaconEnabled());
387 showBeaconNameSetup();
404 s_beaconMenu->setOnSelect(onBeaconMenuSelect);
412 onMsgConsentRequestEvent,
415 onMsgExchangeCompleteEvent,
420 if (MessageTransfer::instance().takeInteractiveRequest()) {
421 s_pickerPending =
true;
422 s_pickerStartMs = nowMs;
425 if (s_pickerPending && (nowMs - s_pickerStartMs >= kPickerScanMs ||
426 MessageTransfer::instance().discoveryDone())) {
427 s_pickerPending =
false;
435 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.
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.
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.
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).