Compare commits

...

18 Commits

Author SHA1 Message Date
5532055cdc snake fix 2025-10-25 23:42:01 +02:00
61f05b4e58 battery percentage fix with notifications 2025-10-25 23:41:56 +02:00
961da2ba33 battery percentage 2025-10-25 23:31:11 +02:00
96f5b1f0ee fix status bar 2025-10-25 23:13:01 +02:00
f5a780c1c8 better lockscreen notifications 2025-10-25 23:10:13 +02:00
5c3cdaaae4 better lockscreen progress 2025-10-25 23:00:44 +02:00
f814c45532 remove font "normalization" 2025-10-25 22:51:02 +02:00
65ee33a141 desktop fix 2025-10-25 18:25:12 +02:00
0e69debf39 reset sdkconfig 2025-10-25 16:01:50 +02:00
9b5521fc28 better lockscreen 2025-10-25 14:28:58 +02:00
278e822600 faster timeout for games 2025-10-25 13:52:50 +02:00
844cf86d8d fixie 2025-10-25 12:51:28 +02:00
f8735d4bce some refactoring 2 2025-10-25 12:34:53 +02:00
1ee132898b some refactoring 2025-10-22 14:46:20 +02:00
5ddd38e5d7 remove notifications 2025-10-21 23:35:11 +02:00
4112efd60b repeat pairing fix 2025-10-21 20:21:12 +02:00
678158c302 more fixes 2025-10-21 00:54:43 +02:00
12e8a0e098 janky notifications 2025-10-21 00:42:35 +02:00
49 changed files with 2597 additions and 3863 deletions

View File

