From 678158c302d5a648f1067c876fd04bd1829ab1f6 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Tue, 21 Oct 2025 00:54:43 +0200 Subject: [PATCH] more fixes --- .../cardboy-companion/TimeSyncManager.swift | 12 ++++ .../backend-esp/src/esp_backend.cpp | 25 +++++++ .../backend-esp/src/time_sync_service.cpp | 14 +++- .../apps/lockscreen/src/lockscreen_app.cpp | 68 ++++++++++++++++--- .../include/cardboy/sdk/services.hpp | 2 + .../cardboy/backend/desktop_backend.hpp | 1 + .../backends/desktop/src/desktop_backend.cpp | 30 +++++++- Firmware/sdkconfig | 2 +- 8 files changed, 139 insertions(+), 15 deletions(-) diff --git a/Firmware/cardboy-companion/cardboy-companion/cardboy-companion/TimeSyncManager.swift b/Firmware/cardboy-companion/cardboy-companion/cardboy-companion/TimeSyncManager.swift index 4122e40..c62d16c 100644 --- a/Firmware/cardboy-companion/cardboy-companion/cardboy-companion/TimeSyncManager.swift +++ b/Firmware/cardboy-companion/cardboy-companion/cardboy-companion/TimeSyncManager.swift @@ -409,6 +409,18 @@ final class TimeSyncManager: NSObject, ObservableObject, UNUserNotificationCente private func startScanning() { guard shouldKeepScanning, central.state == .poweredOn else { return } if isScanning { return } + + let connected = central.retrieveConnectedPeripherals(withServices: [timeServiceUUID, fileServiceUUID]) + if let restored = connected.first { + statusMessage = "Restoring connection…" + connectionState = .connecting + targetPeripheral = restored + restored.delegate = self + central.connect(restored, options: nil) + shouldKeepScanning = false + return + } + central.scanForPeripherals(withServices: [timeServiceUUID, fileServiceUUID], options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ]) diff --git a/Firmware/components/backend-esp/src/esp_backend.cpp b/Firmware/components/backend-esp/src/esp_backend.cpp index ce1c308..e69b02a 100644 --- a/Firmware/components/backend-esp/src/esp_backend.cpp +++ b/Firmware/components/backend-esp/src/esp_backend.cpp @@ -141,6 +141,14 @@ public: capLengths(notification); std::lock_guard lock(mutex); + if (notification.externalId != 0) { + for (auto it = entries.begin(); it != entries.end();) { + if (it->externalId == notification.externalId) + it = entries.erase(it); + else + ++it; + } + } notification.id = nextId++; notification.unread = true; @@ -187,6 +195,23 @@ public: ++revisionCounter; } + void removeByExternalId(std::uint64_t externalId) override { + if (externalId == 0) + return; + std::lock_guard lock(mutex); + bool removed = false; + for (auto it = entries.begin(); it != entries.end();) { + if (it->externalId == externalId) { + it = entries.erase(it); + removed = true; + } else { + ++it; + } + } + if (removed) + ++revisionCounter; + } + private: static constexpr std::size_t kMaxEntries = 8; static constexpr std::size_t kMaxTitleBytes = 96; diff --git a/Firmware/components/backend-esp/src/time_sync_service.cpp b/Firmware/components/backend-esp/src/time_sync_service.cpp index 3e2a173..d6444e7 100644 --- a/Firmware/components/backend-esp/src/time_sync_service.cpp +++ b/Firmware/components/backend-esp/src/time_sync_service.cpp @@ -175,6 +175,15 @@ PendingNotification& ensurePending(uint32_t uid) { return pending; } +void discardPending(uint32_t uid) { + for (auto it = g_pendingNotifications.begin(); it != g_pendingNotifications.end(); ++it) { + if (it->uid == uid) { + g_pendingNotifications.erase(it); + break; + } + } +} + void finalizePending(uint32_t uid) { if (!g_notificationCenter) return; @@ -184,6 +193,7 @@ void finalizePending(uint32_t uid) { cardboy::sdk::INotificationCenter::Notification note{}; note.timestamp = static_cast(time(nullptr)); + note.externalId = uid; if (!it->title.empty()) { note.title = it->title; } else if (!it->appIdentifier.empty()) { @@ -975,7 +985,9 @@ void handleAncsNotificationSource(uint16_t connHandle, const uint8_t* data, uint uid); if (eventId == 2) { // Removed - finalizePending(uid); + discardPending(uid); + g_notificationCenter->removeByExternalId(uid); + ESP_LOGI(kLogTag, "Cleared notification uid=%" PRIu32, uid); return; } diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp index fd1ed5a..76abf27 100644 --- a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -5,6 +5,7 @@ #include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_system.hpp" +#include #include #include #include @@ -24,6 +25,20 @@ constexpr std::uint32_t kUnlockHoldMs = 1500; using Framebuffer = typename AppContext::Framebuffer; using Clock = typename AppContext::Clock; +constexpr std::array kArrowUpGlyph{0b00011000, 0b00111100, 0b01111110, + 0b11111111, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00000000, + 0b00000000}; + +constexpr std::array kArrowDownGlyph{0b00000000, 0b00000000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b00011000, + 0b00011000, 0b00011000, 0b11111111, + 0b01111110, 0b00111100, 0b00011000, + 0b00000000}; + struct TimeSnapshot { bool hasWallTime = false; int hour24 = 0; @@ -264,6 +279,30 @@ private: } } + static void drawArrowGlyph(Framebuffer& fb, int x, int y, + const std::array& glyph, int scale = 1) { + if (scale <= 0) + return; + for (int row = 0; row < font16x8::kGlyphHeight; ++row) { + const std::uint8_t rowBits = glyph[row]; + for (int col = 0; col < font16x8::kGlyphWidth; ++col) { + const auto mask = static_cast(1u << (font16x8::kGlyphWidth - 1 - col)); + if ((rowBits & mask) == 0) + continue; + for (int sx = 0; sx < scale; ++sx) { + for (int sy = 0; sy < scale; ++sy) { + fb.drawPixel(x + col * scale + sx, y + row * scale + sy, true); + } + } + } + } + } + + static void drawArrow(Framebuffer& fb, int x, int y, bool up, int scale = 1) { + const auto& glyph = up ? kArrowUpGlyph : kArrowDownGlyph; + drawArrowGlyph(fb, x, y, glyph, scale); + } + static std::string truncateWithEllipsis(std::string_view text, int maxWidth, int scale, int letterSpacing) { if (font16x8::measureText(text, scale, letterSpacing) <= maxWidth) return std::string(text); @@ -423,11 +462,15 @@ private: const int counterWidth = font16x8::measureText(counter, scaleSmall, 1); const int counterX = cardMarginSide + cardWidth - cardPadding - counterWidth; font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true, 1); - const int arrowX = counterX + (counterWidth - 8) / 2; - int arrowY = cardMarginTop + cardPadding + textLineHeight + 1; - font16x8::drawText(framebuffer, arrowX, arrowY, "^", scaleSmall, true, 0); - arrowY += textLineHeight + 2; - font16x8::drawText(framebuffer, arrowX, arrowY, "v", scaleSmall, true, 0); + const int arrowWidth = font16x8::kGlyphWidth * scaleSmall; + const int arrowSpacing = std::max(1, scaleSmall); + const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing; + const int arrowX = counterX + (counterWidth - arrowsTotalWide) / 2; + const int arrowY = cardMarginTop + cardPadding + textLineHeight + 1; + drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall); + drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall); + const int arrowHeight = font16x8::kGlyphHeight * scaleSmall; + cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop); } if (!bodyLines.empty()) { @@ -450,12 +493,15 @@ private: font16x8::drawText(framebuffer, summaryX, summaryY, summary, scaleSmall, true, 1); if (notifications.size() > 1) { - int arrowX = (framebuffer.width() - 8) / 2; - int arrowY = summaryY + textLineHeight + 1; - font16x8::drawText(framebuffer, arrowX, arrowY, "^", scaleSmall, true, 0); - arrowY += textLineHeight + 2; - font16x8::drawText(framebuffer, arrowX, arrowY, "v", scaleSmall, true, 0); - cardHeight = std::max(cardHeight, arrowY + textLineHeight - cardMarginTop); + const int arrowWidth = font16x8::kGlyphWidth * scaleSmall; + const int arrowSpacing = std::max(1, scaleSmall); + const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing; + const int arrowX = (framebuffer.width() - arrowsTotalWide) / 2; + const int arrowY = summaryY + textLineHeight + 1; + drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall); + drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall); + const int arrowHeight = font16x8::kGlyphHeight * scaleSmall; + cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop); } } } diff --git a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp index 94170e6..d2acbca 100644 --- a/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp +++ b/Firmware/sdk/backend_interface/include/cardboy/sdk/services.hpp @@ -75,6 +75,7 @@ public: struct Notification { std::uint64_t id = 0; std::uint64_t timestamp = 0; + std::uint64_t externalId = 0; std::string title; std::string body; bool unread = true; @@ -87,6 +88,7 @@ public: [[nodiscard]] virtual std::vector recent(std::size_t limit) const = 0; virtual void markAllRead() = 0; virtual void clear() = 0; + virtual void removeByExternalId(std::uint64_t externalId) = 0; }; struct Services { diff --git a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp index 0a0336b..1db3c59 100644 --- a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp +++ b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp @@ -93,6 +93,7 @@ public: [[nodiscard]] std::vector recent(std::size_t limit) const override; void markAllRead() override; void clear() override; + void removeByExternalId(std::uint64_t externalId) override; private: static constexpr std::size_t kMaxEntries = 8; diff --git a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp index 644f1be..f7ee181 100644 --- a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp +++ b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp @@ -157,12 +157,21 @@ void DesktopNotificationCenter::pushNotification(Notification notification) { } std::lock_guard lock(mutex); + if (notification.externalId != 0) { + for (auto it = entries.begin(); it != entries.end();) { + if (it->externalId == notification.externalId) + it = entries.erase(it); + else + ++it; + } + } + notification.id = nextId++; notification.unread = true; - if (entries.size() >= kMaxEntries) - entries.erase(entries.begin()); entries.push_back(std::move(notification)); + while (entries.size() > kMaxEntries) + entries.erase(entries.begin()); ++revisionCounter; } @@ -203,6 +212,23 @@ void DesktopNotificationCenter::clear() { ++revisionCounter; } +void DesktopNotificationCenter::removeByExternalId(std::uint64_t externalId) { + if (externalId == 0) + return; + std::lock_guard lock(mutex); + bool removed = false; + for (auto it = entries.begin(); it != entries.end();) { + if (it->externalId == externalId) { + it = entries.erase(it); + removed = true; + } else { + ++it; + } + } + if (removed) + ++revisionCounter; +} + DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {} int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; } diff --git a/Firmware/sdkconfig b/Firmware/sdkconfig index b5b6f9f..4e9c05e 100644 --- a/Firmware/sdkconfig +++ b/Firmware/sdkconfig @@ -695,7 +695,7 @@ CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y CONFIG_BT_NIMBLE_ROLE_OBSERVER=y CONFIG_BT_NIMBLE_GATT_CLIENT=y CONFIG_BT_NIMBLE_GATT_SERVER=y -# CONFIG_BT_NIMBLE_NVS_PERSIST is not set +CONFIG_BT_NIMBLE_NVS_PERSIST=y # CONFIG_BT_NIMBLE_SMP_ID_RESET is not set CONFIG_BT_NIMBLE_SECURITY_ENABLE=y CONFIG_BT_NIMBLE_SM_LEGACY=y