@@ -1,4 +1,4 @@
To build:
(in zsh)
(in zsh, bash doesn't work)
. "$HOME/esp/esp-idf/export.sh"
idf.py build

View File

@@ -6,30 +6,6 @@
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "cardboy-icon-dark.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "cardboy-icon-tinted.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -13,6 +13,40 @@
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6">
<linearGradient
id="swatch19"
inkscape:swatch="solid">
<stop
style="stop-color:#cccccc;stop-opacity:1;"
offset="0"
id="stop20" />
</linearGradient>
<linearGradient
id="swatch15">
<stop
style="stop-color:#cccccc;stop-opacity:1;"
offset="0.52188009"
id="stop15" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5">
<rect
style="fill:#000000;stroke:#000000;stroke-width:0;stroke-linejoin:bevel;paint-order:stroke markers fill;stop-color:#000000"
id="rect5-4"
width="20"
height="200"
x="530"
y="595" />
</clipPath>
<linearGradient
id="swatch4"
inkscape:swatch="solid">
<stop
style="stop-color:#cccccc;stop-opacity:1;"
offset="0"
id="stop4" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath61">
@@ -1247,109 +1281,109 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.92030791"
inkscape:cx="606.86211"
inkscape:cy="530.80061"
inkscape:window-width="1728"
inkscape:window-height="1186"
inkscape:window-x="832"
inkscape:window-y="99"
inkscape:window-maximized="0"
inkscape:zoom="0.76316607"
inkscape:cx="429.13333"
inkscape:cy="386.54758"
inkscape:window-width="2560"
inkscape:window-height="1381"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
id="path222"
clip-path="url(#clipPath223)"
style="fill:#e6e6e6;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
style="fill:#cccccc;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.18386638,0.18386638,0,-502.11504,-1071.3194)"
d="m 9421.5,3980 1327.75,0.5 c 45.813,0.5 82.75,37.4365 82.75,82.75 v 2905 c 0,45.8135 -36.937,82.75 -82.75,82.75 H 9140.5 l -2667.25,2e-4 c -45.3134,3e-4 -82.2499,-36.9362 -82.75,-82.75 v -2905 c 0.5001,-45.3132 37.4366,-82.2497 82.75,-82.75 z"
sodipodi:nodetypes="ccccccccccc" />
<path
id="path104"
d="m 9098,4379 h 264 v 233 l -132,132 -132,-132 v -233"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath105)" />
<path
id="path106"
d="m 9622,4904 h -232 l -133,-133 133,-132 h 232 v 265"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath107)" />
<path
id="path108"
d="m 10006,5513 v -335 h 264 v 335 h -264"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22983461,0.22983461,0,-755.97458,-1537.3453)"
clip-path="url(#clipPath109)" />
<path
id="path110"
d="m 9457,6189 h -312 v -265 h 312 v 265"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22979634,0.22979634,0,-788.97058,-1495.2529)"
clip-path="url(#clipPath111)" />
<path
id="path114"
d="m 9003,6567 v -265 h 312 v 265 h -312"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22979634,0.22979634,0,-788.97058,-1495.2529)"
clip-path="url(#clipPath115)" />
<path
id="path116"
d="m 10006,5867 v -335 h 264 v 335 h -264"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22983461,0.22983461,0,-755.97458,-1537.3453)"
clip-path="url(#clipPath117)" />
<path
id="path120"
d="m 9362,4931 v 233 h -264 v -233 l 132,-132 132,132"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath121)" />
<path
id="path122"
d="m 9071,4639 132,132 -132,133 h -233 v -265 h 233"
style="fill:#f9f9f9;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath123)" />
<path
id="path126"
d="M 6650,4098 H 8379 V 6933 H 6650 V 4098"
style="fill:#ffffff;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
style="fill:#ffffff;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
transform="matrix(0,0.18386638,0.18386638,0,-502.11504,-1071.3194)"
clip-path="url(#clipPath127)" />
<path
id="path130"
d="M 8379,4098 H 6650"
style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.18386638,0.18386638,0,-502.11504,-1071.3194)"
clip-path="url(#clipPath131)"
sodipodi:nodetypes="cc" />
<path
id="path132"
d="M 6650,6933 H 8379"
style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.18386638,0.18386638,0,-502.11504,-1071.3194)"
clip-path="url(#clipPath133)"
sodipodi:nodetypes="cc" />
<path
id="path158"
d="m 9098,4612 132,132"
style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath159)"
sodipodi:nodetypes="cc" />
<path
id="path164"
d="m 9230,4799 -132,132"
style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath165)"
sodipodi:nodetypes="cc" />
<path
id="path174"
d="M 9622,4639 H 9390"
style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0,0.15345005,0.15345005,0,-334.35379,-809.35838)"
transform="matrix(0,0.22984236,0.22984236,0,-721.48942,-1495.6777)"
clip-path="url(#clipPath175)"
sodipodi:nodetypes="cc"
inkscape:label="path174" />

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -77,6 +77,11 @@ private struct TimeSyncTabView: View {
}
.buttonStyle(.bordered)
}
Button(action: manager.sendTestNotification) {
Label("Send Test Notification", systemImage: "bell.badge.waveform")
}
.buttonStyle(.bordered)
}
VStack(spacing: 8) {

View File

@@ -6,5 +6,7 @@
<array>
<string>bluetooth-central</string>
</array>
<key>NSUserNotificationUsageDescription</key>
<string>Allow Cardboy Companion to send local notifications for testing.</string>
</dict>
</plist>

View File

@@ -2,8 +2,9 @@ import Combine
import CoreBluetooth
import Foundation
import UniformTypeIdentifiers
import UserNotifications
final class TimeSyncManager: NSObject, ObservableObject {
final class TimeSyncManager: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
enum ConnectionState: String {
case idle = "Idle"
case scanning = "Scanning"
@@ -116,6 +117,7 @@ final class TimeSyncManager: NSObject, ObservableObject {
private var pendingListOperationID: UUID?
private var simpleOperationID: UUID?
private var pendingDirectoryRequest: (path: String, operationID: UUID)?
private let notificationCenter = UNUserNotificationCenter.current()
private struct UploadState {
let id: UUID
@@ -141,6 +143,9 @@ final class TimeSyncManager: NSObject, ObservableObject {
// Force central manager to initialise immediately so state updates arrive right away.
_ = central
// Ensure we can present notifications while app is foreground
notificationCenter.delegate = self
if central.state == .poweredOn {
startScanning()
}
@@ -201,6 +206,70 @@ final class TimeSyncManager: NSObject, ObservableObject {
statusMessage = "Time synced at \(timeString)."
}
func sendTestNotification() {
func scheduleNotification() {
DispatchQueue.main.async {
self.statusMessage = "Scheduling test notification…"
}
let content = UNMutableNotificationContent()
content.title = "Cardboy Test Notification"
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .medium
content.body = "Triggered at \(formatter.string(from: Date()))"
content.sound = UNNotificationSound.default
// Schedule slightly later so the notification is visible when the app is foreground
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: "cardboy-test-\(UUID().uuidString)",
content: content,
trigger: trigger)
notificationCenter.add(request) { error in
DispatchQueue.main.async {
if let error {
self.statusMessage = "Failed to schedule test notification: \(error.localizedDescription)"
} else {
self.statusMessage = "Test notification scheduled."
}
}
}
}
func requestPermissionAndSchedule() {
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error {
DispatchQueue.main.async {
self.statusMessage = "Notification permission error: \(error.localizedDescription)"
}
return
}
if granted {
scheduleNotification()
} else {
DispatchQueue.main.async {
self.statusMessage = "Enable notifications in Settings to test."
}
}
}
}
notificationCenter.getNotificationSettings { settings in
switch settings.authorizationStatus {
case .authorized, .provisional, .ephemeral:
scheduleNotification()
case .notDetermined:
requestPermissionAndSchedule()
case .denied:
DispatchQueue.main.async {
self.statusMessage = "Enable notifications in Settings to test."
}
@unknown default:
requestPermissionAndSchedule()
}
}
}
// MARK: - File operations exposed to UI
func refreshDirectory() {
@@ -340,6 +409,18 @@ final class TimeSyncManager: NSObject, ObservableObject {
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
])
@@ -763,7 +844,17 @@ extension TimeSyncManager: CBCentralManagerDelegate {
statusMessage = "Turn on Bluetooth to continue."
stopScanning()
case .poweredOn:
startScanning()
// If there are peripherals already connected that match our services, restore connection.
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)
} else {
startScanning()
}
@unknown default:
connectionState = .failed
statusMessage = "Unknown Bluetooth state."

View File

@@ -4,7 +4,6 @@ idf_component_register(
"src/buttons.cpp"
"src/buzzer.cpp"
"src/esp_backend.cpp"
"src/event_bus.cpp"
"src/display.cpp"
"src/fs_helper.cpp"
"src/i2c_global.cpp"

View File

@@ -18,6 +18,7 @@ public:
float get_voltage() const;
float get_charge() const;
float get_current() const;
float get_percentage() const;
void pooler(); // FIXME:
private:
@@ -33,6 +34,7 @@ private:
volatile float _voltage;
volatile float _current;
volatile float _charge;
volatile float _percentage;
TaskHandle_t _pooler_task;
};

View File

@@ -5,9 +5,10 @@
#ifndef BUTTONS_HPP
#define BUTTONS_HPP
#include "cardboy/sdk/event_bus.hpp"
#include <cstdint>
#include "cardboy/sdk/input_state.hpp"
#include "cardboy/sdk/services.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -24,20 +25,19 @@ typedef enum {
class Buttons {
public:
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
void setEventBus(cardboy::sdk::IEventBus* bus);
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
cardboy::sdk::InputState get_state();
void install_isr();
void setEventBus(cardboy::sdk::IEventBus* bus);
TaskHandle_t _pooler_task;
private:
Buttons();
uint8_t _previous;
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
cardboy::sdk::IEventBus* _eventBus = nullptr;
};

View File

@@ -1,27 +0,0 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
namespace cardboy::backend::esp {
class EventBus final : public cardboy::sdk::IEventBus {
public:
EventBus();
~EventBus() override;
void signal(std::uint32_t bits) override;
void signalFromISR(std::uint32_t bits) override;
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
void scheduleTimerSignal(std::uint32_t delay_ms) override;
void cancelTimerSignal() override;
private:
EventGroupHandle_t group;
TimerHandle_t timer;
};
} // namespace cardboy::backend::esp

View File

@@ -1,5 +1,9 @@
#pragma once
namespace cardboy::sdk {
class INotificationCenter;
} // namespace cardboy::sdk
namespace cardboy::backend::esp {
/**
@@ -16,5 +20,10 @@ void ensure_time_sync_service_started();
*/
void shutdown_time_sync_service();
} // namespace cardboy::backend::esp
/**
* Provide a notification sink that receives mirrored notifications from iOS.
* Passing nullptr disables mirroring.
*/
void set_notification_center(cardboy::sdk::INotificationCenter* center);
} // namespace cardboy::backend::esp

View File

@@ -2,7 +2,6 @@
#include <cardboy/sdk/display_spec.hpp>
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -61,17 +60,24 @@ private:
class HighResClockService;
class FilesystemService;
class LoopHooksService;
class NotificationService;
class TimerService;
class EventBus;
class AppScopedServices;
class AppServiceProvider;
std::unique_ptr<BuzzerService> buzzerService;
std::unique_ptr<BatteryService> batteryService;
std::unique_ptr<StorageService> storageService;
std::unique_ptr<RandomService> randomService;
std::unique_ptr<HighResClockService> highResClockService;
std::unique_ptr<FilesystemService> filesystemService;
std::unique_ptr<EventBus> eventBus;
std::unique_ptr<LoopHooksService> loopHooksService;
std::unique_ptr<BuzzerService> _buzzerService;
std::unique_ptr<BatteryService> _batteryService;
std::unique_ptr<StorageService> _storageService;
std::unique_ptr<RandomService> _randomService;
std::unique_ptr<HighResClockService> _highResClockService;
std::unique_ptr<FilesystemService> _filesystemService;
std::unique_ptr<EventBus> _eventBus;
std::unique_ptr<LoopHooksService> _loopHooksService;
std::unique_ptr<NotificationService> _notificationService;
std::unique_ptr<AppServiceProvider> _appServiceProvider;
cardboy::sdk::Services services{};
cardboy::sdk::Services _services{};
};
struct Backend {

View File

@@ -48,6 +48,8 @@ static constexpr uint16_t DesignCapMah = 180; // 100mOhm
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
// lsb is 1/256%
constexpr float regToPercent(uint16_t reg) { return static_cast<float>(reg) / 256.0f; }
constexpr float regToCurrent(uint16_t reg) {
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
}
@@ -103,6 +105,7 @@ void BatMon::pooler() {
_charge = capToMah(ReadRegister(0x05));
_current = regToCurrent(ReadRegister(0x0B));
_voltage = regToVoltage(ReadRegister(0x09));
_percentage = regToPercent(ReadRegister(0x06));
vTaskDelay(pdMS_TO_TICKS(10000));
if (_voltage < 3.0f) {
Shutdowner::get().shutdown();
@@ -113,3 +116,4 @@ void BatMon::pooler() {
float BatMon::get_voltage() const { return _voltage; }
float BatMon::get_charge() const { return _charge; }
float BatMon::get_current() const { return _current; }
float BatMon::get_percentage() const { return _percentage; }

View File

@@ -13,7 +13,6 @@
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/backend/esp/i2c_global.hpp"
#include "cardboy/sdk/event_bus.hpp"
static i2c_master_dev_handle_t dev_handle;
static inline i2c_device_config_t dev_cfg = {
@@ -76,6 +75,28 @@ static void delay(unsigned long long loop) {
}
}
static cardboy::sdk::InputState buttons_to_input_state(uint8_t pressed) {
cardboy::sdk::InputState state{};
if (pressed & BTN_UP)
state.up = true;
if (pressed & BTN_LEFT)
state.left = true;
if (pressed & BTN_RIGHT)
state.right = true;
if (pressed & BTN_DOWN)
state.down = true;
if (pressed & BTN_A)
state.a = true;
if (pressed & BTN_B)
state.b = true;
if (pressed & BTN_SELECT)
state.select = true;
if (pressed & BTN_START)
state.start = true;
return state;
}
void Buttons::pooler() {
while (true) {
BaseType_t xResult = xTaskNotifyWait(pdFALSE, ULONG_MAX, nullptr, portMAX_DELAY);
@@ -88,15 +109,27 @@ void Buttons::pooler() {
reg = 1;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (_listener)
xTaskNotifyGive(_listener);
if (_eventBus)
_eventBus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
if (_eventBus) {
cardboy::sdk::AppButtonEvent button{};
button.current = buttons_to_input_state(_current);
button.previous = buttons_to_input_state(_previous);
_previous = _current;
cardboy::sdk::AppEvent evt{};
// TODO: dedup?
TickType_t ticks = xTaskGetTickCount();
auto now = static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
evt.timestamp_ms = now;
evt.data = button;
_eventBus->post(evt);
}
}
}
uint8_t Buttons::get_pressed() { return _current; }
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
void Buttons::setEventBus(cardboy::sdk::IEventBus* bus) { _eventBus = bus; }
uint8_t Buttons::get_pressed() { return _current; }
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
cardboy::sdk::InputState Buttons::get_state() { return buttons_to_input_state(get_pressed()); }
void Buttons::setEventBus(cardboy::sdk::IEventBus* bus) { _eventBus = bus; }

View File

@@ -18,14 +18,22 @@
#include "esp_random.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <ctime>
#include <deque>
#include <list>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
namespace cardboy::backend::esp {
@@ -37,6 +45,7 @@ void ensureNvsInit() {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
printf("Erasing flash!\n");
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
@@ -69,6 +78,7 @@ public:
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
[[nodiscard]] float percentage() const override { return BatMon::get().get_percentage(); }
};
class EspRuntime::StorageService final : public cardboy::sdk::IStorage {
@@ -127,33 +137,396 @@ public:
void onLoopIteration() override { vTaskDelay(1); }
};
class EspRuntime::EventBus final : public cardboy::sdk::IEventBus {
public:
explicit EventBus() {
_queueHandle =
xQueueCreateStatic(_kMaxQueueSize, sizeof(cardboy::sdk::AppEvent), _queueStorage.data(), &_queue);
}
~EventBus() override { vQueueDelete(_queueHandle); }
void post(const sdk::AppEvent& event) override { xQueueSendToBack(_queueHandle, &event, portMAX_DELAY); }
std::optional<sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override {
sdk::AppEvent out;
TickType_t ticks = timeout_ms ? pdMS_TO_TICKS(*timeout_ms) : portMAX_DELAY;
if (xQueueReceive(_queueHandle, &out, ticks) == pdTRUE) {
return out;
}
return std::nullopt;
}
private:
static constexpr std::uint32_t _kMaxQueueSize = 32;
StaticQueue_t _queue;
std::array<std::uint8_t, 32 * sizeof(cardboy::sdk::AppEvent)> _queueStorage{};
QueueHandle_t _queueHandle;
};
class EspRuntime::TimerService final : public cardboy::sdk::ITimerService {
public:
explicit TimerService(cardboy::sdk::IEventBus& appBus);
~TimerService() override;
cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
private:
static inline EspRuntime::TimerService* _current = nullptr;
struct InitializedSemaphore {
InitializedSemaphore() {
_handle = xSemaphoreCreateBinary();
xSemaphoreGive(_handle);
}
~InitializedSemaphore() { vSemaphoreDelete(_handle); }
SemaphoreHandle_t operator*() { return _handle; }
SemaphoreHandle_t operator->() { return _handle; }
private:
SemaphoreHandle_t _handle;
};
static inline InitializedSemaphore _currentSemaphore;
struct TimerRecord {
TimerService* owner = nullptr;
TimerHandle_t timer = nullptr;
cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
bool repeat = false;
};
static void timerCallback(TimerHandle_t timer);
void handleTimer(sdk::AppTimerHandle record);
cardboy::sdk::IEventBus& _appEventBus;
SemaphoreHandle_t _mutex;
StaticSemaphore_t _mutexStatic;
std::list<TimerRecord> _timers;
std::atomic<sdk::AppTimerHandle> _nextTimerHandle = 1;
static_assert(std::atomic<sdk::AppTimerHandle>::is_always_lock_free);
};
class EspRuntime::AppScopedServices final : public cardboy::sdk::AppScopedServices {
public:
AppScopedServices(std::unique_ptr<TimerService> timer) : _ownedTimer(std::move(timer)) {
this->timer = _ownedTimer.get();
}
private:
std::unique_ptr<TimerService> _ownedTimer;
};
class EspRuntime::AppServiceProvider final : public cardboy::sdk::IAppServiceProvider {
public:
explicit AppServiceProvider(cardboy::sdk::IEventBus& bus) : eventBus(bus) {}
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override {
(void) generation;
auto timer = std::make_unique<TimerService>(eventBus);
return std::make_unique<AppScopedServices>(std::move(timer));
}
private:
cardboy::sdk::IEventBus& eventBus;
};
class EspRuntime::NotificationService final : public cardboy::sdk::INotificationCenter {
public:
void pushNotification(Notification notification) override {
if (notification.timestamp == 0) {
notification.timestamp = static_cast<std::uint64_t>(std::time(nullptr));
}
capLengths(notification);
std::lock_guard<std::mutex> 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;
entries.push_back(std::move(notification));
if (entries.size() > kMaxEntries)
entries.erase(entries.begin());
++revisionCounter;
}
[[nodiscard]] std::uint32_t revision() const override {
std::lock_guard<std::mutex> lock(mutex);
return revisionCounter;
}
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override {
std::lock_guard<std::mutex> lock(mutex);
std::vector<Notification> out;
const std::size_t count = std::min<std::size_t>(limit, entries.size());
out.reserve(count);
for (std::size_t i = 0; i < count; ++i) {
out.push_back(entries[entries.size() - 1 - i]);
}
return out;
}
void markAllRead() override {
std::lock_guard<std::mutex> lock(mutex);
bool changed = false;
for (auto& entry: entries) {
if (entry.unread) {
entry.unread = false;
changed = true;
}
}
if (changed)
++revisionCounter;
}
void clear() override {
std::lock_guard<std::mutex> lock(mutex);
if (entries.empty())
return;
entries.clear();
++revisionCounter;
}
void removeById(std::uint64_t id) override {
if (id == 0)
return;
std::lock_guard<std::mutex> lock(mutex);
bool removed = false;
for (auto it = entries.begin(); it != entries.end();) {
if (it->id == id) {
it = entries.erase(it);
removed = true;
} else {
++it;
}
}
if (removed)
++revisionCounter;
}
void removeByExternalId(std::uint64_t externalId) override {
if (externalId == 0)
return;
std::lock_guard<std::mutex> 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;
static constexpr std::size_t kMaxBodyBytes = 256;
static void capLengths(Notification& notification) {
if (notification.title.size() > kMaxTitleBytes)
notification.title.resize(kMaxTitleBytes);
if (notification.body.size() > kMaxBodyBytes)
notification.body.resize(kMaxBodyBytes);
}
mutable std::mutex mutex;
std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
EspRuntime::TimerService::TimerService(cardboy::sdk::IEventBus& appBus) : _appEventBus(appBus) {
xSemaphoreTake(*_currentSemaphore, portMAX_DELAY);
assert(_current == nullptr);
_mutex = xSemaphoreCreateBinaryStatic(&_mutexStatic);
assert(_mutex);
xSemaphoreGive(_mutex);
_current = this;
xSemaphoreGive(*_currentSemaphore);
}
EspRuntime::TimerService::~TimerService() {
xSemaphoreTake(*_currentSemaphore, portMAX_DELAY);
assert(_current == this);
_current = nullptr;
cancelAllTimers();
vSemaphoreDelete(_mutex);
xSemaphoreGive(*_currentSemaphore);
}
cardboy::sdk::AppTimerHandle EspRuntime::TimerService::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
TimerRecord record{};
record.owner = this;
record.repeat = repeat;
cardboy::sdk::AppTimerHandle newHandle = cardboy::sdk::kInvalidAppTimer;
do {
newHandle = _nextTimerHandle++;
} while (newHandle == cardboy::sdk::kInvalidAppTimer);
if (_nextTimerHandle == cardboy::sdk::kInvalidAppTimer)
++_nextTimerHandle;
record.handle = newHandle;
xSemaphoreTake(_mutex, portMAX_DELAY);
TimerRecord* storedRecord = &_timers.emplace_back(record);
xSemaphoreGive(_mutex);
const TickType_t ticks = std::max<TickType_t>(pdMS_TO_TICKS(delay_ms), 1);
static_assert(sizeof(void*) >= sizeof(cardboy::sdk::AppTimerHandle));
TimerHandle_t timerHandle =
xTimerCreate("AppSvcTimer", ticks, repeat ? pdTRUE : pdFALSE, reinterpret_cast<void*>(storedRecord->handle),
&TimerService::timerCallback);
storedRecord->timer = timerHandle;
if (xTimerStart(timerHandle, portMAX_DELAY) != pdPASS) {
assert(false);
}
return newHandle;
}
void EspRuntime::TimerService::cancelTimer(cardboy::sdk::AppTimerHandle handle) {
assert(handle != sdk::kInvalidAppTimer);
TimerHandle_t timerHandle = nullptr;
{
xSemaphoreTake(_mutex, portMAX_DELAY);
for (auto it = _timers.begin(); it != _timers.end(); ++it) {
if (it->handle == handle) {
timerHandle = it->timer;
it = _timers.erase(it);
}
}
xSemaphoreGive(_mutex);
}
if (!timerHandle)
return;
xTimerStop(timerHandle, portMAX_DELAY);
xTimerDelete(timerHandle, portMAX_DELAY);
}
void EspRuntime::TimerService::cancelAllTimers() {
if (!_mutex)
return;
std::vector<TimerHandle_t> handles;
handles.resize(_timers.size());
{
xSemaphoreTake(_mutex, portMAX_DELAY);
size_t i = 0;
for (auto& record: _timers) {
if (record.timer) {
assert(record.timer);
handles[i] = record.timer;
}
i++;
}
_timers.clear();
xSemaphoreGive(_mutex);
}
for (auto timerHandle: handles) {
xTimerStop(timerHandle, portMAX_DELAY);
xTimerDelete(timerHandle, portMAX_DELAY);
}
}
void EspRuntime::TimerService::timerCallback(TimerHandle_t timer) {
auto handle = reinterpret_cast<sdk::AppTimerHandle>(pvTimerGetTimerID(timer));
xSemaphoreTake(*_currentSemaphore, portMAX_DELAY);
if (!_current)
return;
_current->handleTimer(handle);
xSemaphoreGive(*_currentSemaphore);
}
void EspRuntime::TimerService::handleTimer(sdk::AppTimerHandle handle) {
TimerHandle_t timerHandle = nullptr;
bool repeat = false;
{
xSemaphoreTake(_mutex, portMAX_DELAY);
for (auto it = _timers.begin(); it != _timers.end(); ++it) {
if (it->handle == handle) {
timerHandle = it->timer;
if (!it->repeat) {
_timers.erase(it);
} else {
repeat = true;
}
break;
}
}
xSemaphoreGive(_mutex);
}
if (!timerHandle) {
printf("Couldn't find handle for timer %lu\n", handle);
return;
}
if (!repeat && timerHandle)
xTimerDelete(timerHandle, portMAX_DELAY);
cardboy::sdk::AppTimerEvent timerEvent{};
timerEvent.handle = handle;
cardboy::sdk::AppEvent event{};
event.timestamp_ms = static_cast<std::uint32_t>(esp_timer_get_time() / 1000ULL);
event.data = timerEvent;
_appEventBus.post(event);
}
EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
initializeHardware();
buzzerService = std::make_unique<BuzzerService>();
batteryService = std::make_unique<BatteryService>();
storageService = std::make_unique<StorageService>();
randomService = std::make_unique<RandomService>();
highResClockService = std::make_unique<HighResClockService>();
filesystemService = std::make_unique<FilesystemService>();
eventBus = std::make_unique<EventBus>();
loopHooksService = std::make_unique<LoopHooksService>();
_buzzerService = std::make_unique<BuzzerService>();
_batteryService = std::make_unique<BatteryService>();
_storageService = std::make_unique<StorageService>();
_randomService = std::make_unique<RandomService>();
_highResClockService = std::make_unique<HighResClockService>();
_filesystemService = std::make_unique<FilesystemService>();
_eventBus = std::make_unique<EventBus>();
_appServiceProvider = std::make_unique<AppServiceProvider>(*_eventBus);
_loopHooksService = std::make_unique<LoopHooksService>();
_notificationService = std::make_unique<NotificationService>();
services.buzzer = buzzerService.get();
services.battery = batteryService.get();
services.storage = storageService.get();
services.random = randomService.get();
services.highResClock = highResClockService.get();
services.filesystem = filesystemService.get();
services.eventBus = eventBus.get();
services.loopHooks = loopHooksService.get();
_services.buzzer = _buzzerService.get();
_services.battery = _batteryService.get();
_services.storage = _storageService.get();
_services.random = _randomService.get();
_services.highResClock = _highResClockService.get();
_services.filesystem = _filesystemService.get();
_services.eventBus = _eventBus.get();
_services.appServices = _appServiceProvider.get();
_services.loopHooks = _loopHooksService.get();
_services.notifications = _notificationService.get();
Buttons::get().setEventBus(eventBus.get());
Buttons::get().setEventBus(_eventBus.get());
set_notification_center(_notificationService.get());
}
EspRuntime::~EspRuntime() { shutdown_time_sync_service(); }
EspRuntime::~EspRuntime() {
set_notification_center(nullptr);
shutdown_time_sync_service();
}
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return services; }
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return _services; }
void EspRuntime::initializeHardware() {
static bool initialized = false;
@@ -197,27 +570,7 @@ void EspFramebuffer::sendFrame_impl(bool clearAfterSend) { SMD::send_frame(clear
bool EspFramebuffer::frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
cardboy::sdk::InputState EspInput::readState_impl() {
cardboy::sdk::InputState state{};
const uint8_t pressed = Buttons::get().get_pressed();
if (pressed & BTN_UP)
state.up = true;
if (pressed & BTN_LEFT)
state.left = true;
if (pressed & BTN_RIGHT)
state.right = true;
if (pressed & BTN_DOWN)
state.down = true;
if (pressed & BTN_A)
state.a = true;
if (pressed & BTN_B)
state.b = true;
if (pressed & BTN_SELECT)
state.select = true;
if (pressed & BTN_START)
state.start = true;
return state;
}
cardboy::sdk::InputState EspInput::readState_impl() { return Buttons::get().get_state(); }
std::uint32_t EspClock::millis_impl() {
TickType_t ticks = xTaskGetTickCount();

View File

@@ -1,81 +0,0 @@
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/event_bus.hpp"
#include "freertos/portmacro.h"
#include <algorithm>
namespace cardboy::backend::esp {
namespace {
[[nodiscard]] TickType_t toTicks(std::uint32_t timeout_ms) {
if (timeout_ms == cardboy::sdk::IEventBus::kWaitForever)
return portMAX_DELAY;
return pdMS_TO_TICKS(timeout_ms);
}
} // namespace
static void timerCallback(TimerHandle_t handle) {
auto* bus = static_cast<EventBus*>(pvTimerGetTimerID(handle));
if (bus)
bus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
}
EventBus::EventBus() :
group(xEventGroupCreate()), timer(xTimerCreate("EventBusTimer", pdMS_TO_TICKS(1), pdFALSE, this, timerCallback)) {}
EventBus::~EventBus() {
if (timer)
xTimerDelete(timer, portMAX_DELAY);
if (group)
vEventGroupDelete(group);
}
void EventBus::signal(std::uint32_t bits) {
if (!group || bits == 0)
return;
xEventGroupSetBits(group, bits);
}
void EventBus::signalFromISR(std::uint32_t bits) {
if (!group || bits == 0)
return;
BaseType_t higherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(group, bits, &higherPriorityTaskWoken);
if (higherPriorityTaskWoken == pdTRUE)
portYIELD_FROM_ISR(higherPriorityTaskWoken);
}
std::uint32_t EventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (!group || mask == 0)
return 0;
const EventBits_t bits = xEventGroupWaitBits(group, mask, pdTRUE, pdFALSE, toTicks(timeout_ms));
return static_cast<std::uint32_t>(bits & mask);
}
void EventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
if (!timer)
return;
xTimerStop(timer, 0);
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
const TickType_t ticks = std::max<TickType_t>(pdMS_TO_TICKS(delay_ms), 1);
if (xTimerChangePeriod(timer, ticks, 0) == pdPASS)
xTimerStart(timer, 0);
}
void EventBus::cancelTimerSignal() {
if (!timer)
return;
xTimerStop(timer, 0);
}
} // namespace cardboy::backend::esp

View File

@@ -15,6 +15,7 @@
#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_hs_mbuf.h"
#include "host/ble_store.h"
#include "host/util/util.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
@@ -29,6 +30,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <dirent.h>
#include <esp_bt.h>
#include <esp_err.h>
@@ -37,6 +39,10 @@
#include <sys/stat.h>
#include <vector>
#include "cardboy/sdk/services.hpp"
extern "C" void ble_store_config_init(void);
namespace cardboy::backend::esp {
namespace {
@@ -44,9 +50,9 @@ namespace {
constexpr char kLogTag[] = "TimeSyncBLE";
constexpr char kDeviceName[] = "Cardboy";
constexpr std::uint16_t kPreferredConnIntervalMin = BLE_GAP_CONN_ITVL_MS(80); // 80 ms
constexpr std::uint16_t kPreferredConnIntervalMax = BLE_GAP_CONN_ITVL_MS(150); // 150 ms
constexpr std::uint16_t kPreferredConnLatency = 2;
constexpr std::uint16_t kPreferredConnIntervalMin = BLE_GAP_CONN_ITVL_MS(200); // 80 ms
constexpr std::uint16_t kPreferredConnIntervalMax = BLE_GAP_CONN_ITVL_MS(300); // 150 ms
constexpr std::uint16_t kPreferredConnLatency = 3;
constexpr std::uint16_t kPreferredSupervisionTimeout = BLE_GAP_SUPERVISION_TIMEOUT_MS(5000); // 5 s
constexpr float connIntervalUnitsToMs(std::uint16_t units) { return static_cast<float>(units) * 1.25f; }
@@ -75,10 +81,12 @@ struct [[gnu::packed]] TimeSyncPayload {
static_assert(sizeof(TimeSyncPayload) == 12, "Unexpected payload size");
static bool g_started = false;
static uint8_t g_ownAddrType = BLE_OWN_ADDR_PUBLIC;
static TaskHandle_t g_hostTaskHandle = nullptr;
static uint16_t g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
static bool g_started = false;
static uint8_t g_ownAddrType = BLE_OWN_ADDR_PUBLIC;
static TaskHandle_t g_hostTaskHandle = nullptr;
static uint16_t g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
static cardboy::sdk::INotificationCenter* g_notificationCenter = nullptr;
static bool g_securityRequested = false;
struct ResponseMessage {
uint8_t opcode;
@@ -112,6 +120,101 @@ struct FileDownloadContext {
static FileUploadContext g_uploadCtx{};
static FileDownloadContext g_downloadCtx{};
static const ble_uuid128_t kAncsServiceUuid = BLE_UUID128_INIT(0xD0, 0x00, 0x2D, 0x12, 0x1E, 0x4B, 0x0F, 0xA4, 0x99,
0x4E, 0xCE, 0xB5, 0x31, 0xF4, 0x05, 0x79);
static const ble_uuid128_t kAncsNotificationSourceUuid = BLE_UUID128_INIT(
0xBD, 0x1D, 0xA2, 0x99, 0xE6, 0x25, 0x58, 0x8C, 0xD9, 0x42, 0x01, 0x63, 0x0D, 0x12, 0xBF, 0x9F);
static const ble_uuid128_t kAncsDataSourceUuid = BLE_UUID128_INIT(0xFB, 0x7B, 0x7C, 0xCE, 0x6A, 0xB3, 0x44, 0xBE, 0xB5,
0x4B, 0xD6, 0x24, 0xE9, 0xC6, 0xEA, 0x22);
static const ble_uuid128_t kAncsControlPointUuid = BLE_UUID128_INIT(0xD9, 0xD9, 0xAA, 0xFD, 0xBD, 0x9B, 0x21, 0x98,
0xA8, 0x49, 0xE1, 0x45, 0xF3, 0xD8, 0xD1, 0x69);
static uint16_t g_ancsServiceEndHandle = 0;
static uint16_t g_ancsNotificationSourceHandle = 0;
static uint16_t g_ancsDataSourceHandle = 0;
static uint16_t g_ancsControlPointHandle = 0;
static uint16_t g_mtuSize = 23;
struct PendingNotification {
uint32_t uid = 0;
uint8_t category = 0;
uint8_t flags = 0;
std::string appIdentifier;
std::string title;
std::string message;
};
static std::vector<PendingNotification> g_pendingNotifications;
static std::vector<uint8_t> g_dataSourceBuffer;
static const ble_uuid16_t kClientConfigUuid = BLE_UUID16_INIT(BLE_GATT_DSC_CLT_CFG_UUID16);
void resetAncsState() {
g_ancsServiceEndHandle = 0;
g_ancsNotificationSourceHandle = 0;
g_ancsDataSourceHandle = 0;
g_ancsControlPointHandle = 0;
g_mtuSize = 23;
g_dataSourceBuffer.clear();
g_pendingNotifications.clear();
}
void clearDeliveredNotifications() {
if (g_notificationCenter)
g_notificationCenter->clear();
}
PendingNotification* findPending(uint32_t uid) {
for (auto& entry: g_pendingNotifications) {
if (entry.uid == uid)
return &entry;
}
return nullptr;
}
PendingNotification& ensurePending(uint32_t uid) {
if (auto* existing = findPending(uid))
return *existing;
g_pendingNotifications.push_back({});
auto& pending = g_pendingNotifications.back();
pending.uid = 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;
for (auto it = g_pendingNotifications.begin(); it != g_pendingNotifications.end(); ++it) {
if (it->uid != uid)
continue;
cardboy::sdk::INotificationCenter::Notification note{};
note.timestamp = static_cast<std::uint64_t>(time(nullptr));
note.externalId = uid;
if (!it->title.empty()) {
note.title = it->title;
} else if (!it->appIdentifier.empty()) {
note.title = it->appIdentifier;
} else {
note.title = "Notification";
}
note.body = it->message;
g_notificationCenter->pushNotification(std::move(note));
ESP_LOGI(kLogTag, "Stored notification uid=%" PRIu32 " title='%s' body='%s'", uid, it->title.c_str(),
it->message.c_str());
g_pendingNotifications.erase(it);
break;
}
}
enum class FileCommandCode : uint8_t {
ListDirectory = 0x01,
UploadBegin = 0x02,
@@ -156,6 +259,15 @@ bool sendFileResponseNow(const ResponseMessage& msg);
void notificationTask(void* param);
bool scheduleDownloadChunk();
void processDownloadChunk();
void handleAncsNotificationSource(uint16_t connHandle, const uint8_t* data, uint16_t length);
bool handleAncsDataSource(const uint8_t* data, uint16_t length);
void requestAncsAttributes(uint16_t connHandle, uint32_t uid);
void applyPreferredConnectionParams(uint16_t connHandle);
int ancsServiceDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_svc* svc, void* arg);
int ancsCharacteristicDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_chr* chr,
void* arg);
int ancsDescriptorDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, uint16_t chrValHandle,
const ble_gatt_dsc* dsc, void* arg);
static const ble_gatt_chr_def kTimeServiceCharacteristics[] = {
{
@@ -811,6 +923,217 @@ void handleRename(const uint8_t* payload, std::size_t length) {
}
}
void requestAncsAttributes(uint16_t connHandle, uint32_t uid) {
if (!g_notificationCenter || g_ancsControlPointHandle == 0)
return;
static constexpr uint16_t kMaxTitle = 96;
static constexpr uint16_t kMaxMessage = 256;
uint8_t buffer[32];
std::size_t index = 0;
buffer[index++] = 0x00; // CommandIDGetNotificationAttributes
buffer[index++] = static_cast<uint8_t>(uid & 0xFF);
buffer[index++] = static_cast<uint8_t>((uid >> 8) & 0xFF);
buffer[index++] = static_cast<uint8_t>((uid >> 16) & 0xFF);
buffer[index++] = static_cast<uint8_t>((uid >> 24) & 0xFF);
buffer[index++] = 0x00; // App Identifier
buffer[index++] = 0x01; // Title
buffer[index++] = static_cast<uint8_t>(kMaxTitle & 0xFF);
buffer[index++] = static_cast<uint8_t>((kMaxTitle >> 8) & 0xFF);
buffer[index++] = 0x03; // Message
buffer[index++] = static_cast<uint8_t>(kMaxMessage & 0xFF);
buffer[index++] = static_cast<uint8_t>((kMaxMessage >> 8) & 0xFF);
const int rc = ble_gattc_write_flat(connHandle, g_ancsControlPointHandle, buffer, index, nullptr, nullptr);
if (rc != 0) {
ESP_LOGW(kLogTag, "ANCS attribute request failed: rc=%d uid=%" PRIu32, rc, uid);
} else {
ESP_LOGI(kLogTag, "Requested ANCS attributes for uid=%" PRIu32, uid);
}
}
void applyPreferredConnectionParams(uint16_t connHandle) {
ble_gap_upd_params params{
.itvl_min = kPreferredConnIntervalMin,
.itvl_max = kPreferredConnIntervalMax,
.latency = kPreferredConnLatency,
.supervision_timeout = kPreferredSupervisionTimeout,
.min_ce_len = 0,
.max_ce_len = 0,
};
const int rc = ble_gap_update_params(connHandle, &params);
if (rc != 0) {
ESP_LOGW(kLogTag, "ble_gap_update_params failed (rc=%d)", rc);
} else {
ESP_LOGI(kLogTag, "Requested conn params: %.1f-%.1f ms latency %u timeout %.0f ms",
connIntervalUnitsToMs(params.itvl_min), connIntervalUnitsToMs(params.itvl_max), params.latency,
supervisionUnitsToMs(params.supervision_timeout));
}
}
void handleAncsNotificationSource(uint16_t connHandle, const uint8_t* data, uint16_t length) {
if (!g_notificationCenter || !data || length < 8)
return;
const uint8_t eventId = data[0];
const uint8_t eventFlags = data[1];
const uint8_t category = data[2];
const uint32_t uid = static_cast<uint32_t>(data[4]) | (static_cast<uint32_t>(data[5]) << 8) |
(static_cast<uint32_t>(data[6]) << 16) | (static_cast<uint32_t>(data[7]) << 24);
ESP_LOGI(kLogTag, "ANCS notification event=%u flags=0x%02x category=%u uid=%" PRIu32, eventId, eventFlags, category,
uid);
if (eventId == 2) { // Removed
discardPending(uid);
g_notificationCenter->removeByExternalId(uid);
ESP_LOGI(kLogTag, "Cleared notification uid=%" PRIu32, uid);
return;
}
auto& pending = ensurePending(uid);
pending.flags = eventFlags;
pending.category = category;
requestAncsAttributes(connHandle, uid);
}
bool handleAncsDataSource(const uint8_t* data, uint16_t length) {
if (!g_notificationCenter || !data || length == 0)
return false;
g_dataSourceBuffer.insert(g_dataSourceBuffer.end(), data, data + length);
if (g_dataSourceBuffer.size() > 2048) {
ESP_LOGW(kLogTag, "Dropping oversized ANCS data buffer (%u bytes)",
static_cast<unsigned>(g_dataSourceBuffer.size()));
g_dataSourceBuffer.clear();
return false;
}
const uint8_t* buffer = g_dataSourceBuffer.data();
const uint16_t total = static_cast<uint16_t>(g_dataSourceBuffer.size());
if (total < 5)
return false;
if (buffer[0] != 0x00)
return false;
const uint32_t uid = static_cast<uint32_t>(buffer[1]) | (static_cast<uint32_t>(buffer[2]) << 8) |
(static_cast<uint32_t>(buffer[3]) << 16) | (static_cast<uint32_t>(buffer[4]) << 24);
PendingNotification* pending = findPending(uid);
if (!pending)
pending = &ensurePending(uid);
std::size_t offset = 5;
while (offset + 3 <= total) {
const uint8_t attrId = buffer[offset];
const uint16_t attrLen =
static_cast<uint16_t>(buffer[offset + 1]) | (static_cast<uint16_t>(buffer[offset + 2]) << 8);
offset += 3;
if (offset + attrLen > total)
return false;
const char* valuePtr = reinterpret_cast<const char*>(buffer + offset);
const std::string value(valuePtr, valuePtr + attrLen);
switch (attrId) {
case 0x00:
pending->appIdentifier = value;
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " appId=%.*s", uid, static_cast<int>(attrLen), valuePtr);
break;
case 0x01:
pending->title = value;
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " title=%.*s", uid, static_cast<int>(attrLen), valuePtr);
break;
case 0x03:
pending->message = value;
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " message=%.*s", uid, static_cast<int>(attrLen), valuePtr);
break;
default:
break;
}
offset += attrLen;
}
if (offset != total)
return false;
ESP_LOGI(kLogTag, "ANCS data complete uid=%" PRIu32, uid);
finalizePending(uid);
g_dataSourceBuffer.clear();
return true;
}
int ancsDescriptorDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, uint16_t /*chr_val_handle*/,
const ble_gatt_dsc* dsc, void* /*arg*/) {
if (error->status == 0 && dsc) {
if (ble_uuid_cmp(&dsc->uuid.u, &kClientConfigUuid.u) == 0) {
const uint8_t enable[2] = {0x01, 0x00};
const int rc = ble_gattc_write_flat(connHandle, dsc->handle, enable, sizeof(enable), nullptr, nullptr);
if (rc != 0)
ESP_LOGW(kLogTag, "Failed to enable ANCS notifications (rc=%d) handle=%u", rc, dsc->handle);
else
ESP_LOGI(kLogTag, "Subscribed ANCS descriptor handle=%u", dsc->handle);
}
return 0;
}
if (error->status == BLE_HS_EDONE)
return 0;
return error->status;
}
int ancsCharacteristicDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_chr* chr,
void* /*arg*/) {
if (error->status == BLE_HS_EDONE)
return 0;
if (error->status != 0)
return error->status;
if (!chr)
return 0;
if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) &&
ble_uuid_cmp(&chr->uuid.u, &kAncsNotificationSourceUuid.u) == 0) {
g_ancsNotificationSourceHandle = chr->val_handle;
ESP_LOGI(kLogTag, "ANCS notification source handle=%u", g_ancsNotificationSourceHandle);
ble_gattc_disc_all_dscs(connHandle, chr->val_handle, g_ancsServiceEndHandle, ancsDescriptorDiscoveredCb,
nullptr);
} else if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) &&
ble_uuid_cmp(&chr->uuid.u, &kAncsDataSourceUuid.u) == 0) {
g_ancsDataSourceHandle = chr->val_handle;
ESP_LOGI(kLogTag, "ANCS data source handle=%u", g_ancsDataSourceHandle);
ble_gattc_disc_all_dscs(connHandle, chr->val_handle, g_ancsServiceEndHandle, ancsDescriptorDiscoveredCb,
nullptr);
} else if ((chr->properties & BLE_GATT_CHR_PROP_WRITE) &&
ble_uuid_cmp(&chr->uuid.u, &kAncsControlPointUuid.u) == 0) {
g_ancsControlPointHandle = chr->val_handle;
ESP_LOGI(kLogTag, "ANCS control point handle=%u", g_ancsControlPointHandle);
}
return 0;
}
int ancsServiceDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_svc* svc, void* /*arg*/) {
if (error->status == BLE_HS_EDONE)
return 0;
if (error->status != 0)
return error->status;
if (!svc) {
ESP_LOGW(kLogTag, "ANCS service missing");
return 0;
}
g_ancsServiceEndHandle = svc->end_handle;
ESP_LOGI(kLogTag, "ANCS service discovered: start=%u end=%u", svc->start_handle, svc->end_handle);
return ble_gattc_disc_all_chrs(connHandle, svc->start_handle, svc->end_handle, ancsCharacteristicDiscoveredCb,
nullptr);
}
void handleGattsRegister(ble_gatt_register_ctxt* ctxt, void* /*arg*/) {
if (ctxt->op == BLE_GATT_REGISTER_OP_CHR) {
if (ble_uuid_cmp(ctxt->chr.chr_def->uuid, &kFileCommandCharUuid.u) == 0) {
@@ -969,27 +1292,6 @@ void logConnectionParams(uint16_t connHandle, const char* context) {
static_cast<unsigned>(desc.conn_latency), timeoutMs);
}
void applyPreferredConnectionParams(uint16_t connHandle) {
ble_gap_upd_params params{
.itvl_min = kPreferredConnIntervalMin,
.itvl_max = kPreferredConnIntervalMax,
.latency = kPreferredConnLatency,
.supervision_timeout = kPreferredSupervisionTimeout,
.min_ce_len = 0,
.max_ce_len = 0,
};
const int rc = ble_gap_update_params(connHandle, &params);
if (rc != 0) {
ESP_LOGW(kLogTag, "Requesting preferred conn params failed (rc=%d)", rc);
return;
}
ESP_LOGI(kLogTag, "Requested conn params: interval=%.0f-%.0f ms latency=%u supervision=%.0f ms",
connIntervalUnitsToMs(kPreferredConnIntervalMin), connIntervalUnitsToMs(kPreferredConnIntervalMax),
kPreferredConnLatency, supervisionUnitsToMs(kPreferredSupervisionTimeout));
}
void startAdvertising() {
ble_hs_adv_fields fields{};
std::memset(&fields, 0, sizeof(fields));
@@ -1075,7 +1377,22 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
ESP_LOGI(kLogTag, "Connected; handle=%d", event->connect.conn_handle);
g_activeConnHandle = event->connect.conn_handle;
logConnectionParams(event->connect.conn_handle, "Initial");
applyPreferredConnectionParams(event->connect.conn_handle);
ble_gap_conn_desc desc{};
if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
ESP_LOGI(kLogTag, "Security state on connect: bonded=%d encrypted=%d authenticated=%d key_size=%u",
desc.sec_state.bonded, desc.sec_state.encrypted, desc.sec_state.authenticated,
static_cast<unsigned>(desc.sec_state.key_size));
if (!desc.sec_state.encrypted && !desc.sec_state.bonded && !g_securityRequested) {
const int src = ble_gap_security_initiate(event->connect.conn_handle);
if (src == 0) {
g_securityRequested = true;
ESP_LOGI(kLogTag, "Security procedure initiated");
} else {
ESP_LOGW(kLogTag, "Failed to initiate security (rc=%d)", src);
}
}
}
} else {
ESP_LOGW(kLogTag, "Connection attempt failed; status=%d", event->connect.status);
startAdvertising();
@@ -1084,7 +1401,9 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(kLogTag, "Disconnected; reason=%d", event->disconnect.reason);
g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
g_securityRequested = false;
resetAncsState();
resetUploadContext();
resetDownloadContext();
if (g_responseQueue)
@@ -1092,6 +1411,20 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
startAdvertising();
break;
case BLE_GAP_EVENT_ENC_CHANGE:
if (event->enc_change.status == 0) {
ESP_LOGI(kLogTag, "Link encrypted; discovering ANCS");
resetAncsState();
g_securityRequested = false;
ble_gattc_disc_svc_by_uuid(event->enc_change.conn_handle, &kAncsServiceUuid.u, ancsServiceDiscoveredCb,
nullptr);
applyPreferredConnectionParams(event->enc_change.conn_handle);
} else {
ESP_LOGW(kLogTag, "Encryption change failed; status=%d", event->enc_change.status);
g_securityRequested = false;
}
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(kLogTag, "Advertising complete; restarting");
startAdvertising();
@@ -1108,24 +1441,50 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
case BLE_GAP_EVENT_CONN_UPDATE_REQ:
if (event->conn_update_req.self_params) {
auto& params = *event->conn_update_req.self_params;
if (params.itvl_max > kPreferredConnIntervalMax)
params.itvl_max = kPreferredConnIntervalMax;
if (params.itvl_min > params.itvl_max)
params.itvl_min = params.itvl_max;
if (params.latency > kPreferredConnLatency)
params.latency = kPreferredConnLatency;
if (params.supervision_timeout > kPreferredSupervisionTimeout)
params.supervision_timeout = kPreferredSupervisionTimeout;
params.min_ce_len = 0;
params.max_ce_len = 0;
const auto& params = *event->conn_update_req.self_params;
ESP_LOGI(kLogTag, "Peer update request -> interval %.1f-%.1f ms latency %u timeout %.0f ms",
connIntervalUnitsToMs(params.itvl_min), connIntervalUnitsToMs(params.itvl_max), params.latency,
supervisionUnitsToMs(params.supervision_timeout));
}
break;
case BLE_GAP_EVENT_NOTIFY_RX:
if (event->notify_rx.attr_handle == g_ancsNotificationSourceHandle) {
handleAncsNotificationSource(event->notify_rx.conn_handle, event->notify_rx.om->om_data,
event->notify_rx.om->om_len);
} else if (event->notify_rx.attr_handle == g_ancsDataSourceHandle) {
const uint16_t len = event->notify_rx.om->om_len;
ESP_LOGD(kLogTag, "ANCS data chunk len=%u", static_cast<unsigned>(len));
handleAncsDataSource(event->notify_rx.om->om_data, len);
}
break;
case BLE_GAP_EVENT_MTU:
g_mtuSize = event->mtu.value;
ESP_LOGI(kLogTag, "MTU updated to %u", g_mtuSize);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING: {
ble_gap_conn_desc desc{};
const int findRc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (findRc != 0) {
ESP_LOGW(kLogTag, "Repeat pairing but failed to fetch connection descriptor (rc=%d)", findRc);
} else {
ESP_LOGI(kLogTag,
"Repeat pairing requested by %02X:%02X:%02X:%02X:%02X:%02X; deleting existing bond to re-pair",
desc.peer_id_addr.val[0], desc.peer_id_addr.val[1], desc.peer_id_addr.val[2],
desc.peer_id_addr.val[3], desc.peer_id_addr.val[4], desc.peer_id_addr.val[5]);
const int deleteRc = ble_store_util_delete_peer(&desc.peer_id_addr);
if (deleteRc != 0) {
ESP_LOGW(kLogTag, "Failed to delete existing bond (rc=%d)", deleteRc);
}
}
resetAncsState();
clearDeliveredNotifications();
g_securityRequested = false;
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
@@ -1153,12 +1512,15 @@ bool initController() {
ble_hs_cfg.gatts_register_cb = handleGattsRegister;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
ble_hs_cfg.sm_bonding = 0;
ble_hs_cfg.sm_bonding = 1;
ble_hs_cfg.sm_mitm = 0;
ble_hs_cfg.sm_sc = 0;
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ESP_ERROR_CHECK(nimble_port_init());
configureGap();
ble_store_config_init();
int gattRc = ble_gatts_count_cfg(kGattServices);
if (gattRc != 0) {
@@ -1182,6 +1544,8 @@ void ensure_time_sync_service_started() {
return;
}
resetAncsState();
if (!initController()) {
ESP_LOGE(kLogTag, "Unable to initialise BLE time sync service");
return;
@@ -1254,6 +1618,10 @@ void shutdown_time_sync_service() {
vQueueDelete(g_responseQueue);
g_responseQueue = nullptr;
}
resetAncsState();
}
void set_notification_center(cardboy::sdk::INotificationCenter* center) { g_notificationCenter = center; }
} // namespace cardboy::backend::esp

View File

@@ -4,6 +4,11 @@ project(cardboy_sdk LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
# add_compile_options(-Werror -O0 -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-variable
# -Wno-error=unused-function
# -Wshadow -Wformat=2 -Wfloat-equal -D_GLIBCXX_DEBUG -Wconversion)
#add_compile_options(-fsanitize=address -fno-sanitize-recover -D_GLIBCXX_DEBUG)
#add_link_options(-fsanitize=address -fno-sanitize-recover -D_GLIBCXX_DEBUG)
add_subdirectory(utils)

View File

@@ -17,7 +17,10 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppTimerEvent;
constexpr const char* kClockAppName = "Clock";
@@ -47,21 +50,20 @@ public:
const auto snap = captureTime();
renderIfNeeded(snap);
lastSnapshot = snap;
refreshTimer = context.scheduleRepeatingTimer(200);
if (auto* timer = context.timer())
refreshTimer = timer->scheduleTimer(200, true);
}
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
switch (event.type) {
case cardboy::sdk::AppEventType::Button:
handleButtonEvent(event.button);
break;
case cardboy::sdk::AppEventType::Timer:
if (event.timer.handle == refreshTimer)
updateDisplay();
break;
}
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer)
updateDisplay();
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:
@@ -76,10 +78,11 @@ private:
TimeSnapshot lastSnapshot{};
void cancelRefreshTimer() {
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(refreshTimer);
refreshTimer = cardboy::sdk::kInvalidAppTimer;
}
if (refreshTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(refreshTimer);
refreshTimer = cardboy::sdk::kInvalidAppTimer;
}
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {

View File

@@ -151,9 +151,11 @@ constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
class GameboyApp;
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppTimerEvent;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
using cardboy::sdk::kInvalidAppTimer;
@@ -223,7 +225,6 @@ public:
::gAudioWriteThunk = &GameboyApp::audioWriteThunk;
apu.attach(this);
apu.reset();
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.resetAll();)
prevInput = context.input.readState();
@@ -232,13 +233,12 @@ public:
scaleMode = ScaleMode::FullHeightWide;
ensureFilesystemReady();
refreshRomList();
mode = Mode::Browse;
browserDirty = true;
scheduleNextTick(0);
mode = Mode::Browse;
browserDirty = true;
nextTimeoutMs = 0;
}
void onStop() override {
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
unloadRom();
@@ -251,21 +251,22 @@ public:
}
}
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
const uint64_t frameStartUs = nowMicros();
performStep();
const uint64_t frameEndUs = nowMicros();
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
scheduleAfterFrame(elapsedUs);
return;
}
if (event.type == AppEventType::Button) {
frameDelayCarryUs = 0;
scheduleNextTick(0);
}
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const AppTimeoutEvent&) {
const uint64_t frameStartUs = nowMicros();
performStep();
const uint64_t frameEndUs = nowMicros();
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
scheduleAfterFrame(elapsedUs);
},
[this](const AppButtonEvent&) {
frameDelayCarryUs = 0;
nextTimeoutMs = 0;
},
[](const AppTimerEvent&) { /* ignore */ }));
return nextTimeoutMs;
}
void performStep() {
@@ -1112,9 +1113,9 @@ public:
cardboy::sdk::IFilesystem* filesystem = nullptr;
cardboy::sdk::IHighResClock* highResClock = nullptr;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
std::optional<std::uint32_t> nextTimeoutMs;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::FullHeightWide;
@@ -1148,18 +1149,6 @@ public:
uint8_t lastLoud = 0;
uint32_t stableFrames = 0;
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
context.cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
}
void scheduleNextTick(uint32_t delayMs) {
cancelTick();
tickTimer = context.scheduleTimer(delayMs, false);
}
uint32_t idleDelayMs() const { return browserDirty ? 50 : 140; }
void scheduleAfterFrame(uint64_t elapsedUs) {
@@ -1168,17 +1157,17 @@ public:
desiredUs += frameDelayCarryUs;
if (desiredUs <= 0) {
frameDelayCarryUs = desiredUs;
scheduleNextTick(0);
nextTimeoutMs = 0;
return;
}
frameDelayCarryUs = desiredUs % 1000;
desiredUs -= frameDelayCarryUs;
uint32_t delayMs = static_cast<uint32_t>(desiredUs / 1000);
scheduleNextTick(delayMs);
nextTimeoutMs = delayMs;
return;
}
frameDelayCarryUs = 0;
scheduleNextTick(idleDelayMs());
nextTimeoutMs = idleDelayMs();
}
bool ensureFilesystemReady() {
@@ -1829,7 +1818,7 @@ public:
promptDirty = true;
mode = Mode::Prompt;
gb.direct.joypad = 0xFF;
scheduleNextTick(0);
// scheduleNextTick(0);
}
void exitPrompt(Mode nextMode) {

View File

@@ -6,23 +6,39 @@
#include "cardboy/sdk/app_system.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <ctime>
#include <string>
#include <string_view>
#include <vector>
namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppTimerEvent;
constexpr std::uint32_t kRefreshIntervalMs = 100;
constexpr std::uint32_t kSlowRefreshMs = 1000;
constexpr std::uint32_t kFastRefreshMs = 20;
constexpr std::uint32_t kUnlockHoldMs = 1500;
using Framebuffer = typename AppContext::Framebuffer;
using Clock = typename AppContext::Clock;
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{
0b00010000, 0b00111000, 0b01111100, 0b11111110, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00000000, 0b00000000};
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{
0b00000000, 0b00000000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
0b00010000, 0b00010000, 0b00010000, 0b11111110, 0b01111100, 0b00111000, 0b00010000, 0b00000000};
struct TimeSnapshot {
bool hasWallTime = false;
int hour24 = 0;
@@ -37,67 +53,168 @@ struct TimeSnapshot {
class LockscreenApp final : public cardboy::sdk::IApp {
public:
explicit LockscreenApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
explicit LockscreenApp(AppContext& ctx) :
context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock), notificationCenter(ctx.notificationCenter()) {}
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
holdActive = false;
holdProgressMs = 0;
dirty = true;
lastSnapshot = {};
holdActive = false;
holdProgressMs = 0;
dirty = true;
lastNotificationInteractionMs = clock.millis();
lastRefreshMs = clock.millis();
refreshNotifications();
const auto snap = captureTime();
renderIfNeeded(snap);
lastSnapshot = snap;
refreshTimer = context.scheduleRepeatingTimer(kRefreshIntervalMs);
rescheduleRefreshTimer(kSlowRefreshMs);
}
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
switch (event.type) {
case cardboy::sdk::AppEventType::Button:
handleButtonEvent(event.button);
break;
case cardboy::sdk::AppEventType::Timer:
if (event.timer.handle == refreshTimer) {
advanceHoldProgress();
updateDisplay();
}
break;
}
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer) {
const std::uint32_t now = clock.millis();
const std::uint32_t elapsed = now - lastRefreshMs;
lastRefreshMs = now;
advanceHoldProgress(elapsed);
updateDisplay();
}
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:
AppContext& context;
Framebuffer& framebuffer;
Clock& clock;
static constexpr std::size_t kMaxDisplayedNotifications = 5;
static constexpr std::uint32_t kNotificationHideMs = 8000;
AppContext& context;
Framebuffer& framebuffer;
Clock& clock;
cardboy::sdk::INotificationCenter* notificationCenter = nullptr;
std::uint32_t lastNotificationRevision = 0;
std::vector<cardboy::sdk::INotificationCenter::Notification> notifications;
std::size_t selectedNotification = 0;
bool dirty = false;
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
bool holdActive = false;
std::uint32_t holdProgressMs = 0;
bool holdActive = false;
std::uint32_t holdProgressMs = 0;
std::uint32_t lastNotificationInteractionMs = 0;
std::uint32_t lastRefreshMs = 0;
void cancelRefreshTimer() {
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(refreshTimer);
refreshTimer = cardboy::sdk::kInvalidAppTimer;
}
if (refreshTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(refreshTimer);
refreshTimer = cardboy::sdk::kInvalidAppTimer;
}
void rescheduleRefreshTimer(std::uint32_t intervalMs) {
cancelRefreshTimer();
if (auto* timer = context.timer())
refreshTimer = timer->scheduleTimer(intervalMs, true);
}
static bool comboPressed(const cardboy::sdk::InputState& state) { return state.a && state.select; }
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
const bool upPressed = button.current.up && !button.previous.up;
const bool downPressed = button.current.down && !button.previous.down;
bool navPressed = false;
if (!notifications.empty() && (upPressed || downPressed)) {
const std::size_t count = notifications.size();
lastNotificationInteractionMs = clock.millis();
if (count > 1) {
if (upPressed && selectedNotification > 0) {
selectedNotification--;
navPressed = true;
} else if (downPressed && selectedNotification < count - 1) {
selectedNotification++;
navPressed = true;
}
}
}
const bool deletePressed = button.current.b && !button.previous.b;
if (deletePressed && notificationCenter && !notifications.empty()) {
const std::size_t index = std::min<std::size_t>(selectedNotification, notifications.size() - 1);
const auto& note = notifications[index];
std::size_t preferredIndex = index;
if (index + 1 < notifications.size())
preferredIndex = index + 1;
else if (index > 0)
preferredIndex = index - 1;
if (note.externalId != 0)
notificationCenter->removeByExternalId(note.externalId);
else
notificationCenter->removeById(note.id);
selectedNotification = preferredIndex;
lastNotificationInteractionMs = clock.millis();
dirty = true;
refreshNotifications();
}
const bool comboNow = comboPressed(button.current);
updateHoldState(comboNow);
if (navPressed)
dirty = true;
updateDisplay();
}
void refreshNotifications() {
if (!notificationCenter) {
if (!notifications.empty() || lastNotificationRevision != 0) {
notifications.clear();
selectedNotification = 0;
lastNotificationRevision = 0;
dirty = true;
}
return;
}
const std::uint32_t revision = notificationCenter->revision();
if (revision == lastNotificationRevision)
return;
lastNotificationRevision = revision;
const std::uint64_t previousId =
(selectedNotification < notifications.size()) ? notifications[selectedNotification].id : 0;
auto latest = notificationCenter->recent(kMaxDisplayedNotifications);
notifications = std::move(latest);
if (notifications.empty()) {
selectedNotification = 0;
} else if (previousId != 0) {
auto it = std::find_if(notifications.begin(), notifications.end(),
[previousId](const auto& note) { return note.id == previousId; });
if (it != notifications.end()) {
selectedNotification = static_cast<std::size_t>(std::distance(notifications.begin(), it));
} else {
selectedNotification = 0;
}
} else {
selectedNotification = 0;
}
lastNotificationInteractionMs = clock.millis();
dirty = true;
}
void updateHoldState(bool comboNow) {
const bool wasActive = holdActive;
if (comboNow) {
if (!holdActive) {
holdActive = true;
dirty = true;
holdActive = true;
holdProgressMs = 0;
lastRefreshMs = clock.millis();
dirty = true;
}
} else {
if (holdActive || holdProgressMs != 0) {
@@ -106,12 +223,14 @@ private:
dirty = true;
}
}
if (wasActive != holdActive) {
rescheduleRefreshTimer(holdActive ? kFastRefreshMs : kSlowRefreshMs);
}
}
void advanceHoldProgress() {
void advanceHoldProgress(std::uint32_t elapsedMs) {
if (holdActive) {
const std::uint32_t next =
std::min<std::uint32_t>(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs);
const std::uint32_t next = std::min<std::uint32_t>(holdProgressMs + elapsedMs, kUnlockHoldMs);
if (next != holdProgressMs) {
holdProgressMs = next;
dirty = true;
@@ -127,6 +246,7 @@ private:
}
void updateDisplay() {
refreshNotifications();
const auto snap = captureTime();
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
@@ -135,8 +255,8 @@ private:
}
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute &&
a.second == b.second && a.day == b.day && a.month == b.month && a.year == b.year;
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second &&
a.day == b.day && a.month == b.month && a.year == b.year;
}
TimeSnapshot captureTime() const {
@@ -195,6 +315,121 @@ private:
}
}
static void drawArrowGlyph(Framebuffer& fb, int x, int y,
const std::array<std::uint8_t, font16x8::kGlyphHeight>& 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<std::uint8_t>(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);
std::string result(text.begin(), text.end());
const std::string ellipsis = "...";
while (!result.empty()) {
result.pop_back();
std::string candidate = result + ellipsis;
if (font16x8::measureText(candidate, scale, letterSpacing) <= maxWidth)
return candidate;
}
return ellipsis;
}
static std::vector<std::string> wrapText(std::string_view text, int maxWidth, int scale, int letterSpacing,
int maxLines) {
std::vector<std::string> lines;
if (text.empty() || maxWidth <= 0 || maxLines <= 0)
return lines;
std::string current;
std::string word;
bool truncated = false;
auto flushCurrent = [&]() {
if (current.empty())
return;
if (lines.size() < static_cast<std::size_t>(maxLines)) {
lines.push_back(current);
} else {
truncated = true;
}
current.clear();
};
for (std::size_t i = 0; i <= text.size(); ++i) {
char ch = (i < text.size()) ? text[i] : ' ';
const bool isBreak = (ch == ' ' || ch == '\n' || ch == '\r' || i == text.size());
if (!isBreak) {
word.push_back(ch);
continue;
}
if (!word.empty()) {
std::string candidate = current.empty() ? word : current + " " + word;
if (!current.empty() && font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
flushCurrent();
if (lines.size() >= static_cast<std::size_t>(maxLines)) {
truncated = true;
break;
}
candidate = word;
}
if (font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
std::string shortened = truncateWithEllipsis(word, maxWidth, scale, letterSpacing);
flushCurrent();
if (lines.size() < static_cast<std::size_t>(maxLines)) {
lines.push_back(shortened);
} else {
truncated = true;
break;
}
current.clear();
} else {
current = candidate;
}
word.clear();
}
if (ch == '\n' || ch == '\r') {
flushCurrent();
if (lines.size() >= static_cast<std::size_t>(maxLines)) {
truncated = true;
break;
}
}
}
flushCurrent();
if (lines.size() > static_cast<std::size_t>(maxLines)) {
truncated = true;
lines.resize(maxLines);
}
if (truncated && !lines.empty()) {
lines.back() = truncateWithEllipsis(lines.back(), maxWidth, scale, letterSpacing);
}
return lines;
}
static std::string formatDate(const TimeSnapshot& snap) {
static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
if (!snap.hasWallTime)
@@ -212,40 +447,142 @@ private:
framebuffer.frameReady();
const int scaleTime = 4;
const int scaleSeconds = 2;
const int scaleSmall = 1;
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const bool showNotificationDetails =
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
if (!showNotificationDetails) {
if (auto* battery = context.battery(); battery && battery->hasData()) {
const float percentage = battery->percentage();
if (std::isfinite(percentage) && percentage >= 0.0f) {
char pct[8];
std::snprintf(pct, sizeof(pct), "%.0f%%", static_cast<double>(percentage));
const int pctWidth = font16x8::measureText(pct, 1, 1);
const int pctX = framebuffer.width() - pctWidth - 4;
const int pctY = 4;
font16x8::drawText(framebuffer, pctX, pctY, pct, 1, true, 1);
}
}
}
const int scaleTime = 4;
const int scaleSeconds = 2;
const int scaleSmall = 1;
const int textLineHeight = font16x8::kGlyphHeight * scaleSmall;
const int cardMarginTop = 4;
const int cardMarginSide = 8;
const int cardPadding = 6;
const int cardLineSpacing = 4;
int cardHeight = 0;
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
if (hasNotifications) {
const auto& note = notifications[selectedNotification];
if (showNotificationDetails) {
std::string title = note.title.empty() ? std::string("Notification") : note.title;
title = truncateWithEllipsis(title, cardWidth - cardPadding * 2, scaleSmall, 1);
auto bodyLines = wrapText(note.body, cardWidth - cardPadding * 2, scaleSmall, 1, 4);
cardHeight = cardPadding * 2 + textLineHeight;
if (!bodyLines.empty()) {
cardHeight += cardLineSpacing;
cardHeight += static_cast<int>(bodyLines.size()) * textLineHeight;
if (bodyLines.size() > 1)
cardHeight += (static_cast<int>(bodyLines.size()) - 1) * cardLineSpacing;
}
if (notifications.size() > 1) {
cardHeight = std::max(cardHeight, cardPadding * 2 + textLineHeight * 2 + cardLineSpacing + 8);
}
drawRectOutline(framebuffer, cardMarginSide, cardMarginTop, cardWidth, cardHeight);
if (cardWidth > 2 && cardHeight > 2)
drawRectOutline(framebuffer, cardMarginSide + 1, cardMarginTop + 1, cardWidth - 2, cardHeight - 2);
font16x8::drawText(framebuffer, cardMarginSide + cardPadding, cardMarginTop + cardPadding, title,
scaleSmall, true, 1);
if (notifications.size() > 1) {
char counter[16];
std::snprintf(counter, sizeof(counter), "%zu/%zu", selectedNotification + 1, notifications.size());
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 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;
if (selectedNotification > 0)
drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall);
if (selectedNotification < notifications.size() - 1)
drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall);
const int arrowHeight = font16x8::kGlyphHeight * scaleSmall;
cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop);
}
if (!bodyLines.empty()) {
int bodyY = cardMarginTop + cardPadding + textLineHeight + cardLineSpacing;
for (const auto& line: bodyLines) {
font16x8::drawText(framebuffer, cardMarginSide + cardPadding, bodyY, line, scaleSmall, true, 1);
bodyY += textLineHeight + cardLineSpacing;
}
}
} else {
cardHeight = textLineHeight + cardPadding * 2;
char summary[32];
if (notifications.size() == 1)
std::snprintf(summary, sizeof(summary), "1 NOTIFICATION");
else
std::snprintf(summary, sizeof(summary), "%zu NOTIFICATIONS", notifications.size());
const int summaryWidth = font16x8::measureText(summary, scaleSmall, 1);
const int summaryX = (framebuffer.width() - summaryWidth) / 2;
const int summaryY = cardMarginTop;
font16x8::drawText(framebuffer, summaryX, summaryY, summary, scaleSmall, true, 1);
}
}
const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY);
char hoursMinutes[6];
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);
const int mainW = font16x8::measureText(hoursMinutes, scaleTime, 0);
const int timeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
const int timeX = (framebuffer.width() - mainW) / 2;
const int secX = timeX + mainW + 12;
const int secY = timeY + font16x8::kGlyphHeight * scaleTime - font16x8::kGlyphHeight * scaleSeconds;
char secs[3];
const int mainW = font16x8::measureText(hoursMinutes, scaleTime, 0);
const int timeX = (framebuffer.width() - mainW) / 2;
const int secX = timeX + mainW + 12;
const int secY = timeY + font16x8::kGlyphHeight * scaleTime - font16x8::kGlyphHeight * scaleSeconds;
char secs[3];
std::snprintf(secs, sizeof(secs), "%02d", snap.second);
font16x8::drawText(framebuffer, timeX, timeY, hoursMinutes, scaleTime, true, 0);
font16x8::drawText(framebuffer, secX, secY, secs, scaleSeconds, true, 0);
const std::string dateLine = formatDate(snap);
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 24, dateLine, scaleSmall, 1);
const char* instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT";
drawCenteredText(framebuffer, framebuffer.height() - 52, instruction, scaleSmall, 1);
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1);
const std::string instruction = "HOLD A+SELECT TO UNLOCK";
const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1);
const int barHeight = 18;
const int barY = framebuffer.height() - 30;
const int textY = barY + (barHeight - textLineHeight) / 2 + 1;
drawCenteredText(framebuffer, textY, instruction, scaleSmall, 1);
const int barWidth = framebuffer.width() - 64;
const int barX = 32;
if (holdActive || holdProgressMs > 0) {
const int barWidth = framebuffer.width() - 64;
const int barHeight = 14;
const int barX = (framebuffer.width() - barWidth) / 2;
const int barY = framebuffer.height() - 32;
const int innerWidth = barWidth - 2;
const int innerHeight = barHeight - 2;
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
const float ratio =
std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
const int fillWidth = static_cast<int>(ratio * innerWidth + 0.5f);
const int innerWidth = barWidth - 2;
const int innerHeight = barHeight - 2;
const float ratio = std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
const int fillWidth = static_cast<int>(ratio * innerWidth + 0.5f);
if (fillWidth > 0)
fillRect(framebuffer, barX + 1, barY + 1, fillWidth, innerHeight);
}

View File

@@ -17,7 +17,11 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppTimerEvent;
using Framebuffer = typename AppContext::Framebuffer;
@@ -41,18 +45,16 @@ public:
void onStop() override { cancelInactivityTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
switch (event.type) {
case cardboy::sdk::AppEventType::Button:
handleButtonEvent(event.button);
break;
case cardboy::sdk::AppEventType::Timer:
if (event.timer.handle == inactivityTimer) {
cancelInactivityTimer();
context.requestAppSwitchByName(kLockscreenAppName);
}
break;
}
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == inactivityTimer) {
cancelInactivityTimer();
context.requestAppSwitchByName(kLockscreenAppName);
}
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:
@@ -189,15 +191,17 @@ private:
}
void cancelInactivityTimer() {
if (inactivityTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(inactivityTimer);
inactivityTimer = cardboy::sdk::kInvalidAppTimer;
}
if (inactivityTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(inactivityTimer);
inactivityTimer = cardboy::sdk::kInvalidAppTimer;
}
void resetInactivityTimer() {
cancelInactivityTimer();
inactivityTimer = context.scheduleTimer(kIdleTimeoutMs);
if (auto* timer = context.timer())
inactivityTimer = timer->scheduleTimer(kIdleTimeoutMs, false);
}
};

View File

@@ -38,41 +38,44 @@ public:
renderIfNeeded();
}
void handleEvent(const cardboy::sdk::AppEvent& event) override {
if (event.type != cardboy::sdk::AppEventType::Button)
return;
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const cardboy::sdk::AppButtonEvent& button) {
const auto& current = button.current;
const auto& previous = button.previous;
const auto& current = event.button.current;
const auto& previous = event.button.previous;
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
bool moved = false;
if (current.down && !previous.down) {
moveSelection(+1);
moved = true;
} else if (current.up && !previous.up) {
moveSelection(-1);
moved = true;
}
bool moved = false;
if (current.down && !previous.down) {
moveSelection(+1);
moved = true;
} else if (current.up && !previous.up) {
moveSelection(-1);
moved = true;
}
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
if (moved)
dirty = true;
if (moved)
dirty = true;
renderIfNeeded();
renderIfNeeded();
},
[](const cardboy::sdk::AppTimerEvent&) { /* ignore */ },
[](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:

View File

@@ -20,21 +20,22 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerEvent;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
constexpr char kSnakeAppName[] = "Snake";
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 20;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;
constexpr int kMinMoveIntervalMs = 80;
constexpr int kBaseMoveIntervalMs = 220;
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 18;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;
constexpr int kMinMoveIntervalMs = 80;
constexpr int kBaseMoveIntervalMs = 220;
constexpr int kIntervalSpeedupPerSegment = 4;
struct Point {
@@ -69,16 +70,12 @@ public:
void onStop() { cancelMoveTimer(); }
void handleEvent(const AppEvent& event) {
switch (event.type) {
case AppEventType::Button:
handleButtons(event.button);
break;
case AppEventType::Timer:
handleTimer(event.timer.handle);
break;
}
std::optional<std::uint32_t> handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); },
[](const AppTimeoutEvent&) { /* ignore */ }));
renderIfNeeded();
return std::nullopt;
}
private:
@@ -94,8 +91,8 @@ private:
bool dirty = false;
int score = 0;
int highScore = 0;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
@@ -163,7 +160,7 @@ private:
}
void advance() {
direction = queuedDirection;
direction = queuedDirection;
Point nextHead = snake.front();
switch (direction) {
case Direction::Up:
@@ -253,14 +250,16 @@ private:
void scheduleMoveTimer() {
cancelMoveTimer();
const std::uint32_t interval = currentInterval();
moveTimer = context.scheduleRepeatingTimer(interval);
if (auto* timer = context.timer())
moveTimer = timer->scheduleTimer(interval, true);
}
void cancelMoveTimer() {
if (moveTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(moveTimer);
moveTimer = cardboy::sdk::kInvalidAppTimer;
}
if (moveTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(moveTimer);
moveTimer = cardboy::sdk::kInvalidAppTimer;
}
[[nodiscard]] std::uint32_t currentInterval() const {
@@ -302,9 +301,7 @@ private:
framebuffer.sendFrame();
}
[[nodiscard]] int boardOriginX() const {
return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
}
[[nodiscard]] int boardOriginX() const { return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2; }
[[nodiscard]] int boardOriginY() const {
const int centered = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
@@ -409,9 +406,9 @@ class SnakeApp final : public cardboy::sdk::IApp {
public:
explicit SnakeApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override { return game.handleEvent(event); }
private:
SnakeGame game;

View File

@@ -22,7 +22,7 @@ namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerEvent;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
@@ -148,16 +148,12 @@ public:
void onStop() { cancelTimers(); }
void handleEvent(const AppEvent& event) {
switch (event.type) {
case AppEventType::Button:
handleButtons(event.button);
break;
case AppEventType::Timer:
handleTimer(event.timer.handle);
break;
}
std::optional<std::uint32_t> handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); },
[](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ }));
renderIfNeeded();
return std::nullopt;
}
private:
@@ -240,35 +236,40 @@ private:
void cancelTimers() {
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(dropTimer);
if (auto* timer = context.timer())
timer->cancelTimer(dropTimer);
dropTimer = cardboy::sdk::kInvalidAppTimer;
}
cancelSoftDropTimer();
}
void cancelSoftDropTimer() {
if (softTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(softTimer);
softTimer = cardboy::sdk::kInvalidAppTimer;
}
if (softTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(softTimer);
softTimer = cardboy::sdk::kInvalidAppTimer;
}
void scheduleDropTimer() {
cancelDropTimer();
const std::uint32_t interval = dropIntervalMs();
dropTimer = context.scheduleRepeatingTimer(interval);
if (auto* timer = context.timer())
dropTimer = timer->scheduleTimer(interval, true);
}
void cancelDropTimer() {
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(dropTimer);
dropTimer = cardboy::sdk::kInvalidAppTimer;
}
if (dropTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(dropTimer);
dropTimer = cardboy::sdk::kInvalidAppTimer;
}
void scheduleSoftDropTimer() {
cancelSoftDropTimer();
softTimer = context.scheduleRepeatingTimer(60);
if (auto* timer = context.timer())
softTimer = timer->scheduleTimer(60, true);
}
[[nodiscard]] std::uint32_t dropIntervalMs() const {
@@ -634,9 +635,9 @@ class TetrisApp final : public cardboy::sdk::IApp {
public:
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override { return game.handleEvent(event); }
private:
TetrisGame game;

View File

@@ -16,7 +16,6 @@ target_sources(cardboy_backend_interface
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/backend/backend_interface.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/backend.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/display_spec.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/event_bus.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/loop_hooks.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/input_state.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp

View File

@@ -0,0 +1,71 @@
#pragma once
#include "cardboy/sdk/input_state.hpp"
#include <cstdint>
#include <type_traits>
#include <utility>
#include <variant>
namespace cardboy::sdk {
using AppTimerHandle = std::uint32_t;
constexpr AppTimerHandle kInvalidAppTimer = 0;
struct AppButtonEvent {
InputState current{};
InputState previous{};
};
struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppTimeoutEvent {};
struct AppEvent {
using Data = std::variant<AppButtonEvent, AppTimerEvent, AppTimeoutEvent>;
std::uint32_t timestamp_ms = 0;
Data data{AppButtonEvent{}};
[[nodiscard]] bool isButton() const { return std::holds_alternative<AppButtonEvent>(data); }
[[nodiscard]] bool isTimer() const { return std::holds_alternative<AppTimerEvent>(data); }
[[nodiscard]] bool isTimeout() const { return std::holds_alternative<AppTimeoutEvent>(data); }
[[nodiscard]] const AppButtonEvent* button() const { return std::get_if<AppButtonEvent>(&data); }
[[nodiscard]] AppButtonEvent* button() { return std::get_if<AppButtonEvent>(&data); }
[[nodiscard]] const AppTimerEvent* timer() const { return std::get_if<AppTimerEvent>(&data); }
[[nodiscard]] AppTimerEvent* timer() { return std::get_if<AppTimerEvent>(&data); }
[[nodiscard]] const AppTimeoutEvent* timeout() const { return std::get_if<AppTimeoutEvent>(&data); }
[[nodiscard]] AppTimeoutEvent* timeout() { return std::get_if<AppTimeoutEvent>(&data); }
template<typename Visitor>
decltype(auto) visit(Visitor&& visitor) {
return std::visit(std::forward<Visitor>(visitor), data);
}
template<typename Visitor>
decltype(auto) visit(Visitor&& visitor) const {
return std::visit(std::forward<Visitor>(visitor), data);
}
};
static_assert(std::is_trivially_copyable_v<AppEvent>);
template<typename... Ts>
struct Overload : Ts... {
using Ts::operator()...;
};
template<typename... Ts>
Overload(Ts...) -> Overload<Ts...>;
template<typename... Ts>
constexpr auto overload(Ts&&... ts) {
return Overload<std::decay_t<Ts>...>{std::forward<Ts>(ts)...};
}
} // namespace cardboy::sdk

View File

@@ -1,42 +0,0 @@
#pragma once
#include <cstdint>
namespace cardboy::sdk {
enum class EventBusSignal : std::uint32_t {
None = 0,
Input = 1u << 0,
Timer = 1u << 1,
External = 1u << 2,
};
inline EventBusSignal operator|(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
}
inline EventBusSignal& operator|=(EventBusSignal& lhs, EventBusSignal rhs) {
lhs = lhs | rhs;
return lhs;
}
inline EventBusSignal operator&(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
}
inline std::uint32_t to_event_bits(EventBusSignal signal) { return static_cast<std::uint32_t>(signal); }
class IEventBus {
public:
static constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
virtual ~IEventBus() = default;
virtual void signal(std::uint32_t bits) = 0;
virtual void signalFromISR(std::uint32_t bits) = 0;
virtual std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) = 0;
virtual void scheduleTimerSignal(std::uint32_t delay_ms) = 0;
virtual void cancelTimerSignal() = 0;
};
} // namespace cardboy::sdk

View File

@@ -11,8 +11,8 @@ public:
static void invokePreSend(void* framebuffer);
private:
static PreSendHook hook_;
static void* userData_;
static PreSendHook _hook;
static void* _userData;
};
} // namespace cardboy::sdk

View File

@@ -11,6 +11,13 @@ struct InputState {
bool b = false;
bool select = false;
bool start = false;
bool operator==(const InputState& other) const {
return up == other.up && left == other.left && right == other.right && down == other.down && a == other.a &&
b == other.b && select == other.select && start == other.start;
}
bool operator!=(const InputState& other) const { return !(*this == other); }
};
} // namespace cardboy::sdk

View File

@@ -1,11 +1,14 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/loop_hooks.hpp"
#include "cardboy/sdk/timer_service.hpp"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace cardboy::sdk {
@@ -36,6 +39,7 @@ public:
[[nodiscard]] virtual float voltage() const { return 0.0f; }
[[nodiscard]] virtual float charge() const { return 0.0f; }
[[nodiscard]] virtual float current() const { return 0.0f; }
[[nodiscard]] virtual float percentage() const { return 0.0f; }
};
class IStorage {
@@ -69,15 +73,59 @@ public:
[[nodiscard]] virtual std::string basePath() const = 0;
};
class INotificationCenter {
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;
};
virtual ~INotificationCenter() = default;
virtual void pushNotification(Notification notification) = 0;
[[nodiscard]] virtual std::uint32_t revision() const = 0;
[[nodiscard]] virtual std::vector<Notification> recent(std::size_t limit) const = 0;
virtual void markAllRead() = 0;
virtual void clear() = 0;
virtual void removeById(std::uint64_t id) = 0;
virtual void removeByExternalId(std::uint64_t externalId) = 0;
};
class IEventBus {
public:
virtual ~IEventBus() = default;
virtual void post(const AppEvent& event) = 0;
virtual std::optional<AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) = 0;
};
struct AppScopedServices {
ITimerService* timer = nullptr;
virtual ~AppScopedServices() = default;
};
class IAppServiceProvider {
public:
virtual ~IAppServiceProvider() = default;
[[nodiscard]] virtual std::unique_ptr<AppScopedServices> createScopedServices(std::uint64_t generation) = 0;
};
struct Services {
IBuzzer* buzzer = nullptr;
IBatteryMonitor* battery = nullptr;
IStorage* storage = nullptr;
IRandom* random = nullptr;
IHighResClock* highResClock = nullptr;
IFilesystem* filesystem = nullptr;
IEventBus* eventBus = nullptr;
ILoopHooks* loopHooks = nullptr;
IBuzzer* buzzer = nullptr;
IBatteryMonitor* battery = nullptr;
IStorage* storage = nullptr;
IRandom* random = nullptr;
IHighResClock* highResClock = nullptr;
IFilesystem* filesystem = nullptr;
IEventBus* eventBus = nullptr;
ILoopHooks* loopHooks = nullptr;
INotificationCenter* notifications = nullptr;
IAppServiceProvider* appServices = nullptr;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,18 @@
#pragma once
#include "cardboy/sdk/app_events.hpp"
#include <cstdint>
namespace cardboy::sdk {
class ITimerService {
public:
virtual ~ITimerService() = default;
virtual AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) = 0;
virtual void cancelTimer(AppTimerHandle handle) = 0;
virtual void cancelAllTimers() = 0;
};
} // namespace cardboy::sdk

View File

@@ -1,6 +1,5 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -9,8 +8,10 @@
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <random>
#include <string>
@@ -39,165 +40,232 @@ public:
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
public:
[[nodiscard]] bool hasData() const override { return false; }
};
class DesktopStorage final : public cardboy::sdk::IStorage {
public:
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
class DesktopStorage final : public cardboy::sdk::IStorage {
public:
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
private:
std::unordered_map<std::string, std::uint32_t> data;
private:
std::unordered_map<std::string, std::uint32_t> data;
static std::string composeKey(std::string_view ns, std::string_view key);
};
static std::string composeKey(std::string_view ns, std::string_view key);
};
class DesktopRandom final : public cardboy::sdk::IRandom {
public:
DesktopRandom();
class DesktopRandom final : public cardboy::sdk::IRandom {
public:
DesktopRandom();
[[nodiscard]] std::uint32_t nextUint32() override;
[[nodiscard]] std::uint32_t nextUint32() override;
private:
std::mt19937 rng;
std::uniform_int_distribution<std::uint32_t> dist;
};
private:
std::mt19937 rng;
std::uniform_int_distribution<std::uint32_t> dist;
};
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
public:
DesktopHighResClock();
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
public:
DesktopHighResClock();
[[nodiscard]] std::uint64_t micros() override;
[[nodiscard]] std::uint64_t micros() override;
private:
const std::chrono::steady_clock::time_point start;
};
private:
const std::chrono::steady_clock::time_point start;
};
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
explicit DesktopEventBus(DesktopRuntime& owner);
~DesktopEventBus() override;
class DesktopNotificationCenter final : public cardboy::sdk::INotificationCenter {
public:
void pushNotification(Notification notification) override;
[[nodiscard]] std::uint32_t revision() const override;
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override;
void markAllRead() override;
void clear() override;
void removeById(std::uint64_t id) override;
void removeByExternalId(std::uint64_t externalId) override;
void signal(std::uint32_t bits) override;
void signalFromISR(std::uint32_t bits) override;
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
void scheduleTimerSignal(std::uint32_t delay_ms) override;
void cancelTimerSignal() override;
private:
static constexpr std::size_t kMaxEntries = 8;
private:
DesktopRuntime& runtime;
std::mutex mutex;
std::condition_variable cv;
std::uint32_t pendingBits = 0;
mutable std::mutex mutex;
std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
std::mutex timerMutex;
std::condition_variable timerCv;
std::thread timerThread;
bool timerCancel = false;
};
class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks {
public:
explicit DesktopLoopHooks(DesktopRuntime& owner);
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
void onLoopIteration() override;
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
[[nodiscard]] bool frameInFlight_impl() const { return false; }
private:
DesktopRuntime& runtime;
};
private:
DesktopRuntime& runtime;
};
class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
void post(const cardboy::sdk::AppEvent& event) override;
std::optional<cardboy::sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override;
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
private:
std::mutex mutex;
std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> queue;
};
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
class DesktopTimerService final : public cardboy::sdk::ITimerService {
public:
DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus);
~DesktopTimerService() override;
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
private:
struct TimerRecord {
cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
std::chrono::steady_clock::time_point due;
std::chrono::milliseconds interval{0};
bool repeat = false;
bool active = true;
};
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
void workerLoop();
void wakeWorker();
void cleanupInactive();
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
std::mutex mutex;
std::condition_variable cv;
std::vector<TimerRecord> timers;
bool stopWorker = false;
std::thread worker;
cardboy::sdk::AppTimerHandle nextHandle = 1;
};
class DesktopRuntime {
public:
DesktopRuntime();
class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider {
public:
DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus);
cardboy::sdk::Services& serviceRegistry();
void processEvents();
void presentIfNeeded();
void sleepFor(std::uint32_t ms);
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
[[nodiscard]] bool isRunning() const { return running; }
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
};
DesktopFramebuffer framebuffer;
DesktopInput input;
DesktopClock clock;
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
};
private:
friend class DesktopFramebuffer;
friend class DesktopInput;
friend class DesktopClock;
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
void setPixel(int x, int y, bool on);
void clearPixels(bool on);
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
[[nodiscard]] bool frameInFlight_impl() const { return false; }
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::vector<std::uint8_t> pixels;
bool dirty = true;
bool running = true;
bool clearNextFrame = true;
private:
DesktopRuntime& runtime;
};
DesktopBuzzer buzzerService;
DesktopBattery batteryService;
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
cardboy::sdk::Services services{};
};
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
class DesktopRuntime {
public:
DesktopRuntime();
cardboy::sdk::Services& serviceRegistry();
void processEvents();
void presentIfNeeded();
void sleepFor(std::uint32_t ms);
[[nodiscard]] bool isRunning() const { return running; }
DesktopFramebuffer framebuffer;
DesktopInput input;
DesktopClock clock;
private:
friend class DesktopFramebuffer;
friend class DesktopInput;
friend class DesktopClock;
void setPixel(int x, int y, bool on);
void clearPixels(bool on);
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::vector<std::uint8_t> pixels;
bool dirty = true;
bool running = true;
bool clearNextFrame = true;
DesktopBuzzer buzzerService;
DesktopBattery batteryService;
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
DesktopAppServiceProvider appServiceProvider;
DesktopNotificationCenter notificationService;
DesktopLoopHooks loopHooksService;
cardboy::sdk::Services services{};
};
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
} // namespace cardboy::backend::desktop
namespace cardboy::backend {
using DesktopBackend = desktop::Backend;
using DesktopBackend = desktop::Backend;
} // namespace cardboy::backend

View File

@@ -8,95 +8,167 @@
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <stdexcept>
#include <system_error>
#include <utility>
namespace cardboy::backend::desktop {
DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {}
namespace {
constexpr std::size_t kDesktopEventQueueLimit = 64;
} // namespace
DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); }
void DesktopEventBus::post(const cardboy::sdk::AppEvent& event) {
std::lock_guard<std::mutex> lock(mutex);
queue.push_back(event);
cv.notify_one();
}
void DesktopEventBus::signal(std::uint32_t bits) {
if (bits == 0)
return;
std::optional<cardboy::sdk::AppEvent> DesktopEventBus::pop(std::optional<std::uint32_t> timeout_ms) {
std::unique_lock<std::mutex> lock(mutex);
if (queue.empty()) {
if (!timeout_ms) {
return std::nullopt;
}
auto timeout = std::chrono::milliseconds(*timeout_ms);
cv.wait_for(lock, timeout, [this] { return !queue.empty(); });
if (queue.empty()) {
return std::nullopt;
}
}
auto event = queue.front();
queue.pop_front();
return event;
}
DesktopTimerService::DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus) :
runtime(owner), eventBus(eventBus) {
worker = std::thread(&DesktopTimerService::workerLoop, this);
}
DesktopTimerService::~DesktopTimerService() {
{
std::lock_guard<std::mutex> lock(mutex);
pendingBits |= bits;
stopWorker = true;
}
cv.notify_all();
if (worker.joinable())
worker.join();
cancelAllTimers();
}
void DesktopEventBus::signalFromISR(std::uint32_t bits) { signal(bits); }
cardboy::sdk::AppTimerHandle DesktopTimerService::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
const auto now = std::chrono::steady_clock::now();
const auto effectiveDelayMs = std::chrono::milliseconds(delay_ms);
const auto dueTime = delay_ms == 0 ? now : now + effectiveDelayMs;
const auto interval = std::chrono::milliseconds(
std::max<std::uint32_t>(1, repeat ? std::max(delay_ms, 1u) : std::max(delay_ms, 1u)));
std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (mask == 0)
return 0;
TimerRecord record{};
record.repeat = repeat;
record.interval = interval;
record.due = dueTime;
record.active = true;
const auto start = std::chrono::steady_clock::now();
const bool infinite = timeout_ms == cardboy::sdk::IEventBus::kWaitForever;
{
std::lock_guard<std::mutex> lock(mutex);
cardboy::sdk::AppTimerHandle handle = cardboy::sdk::kInvalidAppTimer;
do {
handle = nextHandle++;
} while (handle == cardboy::sdk::kInvalidAppTimer);
if (nextHandle == cardboy::sdk::kInvalidAppTimer)
++nextHandle;
record.handle = handle;
timers.push_back(record);
wakeWorker();
return handle;
}
}
while (true) {
{
std::lock_guard<std::mutex> lock(mutex);
const std::uint32_t ready = pendingBits & mask;
if (ready != 0) {
pendingBits &= ~mask;
return ready;
void DesktopTimerService::cancelTimer(cardboy::sdk::AppTimerHandle handle) {
if (handle == cardboy::sdk::kInvalidAppTimer)
return;
std::lock_guard<std::mutex> lock(mutex);
for (auto& record: timers) {
if (record.handle == handle)
record.active = false;
}
cleanupInactive();
wakeWorker();
}
void DesktopTimerService::cancelAllTimers() {
std::lock_guard<std::mutex> lock(mutex);
for (auto& record: timers) {
record.active = false;
}
cleanupInactive();
wakeWorker();
}
void DesktopTimerService::workerLoop() {
std::unique_lock<std::mutex> lock(mutex);
while (!stopWorker) {
cleanupInactive();
if (timers.empty()) {
cv.wait(lock, [this] { return stopWorker || !timers.empty(); });
continue;
}
auto nextIt = std::min_element(timers.begin(), timers.end(),
[](const TimerRecord& a, const TimerRecord& b) { return a.due < b.due; });
if (nextIt == timers.end())
continue;
if (!nextIt->active)
continue;
const auto now = std::chrono::steady_clock::now();
if (now >= nextIt->due) {
TimerRecord record = *nextIt;
if (record.repeat) {
nextIt->due = now + record.interval;
} else {
nextIt->active = false;
}
lock.unlock();
cardboy::sdk::AppTimerEvent timerEvent{};
timerEvent.handle = record.handle;
cardboy::sdk::AppEvent event{};
event.timestamp_ms = runtime.clock.millis();
event.data = timerEvent;
eventBus.post(event);
lock.lock();
continue;
}
if (!infinite) {
const auto now = std::chrono::steady_clock::now();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
if (elapsedMs >= static_cast<std::int64_t>(timeout_ms))
return 0;
const auto remaining = timeout_ms - static_cast<std::uint32_t>(elapsedMs);
runtime.sleepFor(std::min<std::uint32_t>(remaining, 8));
} else {
runtime.sleepFor(8);
}
cv.wait_until(lock, nextIt->due, [this] { return stopWorker; });
}
}
void DesktopEventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
cancelTimerSignal();
void DesktopTimerService::wakeWorker() { cv.notify_all(); }
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
timerThread = std::thread([this, delay_ms]() {
std::unique_lock<std::mutex> lock(timerMutex);
const bool cancelled =
timerCv.wait_for(lock, std::chrono::milliseconds(delay_ms), [this] { return timerCancel; });
lock.unlock();
if (!cancelled)
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
});
void DesktopTimerService::cleanupInactive() {
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& record) { return !record.active; }),
timers.end());
}
void DesktopEventBus::cancelTimerSignal() {
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = true;
}
timerCv.notify_all();
if (timerThread.joinable())
timerThread.join();
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
DesktopAppServiceProvider::DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus) :
runtime(owner), eventBus(bus) {}
std::unique_ptr<cardboy::sdk::AppScopedServices>
DesktopAppServiceProvider::createScopedServices(std::uint64_t generation) {
(void) generation;
auto scoped = std::make_unique<ScopedServices>();
scoped->ownedTimer = std::make_unique<DesktopTimerService>(runtime, eventBus);
scoped->timer = scoped->ownedTimer.get();
return scoped;
}
bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) {
@@ -149,6 +221,108 @@ bool DesktopFilesystem::mount() {
return mounted;
}
void DesktopNotificationCenter::pushNotification(Notification notification) {
if (notification.timestamp == 0) {
notification.timestamp = static_cast<std::uint64_t>(std::time(nullptr));
}
std::lock_guard<std::mutex> 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;
entries.push_back(std::move(notification));
while (entries.size() > kMaxEntries)
entries.erase(entries.begin());
++revisionCounter;
}
std::uint32_t DesktopNotificationCenter::revision() const {
std::lock_guard<std::mutex> lock(mutex);
return revisionCounter;
}
std::vector<DesktopNotificationCenter::Notification> DesktopNotificationCenter::recent(std::size_t limit) const {
std::lock_guard<std::mutex> lock(mutex);
const std::size_t count = std::min<std::size_t>(limit, entries.size());
std::vector<Notification> result;
result.reserve(count);
for (std::size_t i = 0; i < count; ++i) {
result.push_back(entries[entries.size() - 1 - i]);
}
return result;
}
void DesktopNotificationCenter::markAllRead() {
std::lock_guard<std::mutex> lock(mutex);
bool changed = false;
for (auto& entry: entries) {
if (entry.unread) {
entry.unread = false;
changed = true;
}
}
if (changed)
++revisionCounter;
}
void DesktopNotificationCenter::clear() {
std::lock_guard<std::mutex> lock(mutex);
if (entries.empty())
return;
entries.clear();
++revisionCounter;
}
void DesktopNotificationCenter::removeById(std::uint64_t id) {
if (id == 0)
return;
std::lock_guard<std::mutex> lock(mutex);
bool removed = false;
for (auto it = entries.begin(); it != entries.end();) {
if (it->id == id) {
it = entries.erase(it);
removed = true;
} else {
++it;
}
}
if (removed)
++revisionCounter;
}
void DesktopNotificationCenter::removeByExternalId(std::uint64_t externalId) {
if (externalId == 0)
return;
std::lock_guard<std::mutex> 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;
}
DesktopLoopHooks::DesktopLoopHooks(DesktopRuntime& owner) : runtime(owner) {}
void DesktopLoopHooks::onLoopIteration() {
runtime.processEvents();
runtime.presentIfNeeded();
}
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
@@ -176,26 +350,29 @@ DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
bool handled = true;
const auto oldState = state;
bool handled = true;
switch (key) {
case sf::Keyboard::Key::Up:
case sf::Keyboard::Key::W:
state.up = pressed;
break;
case sf::Keyboard::Key::Down:
case sf::Keyboard::Key::S:
state.down = pressed;
break;
case sf::Keyboard::Key::Left:
case sf::Keyboard::Key::A:
state.left = pressed;
break;
case sf::Keyboard::Key::Right:
case sf::Keyboard::Key::D:
state.right = pressed;
break;
case sf::Keyboard::Key::Z:
case sf::Keyboard::Key::A:
state.a = pressed;
break;
case sf::Keyboard::Key::X:
case sf::Keyboard::Key::S:
state.b = pressed;
break;
case sf::Keyboard::Key::Space:
@@ -208,8 +385,11 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
handled = false;
break;
}
if (handled)
runtime.eventBusService.signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
if (handled && oldState != state) {
cardboy::sdk::AppButtonEvent btnEvent{oldState, state};
cardboy::sdk::AppEvent event{runtime.clock.millis(), btnEvent};
runtime.eventBusService.post(event);
}
}
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
@@ -227,23 +407,29 @@ DesktopRuntime::DesktopRuntime() :
"Cardboy Desktop"),
texture(), sprite(texture),
pixels(static_cast<std::size_t>(cardboy::sdk::kDisplayWidth * cardboy::sdk::kDisplayHeight) * 4, 0),
framebuffer(*this), input(*this), clock(*this), eventBusService(*this) {
framebuffer(*this), input(*this), clock(*this), eventBusService(), appServiceProvider(*this, eventBusService),
loopHooksService(*this) {
window.setFramerateLimit(60);
if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight}))
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
sprite.setTexture(texture, true);
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
clearPixels(true);
clearPixels(false);
presentIfNeeded();
window.requestFocus();
services.buzzer = &buzzerService;
services.battery = &batteryService;
services.storage = &storageService;
services.random = &randomService;
services.highResClock = &highResService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
services.loopHooks = nullptr;
std::cout << "Desktop window initialized and presented." << std::endl;
services.buzzer = &buzzerService;
services.battery = &batteryService;
services.storage = &storageService;
services.random = &randomService;
services.highResClock = &highResService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
services.appServices = &appServiceProvider;
services.loopHooks = &loopHooksService;
services.notifications = &notificationService;
}
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }

View File

@@ -14,8 +14,6 @@ constexpr unsigned char kFallbackChar = '?';
inline unsigned char normalizeChar(char ch) {
unsigned char uc = static_cast<unsigned char>(ch);
if (uc >= 'a' && uc <= 'z')
uc = static_cast<unsigned char>(std::toupper(static_cast<unsigned char>(uc)));
if (!std::isprint(static_cast<unsigned char>(uc)))
return kFallbackChar;
return uc;

View File

@@ -1,11 +1,13 @@
#pragma once
#include <cardboy/sdk/app_events.hpp>
#include <cardboy/sdk/backend.hpp>
#include <cardboy/sdk/platform.hpp>
#include <cardboy/sdk/services.hpp>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -14,30 +16,6 @@ namespace cardboy::sdk {
class AppSystem;
using AppTimerHandle = std::uint32_t;
constexpr AppTimerHandle kInvalidAppTimer = 0;
enum class AppEventType {
Button,
Timer,
};
struct AppButtonEvent {
InputState current{};
InputState previous{};
};
struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppEvent {
AppEventType type;
std::uint32_t timestamp_ms = 0;
AppButtonEvent button{};
AppTimerEvent timer{};
};
using ActiveBackend = cardboy::backend::ActiveBackend;
struct AppContext {
@@ -56,72 +34,52 @@ struct AppContext {
[[nodiscard]] Services* getServices() const { return services; }
[[nodiscard]] IBuzzer* buzzer() const { return services ? services->buzzer : nullptr; }
[[nodiscard]] IBatteryMonitor* battery() const { return services ? services->battery : nullptr; }
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
[[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; }
[[nodiscard]] ILoopHooks* loopHooks() const { return services ? services->loopHooks : nullptr; }
[[nodiscard]] IBuzzer* buzzer() const { return services ? services->buzzer : nullptr; }
[[nodiscard]] IBatteryMonitor* battery() const { return services ? services->battery : nullptr; }
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
[[nodiscard]] AppScopedServices* appServices() const { return _scopedServices; }
[[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; }
[[nodiscard]] ITimerService* timer() const { return _scopedServices ? _scopedServices->timer : nullptr; }
[[nodiscard]] ILoopHooks* loopHooks() const { return services ? services->loopHooks : nullptr; }
[[nodiscard]] INotificationCenter* notificationCenter() const {
return services ? services->notifications : nullptr;
}
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;
pendingAppName.clear();
pendingSwitchByName = false;
pendingSwitch = true;
_pendingAppIndex = index;
_pendingAppName.clear();
_pendingSwitchByName = false;
_pendingSwitch = true;
}
void requestAppSwitchByName(std::string_view name) {
pendingAppName.assign(name.begin(), name.end());
pendingSwitchByName = true;
pendingSwitch = true;
_pendingAppName.assign(name.begin(), name.end());
_pendingSwitchByName = true;
_pendingSwitch = true;
}
[[nodiscard]] bool hasPendingAppSwitch() const { return pendingSwitch; }
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat = false) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(delay_ms, repeat);
}
AppTimerHandle scheduleRepeatingTimer(std::uint32_t interval_ms) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(interval_ms, true);
}
void cancelTimer(AppTimerHandle handle) {
if (!system)
return;
cancelTimerInternal(handle);
}
void cancelAllTimers() {
if (!system)
return;
cancelAllTimersInternal();
}
[[nodiscard]] bool hasPendingAppSwitch() const { return _pendingSwitch; }
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
bool _pendingSwitch = false;
bool _pendingSwitchByName = false;
std::size_t _pendingAppIndex = 0;
std::string _pendingAppName;
AppScopedServices* _scopedServices = nullptr;
AppTimerHandle scheduleTimerInternal(std::uint32_t delay_ms, bool repeat);
void cancelTimerInternal(AppTimerHandle handle);
void cancelAllTimersInternal();
void setScopedServices(AppScopedServices* services) { _scopedServices = services; }
};
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
virtual void onStart() {}
virtual void onStop() {}
virtual std::optional<std::uint32_t> handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {

View File

@@ -1,6 +1,5 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "app_framework.hpp"
#include <cstdint>
@@ -16,68 +15,31 @@ public:
~AppSystem();
void registerApp(std::unique_ptr<IAppFactory> factory);
bool startApp(const std::string& name);
bool startAppByIndex(std::size_t index);
void startApp(const std::string& name);
void startAppByIndex(std::size_t index);
void run();
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] std::size_t appCount() const { return _factories.size(); }
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
[[nodiscard]] std::size_t currentFactoryIndex() const { return _activeIndex; }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
[[nodiscard]] const IApp* currentApp() const { return _current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return _activeFactory; }
private:
friend struct AppContext;
void handlePendingSwitchRequest();
std::unique_ptr<AppScopedServices> createAppScopedServices(std::uint64_t generation);
struct TimerRecord {
AppTimerHandle id = kInvalidAppTimer;
std::uint32_t generation = 0;
std::uint32_t due_ms = 0;
std::uint32_t interval_ms = 0;
bool repeat = false;
bool active = false;
};
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat);
void cancelTimer(AppTimerHandle handle);
void cancelAllTimers();
void dispatchEvent(const AppEvent& event);
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
void notifyEventBus(EventBusSignal signal);
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
bool suppressInputs = false;
AppContext _context;
std::vector<std::unique_ptr<IAppFactory>> _factories;
std::unique_ptr<IApp> _current;
IAppFactory* _activeFactory = nullptr;
std::size_t _activeIndex = static_cast<std::size_t>(-1);
std::unique_ptr<AppScopedServices> _scopedServices;
std::uint64_t _nextScopedGeneration = 1;
std::optional<std::uint32_t> _currentTimeout;
};
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
}
inline void AppContext::cancelTimerInternal(AppTimerHandle handle) {
if (system)
system->cancelTimer(handle);
}
inline void AppContext::cancelAllTimersInternal() {
if (system)
system->cancelAllTimers();
}
} // namespace cardboy::sdk

View File

@@ -13,8 +13,8 @@ public:
static void invokePreSend(void* framebuffer);
private:
static PreSendHook hook_;
static void* userData_;
static PreSendHook _hook;
static void* _userData;
};
} // namespace cardboy::sdk

View File

@@ -17,11 +17,11 @@ class StatusBar {
public:
static StatusBar& instance();
void setServices(Services* services) { services_ = services; }
void setServices(Services* services) { _services = services; }
void setEnabled(bool value);
void toggle();
[[nodiscard]] bool isEnabled() const { return enabled_; }
[[nodiscard]] bool isEnabled() const { return _enabled; }
void setCurrentAppName(std::string_view name);
@@ -29,7 +29,7 @@ public:
template<typename Framebuffer>
void renderIfEnabled(Framebuffer& fb) {
if (!enabled_)
if (!_enabled)
return;
renderBar(fb);
}
@@ -56,9 +56,6 @@ private:
fb.drawPixel(x, y, true);
}
for (int x = 0; x < width; ++x)
fb.drawPixel(x, 0, false);
const int textY = 1;
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
if (bottomSeparatorY < fillHeight) {
@@ -84,9 +81,9 @@ private:
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
[[nodiscard]] std::string prepareRightText() const;
bool enabled_ = false;
Services* services_ = nullptr;
std::string appName_{};
bool _enabled = false;
Services* _services = nullptr;
std::string _appName{};
};
} // namespace cardboy::sdk

View File

@@ -3,19 +3,11 @@
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <limits>
#include <optional>
#include <utility>
namespace cardboy::sdk {
namespace {
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a || a.b != b.b ||
a.select != b.select || a.start != b.start;
}
[[nodiscard]] bool anyButtonPressed(const InputState& state) {
return state.up || state.down || state.left || state.right || state.a || state.b || state.select || state.start;
}
template<typename Framebuffer>
void statusBarPreSendHook(void* framebuffer, void* userData) {
@@ -26,263 +18,128 @@ void statusBarPreSendHook(void* framebuffer, void* userData) {
}
} // namespace
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
context.system = this;
AppSystem::AppSystem(AppContext ctx) : _context(std::move(ctx)) {
_context.system = this;
auto& statusBar = StatusBar::instance();
statusBar.setServices(context.services);
using FBType = typename AppContext::Framebuffer;
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
statusBar.setServices(_context.services);
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<AppContext::Framebuffer>, &statusBar);
}
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
if (!factory)
return;
factories.emplace_back(std::move(factory));
assert(factory);
_factories.emplace_back(std::move(factory));
}
bool AppSystem::startApp(const std::string& name) {
for (std::size_t i = 0; i < factories.size(); ++i) {
if (factories[i]->name() == name)
return startAppByIndex(i);
void AppSystem::startApp(const std::string& name) {
for (std::size_t i = 0; i < _factories.size(); ++i) {
if (_factories[i]->name() == name)
startAppByIndex(i);
}
return false;
}
bool AppSystem::startAppByIndex(std::size_t index) {
if (index >= factories.size())
return false;
void AppSystem::startAppByIndex(std::size_t index) {
assert(index < _factories.size());
context.system = this;
auto& factory = factories[index];
auto app = factory->create(context);
if (!app)
return false;
if (current) {
current->onStop();
current.reset();
if (_current) {
_current->onStop();
_current.reset();
}
activeFactory = factory.get();
activeIndex = index;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
clearTimersForCurrentApp();
current = std::move(app);
lastInputState = context.input.readState();
suppressInputs = true;
StatusBar::instance().setServices(context.services);
StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : "");
current->onStart();
return true;
_context.system = this;
auto& factory = _factories[index];
const std::uint64_t newGeneration = _nextScopedGeneration++;
auto app = factory->create(_context);
assert(app);
_scopedServices.reset();
auto scoped = createAppScopedServices(newGeneration);
_scopedServices = std::move(scoped);
_context.setScopedServices(_scopedServices.get());
_activeFactory = factory.get();
_activeIndex = index;
_context._pendingSwitch = false;
_context._pendingSwitchByName = false;
_context._pendingAppName.clear();
_current = std::move(app);
_currentTimeout = std::nullopt;
StatusBar::instance().setServices(_context.services);
StatusBar::instance().setCurrentAppName(_activeFactory ? _activeFactory->name() : "");
_current->onStart();
}
void AppSystem::run() {
if (!current) {
if (factories.empty() || !startAppByIndex(0))
return;
if (!_current) {
assert(!_factories.empty());
startAppByIndex(0);
}
std::vector<AppEvent> events;
events.reserve(4);
while (true) {
if (auto* hooks = context.loopHooks())
if (auto* hooks = _context.loopHooks())
hooks->onLoopIteration();
events.clear();
const std::uint32_t now = context.clock.millis();
processDueTimers(now, events);
const InputState inputNow = context.input.readState();
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
if (suppressInputs) {
lastInputState = inputNow;
if (!anyButtonPressed(inputNow))
suppressInputs = false;
} else if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) {
AppEvent evt{};
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
evt.button.current = inputNow;
evt.button.previous = lastInputState;
events.push_back(evt);
lastInputState = inputNow;
} else if (consumedByStatusToggle) {
lastInputState = inputNow;
}
for (const auto& evt: events) {
dispatchEvent(evt);
if (handlePendingSwitchRequest())
break;
}
const std::uint32_t waitBase = context.clock.millis();
std::uint32_t waitMs = nextTimerDueMs(waitBase);
if (waitMs == 0)
continue;
auto* eventBus = context.eventBus();
if (!eventBus)
return;
const std::uint32_t mask = to_event_bits(EventBusSignal::Input) | to_event_bits(EventBusSignal::Timer);
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
eventBus->cancelTimerSignal();
eventBus->wait(mask, IEventBus::kWaitForever);
AppEvent event;
auto event_opt = _context.eventBus()->pop(_currentTimeout);
if (!event_opt) {
event = AppEvent{_context.clock.millis(), AppTimeoutEvent{}};
} else {
eventBus->scheduleTimerSignal(waitMs);
eventBus->wait(mask, IEventBus::kWaitForever);
event = *event_opt;
}
if (const auto* btn = event.button()) {
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(btn->current, btn->previous);
if (consumedByStatusToggle) {
continue;
}
}
_currentTimeout = _current->handleEvent(event);
if (_context._pendingSwitch) {
handlePendingSwitchRequest();
continue;
}
}
}
const IAppFactory* AppSystem::factoryAt(std::size_t index) const {
if (index >= factories.size())
if (index >= _factories.size())
return nullptr;
return factories[index].get();
return _factories[index].get();
}
std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
if (!factory)
return static_cast<std::size_t>(-1);
for (std::size_t i = 0; i < factories.size(); ++i) {
if (factories[i].get() == factory)
for (std::size_t i = 0; i < _factories.size(); ++i) {
if (_factories[i].get() == factory)
return i;
}
return static_cast<std::size_t>(-1);
}
AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
if (!current)
return kInvalidAppTimer;
TimerRecord record;
record.id = nextTimerId++;
if (record.id == kInvalidAppTimer)
record.id = nextTimerId++;
record.generation = currentGeneration;
const auto now = context.clock.millis();
record.due_ms = now + delay_ms;
record.interval_ms = repeat ? std::max<std::uint32_t>(1, delay_ms) : 0;
record.repeat = repeat;
record.active = true;
timers.push_back(record);
notifyEventBus(EventBusSignal::Timer);
return record.id;
}
void AppSystem::cancelTimer(AppTimerHandle handle) {
auto* timer = findTimer(handle);
if (!timer)
return;
timer->active = false;
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
notifyEventBus(EventBusSignal::Timer);
}
void AppSystem::cancelAllTimers() {
bool changed = false;
for (auto& timer: timers) {
if (timer.generation == currentGeneration && timer.active) {
timer.active = false;
changed = true;
}
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
if (changed)
notifyEventBus(EventBusSignal::Timer);
}
void AppSystem::dispatchEvent(const AppEvent& event) {
if (current)
current->handleEvent(event);
}
void AppSystem::processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents) {
for (auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0) {
AppEvent ev{};
ev.type = AppEventType::Timer;
ev.timestamp_ms = now;
ev.timer.handle = timer.id;
outEvents.push_back(ev);
if (timer.repeat) {
const std::uint32_t interval = timer.interval_ms ? timer.interval_ms : 1;
timer.due_ms = now + interval;
} else {
timer.active = false;
}
}
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const {
std::uint32_t minWait = std::numeric_limits<std::uint32_t>::max();
for (const auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0)
return 0;
const std::uint32_t delta = timer.due_ms - now;
if (delta < minWait)
minWait = delta;
}
return minWait;
}
void AppSystem::clearTimersForCurrentApp() {
const bool hadTimers = !timers.empty();
++currentGeneration;
timers.clear();
if (hadTimers)
notifyEventBus(EventBusSignal::Timer);
}
AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
for (auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (timer.id == handle)
return &timer;
}
return nullptr;
}
bool AppSystem::handlePendingSwitchRequest() {
if (!context.pendingSwitch)
return false;
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
void AppSystem::handlePendingSwitchRequest() {
assert(_context._pendingSwitch);
const bool byName = _context._pendingSwitchByName;
const std::size_t reqIndex = _context._pendingAppIndex;
const std::string reqName = _context._pendingAppName;
_context._pendingSwitch = false;
_context._pendingSwitchByName = false;
_context._pendingAppName.clear();
bool switched = false;
if (byName)
switched = startApp(reqName);
startApp(reqName);
else
switched = startAppByIndex(reqIndex);
return switched;
startAppByIndex(reqIndex);
}
void AppSystem::notifyEventBus(EventBusSignal signal) {
if (signal == EventBusSignal::None)
return;
if (auto* bus = context.eventBus())
bus->signal(to_event_bits(signal));
std::unique_ptr<AppScopedServices> AppSystem::createAppScopedServices(std::uint64_t generation) {
if (!_context.services || !_context.services->appServices)
return nullptr;
return _context.services->appServices->createScopedServices(generation);
}
} // namespace cardboy::sdk

View File

@@ -2,22 +2,22 @@
namespace cardboy::sdk {
FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr;
void* FramebufferHooks::userData_ = nullptr;
FramebufferHooks::PreSendHook FramebufferHooks::_hook = nullptr;
void* FramebufferHooks::_userData = nullptr;
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
hook_ = hook;
userData_ = userData;
_hook = hook;
_userData = userData;
}
void FramebufferHooks::clearPreSendHook() {
hook_ = nullptr;
userData_ = nullptr;
_hook = nullptr;
_userData = nullptr;
}
void FramebufferHooks::invokePreSend(void* framebuffer) {
if (hook_)
hook_(framebuffer, userData_);
if (_hook)
_hook(framebuffer, _userData);
}
} // namespace cardboy::sdk

View File

@@ -12,17 +12,17 @@ StatusBar& StatusBar::instance() {
return bar;
}
void StatusBar::setEnabled(bool value) { enabled_ = value; }
void StatusBar::setEnabled(bool value) { _enabled = value; }
void StatusBar::toggle() {
enabled_ = !enabled_;
if (services_ && services_->buzzer)
services_->buzzer->beepMove();
_enabled = !_enabled;
if (_services && _services->buzzer)
_services->buzzer->beepMove();
}
void StatusBar::setCurrentAppName(std::string_view name) {
appName_.assign(name.begin(), name.end());
std::transform(appName_.begin(), appName_.end(), appName_.begin(),
_appName.assign(name.begin(), name.end());
std::transform(_appName.begin(), _appName.end(), _appName.begin(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
}
@@ -37,7 +37,7 @@ bool StatusBar::handleToggleInput(const InputState& current, const InputState& p
}
std::string StatusBar::prepareLeftText(int displayWidth) const {
std::string text = appName_.empty() ? std::string("CARDBOY") : appName_;
std::string text = _appName.empty() ? std::string("CARDBOY") : _appName;
int maxWidth = std::max(0, displayWidth - 32);
while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth)
text.pop_back();
@@ -45,25 +45,21 @@ std::string StatusBar::prepareLeftText(int displayWidth) const {
}
std::string StatusBar::prepareRightText() const {
if (!services_)
if (!_services)
return {};
std::string right;
if (services_->battery && services_->battery->hasData()) {
const float current = services_->battery->current();
const float chargeMah = services_->battery->charge();
const float fallbackV = services_->battery->voltage();
if (_services->battery && _services->battery->hasData()) {
const float current = _services->battery->current();
const float chargeMah = _services->battery->charge();
const float percentage = _services->battery->percentage();
char buf[64];
if (std::isfinite(current) && std::isfinite(chargeMah)) {
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current),
static_cast<double>(chargeMah));
} else {
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
}
std::snprintf(buf, sizeof(buf), "%.2fmA %.2fmAh %.0f%%", static_cast<double>(current),
static_cast<double>(chargeMah), static_cast<double>(percentage));
right.assign(buf);
}
if (services_->buzzer && services_->buzzer->isMuted()) {
if (_services->buzzer && _services->buzzer->isMuted()) {
if (!right.empty())
right.append(" ");
right.append("MUTE");

View File

@@ -567,9 +567,9 @@ CONFIG_SECURE_TEE_LOG_LEVEL=0
# Serial flasher config
#
# CONFIG_ESPTOOLPY_NO_STUB is not set
# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
# CONFIG_ESPTOOLPY_FLASHMODE_DIO is not set
# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set
CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y
CONFIG_ESPTOOLPY_FLASHMODE="dio"
@@ -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
@@ -1217,7 +1217,7 @@ CONFIG_SPI_SLAVE_ISR_IN_IRAM=y
# ESP-Driver:USB Serial/JTAG Configuration
#
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=y
# CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION is not set
CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION=y
# end of ESP-Driver:USB Serial/JTAG Configuration
#
@@ -2458,9 +2458,9 @@ CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y
# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set
CONFIG_LOG_BOOTLOADER_LEVEL=3
# CONFIG_FLASH_ENCRYPTION_ENABLED is not set
# CONFIG_FLASHMODE_QIO is not set
CONFIG_FLASHMODE_QIO=y
# CONFIG_FLASHMODE_QOUT is not set
CONFIG_FLASHMODE_DIO=y
# CONFIG_FLASHMODE_DIO is not set
# CONFIG_FLASHMODE_DOUT is not set
CONFIG_MONITOR_BAUD=115200
# CONFIG_OPTIMIZATION_LEVEL_DEBUG is not set
@@ -2497,7 +2497,7 @@ CONFIG_NIMBLE_ROLE_CENTRAL=y
CONFIG_NIMBLE_ROLE_PERIPHERAL=y
CONFIG_NIMBLE_ROLE_BROADCASTER=y
CONFIG_NIMBLE_ROLE_OBSERVER=y
# CONFIG_NIMBLE_NVS_PERSIST is not set
CONFIG_NIMBLE_NVS_PERSIST=y
CONFIG_NIMBLE_SM_LEGACY=y
CONFIG_NIMBLE_SM_SC=y
# CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set

View File

@@ -599,13 +599,13 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
#
# Partition Table
#
CONFIG_PARTITION_TABLE_SINGLE_APP=y
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
# CONFIG_PARTITION_TABLE_CUSTOM is not set
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y
# end of Partition Table
@@ -644,7 +644,7 @@ CONFIG_COMPILER_RT_LIB_GCCLIB=y
CONFIG_COMPILER_RT_LIB_NAME="gcc"
CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set
CONFIG_COMPILER_STATIC_ANALYZER=y
# CONFIG_COMPILER_STATIC_ANALYZER is not set
# end of Compiler options
#
@@ -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
@@ -1699,9 +1699,13 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1
# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set
# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set
# CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID is not set
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U32=y
# CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U64 is not set
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set
@@ -1722,6 +1726,7 @@ CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y
CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y
# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set
# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
# end of Port
@@ -1757,16 +1762,16 @@ CONFIG_HAL_WDT_USE_ROM_IMPL=y
#
# Heap memory debugging
#
CONFIG_HEAP_POISONING_DISABLED=y
# CONFIG_HEAP_POISONING_DISABLED is not set
# CONFIG_HEAP_POISONING_LIGHT is not set
# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_HEAP_TRACING_OFF=y
# CONFIG_HEAP_TRACING_STANDALONE is not set
# CONFIG_HEAP_TRACING_TOHOST is not set
# CONFIG_HEAP_USE_HOOKS is not set
# CONFIG_HEAP_TASK_TRACKING is not set
# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set
CONFIG_HEAP_TLSF_USE_ROM_IMPL=y
# CONFIG_HEAP_TLSF_USE_ROM_IMPL is not set
# CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH is not set
# end of Heap memory debugging
@@ -2410,6 +2415,211 @@ CONFIG_WL_SECTOR_SIZE=4096
CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
CONFIG_WIFI_PROV_BLE_SEC_CONN=y
#
# LittleFS
#
# CONFIG_LITTLEFS_SDMMC_SUPPORT is not set
CONFIG_LITTLEFS_MAX_PARTITIONS=3
CONFIG_LITTLEFS_PAGE_SIZE=256
CONFIG_LITTLEFS_OBJ_NAME_LEN=64
CONFIG_LITTLEFS_READ_SIZE=128
CONFIG_LITTLEFS_WRITE_SIZE=128
CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128
CONFIG_LITTLEFS_CACHE_SIZE=512
CONFIG_LITTLEFS_BLOCK_CYCLES=512
CONFIG_LITTLEFS_USE_MTIME=y
# CONFIG_LITTLEFS_USE_ONLY_HASH is not set
# CONFIG_LITTLEFS_HUMAN_READABLE is not set
CONFIG_LITTLEFS_MTIME_USE_SECONDS=y
# CONFIG_LITTLEFS_MTIME_USE_NONCE is not set
# CONFIG_LITTLEFS_SPIFFS_COMPAT is not set
# CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set
# CONFIG_LITTLEFS_FCNTL_GET_PATH is not set
# CONFIG_LITTLEFS_MULTIVERSION is not set
# CONFIG_LITTLEFS_MALLOC_STRATEGY_DISABLE is not set
CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y
# CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL is not set
CONFIG_LITTLEFS_ASSERTS=y
# CONFIG_LITTLEFS_MMAP_PARTITION is not set
# end of LittleFS
# end of Component config
# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set
# Deprecated options for backward compatibility
# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set
# CONFIG_NO_BLOBS is not set
# CONFIG_APP_ROLLBACK_ENABLE is not set
# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set
# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set
# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set
CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y
# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set
# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set
CONFIG_LOG_BOOTLOADER_LEVEL=3
# CONFIG_FLASH_ENCRYPTION_ENABLED is not set
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
CONFIG_FLASHMODE_DIO=y
# CONFIG_FLASHMODE_DOUT is not set
CONFIG_MONITOR_BAUD=115200
CONFIG_OPTIMIZATION_LEVEL_DEBUG=y
CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y
CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y
# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set
# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set
CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y
# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set
# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set
CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2
# CONFIG_CXX_EXCEPTIONS is not set
# CONFIG_STACK_CHECK_NONE is not set
# CONFIG_STACK_CHECK_NORM is not set
CONFIG_STACK_CHECK_STRONG=y
# CONFIG_STACK_CHECK_ALL is not set
CONFIG_STACK_CHECK=y
CONFIG_WARN_WRITE_STRINGS=y
# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set
CONFIG_ESP32_APPTRACE_DEST_NONE=y
CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
# CONFIG_BLUEDROID_ENABLED is not set
CONFIG_NIMBLE_ENABLED=y
CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
CONFIG_NIMBLE_MAX_CONNECTIONS=3
CONFIG_NIMBLE_MAX_BONDS=3
CONFIG_NIMBLE_MAX_CCCDS=8
CONFIG_NIMBLE_L2CAP_COC_MAX_NUM=0
CONFIG_NIMBLE_PINNED_TO_CORE=0
CONFIG_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_NIMBLE_ROLE_CENTRAL=y
CONFIG_NIMBLE_ROLE_PERIPHERAL=y
CONFIG_NIMBLE_ROLE_BROADCASTER=y
CONFIG_NIMBLE_ROLE_OBSERVER=y
CONFIG_NIMBLE_NVS_PERSIST=y
CONFIG_NIMBLE_SM_LEGACY=y
CONFIG_NIMBLE_SM_SC=y
# CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set
CONFIG_BT_NIMBLE_SM_SC_LVL=0
# CONFIG_NIMBLE_DEBUG is not set
CONFIG_NIMBLE_SVC_GAP_DEVICE_NAME="nimble"
CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31
CONFIG_NIMBLE_ATT_PREFERRED_MTU=256
CONFIG_NIMBLE_SVC_GAP_APPEARANCE=0
CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=24
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=24
CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255
CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=30
CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT=8
CONFIG_NIMBLE_RPA_TIMEOUT=900
# CONFIG_NIMBLE_MESH is not set
CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y
# CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN is not set
CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y
CONFIG_SW_COEXIST_ENABLE=y
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
# CONFIG_ANA_CMPR_ISR_IRAM_SAFE is not set
# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set
# CONFIG_MCPWM_ISR_IRAM_SAFE is not set
# CONFIG_EVENT_LOOP_PROFILING is not set
CONFIG_POST_EVENTS_FROM_ISR=y
CONFIG_POST_EVENTS_FROM_IRAM_ISR=y
CONFIG_GDBSTUB_SUPPORT_TASKS=y
CONFIG_GDBSTUB_MAX_TASKS=32
# CONFIG_OTA_ALLOW_HTTP is not set
CONFIG_ESP_SYSTEM_PD_FLASH=y
CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y
CONFIG_BROWNOUT_DET=y
# CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set
# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set
# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set
# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set
CONFIG_BROWNOUT_DET_LVL_SEL_3=y
# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set
# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set
# CONFIG_BROWNOUT_DET_LVL_SEL_0 is not set
CONFIG_BROWNOUT_DET_LVL=3
CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y
CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y
CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20
CONFIG_ESP32_PHY_MAX_TX_POWER=20
# CONFIG_REDUCE_PHY_TX_POWER is not set
# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set
CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y
CONFIG_ESP32_RTC_XTAL_BOOTSTRAP_CYCLES=0
CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304
CONFIG_MAIN_TASK_STACK_SIZE=3584
CONFIG_CONSOLE_UART_DEFAULT=y
# CONFIG_CONSOLE_UART_CUSTOM is not set
# CONFIG_CONSOLE_UART_NONE is not set
# CONFIG_ESP_CONSOLE_UART_NONE is not set
CONFIG_CONSOLE_UART=y
CONFIG_CONSOLE_UART_NUM=0
CONFIG_CONSOLE_UART_BAUDRATE=115200
CONFIG_INT_WDT=y
CONFIG_INT_WDT_TIMEOUT_MS=300
CONFIG_TASK_WDT=y
CONFIG_ESP_TASK_WDT=y
# CONFIG_TASK_WDT_PANIC is not set
CONFIG_TASK_WDT_TIMEOUT_S=5
CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set
CONFIG_IPC_TASK_STACK_SIZE=1024
CONFIG_TIMER_TASK_STACK_SIZE=3584
# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set
# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set
CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y
CONFIG_TIMER_TASK_PRIORITY=1
CONFIG_TIMER_TASK_STACK_DEPTH=2048
CONFIG_TIMER_QUEUE_LENGTH=10
# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set
# CONFIG_HAL_ASSERTION_SILIENT is not set
# CONFIG_L2_TO_L3_COPY is not set
CONFIG_ESP_GRATUITOUS_ARP=y
CONFIG_GARP_TMR_INTERVAL=60
CONFIG_TCPIP_RECVMBOX_SIZE=32
CONFIG_TCP_MAXRTX=12
CONFIG_TCP_SYNMAXRTX=12
CONFIG_TCP_MSS=1440
CONFIG_TCP_MSL=60000
CONFIG_TCP_SND_BUF_DEFAULT=5760
CONFIG_TCP_WND_DEFAULT=5760
CONFIG_TCP_RECVMBOX_SIZE=6
CONFIG_TCP_QUEUE_OOSEQ=y
CONFIG_TCP_OVERSIZE_MSS=y
# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set
# CONFIG_TCP_OVERSIZE_DISABLE is not set
CONFIG_UDP_RECVMBOX_SIZE=6
CONFIG_TCPIP_TASK_STACK_SIZE=3072
CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y
# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set
CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF
# CONFIG_PPP_SUPPORT is not set
CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y
# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set
# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set
# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set
# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set
CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y
# CONFIG_NEWLIB_NANO_FORMAT is not set
CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y
# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set
# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set
# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set
CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5
CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072
CONFIG_ESP32_PTHREAD_STACK_MIN=768
CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1
CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread"
CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y
# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set
# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set
CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y
CONFIG_SUPPORT_TERMIOS=y
CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1
# End of deprecated options