Compare commits

...

64 Commits

Author SHA1 Message Date
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
fc633d7c90 a bit higher tx power 2025-10-20 08:48:59 +02:00
e8ae1cbec4 overlay fixes 2025-10-20 00:58:33 +02:00
b72ea4f417 some fixes 2025-10-20 00:50:21 +02:00
bf0ffe8632 rescan fix 2025-10-20 00:38:52 +02:00
96bfaaf64b lower power consumption bluetooth 2025-10-20 00:34:25 +02:00
cf5a848741 app fixes 2025-10-20 00:04:18 +02:00
7c492627f0 lockscreen show progressbar only on hold 2025-10-19 23:48:14 +02:00
be2629a008 connected overlay in file manager 2025-10-19 23:27:27 +02:00
016629eb82 lockscreen app 2025-10-19 23:27:16 +02:00
de1ac0e7a2 fixie 2025-10-19 23:09:55 +02:00
3ab2a7bf26 somewhat working file sync 2025-10-19 23:07:20 +02:00
b4f11851d7 fix 2025-10-19 20:30:11 +02:00
eeedc629d7 background sync 2025-10-19 20:26:40 +02:00
8bb48daf6c some power savings 2025-10-19 20:11:22 +02:00
7c741c42dc time sync 2025-10-19 19:54:24 +02:00
ecbcce12ea fix 2025-10-15 20:51:52 +02:00
f6c800fc63 gameboy save states 2025-10-15 20:46:48 +02:00
5e63875d35 bad sound in correct place 2025-10-13 22:02:55 +02:00
cc805abe80 snake app 2025-10-13 14:36:27 +02:00
1bc5b75dba format 2025-10-13 00:39:34 +02:00
e37f8e3dc8 a little better sound 2025-10-13 00:39:26 +02:00
df7c4ff3b9 even better sound 2025-10-13 00:27:45 +02:00
07186b4b73 sound upgrade 2025-10-13 00:17:16 +02:00
6a1f7d48ce simple music 2025-10-13 00:05:56 +02:00
031ff1952b get rid of powerhelper 2025-10-12 22:54:18 +02:00
df55b8f2e1 hang fix fix 2025-10-12 20:43:13 +02:00
ed1cee82d2 fix hanging 2 2025-10-12 20:37:39 +02:00
7474f65aaa fix hanging 2025-10-12 20:37:30 +02:00
088c6e47bd better timers 2025-10-12 20:34:05 +02:00
d91b7540fc fix intr 2025-10-12 17:44:19 +02:00
d5506b9455 fix gameboy autostart 2025-10-12 17:32:33 +02:00
db88e16aaa better controls 2025-10-12 17:29:15 +02:00
a6713859b2 better statusbar 2025-10-12 17:16:07 +02:00
aaac0514c0 more settings 2025-10-12 17:11:22 +02:00
1b6e9a0f78 settings app 2025-10-12 16:57:40 +02:00
c64f03a09f status bar 2 2025-10-12 15:18:42 +02:00
5ab8662332 statusbar 2025-10-12 15:03:34 +02:00
6d8834d9b2 nicer tetris 2025-10-12 14:04:06 +02:00
83ba775971 some stuff 2025-10-12 13:51:50 +02:00
df57e55171 better default scale mode 2025-10-12 00:52:58 +02:00
a3b837f329 a bit more speedup 2025-10-12 00:33:48 +02:00
fc9e85aea0 fast wide scale 2025-10-12 00:20:22 +02:00
b55feb68f8 some opt 2025-10-12 00:03:01 +02:00
f04b026d46 a little faster gameboy 2025-10-11 22:21:31 +02:00
e18278e130 disable analyzer 2025-10-11 20:59:29 +02:00
9a392d6aec check macro 2025-10-11 20:36:43 +02:00
961453e28a 8bit draw 2025-10-11 20:03:00 +02:00
a4c2719077 text 2025-10-11 16:54:41 +02:00
74 changed files with 10273 additions and 1226 deletions

3
Firmware/.gitignore vendored
View File

@@ -2,4 +2,5 @@ build
cmake-build*
.idea
.cache
managed_components
managed_components
*.gb

4
Firmware/AGENTS.md Normal file
View File

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

62
Firmware/cardboy-companion/.gitignore vendored Normal file
View File

@@ -0,0 +1,62 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

View File

@@ -0,0 +1,89 @@
# Cardboy Time Sync Companion
This SwiftUI app connects to the Cardboy device over Bluetooth Low Energy and updates its wall clock using the custom time sync service exposed by the firmware. The sources live inside the existing `cardboy-companion/cardboy-companion.xcodeproj` so you can open and run them directly in Xcode.
## Requirements
- Xcode 15 or newer
- iOS 16 or newer deployment target (can be lowered to 15 with minor API tweaks)
- A Cardboy running firmware that includes the BLE time sync service
## How it works
1. The app scans for peripherals exposing service UUID `00000001-CA7B-4EFD-B5A6-10C3F4D3F230`.
2. Once connected it discovers characteristic `00000002-CA7B-4EFD-B5A6-10C3F4D3F231`.
3. Tapping **Sync Now** writes a 12byte payload containing:
- 8 bytes Unix epoch seconds (little endian)
- 2 bytes time zone offset in minutes from UTC (little endian)
- 1 byte DST flag (`1` if daylight saving is active)
- 1 reserved byte (`0`)
4. The firmware applies the timestamp with `settimeofday()` and updates the TZ environment variable so the clock app renders local time.
## Usage
1. Open `cardboy-companion/cardboy-companion.xcodeproj` in Xcode.
2. Ensure the `CoreBluetooth` capability is enabled for the `cardboy-companion` target and keep the *Uses Bluetooth LE accessories* background mode on (preconfigured in this project).
3. Build & run on a real device (BLE is not available in the simulator).
4. Allow Bluetooth permissions when prompted. The app keeps scanning in the background, so the Cardboy can request a sync even while the companion is not foregrounded. Tap **Sync Now** any time you want to trigger a manual refresh.
5. Switch to the **Files** tab to browse the LittleFS volume on the Cardboy: you can upload ROMs from the Files picker, create/remove folders, rename entries, delete files, and download items back to the phone for sharing.
## BLE File Service Protocol
The ESP firmware exposes a custom GATT service (UUID `00000010-CA7B-4EFD-B5A6-10C3F4D3F230`) with two characteristics:
| Characteristic | UUID | Properties | Direction | Description |
| --- | --- | --- | --- | --- |
| File Command | `00000011-CA7B-4EFD-B5A6-10C3F4D3F231` | Write / Write Without Response | iOS → ESP | Sends file management requests |
| File Response | `00000012-CA7B-4EFD-B5A6-10C3F4D3F232` | Notify | ESP → iOS | Streams command results (responses or data) |
All payloads share the same framing. Commands written to the File Command characteristic use:
```
Offset | Size | Description
-------+------+------------
0 | 1 | Opcode (see table below)
1 | 1 | Reserved (set to 0)
2 | 2 | Little-endian payload length in bytes (N)
4 | N | Command payload
```
Notifications from the File Response characteristic use:
```
Offset | Size | Description
-------+------+------------
0 | 1 | Opcode (echoed from command)
1 | 1 | Status byte (bit 7 = completion flag; lower 7 bits = error code)
2 | 2 | Little-endian payload length (N)
4 | N | Response payload (command-specific)
```
Status byte semantics:
- Bit 7 (0x80) set → final packet for the current command (no further fragments).
- Lower 7 bits = error code (`0` = success, otherwise `errno`-style code echoed back).
- On error the response payload may contain a UTF-8 message.
### Opcodes and Payloads
| Opcode | Name | Command Payload | Response Payload |
| --- | --- | --- | --- |
| `0x01` | List Directory | `uint16 path_len` + UTF-8 path | One or more fragments, each entry encoded as:<br> - `uint8 type` (0=file, 1=dir)<br> - `uint8 reserved`<br> - `uint16 name_len`<br> - `uint32 size` (0 for dirs)<br> - `name_len` bytes UTF-8 name<br> Final notification has completion bit set. |
| `0x02` | Upload Begin | `uint16 path_len` + UTF-8 path + `uint32 file_size` | Empty payload on success. Starts upload session (expects `UploadChunk` packets). |
| `0x03` | Upload Chunk | Raw file bytes | Empty payload ack for each chunk. |
| `0x04` | Upload End | No payload | Empty payload confirming completion. |
| `0x05` | Download Request | `uint16 path_len` + UTF-8 path | First notification: 4-byte little-endian total file size; subsequent notifications stream raw file data fragments. Completion bit marks the final chunk. |
| `0x06` | Delete File | `uint16 path_len` + UTF-8 path | Empty payload on success. |
| `0x07` | Create Directory | `uint16 path_len` + UTF-8 path | Empty payload on success. |
| `0x08` | Delete Directory | `uint16 path_len` + UTF-8 path | Empty payload on success. |
| `0x09` | Rename Path | `uint16 src_len` + UTF-8 source path + `uint16 dst_len` + UTF-8 destination path | Empty payload on success. |
### Notes
- Paths are absolute within the LittleFS volume; the firmware normalizes them and rejects entries containing `..`.
- Large responses (directory lists, downloads) may arrive in multiple notifications; the iOS client aggregates fragments until it sees the completion flag.
- Uploads are initiated with `Upload Begin` (including total size), followed by one or more `Upload Chunk` writes, and `Upload End` when done.
- Errors from the firmware propagate via the status byte; when `status & 0x7F != 0`, the notification payload typically includes a UTF-8 error message (e.g., `"stat failed"`).
This protocol mirrors the implementation in `components/backend-esp/src/time_sync_service.cpp` and the Swift client in `TimeSyncManager.swift`. Update both sides if new commands are added.
Optionally bundle this code into your existing app—`TimeSyncManager` is selfcontained and can be reused.

View File

@@ -0,0 +1,358 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
ECAB9A832EA550D9004BA9DE /* cardboy-companion.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "cardboy-companion.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
ECAB9ABA2EA562CD004BA9DE /* Exceptions for "cardboy-companion" folder in "cardboy-companion" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = ECAB9A822EA550D9004BA9DE /* cardboy-companion */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
ECAB9A852EA550D9004BA9DE /* cardboy-companion */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
ECAB9ABA2EA562CD004BA9DE /* Exceptions for "cardboy-companion" folder in "cardboy-companion" target */,
);
path = "cardboy-companion";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
ECAB9A802EA550D9004BA9DE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
ECAB9A7A2EA550D9004BA9DE = {
isa = PBXGroup;
children = (
ECAB9A852EA550D9004BA9DE /* cardboy-companion */,
ECAB9A842EA550D9004BA9DE /* Products */,
);
sourceTree = "<group>";
};
ECAB9A842EA550D9004BA9DE /* Products */ = {
isa = PBXGroup;
children = (
ECAB9A832EA550D9004BA9DE /* cardboy-companion.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
ECAB9A822EA550D9004BA9DE /* cardboy-companion */ = {
isa = PBXNativeTarget;
buildConfigurationList = ECAB9A8E2EA550DB004BA9DE /* Build configuration list for PBXNativeTarget "cardboy-companion" */;
buildPhases = (
ECAB9A7F2EA550D9004BA9DE /* Sources */,
ECAB9A802EA550D9004BA9DE /* Frameworks */,
ECAB9A812EA550D9004BA9DE /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
ECAB9A852EA550D9004BA9DE /* cardboy-companion */,
);
name = "cardboy-companion";
packageProductDependencies = (
);
productName = "cardboy-companion";
productReference = ECAB9A832EA550D9004BA9DE /* cardboy-companion.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
ECAB9A7B2EA550D9004BA9DE /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2600;
TargetAttributes = {
ECAB9A822EA550D9004BA9DE = {
CreatedOnToolsVersion = 26.0.1;
};
};
};
buildConfigurationList = ECAB9A7E2EA550D9004BA9DE /* Build configuration list for PBXProject "cardboy-companion" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = ECAB9A7A2EA550D9004BA9DE;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = ECAB9A842EA550D9004BA9DE /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
ECAB9A822EA550D9004BA9DE /* cardboy-companion */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
ECAB9A812EA550D9004BA9DE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
ECAB9A7F2EA550D9004BA9DE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
ECAB9A8C2EA550DB004BA9DE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
ECAB9A8D2EA550DB004BA9DE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
ECAB9A8F2EA550DB004BA9DE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = WX524QS7SH;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "cardboy-companion/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cardboy;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.usatiuk.cardboy-companion";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
ECAB9A902EA550DB004BA9DE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = WX524QS7SH;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "cardboy-companion/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cardboy;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.usatiuk.cardboy-companion";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
ECAB9A7E2EA550D9004BA9DE /* Build configuration list for PBXProject "cardboy-companion" */ = {
isa = XCConfigurationList;
buildConfigurations = (
ECAB9A8C2EA550DB004BA9DE /* Debug */,
ECAB9A8D2EA550DB004BA9DE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
ECAB9A8E2EA550DB004BA9DE /* Build configuration list for PBXNativeTarget "cardboy-companion" */ = {
isa = XCConfigurationList;
buildConfigurations = (
ECAB9A8F2EA550DB004BA9DE /* Debug */,
ECAB9A902EA550DB004BA9DE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = ECAB9A7B2EA550D9004BA9DE /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "cardboy-icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,548 @@
import Combine
import SwiftUI
import UIKit
import UniformTypeIdentifiers
struct ContentView: View {
@EnvironmentObject private var manager: TimeSyncManager
@State private var selectedTab = 0
@State private var shareURL: URL?
@State private var errorWrapper: ErrorWrapper?
var body: some View {
TabView(selection: $selectedTab) {
TimeSyncTabView()
.tabItem { Label("Clock", systemImage: "clock.arrow.circlepath") }
.tag(0)
FileManagerTabView(shareURL: $shareURL, errorWrapper: $errorWrapper)
.tabItem { Label("Files", systemImage: "folder") }
.tag(1)
}
.sheet(item: $shareURL) { url in
ShareSheet(items: [url])
}
.alert(item: $errorWrapper) { wrapper in
Alert(title: Text("Error"), message: Text(wrapper.message), dismissButton: .default(Text("OK")))
}
.onReceive(manager.$fileErrorMessage.compactMap { $0 }) { message in
errorWrapper = ErrorWrapper(message: message)
manager.fileErrorMessage = nil
}
.onReceive(manager.$downloadedFileURL.compactMap { $0 }) { url in
shareURL = url
manager.downloadedFileURL = nil
}
}
}
private struct ErrorWrapper: Identifiable {
let id = UUID()
let message: String
}
private struct TimeSyncTabView: View {
@EnvironmentObject private var manager: TimeSyncManager
private var formattedLastSync: String {
guard let date = manager.lastSyncDate else { return "Never" }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
return formatter.string(from: date)
}
var body: some View {
VStack(spacing: 24) {
Text("Cardboy Clock Sync")
.font(.title.bold())
VStack(spacing: 12) {
Text("State: \(manager.connectionState.rawValue)")
.font(.headline)
Text(manager.statusMessage)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
HStack(spacing: 16) {
Button(action: manager.sendCurrentTime) {
Label("Sync Now", systemImage: "clock.arrow.2.circlepath")
}
.buttonStyle(.borderedProminent)
.disabled(manager.connectionState != .ready)
Button(action: manager.forceRescan) {
Label("Rescan", systemImage: "dot.radiowaves.left.and.right")
}
.buttonStyle(.bordered)
}
Button(action: manager.sendTestNotification) {
Label("Send Test Notification", systemImage: "bell.badge.waveform")
}
.buttonStyle(.bordered)
}
VStack(spacing: 8) {
Text("Last Sync:")
.font(.caption)
.foregroundColor(.secondary)
Text(formattedLastSync)
.font(.body.monospaced())
}
Spacer()
}
.padding()
}
}
private struct FileManagerTabView: View {
@EnvironmentObject private var manager: TimeSyncManager
@Binding var shareURL: URL?
@Binding var errorWrapper: ErrorWrapper?
@State private var navigationPath: [String] = []
var body: some View {
ZStack {
NavigationStack(path: $navigationPath) {
DirectoryView(
path: "/",
navigationPath: $navigationPath,
shareURL: $shareURL,
errorWrapper: $errorWrapper
)
.navigationDestination(for: String.self) { destination in
DirectoryView(
path: destination,
navigationPath: $navigationPath,
shareURL: $shareURL,
errorWrapper: $errorWrapper
)
}
}
if let operation = manager.activeFileOperation {
FileOperationHUD(operation: operation)
}
if manager.connectionState != .ready {
ConnectionOverlay(
state: manager.connectionState,
statusMessage: manager.statusMessage,
retryAction: manager.forceRescan
)
}
}
.onAppear {
if manager.currentDirectory != "/" {
manager.changeDirectory(to: "/")
}
navigationPath = stackForPath(manager.currentDirectory)
}
.onChange(of: manager.currentDirectory) { newValue in
let desired = stackForPath(newValue)
if desired != navigationPath {
navigationPath = desired
}
}
.onChange(of: navigationPath) { newValue in
let target = newValue.last ?? "/"
if target != manager.currentDirectory {
manager.changeDirectory(to: target)
}
}
.onChange(of: manager.connectionState) { newState in
if newState == .ready, !manager.isFileBusy {
manager.refreshDirectory()
}
}
}
}
private struct DirectoryView: View {
let path: String
@Binding var navigationPath: [String]
@Binding var shareURL: URL?
@Binding var errorWrapper: ErrorWrapper?
@EnvironmentObject private var manager: TimeSyncManager
@State private var showingImporter = false
@State private var showingNewFolderSheet = false
@State private var showingRenameSheet = false
@State private var newFolderName = ""
@State private var renameText = ""
@State private var renameTarget: TimeSyncManager.RemoteFileEntry?
private var pathSegments: [(name: String, fullPath: String)] {
var segments: [(String, String)] = []
var current = ""
for component in path.split(separator: "/").map(String.init) {
current += "/" + component
segments.append((component, current))
}
return segments
}
private var displayTitle: String {
pathSegments.last?.name ?? "Files"
}
var body: some View {
ZStack {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Path:")
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
Button(action: { navigationPath = [] }) {
Text("/")
}
.buttonStyle(.bordered)
ForEach(Array(pathSegments.enumerated()), id: \.element.fullPath) { index, segment in
Button(action: {
navigationPath = Array(pathSegments.prefix(index + 1).map(\.fullPath))
}) {
Text(segment.name)
}
.buttonStyle(.bordered)
}
}
}
}
.padding(.horizontal)
List {
ForEach(manager.directoryEntries) { entry in
if entry.isDirectory {
NavigationLink(value: entry.path) {
FileRow(entry: entry)
}
.contextMenu {
Button("Open") {
navigationPath = stackForPath(entry.path)
}
Button("Rename") {
renameTarget = entry
renameText = entry.name
showingRenameSheet = true
}
Button(role: .destructive) {
manager.delete(entry: entry)
} label: {
Text("Delete")
}
}
} else {
FileRow(entry: entry)
.contextMenu {
Button("Download") {
manager.download(entry: entry) { result in
switch result {
case .success(let url):
shareURL = url
case .failure(let error):
errorWrapper = ErrorWrapper(message: error.localizedDescription)
}
}
}
Button("Rename") {
renameTarget = entry
renameText = entry.name
showingRenameSheet = true
}
Button(role: .destructive) {
manager.delete(entry: entry)
} label: {
Text("Delete")
}
}
}
}
}
.listStyle(.plain)
HStack(spacing: 12) {
Menu {
Button {
showingImporter = true
} label: {
Label("Upload File", systemImage: "square.and.arrow.up")
}
Button {
showingNewFolderSheet = true
newFolderName = ""
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
} label: {
Label("Actions", systemImage: "ellipsis.circle")
}
.buttonStyle(.bordered)
.controlSize(.large)
Spacer()
Button {
manager.refreshDirectory()
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding([.horizontal, .bottom])
}
}
.navigationTitle(displayTitle)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
if manager.currentDirectory != path {
manager.changeDirectory(to: path)
} else {
manager.refreshDirectory()
}
}
.fileImporter(
isPresented: $showingImporter,
allowedContentTypes: [.data],
allowsMultipleSelection: false
) { result in
switch result {
case .success(let urls):
guard let url = urls.first else { return }
manager.uploadFile(from: url) { uploadResult in
if case let .failure(error) = uploadResult {
errorWrapper = ErrorWrapper(message: error.localizedDescription)
}
}
case .failure(let error):
errorWrapper = ErrorWrapper(message: error.localizedDescription)
}
}
.sheet(isPresented: $showingNewFolderSheet) {
NavigationView {
Form {
Section(header: Text("Folder Name")) {
TextField("Name", text: $newFolderName)
.autocapitalization(.none)
}
}
.navigationTitle("New Folder")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showingNewFolderSheet = false }
}
ToolbarItem(placement: .confirmationAction) {
Button("Create") {
let name = newFolderName.trimmingCharacters(in: .whitespacesAndNewlines)
if !name.isEmpty {
manager.createDirectory(named: name)
}
showingNewFolderSheet = false
}
}
}
}
}
.sheet(isPresented: $showingRenameSheet) {
NavigationView {
Form {
Section(header: Text("New Name")) {
TextField("Name", text: $renameText)
.autocapitalization(.none)
}
}
.navigationTitle("Rename")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showingRenameSheet = false }
}
ToolbarItem(placement: .confirmationAction) {
Button("Rename") {
if let target = renameTarget {
let newName = renameText.trimmingCharacters(in: .whitespacesAndNewlines)
if !newName.isEmpty {
manager.rename(entry: target, to: newName)
}
}
showingRenameSheet = false
}
}
}
}
}
}
}
private func stackForPath(_ path: String) -> [String] {
guard path != "/" else { return [] }
var stack: [String] = []
var current = ""
for component in path.split(separator: "/").map(String.init) {
current += "/" + component
stack.append(current)
}
return stack
}
private struct FileRow: View {
let entry: TimeSyncManager.RemoteFileEntry
private var iconName: String {
entry.isDirectory ? "folder" : "doc.text"
}
private var formattedSize: String {
guard !entry.isDirectory else { return "" }
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.allowedUnits = [.useKB, .useMB, .useGB]
byteCountFormatter.countStyle = .file
return byteCountFormatter.string(fromByteCount: Int64(entry.size))
}
var body: some View {
HStack {
Image(systemName: iconName)
.foregroundColor(entry.isDirectory ? .accentColor : .primary)
VStack(alignment: .leading) {
Text(entry.name)
.fontWeight(entry.isDirectory ? .semibold : .regular)
if !entry.isDirectory, !formattedSize.isEmpty {
Text(formattedSize)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.padding(.vertical, 4)
}
}
private struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
private struct FileOperationHUD: View {
let operation: TimeSyncManager.FileOperationProgress
var body: some View {
ZStack {
Color.black.opacity(0.3)
.ignoresSafeArea()
VStack(spacing: 12) {
Text(operation.title)
.font(.headline)
Text(operation.message)
.font(.subheadline)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
if let progress = operation.progress {
ProgressView(value: progress)
.progressViewStyle(.linear)
.frame(maxWidth: 240)
Text("\(Int(progress * 100))%")
.font(.caption.monospacedDigit())
.foregroundColor(.secondary)
} else {
ProgressView()
.progressViewStyle(.circular)
}
}
.padding(24)
.background(.ultraThinMaterial)
.cornerRadius(16)
.shadow(radius: 10)
.padding()
}
.transition(.opacity)
}
}
private struct ConnectionOverlay: View {
let state: TimeSyncManager.ConnectionState
let statusMessage: String
let retryAction: () -> Void
private var showsSpinner: Bool {
switch state {
case .scanning, .connecting, .discovering:
return true
default:
return false
}
}
private var canRetry: Bool {
switch state {
case .failed, .idle:
return true
default:
return false
}
}
var body: some View {
ZStack {
Color.black.opacity(0.35)
.ignoresSafeArea()
VStack(spacing: 16) {
if showsSpinner {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(1.2)
} else {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 42, weight: .semibold))
.foregroundColor(.yellow)
}
Text(statusMessage)
.multilineTextAlignment(.center)
.font(.headline)
.foregroundColor(.primary)
if canRetry {
Button("Try Again", action: retryAction)
.buttonStyle(.borderedProminent)
}
}
.padding(28)
.frame(maxWidth: 320)
.background(.ultraThinMaterial)
.cornerRadius(20)
.shadow(radius: 12)
}
.transition(.opacity)
}
}
extension URL: Identifiable {
public var id: String { absoluteString }
}
#Preview {
ContentView()
.environmentObject(TimeSyncManager())
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
<key>NSUserNotificationUsageDescription</key>
<string>Allow Cardboy Companion to send local notifications for testing.</string>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import SwiftUI
@main
struct cardboy_companionApp: App {
@StateObject private var manager = TimeSyncManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(manager)
}
}
}

View File

@@ -7,9 +7,9 @@ idf_component_register(
"src/display.cpp"
"src/fs_helper.cpp"
"src/i2c_global.cpp"
"src/power_helper.cpp"
"src/shutdowner.cpp"
"src/spi_global.cpp"
"src/time_sync_service.cpp"
INCLUDE_DIRS
"include"
PRIV_REQUIRES
@@ -19,8 +19,10 @@ idf_component_register(
esp_driver_spi
littlefs
nvs_flash
bt
)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/utils" cardboy_utils_esp)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/backend_interface" backend_interface_from_backend_esp)
add_library(cardboy_backend_esp INTERFACE)

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,12 @@
#ifndef BUTTONS_HPP
#define BUTTONS_HPP
#include <cstdint>
#include "cardboy/sdk/input_state.hpp"
#include "cardboy/sdk/services.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <cstdint>
typedef enum {
BTN_START = 1 << 1,
@@ -22,19 +25,20 @@ typedef enum {
class Buttons {
public:
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
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;
TaskHandle_t _pooler_task;
private:
Buttons();
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
uint8_t _previous;
volatile uint8_t _current;
cardboy::sdk::IEventBus* _eventBus = nullptr;
};

View File

@@ -6,7 +6,7 @@
class Buzzer {
public:
static Buzzer &get();
static Buzzer& get();
void init(); // call once from app_main
@@ -17,8 +17,8 @@ public:
void beepRotate();
void beepMove();
void beepLock();
void beepLines(int lines); // 1..4 lines
void beepLevelUp(int level); // after increment
void beepLines(int lines); // 1..4 lines
void beepLevelUp(int level); // after increment
void beepGameOver();
// Mute controls
@@ -26,29 +26,29 @@ public:
void toggleMuted();
bool isMuted() const { return _muted; }
// Persistence
void loadState();
void saveState();
private:
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
struct Step {
uint32_t freq;
uint32_t dur_ms;
uint32_t gap_ms;
};
static constexpr int MAX_QUEUE = 16;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = false;
bool _in_gap = false;
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
bool _muted = false;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = false;
bool _in_gap = false;
void* _timer = nullptr; // esp_timer_handle_t (opaque here)
bool _muted = false;
Buzzer() = default;
void enqueue(const Step &s);
bool empty() const { return _q_head == _q_tail; }
Step &front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void *arg);
void clearQueue() { _q_head = _q_tail = 0; }
void enqueue(const Step& s);
bool empty() const { return _q_head == _q_tail; }
Step& front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void* arg);
void clearQueue() { _q_head = _q_tail = 0; }
};

View File

@@ -6,6 +6,7 @@
#define CB_DISPLAY_HPP
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/utils/utils.hpp"
#include "driver/spi_master.h"
// (Async memcpy removed for debugging simplification)
@@ -13,7 +14,7 @@
#include <array>
#include <bitset>
#include <cassert>
#include <cstdint>
namespace SMD {
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
@@ -34,7 +35,7 @@ void frame_ready();
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
@@ -46,6 +47,14 @@ __attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
}
}
__attribute__((always_inline)) static void set_pixel_8(int x, int y, std::uint8_t value) {
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK((x % 8) == 0);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
dma_buf[lineIdx] = ~value;
}
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
static inline spi_device_interface_config_t _devcfg = {

View File

@@ -22,6 +22,7 @@ private:
.scl_io_num = I2C_SCL,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 2,
.flags = {.enable_internal_pullup = true, .allow_pd = true},
};
i2c_master_bus_handle_t _bus_handle;

View File

@@ -1,30 +0,0 @@
//
// Created by Stepan Usatiuk on 03.03.2025.
//
#ifndef POWER_HELPER_HPP
#define POWER_HELPER_HPP
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
class PowerHelper {
public:
static PowerHelper& get();
bool is_slow() const;
void set_slow(bool slow);
BaseType_t reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken);
void delay(int slow_ms, int normal_ms);
void install_isr();
private:
PowerHelper();
bool _slow = false;
EventGroupHandle_t _event_group;
};
#endif // POWER_HELPER_HPP

View File

@@ -0,0 +1,29 @@
#pragma once
namespace cardboy::sdk {
class INotificationCenter;
} // namespace cardboy::sdk
namespace cardboy::backend::esp {
/**
* Ensure the BLE time synchronisation service is running.
*
* Safe to call multiple times; subsequent calls become no-ops once the
* service has been started successfully.
*/
void ensure_time_sync_service_started();
/**
* Stop the BLE time synchronisation service if it is running.
* A no-op on platforms that do not support the BLE implementation.
*/
void shutdown_time_sync_service();
/**
* 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

@@ -1,5 +1,7 @@
#pragma once
#include <cardboy/sdk/display_spec.hpp>
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -14,9 +16,12 @@ class EspFramebuffer final : public cardboy::sdk::FramebufferFacade<EspFramebuff
public:
EspFramebuffer() = default;
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
[[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
[[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
void __attribute__((always_inline)) drawPixel_impl(int x, int y, bool on) { SMD::set_pixel(x, y, on); }
void __attribute__((always_inline)) drawBits8_impl(int x, int y, std::uint8_t bits) {
SMD::set_pixel_8(x, y, bits);
}
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
@@ -53,18 +58,26 @@ private:
class StorageService;
class RandomService;
class HighResClockService;
class PowerService;
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<PowerService> powerService;
std::unique_ptr<FilesystemService> filesystemService;
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

@@ -4,8 +4,6 @@
#include "cardboy/backend/esp/bat_mon.hpp"
#include "cardboy/backend/esp/power_helper.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -50,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
}
@@ -105,7 +105,8 @@ void BatMon::pooler() {
_charge = capToMah(ReadRegister(0x05));
_current = regToCurrent(ReadRegister(0x0B));
_voltage = regToVoltage(ReadRegister(0x09));
PowerHelper::get().delay(10000, 1000);
_percentage = regToPercent(ReadRegister(0x06));
vTaskDelay(pdMS_TO_TICKS(10000));
if (_voltage < 3.0f) {
Shutdowner::get().shutdown();
}
@@ -115,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

@@ -6,7 +6,6 @@
#include <driver/gpio.h>
#include <esp_err.h>
#include "cardboy/backend/esp/power_helper.hpp"
#include <rom/ets_sys.h>
#include "freertos/FreeRTOS.h"
@@ -37,12 +36,10 @@ static void wakeup(void* arg) {
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_HIGH_LEVEL));
is_on_low = false;
BaseType_t xResult = pdFAIL;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(Buttons::get()._pooler_task, 0, eNoAction, &xHigherPriorityTaskWoken);
PowerHelper::get().reset_slow_isr(&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
} else {
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
@@ -78,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);
@@ -90,11 +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) {
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; }
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

@@ -5,30 +5,18 @@
#include <driver/ledc.h>
#include <esp_err.h>
#include <esp_timer.h>
#include <nvs_flash.h>
#include <nvs.h>
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
Buzzer &Buzzer::get() {
Buzzer& Buzzer::get() {
static Buzzer b;
return b;
}
void Buzzer::init() {
// Initialize NVS once (safe if already done)
static bool nvsInited = false;
if (!nvsInited) {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
nvsInited = true;
}
ledc_timer_config_t tcfg{};
tcfg.speed_mode = LEDC_MODE;
tcfg.timer_num = LEDC_TIMER;
@@ -52,7 +40,6 @@ void Buzzer::init() {
args.arg = this;
args.name = "buzz";
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
loadState();
}
void Buzzer::applyFreq(uint32_t freq) {
@@ -65,7 +52,7 @@ void Buzzer::applyFreq(uint32_t freq) {
ledc_update_duty(LEDC_MODE, LEDC_CH);
}
void Buzzer::enqueue(const Step &s) {
void Buzzer::enqueue(const Step& s) {
int nextTail = (_q_tail + 1) % MAX_QUEUE;
if (nextTail == _q_head) { // full, drop oldest
_q_head = (_q_head + 1) % MAX_QUEUE;
@@ -87,21 +74,23 @@ void Buzzer::startNext() {
}
_running = true;
_in_gap = false;
Step &s = front();
Step& s = front();
applyFreq(s.freq);
schedule(s.dur_ms, false);
}
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
if (!_timer) return;
if (!_timer)
return;
_in_gap = gapPhase;
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t) ms * 1000ULL);
}
void Buzzer::timerCb(void *arg) {
auto *self = static_cast<Buzzer*>(arg);
if (!self) return;
void Buzzer::timerCb(void* arg) {
auto* self = static_cast<Buzzer*>(arg);
if (!self)
return;
if (self->_in_gap) {
self->popFront();
self->startNext();
@@ -109,7 +98,7 @@ void Buzzer::timerCb(void *arg) {
}
// Tone finished
if (!self->empty()) {
auto &s = self->front();
auto& s = self->front();
if (s.gap_ms) {
self->applyFreq(0);
self->schedule(s.gap_ms, true);
@@ -121,7 +110,8 @@ void Buzzer::timerCb(void *arg) {
}
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
if (_muted) return; // ignore while muted
if (_muted)
return; // ignore while muted
Step s{freq, duration_ms, gap_ms};
enqueue(s);
if (!_running)
@@ -149,7 +139,8 @@ void Buzzer::beepGameOver() {
}
void Buzzer::setMuted(bool m) {
if (m == _muted) return;
if (m == _muted)
return;
_muted = m;
if (_muted) {
clearQueue();
@@ -164,28 +155,6 @@ void Buzzer::setMuted(bool m) {
tone(1500, 40, 10);
tone(1900, 60, 0);
}
saveState();
}
void Buzzer::toggleMuted() { setMuted(!_muted); }
void Buzzer::loadState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
uint8_t v = 0;
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
_muted = (v != 0);
}
nvs_close(h);
}
if (_muted) applyFreq(0);
}
void Buzzer::saveState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, "mute", _muted ? 1 : 0);
nvs_commit(h);
nvs_close(h);
}
}

View File

@@ -7,9 +7,9 @@
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/backend/esp/fs_helper.hpp"
#include "cardboy/backend/esp/i2c_global.hpp"
#include "cardboy/backend/esp/power_helper.hpp"
#include "cardboy/backend/esp/shutdowner.hpp"
#include "cardboy/backend/esp/spi_global.hpp"
#include "cardboy/backend/esp/time_sync_service.hpp"
#include "cardboy/sdk/display_spec.hpp"
@@ -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();
}
@@ -58,17 +67,18 @@ public:
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
void beepGameOver() override { Buzzer::get().beepGameOver(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
};
class EspRuntime::BatteryService final : public cardboy::sdk::IBatteryMonitor {
public:
[[nodiscard]] bool hasData() const override { return true; }
[[nodiscard]] bool hasData() const override { return true; }
[[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 {
@@ -112,45 +122,411 @@ public:
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
};
class EspRuntime::PowerService final : public cardboy::sdk::IPowerManager {
public:
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
};
class EspRuntime::FilesystemService final : public cardboy::sdk::IFilesystem {
public:
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
[[nodiscard]] std::string basePath() const override {
const char* path = FsHelper::get().basePath();
return path ? std::string(path) : std::string{};
}
};
class EspRuntime::LoopHooksService final : public cardboy::sdk::ILoopHooks {
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>();
powerService = std::make_unique<PowerService>();
filesystemService = std::make_unique<FilesystemService>();
_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.powerManager = powerService.get();
services.filesystem = filesystemService.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());
set_notification_center(_notificationService.get());
}
EspRuntime::~EspRuntime() = default;
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;
@@ -160,7 +536,6 @@ void EspRuntime::initializeHardware() {
ensureNvsInit();
PowerHelper::get();
Shutdowner::get();
Buttons::get();
@@ -170,7 +545,6 @@ void EspRuntime::initializeHardware() {
}
Shutdowner::get().install_isr();
PowerHelper::get().install_isr();
Buttons::get().install_isr();
I2cGlobal::get();
@@ -180,16 +554,8 @@ void EspRuntime::initializeHardware() {
Buzzer::get().init();
FsHelper::get().mount();
}
int EspFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
int EspFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
void EspFramebuffer::drawPixel_impl(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= width_impl() || y >= height_impl())
return;
SMD::set_pixel(x, y, on);
ensure_time_sync_service_started();
}
void EspFramebuffer::clear_impl(bool on) {
@@ -204,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();
@@ -234,7 +580,7 @@ std::uint32_t EspClock::millis_impl() {
void EspClock::sleep_ms_impl(std::uint32_t ms) {
if (ms == 0)
return;
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
vTaskDelay(pdMS_TO_TICKS(ms));
}
} // namespace cardboy::backend::esp

View File

@@ -1,58 +0,0 @@
//
// Created by Stepan Usatiuk on 03.03.2025.
//
#include "cardboy/backend/esp/power_helper.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
PowerHelper& PowerHelper::get() {
static PowerHelper powerHelper;
return powerHelper;
}
bool PowerHelper::is_slow() const { return _slow; }
void PowerHelper::set_slow(bool slow) {
_slow = slow;
if (_slow) {
xEventGroupClearBits(_event_group, 1);
} else {
xEventGroupSetBits(_event_group, 1);
}
}
BaseType_t PowerHelper::reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken) {
_slow = false;
return xEventGroupSetBitsFromISR(_event_group, 1, xHigherPriorityTaskWoken);
}
static void wakeup(void* arg) {
BaseType_t xHigherPriorityTaskWoken, xResult;
xHigherPriorityTaskWoken = pdFALSE;
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
if (xResult != pdFAIL) {
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
};
}
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
void PowerHelper::delay(int slow_ms, int normal_ms) {
if (is_slow()) {
auto cur_ticks = xTaskGetTickCount();
TickType_t to_wait = slow_ms / portTICK_PERIOD_MS;
TickType_t to_wait_normal = normal_ms / portTICK_PERIOD_MS;
auto expected_ticks = cur_ticks + to_wait_normal;
xEventGroupWaitBits(_event_group, 1, pdFALSE, pdTRUE, to_wait);
auto realTicks = xTaskGetTickCount();
if (realTicks < expected_ticks) {
vTaskDelay(expected_ticks - realTicks);
}
} else {
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
}
}
void PowerHelper::install_isr() {
// gpio_isr_handler_add(EXP_INT, wakeup, this);
}

File diff suppressed because it is too large Load Diff

248
Firmware/ghettoprof.sh Executable file
View File

@@ -0,0 +1,248 @@
#!/usr/bin/env bash
# parallel-pc-profile.sh — parallel symbol resolver + optional annotated disassembly
# Supports C++ demangling, LLVM disassembler, and optional no-inlines aggregation (symbol-table based).
#
# Usage:
# ./parallel-pc-profile.sh [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt
set -euo pipefail
usage() {
echo "Usage: $0 [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt"
exit 1
}
ANNOTATE=0
JOBS=""
NO_INLINES=0
# ---- args ----
while [[ $# -gt 0 ]]; do
case "$1" in
-j) JOBS="$2"; shift 2 ;;
--annotate) ANNOTATE=1; shift ;;
--no-inlines) NO_INLINES=1; shift ;;
-h|--help) usage ;;
*) break ;;
esac
done
[[ $# -lt 2 ]] && usage
ELF="$1"
PCS_IN="$2"
[[ ! -f "$ELF" ]] && { echo "ELF not found: $ELF" >&2; exit 2; }
[[ ! -f "$PCS_IN" ]] && { echo "PC log not found: $PCS_IN" >&2; exit 3; }
# ---- tools ----
ADDR2LINE=""
for t in llvm-addr2line eu-addr2line riscv32-esp-elf-addr2line xtensa-esp32-elf-addr2line addr2line; do
if command -v "$t" >/dev/null 2>&1; then ADDR2LINE="$t"; break; fi
done
[[ -z "$ADDR2LINE" ]] && { echo "No addr2line found"; exit 4; }
if command -v llvm-objdump >/dev/null 2>&1; then
OBJDUMP="llvm-objdump"
else
for t in riscv32-esp-elf-objdump xtensa-esp32-elf-objdump objdump; do
if command -v "$t" >/dev/null 2>&1; then OBJDUMP="$t"; break; fi
done
fi
[[ -z "${OBJDUMP:-}" ]] && { echo "No objdump found"; exit 5; }
if command -v llvm-nm >/dev/null 2>&1; then
NM="llvm-nm"
elif command -v nm >/dev/null 2>&1; then
NM="nm"
else
NM=""
fi
if command -v c++filt >/dev/null 2>&1; then
CPPFILT="c++filt"
elif command -v llvm-cxxfilt >/dev/null 2>&1; then
CPPFILT="llvm-cxxfilt"
else
CPPFILT=""
fi
# ---- cores ----
if [[ -z "$JOBS" ]]; then
if command -v nproc >/dev/null 2>&1; then JOBS=$(nproc)
elif [[ "$OSTYPE" == "darwin"* ]]; then JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
else JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
fi
fi
(( JOBS = JOBS > 1 ? JOBS - 1 : 1 ))
echo ">> Using $JOBS parallel jobs"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# ---- extract PCs ----
grep -aoE '0x[0-9a-fA-F]+' "$PCS_IN" | tr 'A-F' 'a-f' | sort | uniq -c >"$TMP/pc_counts.txt" || true
awk '{print $2}' "$TMP/pc_counts.txt" >"$TMP/addrs.txt"
[[ ! -s "$TMP/addrs.txt" ]] && { echo "No addresses found"; exit 5; }
# ---- parallel addr2line (live PC -> function to stderr) ----
CHUNK=400
split -l "$CHUNK" "$TMP/addrs.txt" "$TMP/chunk."
find "$TMP" -name 'chunk.*' -type f -print0 \
| xargs -0 -I{} -n1 -P "$JOBS" bash -c '
set -euo pipefail
ADDR2LINE="$1"; ELF="$2"; CHUNK="$3"; CPP="$4"
OUT="${CHUNK}.sym"
"$ADDR2LINE" -a -f -e "$ELF" $(cat "$CHUNK") \
| tee "$OUT" \
| awk '"'"'NR%3==1{a=$0;next} NR%3==2{f=$0; printf "%s\t%s\n",a,f; next} NR%3==0{next}'"'"' \
| { if [[ -n "$CPP" ]]; then "$CPP"; else cat; fi; } 1>&2
' _ "$ADDR2LINE" "$ELF" {} "$CPPFILT"
# Collate triplets
cat "$TMP"/chunk.*.sym > "$TMP/symbols.raw"
# ---- parse 3-line addr/func/file:line ----
# Normalize leading zeros in addresses so joins match grep-extracted PCs
awk 'NR%3==1{a=$0; sub(/^0x0+/, "0x", a); next} NR%3==2{f=$0; next} NR%3==0{print a "\t" f "\t" $0}' \
"$TMP/symbols.raw" >"$TMP/map.tsv"
# ---- counts: addr -> samplecount ----
awk '{printf "%s\t%s\n",$2,$1}' "$TMP/pc_counts.txt" | sort -k1,1 >"$TMP/counts.tsv"
# ---- choose mapping: default (addr2line; may show inlined names) vs --no-inlines (symbol-table) ----
DEFAULT_ADDR_FUNC="$TMP/addr_func.tsv"
cut -f1,2 "$TMP/map.tsv" | sort -k1,1 >"$DEFAULT_ADDR_FUNC"
if [[ "$NO_INLINES" == "1" ]]; then
if [[ -z "$NM" ]]; then
echo "WARNING: nm/llvm-nm not found; falling back to inline-aware mapping." >&2
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
else
echo ">> Building symbol table for no-inlines mapping..."
# Create sorted function symbols: hexaddr\tname (demangled if possible afterwards)
# Try llvm-nm format first; fall back to generic nm.
if [[ "$NM" == "llvm-nm" ]]; then
# llvm-nm -n --defined-only emits: ADDRESS TYPE NAME
"$NM" -n --defined-only "$ELF" \
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw"
else
# generic nm -n emits: ADDRESS TYPE NAME (varies a bit across platforms)
"$NM" -n --defined-only "$ELF" 2>/dev/null \
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
# macOS nm might output different columns; handle common alt layout:
if [[ ! -s "$TMP/syms.raw" ]]; then
"$NM" -n "$ELF" 2>/dev/null | awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
fi
fi
if [[ -n "$CPPFILT" && -s "$TMP/syms.raw" ]]; then
"$CPPFILT" < "$TMP/syms.raw" > "$TMP/syms.dem.raw" || cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
else
cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
fi
# Normalize addresses and sort ascending
awk '{addr=$1; sub(/^0x0+/, "0x", addr); print addr "\t" $2}' "$TMP/syms.dem.raw" \
| awk 'NF' \
| sort -k1,1 > "$TMP/syms.tsv"
if [[ ! -s "$TMP/syms.tsv" ]]; then
echo "WARNING: no text symbols found; falling back to inline-aware mapping." >&2
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
else
# Map each PC to the *containing* function: last symbol with addr <= PC.
# Both syms.tsv and addrs.txt are sorted asc → single pass merge.
awk '
function hex2num(h, x, n,i,c) {
gsub(/^0x/,"",h); n=0
for(i=1;i<=length(h);i++){ c=substr(h,i,1)
x = index("0123456789abcdef", tolower(c)) - 1
if (x<0) x = index("0123456789ABCDEF", c) - 1
n = n*16 + x
}
return n
}
BEGIN {
# preload symbols
while ((getline < ARGV[1]) > 0) {
saddr[NSYM]=$1; sname[NSYM]=$2; NSYM++
}
# load PCs
while ((getline < ARGV[2]) > 0) {
pc[NPC]=$0; NPC++
}
# pointers
si=0
for (i=0; i<NPC; i++) {
p=pc[i]; pn=hex2num(p)
# advance symbol index while next symbol start <= pc
while (si+1<NSYM && hex2num(saddr[si+1]) <= pn) si++
# output mapping: p -> sname[si] (if any)
if (si<NSYM && hex2num(saddr[si]) <= pn)
printf "%s\t%s\n", p, sname[si]
else
printf "%s\t<unknown>\n", p
}
exit 0
}
' "$TMP/syms.tsv" "$TMP/addrs.txt" \
| sort -k1,1 > "$TMP/addr_func.noinline.tsv"
ADDR_FUNC_FILE="$TMP/addr_func.noinline.tsv"
fi
fi
else
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
fi
# ---- aggregate to hot functions ----
join -t $'\t' -a1 -e "<unknown>" -o 1.2,2.2 "$TMP/counts.tsv" "$ADDR_FUNC_FILE" \
| awk -F'\t' '{s[$2]+=$1} END{for(k in s) printf "%8d %s\n",s[k],k}' \
| sort -nr > "$TMP/hot.txt"
# ---- demangle final hot list (if available) ----
if [[ -n "$CPPFILT" ]]; then
"$CPPFILT" < "$TMP/hot.txt" > hot_functions.txt
else
cp "$TMP/hot.txt" hot_functions.txt
fi
echo "=== Top 50 hot functions ==="
head -50 hot_functions.txt
echo "Full list in: hot_functions.txt"
# ---- annotated source+assembly (optional) ----
if (( ANNOTATE )); then
echo ">> Generating annotated source+assembly..."
awk '{printf "%s %s\n",$2,$1}' "$TMP/pc_counts.txt" >"$TMP/count.map"
if [[ "$OBJDUMP" == "llvm-objdump" ]]; then
# Portable across llvm-objdump versions
"$OBJDUMP" --source -l --demangle -d "$ELF" >"$TMP/disasm.txt"
else
"$OBJDUMP" -S -C -l -d "$ELF" >"$TMP/disasm.txt"
fi
# Overlay hit counts onto the disassembly
awk -v counts="$TMP/count.map" '
BEGIN {
while ((getline < counts) > 0) {
addr=$1; cnt=$2
gsub(/^0x/,"",addr)
map[addr]=cnt
}
}
/^[[:space:]]*[0-9a-f]+:/ {
split($1,a,":"); key=a[1]
if (key in map)
printf("%-12s %6d | %s\n", $1, map[key], substr($0, index($0,$2)))
else
print $0
next
}
{ print }
' "$TMP/disasm.txt" > annotated.S
echo "Annotated source + assembly written to: annotated.S"
echo "Tip: less -R annotated.S"
fi

View File

@@ -1,17 +1,22 @@
// Cardboy firmware entry point: boot platform services and run the modular app system.
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/apps/clock_app.hpp"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/lockscreen_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_pm.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <algorithm>
@@ -19,9 +24,9 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <span>
#include <string>
#include <string_view>
#include <span>
#include <vector>
namespace {
@@ -53,11 +58,11 @@ constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
namespace {
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
constexpr uint32_t kStatsTaskStack = 4096;
constexpr char kStatsTaskName[] = "TaskStats";
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
constexpr uint32_t kStatsTaskStack = 4096;
constexpr char kStatsTaskName[] = "TaskStats";
struct TaskRuntimeSample {
TaskHandle_t handle;
@@ -65,11 +70,11 @@ struct TaskRuntimeSample {
};
struct TaskUsageRow {
std::string name;
uint64_t delta;
UBaseType_t priority;
uint32_t stackHighWaterBytes;
bool isIdle;
std::string name;
uint64_t delta;
UBaseType_t priority;
uint32_t stackHighWaterBytes;
bool isIdle;
};
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
@@ -79,7 +84,7 @@ struct TaskUsageRow {
}
void task_usage_monitor(void*) {
static constexpr char tag[] = "TaskUsage";
static constexpr char tag[] = "TaskUsage";
std::vector<TaskRuntimeSample> lastSamples;
uint32_t lastTotal = 0;
@@ -94,7 +99,7 @@ void task_usage_monitor(void*) {
std::vector<TaskStatus_t> statusBuffer(taskCount);
uint32_t totalRuntime = 0;
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
if (captured == 0)
continue;
statusBuffer.resize(captured);
@@ -118,8 +123,8 @@ void task_usage_monitor(void*) {
std::vector<TaskUsageRow> rows;
rows.reserve(statusBuffer.size());
uint64_t idleDelta = 0;
uint64_t activeDelta = 0;
uint64_t idleDelta = 0;
uint64_t activeDelta = 0;
uint64_t accountedDelta = 0;
for (const auto& status: statusBuffer) {
@@ -128,18 +133,18 @@ void task_usage_monitor(void*) {
});
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
const uint64_t taskDelta =
(it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
TaskUsageRow row{
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
.delta = taskDelta,
.priority = status.uxCurrentPriority,
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
};
TaskUsageRow row{.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
.delta = taskDelta,
.priority = status.uxCurrentPriority,
.stackHighWaterBytes =
static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)};
rows.push_back(std::move(row));
@@ -156,9 +161,8 @@ void task_usage_monitor(void*) {
if (rows.empty())
continue;
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
return a.delta > b.delta;
});
std::sort(rows.begin(), rows.end(),
[](const TaskUsageRow& a, const TaskUsageRow& b) { return a.delta > b.delta; });
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
@@ -181,14 +185,20 @@ void task_usage_monitor(void*) {
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
}
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag, (activeDelta * 100.0) / static_cast<double>(totalDelta),
idlePct);
const uint32_t heapFree = esp_get_free_heap_size();
const uint32_t heapMinimum = esp_get_minimum_free_heap_size();
std::printf("[%s] Heap free %lu B | Min free %lu B\n", tag, static_cast<unsigned long>(heapFree),
static_cast<unsigned long>(heapMinimum));
std::fflush(stdout);
}
}
void start_task_usage_monitor() {
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr,
0);
}
} // namespace
@@ -197,15 +207,6 @@ inline void start_task_usage_monitor() {}
#endif
extern "C" void app_main() {
#ifdef CONFIG_PM_ENABLE
// const esp_pm_config_t pm_config = {
// .max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
// .min_freq_mhz = 16,
// .light_sleep_enable = true};
// ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
#endif
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
static cardboy::backend::esp::EspRuntime runtime;
@@ -215,12 +216,32 @@ extern "C" void app_main() {
cardboy::sdk::AppSystem system(context);
context.system = &system;
const cardboy::sdk::PersistentSettings persistentSettings =
cardboy::sdk::loadPersistentSettings(context.getServices());
if (auto* buzzer = context.buzzer())
buzzer->setMuted(persistentSettings.mute);
#ifdef CONFIG_PM_ENABLE
if (persistentSettings.autoLightSleep) {
const esp_pm_config_t pm_config = {
.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
.min_freq_mhz = 16,
.light_sleep_enable = true,
};
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
}
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
#endif
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createLockscreenAppFactory());
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createSnakeAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.registerApp(apps::createGameboyAppFactory());
start_task_usage_monitor();
// start_task_usage_monitor();
system.run();
}

View File

@@ -4,6 +4,13 @@ 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)
add_subdirectory(backend_interface)

View File

@@ -13,6 +13,9 @@ target_link_libraries(cardboy_apps
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
add_subdirectory(menu)
add_subdirectory(lockscreen)
add_subdirectory(clock)
add_subdirectory(settings)
add_subdirectory(gameboy)
add_subdirectory(snake)
add_subdirectory(tetris)

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";
@@ -25,14 +28,14 @@ using Framebuffer = typename AppContext::Framebuffer;
using Clock = typename AppContext::Clock;
struct TimeSnapshot {
bool hasWallTime = false;
int hour24 = 0;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
bool hasWallTime = false;
int hour24 = 0;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
std::uint64_t uptimeSeconds = 0;
};
@@ -42,26 +45,25 @@ public:
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
dirty = true;
lastSnapshot = {};
dirty = true;
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:
@@ -69,17 +71,18 @@ private:
Framebuffer& framebuffer;
Clock& clock;
bool use24Hour = true;
bool dirty = false;
bool use24Hour = true;
bool dirty = false;
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
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) {
@@ -199,7 +202,7 @@ private:
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
if (!snap.hasWallTime) {
char uptimeLine[32];
char uptimeLine[32];
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
@@ -225,7 +228,7 @@ private:
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kClockAppName; }
const char* name() const override { return kClockAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<ClockApp>(context);
}

View File

@@ -577,15 +577,6 @@ union cart_rtc
*/
struct gb_s
{
/**
* Return byte from ROM at given address.
*
* \param gb_s emulator context
* \param addr address
* \return byte at address in ROM
*/
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t addr);
/**
* Return byte from cart RAM at given address.
*
@@ -738,9 +729,14 @@ struct gb_s
/* Implementation defined data. Set to NULL if not required. */
void *priv;
const uint8_t* rom;
} direct;
};
__attribute__((always_inline)) inline uint8_t gb_rom_read(struct gb_s* s, const uint_fast32_t addr) {
return s->direct.rom[addr];
}
#ifndef PEANUT_GB_HEADER_ONLY
#define IO_JOYP 0x00
@@ -796,17 +792,17 @@ uint8_t __gb_read(struct gb_s *gb, uint16_t addr)
case 0x1:
case 0x2:
case 0x3:
return gb->gb_rom_read(gb, addr);
return gb_rom_read(gb, addr);
case 0x4:
case 0x5:
case 0x6:
case 0x7:
if(gb->mbc == 1 && gb->cart_mode_select)
return gb->gb_rom_read(gb,
return gb_rom_read(gb,
addr + ((gb->selected_rom_bank & 0x1F) - 1) * ROM_BANK_SIZE);
else
return gb->gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
return gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
case 0x8:
case 0x9:
@@ -3542,7 +3538,7 @@ int gb_get_save_size_s(struct gb_s *gb, size_t *ram_size)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3570,7 +3566,7 @@ uint_fast32_t gb_get_save_size(struct gb_s *gb)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3603,7 +3599,7 @@ uint8_t gb_colour_hash(struct gb_s *gb)
uint16_t i;
for(i = ROM_TITLE_START_ADDR; i <= ROM_TITLE_END_ADDR; i++)
x += gb->gb_rom_read(gb, i);
x += gb_rom_read(gb, i);
return x;
}
@@ -3626,7 +3622,7 @@ void gb_reset(struct gb_s *gb)
if(gb->gb_bootrom_read == NULL)
{
uint8_t hdr_chk;
hdr_chk = gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
hdr_chk = gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
gb->cpu_reg.a = 0x01;
gb->cpu_reg.f.f_bits.z = 1;
@@ -3692,11 +3688,10 @@ void gb_reset(struct gb_s *gb)
}
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv)
void *priv, const uint8_t* rom_data)
{
const uint16_t mbc_location = 0x0147;
const uint16_t bank_count_location = 0x0148;
@@ -3731,11 +3726,11 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
* some early homebrew ROMs supposedly may use this value. */
const uint8_t num_ram_banks[] = { 0, 1, 1, 4, 16, 8 };
gb->gb_rom_read = gb_rom_read;
gb->gb_cart_ram_read = gb_cart_ram_read;
gb->gb_cart_ram_write = gb_cart_ram_write;
gb->gb_error = gb_error;
gb->direct.priv = priv;
gb->direct.rom = rom_data;
/* Initialise serial transfer function to NULL. If the front-end does
* not provide serial support, Peanut-GB will emulate no cable connected
@@ -3751,24 +3746,24 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
uint16_t i;
for(i = 0x0134; i <= 0x014C; i++)
x = x - gb->gb_rom_read(gb, i) - 1;
x = x - gb_rom_read(gb, i) - 1;
if(x != gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
if(x != gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
return GB_INIT_INVALID_CHECKSUM;
}
/* Check if cartridge type is supported, and set MBC type. */
{
const uint8_t mbc_value = gb->gb_rom_read(gb, mbc_location);
const uint8_t mbc_value = gb_rom_read(gb, mbc_location);
if(mbc_value > sizeof(cart_mbc) - 1 ||
(gb->mbc = cart_mbc[mbc_value]) == -1)
return GB_INIT_CARTRIDGE_UNSUPPORTED;
}
gb->num_rom_banks_mask = num_rom_banks_mask[gb->gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb->gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb->gb_rom_read(gb, ram_size_location)];
gb->num_rom_banks_mask = num_rom_banks_mask[gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb_rom_read(gb, ram_size_location)];
/* If the ROM says that it support RAM, but has 0 RAM banks, then
* disable RAM reads from the cartridge. */
@@ -3804,7 +3799,7 @@ const char* gb_get_rom_name(struct gb_s* gb, char *title_str)
for(; title_loc <= title_end; title_loc++)
{
const char title_char = gb->gb_rom_read(gb, title_loc);
const char title_char = gb_rom_read(gb, title_loc);
if(title_char >= ' ' && title_char <= '_')
{
@@ -3886,11 +3881,10 @@ void gb_set_rtc(struct gb_s *gb, const struct tm * const time)
* \returns 0 on success or an enum that describes the error.
*/
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv);
void *priv, const uint8_t* rom_data);
/**
* Executes the emulator and runs for the duration of time equal to one frame.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/lockscreen_app.cpp
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

View File

@@ -0,0 +1,16 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kLockscreenAppName[] = "Lockscreen";
inline constexpr std::string_view kLockscreenAppNameView = kLockscreenAppName;
std::unique_ptr<cardboy::sdk::IAppFactory> createLockscreenAppFactory();
} // namespace apps

View File

@@ -0,0 +1,605 @@
#include "cardboy/apps/lockscreen_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#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;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
std::uint64_t uptimeSeconds = 0;
};
class LockscreenApp final : public cardboy::sdk::IApp {
public:
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;
lastNotificationInteractionMs = clock.millis();
lastRefreshMs = clock.millis();
refreshNotifications();
const auto snap = captureTime();
renderIfNeeded(snap);
lastSnapshot = snap;
rescheduleRefreshTimer(kSlowRefreshMs);
}
void onStop() override { cancelRefreshTimer(); }
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:
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;
std::uint32_t lastNotificationInteractionMs = 0;
std::uint32_t lastRefreshMs = 0;
void cancelRefreshTimer() {
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;
holdProgressMs = 0;
lastRefreshMs = clock.millis();
dirty = true;
}
} else {
if (holdActive || holdProgressMs != 0) {
holdActive = false;
holdProgressMs = 0;
dirty = true;
}
}
if (wasActive != holdActive) {
rescheduleRefreshTimer(holdActive ? kFastRefreshMs : kSlowRefreshMs);
}
}
void advanceHoldProgress(std::uint32_t elapsedMs) {
if (holdActive) {
const std::uint32_t next = std::min<std::uint32_t>(holdProgressMs + elapsedMs, kUnlockHoldMs);
if (next != holdProgressMs) {
holdProgressMs = next;
dirty = true;
}
if (holdProgressMs >= kUnlockHoldMs) {
holdActive = false;
context.requestAppSwitchByName(kMenuAppName);
}
} else if (holdProgressMs != 0) {
holdProgressMs = 0;
dirty = true;
}
}
void updateDisplay() {
refreshNotifications();
const auto snap = captureTime();
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
lastSnapshot = snap;
}
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;
}
TimeSnapshot captureTime() const {
TimeSnapshot snap{};
snap.uptimeSeconds = clock.millis() / 1000ULL;
std::time_t raw = 0;
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
std::tm tm{};
if (localtime_r(&raw, &tm) != nullptr) {
snap.hasWallTime = true;
snap.hour24 = tm.tm_hour;
snap.minute = tm.tm_min;
snap.second = tm.tm_sec;
snap.year = tm.tm_year + 1900;
snap.month = tm.tm_mon + 1;
snap.day = tm.tm_mday;
snap.weekday = tm.tm_wday;
return snap;
}
}
snap.hasWallTime = false;
snap.hour24 = static_cast<int>((snap.uptimeSeconds / 3600ULL) % 24ULL);
snap.minute = static_cast<int>((snap.uptimeSeconds / 60ULL) % 60ULL);
snap.second = static_cast<int>(snap.uptimeSeconds % 60ULL);
return snap;
}
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
const int width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
}
static void drawRectOutline(Framebuffer& fb, int x, int y, int w, int h) {
if (w <= 0 || h <= 0)
return;
for (int dx = 0; dx < w; ++dx) {
fb.drawPixel(x + dx, y, true);
fb.drawPixel(x + dx, y + h - 1, true);
}
for (int dy = 0; dy < h; ++dy) {
fb.drawPixel(x, y + dy, true);
fb.drawPixel(x + w - 1, y + dy, true);
}
}
static void fillRect(Framebuffer& fb, int x, int y, int w, int h) {
if (w <= 0 || h <= 0)
return;
for (int dy = 0; dy < h; ++dy) {
for (int dx = 0; dx < w; ++dx) {
fb.drawPixel(x + dx, y + dy, true);
}
}
}
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)
return "UPTIME MODE";
const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : "";
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day);
return buffer;
}
void renderIfNeeded(const TimeSnapshot& snap) {
if (!dirty)
return;
dirty = false;
framebuffer.frameReady();
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;
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const bool showNotificationDetails =
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
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 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 + 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) {
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
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);
}
framebuffer.sendFrame();
}
};
class LockscreenAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kLockscreenAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<LockscreenApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createLockscreenAppFactory() {
return std::make_unique<LockscreenAppFactory>();
}
} // namespace apps

View File

@@ -1,4 +1,5 @@
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/lockscreen_app.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
@@ -6,6 +7,7 @@
#include "cardboy/gfx/font16x8.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <string_view>
@@ -15,10 +17,16 @@ 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;
constexpr std::uint32_t kIdleTimeoutMs = 15000;
struct MenuEntry {
std::string name;
std::size_t index = 0;
@@ -31,27 +39,22 @@ public:
void onStart() override {
refreshEntries();
dirty = true;
resetInactivityTimer();
renderIfNeeded();
}
void handleEvent(const cardboy::sdk::AppEvent& event) override {
if (event.type != cardboy::sdk::AppEventType::Button)
return;
void onStop() override { cancelInactivityTimer(); }
const auto& current = event.button.current;
const auto& previous = event.button.previous;
if (current.left && !previous.left) {
moveSelection(-1);
} else if (current.right && !previous.right) {
moveSelection(+1);
}
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
if (launch)
launchSelected();
renderIfNeeded();
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:
@@ -62,6 +65,32 @@ private:
bool dirty = false;
cardboy::sdk::AppTimerHandle inactivityTimer = cardboy::sdk::kInvalidAppTimer;
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
resetInactivityTimer();
const auto& current = button.current;
const auto& previous = button.previous;
if (current.b && !previous.b) {
context.requestAppSwitchByName(kLockscreenAppName);
return;
}
if (current.left && !previous.left) {
moveSelection(-1);
} else if (current.right && !previous.right) {
moveSelection(+1);
}
const bool launch = (current.a && !previous.a) || (current.start && !previous.start);
if (launch)
launchSelected();
renderIfNeeded();
}
void moveSelection(int step) {
if (entries.empty())
return;
@@ -93,7 +122,8 @@ private:
const char* name = factory->name();
if (!name)
continue;
if (std::string_view(name) == kMenuAppNameView)
const std::string_view appName(name);
if (appName == kMenuAppNameView || appName == kLockscreenAppNameView)
continue;
entries.push_back(MenuEntry{std::string(name), i});
}
@@ -153,17 +183,31 @@ private:
font16x8::drawText(framebuffer, topRightX, 20, indexLabel, 1, true, 0);
drawPagerDots();
drawCenteredText(framebuffer, framebuffer.height() - 48, "A/SELECT START", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 48, "A START APP", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
}
framebuffer.sendFrame();
}
void cancelInactivityTimer() {
if (inactivityTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(inactivityTimer);
inactivityTimer = cardboy::sdk::kInvalidAppTimer;
}
void resetInactivityTimer() {
cancelInactivityTimer();
if (auto* timer = context.timer())
inactivityTimer = timer->scheduleTimer(kIdleTimeoutMs, false);
}
};
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kMenuAppName; }
const char* name() const override { return kMenuAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<MenuApp>(context);
}

View File

@@ -0,0 +1,9 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/settings_app.cpp
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

View File

@@ -0,0 +1,13 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
inline constexpr char kSettingsAppName[] = "Settings";
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory();
} // namespace apps

View File

@@ -0,0 +1,201 @@
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include <array>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
namespace apps {
namespace {
using cardboy::sdk::AppContext;
using Framebuffer = typename AppContext::Framebuffer;
enum class SettingOption {
Sound,
AutoLightSleep,
};
constexpr std::array<SettingOption, 2> kOptions = {
SettingOption::Sound,
SettingOption::AutoLightSleep,
};
class SettingsApp final : public cardboy::sdk::IApp {
public:
explicit SettingsApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
void onStart() override {
loadSettings();
dirty = true;
renderIfNeeded();
}
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 bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
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;
}
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
if (moved)
dirty = true;
renderIfNeeded();
},
[](const cardboy::sdk::AppTimerEvent&) { /* ignore */ },
[](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:
AppContext& context;
Framebuffer& framebuffer;
bool buzzerAvailable = false;
cardboy::sdk::PersistentSettings settings{};
std::size_t selectedIndex = 0;
bool dirty = false;
void loadSettings() {
settings = cardboy::sdk::loadPersistentSettings(context.getServices());
syncBuzzerState();
}
void syncBuzzerState() {
auto* buzzer = context.buzzer();
buzzerAvailable = (buzzer != nullptr);
if (!buzzer)
return;
if (buzzer->isMuted() != settings.mute)
buzzer->setMuted(settings.mute);
}
void moveSelection(int delta) {
const int count = static_cast<int>(kOptions.size());
if (count == 0)
return;
const int current = static_cast<int>(selectedIndex);
int next = (current + delta) % count;
if (next < 0)
next += count;
selectedIndex = static_cast<std::size_t>(next);
}
void handleToggle() {
switch (kOptions[selectedIndex]) {
case SettingOption::Sound:
toggleSound();
break;
case SettingOption::AutoLightSleep:
toggleAutoLightSleep();
break;
}
}
void toggleSound() {
if (!buzzerAvailable)
return;
settings.mute = !settings.mute;
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
syncBuzzerState();
if (!settings.mute) {
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
}
dirty = true;
}
void toggleAutoLightSleep() {
settings.autoLightSleep = !settings.autoLightSleep;
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
dirty = true;
}
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 1) {
const int width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
}
void drawOptionRow(int row, std::string_view label, std::string_view value, bool selected) {
std::string prefix = selected ? "> " : " ";
std::string line = prefix;
line.append(label);
line.append(": ");
line.append(value);
const int x = 24;
const int y = 56 + row * 24;
font16x8::drawText(framebuffer, x, y, line, 1, true, 1);
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
drawCenteredText(framebuffer, 24, "SETTINGS", 1, 1);
const std::string soundValue = buzzerAvailable ? (settings.mute ? "OFF" : "ON") : "N/A";
drawOptionRow(0, "SOUND", soundValue, selectedIndex == 0);
const std::string lightSleepValue = settings.autoLightSleep ? "ON" : "OFF";
drawOptionRow(1, "AUTO LIGHT SLEEP", lightSleepValue, selectedIndex == 1);
if (!buzzerAvailable)
drawCenteredText(framebuffer, 120, "SOUND CONTROL UNAVAILABLE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 54, "UP/DOWN MOVE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 36, "A/START/SELECT TOGGLE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK | LIGHT SLEEP AFTER RESET", 1, 1);
framebuffer.sendFrame();
}
};
class SettingsAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kSettingsAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<SettingsApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory() { return std::make_unique<SettingsAppFactory>(); }
} // namespace apps

View File

@@ -0,0 +1,9 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/snake_app.cpp
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

View File

@@ -0,0 +1,11 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<cardboy::sdk::IAppFactory> createSnakeAppFactory();
} // namespace apps

View File

@@ -0,0 +1,429 @@
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/input_state.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <random>
#include <string>
#include <string_view>
#include <vector>
namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
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 kIntervalSpeedupPerSegment = 4;
struct Point {
int x = 0;
int y = 0;
bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};
enum class Direction { Up, Down, Left, Right };
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
if (auto* rnd = ctx.random())
return rnd->nextUint32();
static std::random_device rd;
return rd();
}
class SnakeGame {
public:
explicit SnakeGame(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {
rng.seed(randomSeed(context));
loadHighScore();
reset();
}
void onStart() {
scheduleMoveTimer();
dirty = true;
renderIfNeeded();
}
void onStop() { cancelMoveTimer(); }
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:
AppContext& context;
typename AppContext::Framebuffer& framebuffer;
std::deque<Point> snake;
Point food{};
Direction direction = Direction::Right;
Direction queuedDirection = Direction::Right;
bool paused = false;
bool gameOver = false;
bool dirty = false;
int score = 0;
int highScore = 0;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
const auto& prev = evt.previous;
if (cur.b && !prev.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (cur.select && !prev.select) {
reset();
return;
}
if (cur.start && !prev.start) {
if (gameOver)
reset();
else {
paused = !paused;
dirty = true;
}
}
if (gameOver)
return;
if (cur.up && !prev.up)
queueDirection(Direction::Up);
else if (cur.down && !prev.down)
queueDirection(Direction::Down);
else if (cur.left && !prev.left)
queueDirection(Direction::Left);
else if (cur.right && !prev.right)
queueDirection(Direction::Right);
if (cur.a && !prev.a && !paused)
advance();
}
void handleTimer(AppTimerHandle handle) {
if (handle == moveTimer && !paused && !gameOver)
advance();
}
void reset() {
cancelMoveTimer();
snake.clear();
const int centerX = kBoardWidth / 2;
const int centerY = kBoardHeight / 2;
for (int i = 0; i < kInitialSnakeLength; ++i)
snake.push_back(Point{centerX - i, centerY});
direction = Direction::Right;
queuedDirection = Direction::Right;
paused = false;
gameOver = false;
score = 0;
dirty = true;
if (!spawnFood())
onGameOver();
scheduleMoveTimer();
}
void advance() {
direction = queuedDirection;
Point nextHead = snake.front();
switch (direction) {
case Direction::Up:
--nextHead.y;
break;
case Direction::Down:
++nextHead.y;
break;
case Direction::Left:
--nextHead.x;
break;
case Direction::Right:
++nextHead.x;
break;
}
if (isCollision(nextHead)) {
onGameOver();
return;
}
snake.push_front(nextHead);
if (nextHead == food) {
score += kScorePerFood;
updateHighScore();
if (!spawnFood()) {
onGameOver();
return;
}
scheduleMoveTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
} else {
snake.pop_back();
}
dirty = true;
}
[[nodiscard]] bool isCollision(const Point& nextHead) const {
if (nextHead.x < 0 || nextHead.x >= kBoardWidth || nextHead.y < 0 || nextHead.y >= kBoardHeight)
return true;
return std::find(snake.begin(), snake.end(), nextHead) != snake.end();
}
void onGameOver() {
if (gameOver)
return;
gameOver = true;
cancelMoveTimer();
dirty = true;
if (auto* buzzer = context.buzzer())
buzzer->beepGameOver();
}
void queueDirection(Direction next) {
if (isOpposite(direction, next) || isOpposite(queuedDirection, next))
return;
queuedDirection = next;
}
[[nodiscard]] static bool isOpposite(Direction a, Direction b) {
if ((a == Direction::Up && b == Direction::Down) || (a == Direction::Down && b == Direction::Up))
return true;
if ((a == Direction::Left && b == Direction::Right) || (a == Direction::Right && b == Direction::Left))
return true;
return false;
}
bool spawnFood() {
std::vector<Point> freeCells;
freeCells.reserve(kBoardWidth * kBoardHeight - static_cast<int>(snake.size()));
for (int y = 0; y < kBoardHeight; ++y) {
for (int x = 0; x < kBoardWidth; ++x) {
Point p{x, y};
if (std::find(snake.begin(), snake.end(), p) == snake.end())
freeCells.push_back(p);
}
}
if (freeCells.empty())
return false;
std::uniform_int_distribution<std::size_t> dist(0, freeCells.size() - 1);
food = freeCells[dist(rng)];
return true;
}
void scheduleMoveTimer() {
cancelMoveTimer();
const std::uint32_t interval = currentInterval();
if (auto* timer = context.timer())
moveTimer = timer->scheduleTimer(interval, true);
}
void cancelMoveTimer() {
if (moveTimer == cardboy::sdk::kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(moveTimer);
moveTimer = cardboy::sdk::kInvalidAppTimer;
}
[[nodiscard]] std::uint32_t currentInterval() const {
int interval = kBaseMoveIntervalMs - static_cast<int>(snake.size()) * kIntervalSpeedupPerSegment;
if (interval < kMinMoveIntervalMs)
interval = kMinMoveIntervalMs;
return static_cast<std::uint32_t>(interval);
}
void updateHighScore() {
if (score <= highScore)
return;
highScore = score;
if (auto* storage = context.storage())
storage->writeUint32("snake", "best", static_cast<std::uint32_t>(highScore));
}
void loadHighScore() {
if (auto* storage = context.storage()) {
std::uint32_t stored = 0;
if (storage->readUint32("snake", "best", stored))
highScore = static_cast<int>(stored);
}
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
drawBoard();
drawFood();
drawSnake();
drawHud();
framebuffer.sendFrame();
}
[[nodiscard]] int boardOriginX() const { return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2; }
[[nodiscard]] int boardOriginY() const {
const int centered = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
return std::max(24, centered);
}
void drawBoard() {
const int originX = boardOriginX();
const int originY = boardOriginY();
const int width = kBoardWidth * kCellSize;
const int height = kBoardHeight * kCellSize;
const int x0 = originX;
const int y0 = originY;
const int x1 = originX + width - 1;
const int y1 = originY + height - 1;
for (int x = x0; x <= x1; ++x) {
framebuffer.drawPixel(x, y0, true);
framebuffer.drawPixel(x, y1, true);
}
for (int y = y0; y <= y1; ++y) {
framebuffer.drawPixel(x0, y, true);
framebuffer.drawPixel(x1, y, true);
}
}
void drawSnake() {
if (snake.empty())
return;
std::size_t index = 0;
for (const auto& segment: snake) {
drawSnakeSegment(segment, index == 0);
++index;
}
}
void drawSnakeSegment(const Point& segment, bool head) {
const int originX = boardOriginX() + segment.x * kCellSize;
const int originY = boardOriginY() + segment.y * kCellSize;
for (int dy = 0; dy < kCellSize; ++dy) {
for (int dx = 0; dx < kCellSize; ++dx) {
const bool border = dx == 0 || dy == 0 || dx == kCellSize - 1 || dy == kCellSize - 1;
bool fill = ((dx + dy) & 0x1) == 0;
if (head)
fill = true;
const bool on = border || fill;
framebuffer.drawPixel(originX + dx, originY + dy, on);
}
}
}
void drawFood() {
const int cx = boardOriginX() + food.x * kCellSize + kCellSize / 2;
const int cy = boardOriginY() + food.y * kCellSize + kCellSize / 2;
const int r = std::max(2, kCellSize / 2 - 1);
for (int dy = -r; dy <= r; ++dy) {
for (int dx = -r; dx <= r; ++dx) {
if (std::abs(dx) + std::abs(dy) <= r)
framebuffer.drawPixel(cx + dx, cy + dy, true);
}
}
}
void drawHud() {
const int margin = 12;
const int textY = 8;
const std::string scoreStr = "SCORE " + std::to_string(score);
const std::string bestStr = "BEST " + std::to_string(highScore);
font16x8::drawText(framebuffer, margin, textY, scoreStr, 1, true, 1);
const int bestX = cardboy::sdk::kDisplayWidth - font16x8::measureText(bestStr, 1, 1) - margin;
font16x8::drawText(framebuffer, bestX, textY, bestStr, 1, true, 1);
const int footerY = cardboy::sdk::kDisplayHeight - 24;
const std::string menuStr = "B MENU";
const std::string selectStr = "SELECT RESET";
const std::string startStr = "START PAUSE";
const int selectX = (cardboy::sdk::kDisplayWidth - font16x8::measureText(selectStr, 1, 1)) / 2;
const int startX = cardboy::sdk::kDisplayWidth - font16x8::measureText(startStr, 1, 1) - margin;
font16x8::drawText(framebuffer, margin, footerY, menuStr, 1, true, 1);
font16x8::drawText(framebuffer, selectX, footerY, selectStr, 1, true, 1);
font16x8::drawText(framebuffer, startX, footerY, startStr, 1, true, 1);
if (paused && !gameOver)
drawBanner("PAUSED");
else if (gameOver)
drawBanner("GAME OVER");
}
void drawBanner(std::string_view text) {
const int w = font16x8::measureText(text, 2, 1);
const int h = font16x8::kGlyphHeight * 2;
const int x = (cardboy::sdk::kDisplayWidth - w) / 2;
const int y = boardOriginY() + kBoardHeight * kCellSize / 2 - h / 2;
for (int yy = -4; yy < h + 4; ++yy)
for (int xx = -6; xx < w + 6; ++xx)
framebuffer.drawPixel(x + xx, y + yy, yy == -4 || yy == h + 3 || xx == -6 || xx == w + 5);
font16x8::drawText(framebuffer, x, y, text, 2, true, 1);
}
};
class SnakeApp final : public cardboy::sdk::IApp {
public:
explicit SnakeApp(AppContext& ctx) : game(ctx) {}
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;
};
class SnakeFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kSnakeAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
return std::make_unique<SnakeApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createSnakeAppFactory() { return std::make_unique<SnakeFactory>(); }
} // namespace apps

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;
@@ -70,13 +70,13 @@ constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks)
}
constexpr std::array<Tetromino, 7> kPieces = {{
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
}};
class RandomBag {
@@ -107,22 +107,22 @@ private:
};
struct ActivePiece {
int type = 0;
int rotation = 0;
int x = 0;
int y = 0;
int type = 0;
int rotation = 0;
int x = 0;
int y = 0;
};
struct GameState {
std::array<int, kBoardWidth * kBoardHeight> board{};
ActivePiece current{};
int nextPiece = 0;
int level = 1;
int linesCleared = 0;
int score = 0;
int highScore = 0;
bool paused = false;
bool gameOver = false;
ActivePiece current{};
int nextPiece = 0;
int level = 1;
int linesCleared = 0;
int score = 0;
int highScore = 0;
bool paused = false;
bool gameOver = false;
};
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
@@ -148,34 +148,30 @@ 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:
AppContext& context;
AppContext& context;
typename AppContext::Framebuffer& framebuffer;
GameState state;
RandomBag bag;
InputState lastInput{};
bool dirty = false;
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
GameState state;
RandomBag bag;
InputState lastInput{};
bool dirty = false;
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
void reset() {
cancelTimers();
int oldHigh = state.highScore;
state = {};
state.highScore = oldHigh;
int oldHigh = state.highScore;
state = {};
state.highScore = oldHigh;
state.current.type = bag.next();
state.nextPiece = bag.next();
state.current.x = kBoardWidth / 2;
@@ -185,12 +181,10 @@ private:
state.paused = false;
dirty = true;
scheduleDropTimer();
if (auto* power = context.powerManager())
power->setSlowMode(false);
}
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
const auto& cur = evt.current;
const auto& prev = evt.previous;
lastInput = cur;
@@ -204,8 +198,6 @@ private:
reset();
} else {
state.paused = !state.paused;
if (auto* power = context.powerManager())
power->setSlowMode(state.paused);
}
dirty = true;
}
@@ -244,40 +236,45 @@ 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 {
const int base = 700;
const int step = 50;
const int base = 700;
const int step = 50;
int interval = base - (state.level - 1) * step;
if (interval < 120)
interval = 120;
@@ -322,7 +319,7 @@ private:
void rotate(int direction) {
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
nextRot = ((nextRot % 4) + 4) % 4;
nextRot = ((nextRot % 4) + 4) % 4;
if (canPlace(state.current.x, state.current.y, nextRot)) {
state.current.rotation = nextRot;
dirty = true;
@@ -387,8 +384,6 @@ private:
cancelDropTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepGameOver();
if (auto* power = context.powerManager())
power->setSlowMode(true);
} else {
if (auto* buzzer = context.buzzer())
buzzer->beepLock();
@@ -484,11 +479,10 @@ private:
for (int y = 0; y < kBoardHeight; ++y) {
for (int x = 0; x < kBoardWidth; ++x) {
if (int value = cellAt(x, y); value != 0)
drawCell(originX, originY, x, y, value, true);
drawCell(originX, originY, x, y, value - 1, true);
}
}
drawGuides(originX, originY);
drawBoardFrame(originX, originY);
}
void drawActivePiece() {
@@ -502,32 +496,76 @@ private:
int gy = state.current.y + block.y;
if (gy < 0)
continue;
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
drawCell(originX, originY, gx, gy, state.current.type, false);
}
}
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
const int x0 = originX + cx * kCellSize;
const int y0 = originY + cy * kCellSize;
for (int dy = 0; dy < kCellSize; ++dy) {
for (int dx = 0; dx < kCellSize; ++dx) {
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
static bool patternPixel(int pieceIndex, int dx, int dy) {
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
const int mx = dx & 0x3;
const int my = dy & 0x3;
switch (idx) {
case 0: // I - vertical stripes
return mx < 2;
case 1: // J - horizontal stripes
return my < 2;
case 2: { // L - forward diagonal
const int sum = (mx + my) & 0x3;
return sum < 2;
}
case 3: // O - diamond centerpiece
return (mx == 1 && my == 1) || (mx == 2 && my == 1) || (mx == 1 && my == 2) || (mx == 2 && my == 2);
case 4: // S - checkerboard
return ((mx ^ my) & 0x1) == 0;
case 5: { // T - cross
return (mx == 0) || (my == 0);
}
case 6: { // Z - backward diagonal
int diff = mx - my;
if (diff < 0)
diff += 4;
diff &= 0x3;
return diff < 2;
}
}
return true;
}
void drawPatternBlock(int x0, int y0, int size, int pieceIndex, bool locked) {
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
for (int dy = 0; dy < size; ++dy) {
for (int dx = 0; dx < size; ++dx) {
const bool border = dx == 0 || dx == size - 1 || dy == 0 || dy == size - 1;
bool fill = patternPixel(idx, dx, dy);
if (!locked && !border)
fill = fill && (((dx + dy) & 0x1) == 0);
const bool on = border || fill;
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
}
}
(void) value; // value currently unused (monochrome display)
}
void drawGuides(int originX, int originY) {
for (int y = 0; y <= kBoardHeight; ++y) {
const int py = originY + y * kCellSize;
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
void drawCell(int originX, int originY, int cx, int cy, int pieceIndex, bool locked) {
const int x0 = originX + cx * kCellSize;
const int y0 = originY + cy * kCellSize;
drawPatternBlock(x0, y0, kCellSize, pieceIndex, locked);
}
void drawBoardFrame(int originX, int originY) {
const int widthPixels = kBoardWidth * kCellSize;
const int heightPixels = kBoardHeight * kCellSize;
const int x0 = originX;
const int y0 = originY;
const int x1 = originX + widthPixels - 1;
const int y1 = originY + heightPixels - 1;
for (int x = x0; x <= x1; ++x) {
framebuffer.drawPixel(x, y0, true);
framebuffer.drawPixel(x, y1, true);
}
for (int x = 0; x <= kBoardWidth; ++x) {
const int px = originX + x * kCellSize;
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
for (int y = y0; y <= y1; ++y) {
framebuffer.drawPixel(x0, y, true);
framebuffer.drawPixel(x1, y, true);
}
}
@@ -539,15 +577,14 @@ private:
for (int dy = 0; dy < boxSize; ++dy)
for (int dx = 0; dx < boxSize; ++dx)
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
framebuffer.drawPixel(originX + dx, originY + dy,
(dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
const auto& piece = kPieces[state.nextPiece];
for (const auto& block: piece.rotations[0]) {
const int px = originX + (block.x + 1) * blockSize;
const int py = originY + (block.y + 1) * blockSize;
for (int dy = 1; dy < blockSize - 1; ++dy)
for (int dx = 1; dx < blockSize - 1; ++dx)
framebuffer.drawPixel(px + dx, py + dy, true);
const int px = originX + (block.x + 1) * blockSize + 1;
const int py = originY + (block.y + 1) * blockSize + 1;
drawPatternBlock(px, py, blockSize - 2, state.nextPiece, true);
}
}
@@ -598,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;
@@ -608,7 +645,7 @@ private:
class TetrisFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kTetrisAppName; }
const char* name() const override { return kTetrisAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
return std::make_unique<TetrisApp>(context);
}
@@ -616,8 +653,6 @@ public:
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
return std::make_unique<TetrisFactory>();
}
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() { return std::make_unique<TetrisFactory>(); }
} // namespace apps

View File

@@ -1,5 +1,7 @@
add_library(cardboy_backend_interface INTERFACE)
target_link_libraries(cardboy_backend_interface INTERFACE cardboy_utils)
set_target_properties(cardboy_backend_interface PROPERTIES
EXPORT_NAME backend_interface
)
@@ -14,6 +16,7 @@ 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/loop_hooks.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/input_state.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/services.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

@@ -0,0 +1,18 @@
#pragma once
namespace cardboy::sdk {
class FramebufferHooks {
public:
using PreSendHook = void (*)(void* framebuffer, void* userData);
static void setPreSendHook(PreSendHook hook, void* userData);
static void clearPreSendHook();
static void invokePreSend(void* framebuffer);
private:
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

@@ -0,0 +1,11 @@
#pragma once
namespace cardboy::sdk {
class ILoopHooks {
public:
virtual ~ILoopHooks() = default;
virtual void onLoopIteration() = 0;
};
} // namespace cardboy::sdk

View File

@@ -1,5 +1,6 @@
#pragma once
#include "cardboy/sdk/framebuffer_hooks.hpp"
#include "input_state.hpp"
#include <concepts>
@@ -23,6 +24,9 @@ concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
{ impl.sendFrame_impl(flag) };
};
template<typename Impl>
concept HasDrawBits8Impl = requires(Impl& impl, int x, int y, std::uint8_t bits) { impl.drawBits8_impl(x, y, bits); };
template<typename Impl>
concept HasFrameInFlightImpl = requires(const Impl& impl) {
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
@@ -37,11 +41,19 @@ concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
template<typename Impl>
class FramebufferFacade {
public:
[[nodiscard]] int width() const { return impl().width_impl(); }
[[nodiscard]] int height() const { return impl().height_impl(); }
[[nodiscard]] __attribute__((always_inline)) int width() const { return impl().width_impl(); }
[[nodiscard]] __attribute__((always_inline)) int height() const { return impl().height_impl(); }
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
__attribute__((always_inline)) void drawBits8(int x, int y, std::uint8_t bits) {
if constexpr (detail::HasDrawBits8Impl<Impl>) {
impl().drawBits8_impl(x, y, bits);
} else {
defaultDrawBits8(x, y, bits);
}
}
void clear(bool on) {
if constexpr (detail::HasClearImpl<Impl>) {
impl().clear_impl(on);
@@ -56,8 +68,10 @@ public:
}
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
if constexpr (detail::HasSendFrameImpl<Impl>)
if constexpr (detail::HasSendFrameImpl<Impl>) {
FramebufferHooks::invokePreSend(&impl());
impl().sendFrame_impl(clearDrawBuffer);
}
}
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
@@ -79,6 +93,14 @@ private:
for (int x = 0; x < width(); ++x)
drawPixel(x, y, on);
}
void defaultDrawBits8(int x, int y, std::uint8_t bits) {
for (int col = 0; col < 8; ++col) {
const std::uint8_t mask = static_cast<std::uint8_t>(1u << (7 - col));
const bool pixelOn = (bits & mask) != 0;
drawPixel(x + col, y, pixelOn);
}
}
};
template<typename Impl>

View File

@@ -1,8 +1,14 @@
#pragma once
#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 {
@@ -33,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 {
@@ -57,14 +64,6 @@ public:
[[nodiscard]] virtual std::uint64_t micros() = 0;
};
class IPowerManager {
public:
virtual ~IPowerManager() = default;
virtual void setSlowMode(bool enable) = 0;
[[nodiscard]] virtual bool isSlowMode() const = 0;
};
class IFilesystem {
public:
virtual ~IFilesystem() = default;
@@ -74,14 +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;
IPowerManager* powerManager = nullptr;
IFilesystem* filesystem = 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

@@ -6,9 +6,13 @@
#include <SFML/Graphics.hpp>
#include <SFML/Window/Keyboard.hpp>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <random>
#include <string>
#include <string_view>
@@ -36,151 +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 DesktopPowerManager final : public cardboy::sdk::IPowerManager {
public:
void setSlowMode(bool enable) override { slowMode = enable; }
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
private:
bool slowMode = false;
};
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
bool mount() override;
[[nodiscard]] bool isMounted() const override { return mounted; }
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
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;
private:
std::filesystem::path basePathPath;
bool mounted = false;
};
private:
static constexpr std::size_t kMaxEntries = 8;
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
mutable std::mutex mutex;
std::vector<Notification> entries;
std::uint64_t nextId = 1;
std::uint32_t revisionCounter = 0;
};
[[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; }
class DesktopLoopHooks final : public cardboy::sdk::ILoopHooks {
public:
explicit DesktopLoopHooks(DesktopRuntime& owner);
private:
DesktopRuntime& runtime;
};
void onLoopIteration() override;
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
private:
DesktopRuntime& runtime;
};
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
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;
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
private:
std::mutex mutex;
std::condition_variable cv;
std::deque<cardboy::sdk::AppEvent> queue;
};
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
class DesktopTimerService final : public cardboy::sdk::ITimerService {
public:
DesktopTimerService(DesktopRuntime& owner, cardboy::sdk::IEventBus& eventBus);
~DesktopTimerService() override;
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
cardboy::sdk::AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat) override;
void cancelTimer(cardboy::sdk::AppTimerHandle handle) override;
void cancelAllTimers() override;
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
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;
};
class DesktopRuntime {
public:
DesktopRuntime();
void workerLoop();
void wakeWorker();
void cleanupInactive();
cardboy::sdk::Services& serviceRegistry();
void processEvents();
void presentIfNeeded();
void sleepFor(std::uint32_t ms);
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;
};
[[nodiscard]] bool isRunning() const { return running; }
class DesktopAppServiceProvider final : public cardboy::sdk::IAppServiceProvider {
public:
DesktopAppServiceProvider(DesktopRuntime& owner, cardboy::sdk::IEventBus& bus);
DesktopFramebuffer framebuffer;
DesktopInput input;
DesktopClock clock;
[[nodiscard]] std::unique_ptr<cardboy::sdk::AppScopedServices>
createScopedServices(std::uint64_t generation) override;
private:
friend class DesktopFramebuffer;
friend class DesktopInput;
friend class DesktopClock;
private:
struct ScopedServices final : cardboy::sdk::AppScopedServices {
std::unique_ptr<DesktopTimerService> ownedTimer;
};
void setPixel(int x, int y, bool on);
void clearPixels(bool on);
DesktopRuntime& runtime;
cardboy::sdk::IEventBus& eventBus;
};
sf::RenderWindow window;
sf::Texture texture;
sf::Sprite sprite;
std::vector<std::uint8_t> pixels;
bool dirty = true;
bool running = true;
bool clearNextFrame = true;
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
DesktopBuzzer buzzerService;
DesktopBattery batteryService;
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopPowerManager powerService;
DesktopFilesystem filesystemService;
cardboy::sdk::Services services{};
};
[[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; }
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
private:
DesktopRuntime& runtime;
};
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
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,11 +8,169 @@
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <stdexcept>
#include <system_error>
#include <utility>
namespace cardboy::backend::desktop {
namespace {
constexpr std::size_t kDesktopEventQueueLimit = 64;
} // namespace
void DesktopEventBus::post(const cardboy::sdk::AppEvent& event) {
std::lock_guard<std::mutex> lock(mutex);
queue.push_back(event);
cv.notify_one();
}
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);
stopWorker = true;
}
cv.notify_all();
if (worker.joinable())
worker.join();
cancelAllTimers();
}
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)));
TimerRecord record{};
record.repeat = repeat;
record.interval = interval;
record.due = dueTime;
record.active = true;
{
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;
}
}
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;
}
cv.wait_until(lock, nextIt->due, [this] { return stopWorker; });
}
}
void DesktopTimerService::wakeWorker() { cv.notify_all(); }
void DesktopTimerService::cleanupInactive() {
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& record) { return !record.active; }),
timers.end());
}
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) {
auto it = data.find(composeKey(ns, key));
if (it == data.end())
@@ -63,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; }
@@ -90,25 +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) {
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:
@@ -118,8 +382,14 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
state.start = pressed;
break;
default:
handled = false;
break;
}
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()) {}
@@ -137,7 +407,8 @@ 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) {
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");
@@ -145,14 +416,20 @@ DesktopRuntime::DesktopRuntime() :
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
clearPixels(false);
presentIfNeeded();
window.requestFocus();
services.buzzer = &buzzerService;
services.battery = &batteryService;
services.storage = &storageService;
services.random = &randomService;
services.highResClock = &highResService;
services.powerManager = &powerService;
services.filesystem = &filesystemService;
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; }
@@ -161,7 +438,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
return;
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
pixels[idx + 0] = value;
pixels[idx + 1] = value;
pixels[idx + 2] = value;
@@ -170,7 +447,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) {
}
void DesktopRuntime::clearPixels(bool on) {
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
for (std::size_t i = 0; i < pixels.size(); i += 4) {
pixels[i + 0] = value;
pixels[i + 1] = value;

View File

@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.16)
add_library(cardboy_sdk STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
)
set_target_properties(cardboy_sdk PROPERTIES

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;
@@ -26,22 +24,76 @@ inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
return fonts_Terminess_Powerline[uc];
}
enum class Rotation {
None,
Clockwise90,
CounterClockwise90,
};
struct TextBounds {
int width = 0;
int height = 0;
};
template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto& rows = glyphBitmap(ch);
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
fb.drawBits8(x, y + row, rowBits);
}
return;
}
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
for (int col = 0; col < kGlyphWidth; ++col) {
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
if (rowBits & mask) {
for (int sx = 0; sx < scale; ++sx)
for (int sy = 0; sy < scale; ++sy)
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, on);
if ((rowBits & mask) == 0)
continue;
for (int sx = 0; sx < scale; ++sx) {
for (int sy = 0; sy < scale; ++sy) {
int dstX = x;
int dstY = y;
switch (rotation) {
case Rotation::None:
dstX += col * scale + sx;
dstY += row * scale + sy;
break;
case Rotation::Clockwise90:
dstX += row * scale + sx;
dstY += (kGlyphWidth - 1 - col) * scale + sy;
break;
case Rotation::CounterClockwise90:
dstX += (kGlyphHeight - 1 - row) * scale + sx;
dstY += col * scale + sy;
break;
}
fb.drawPixel(dstX, dstY, on);
}
}
}
}
}
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
Rotation rotation = Rotation::None) {
if (text.empty())
return {};
const int advance = (kGlyphWidth + letterSpacing) * scale;
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
const int height = kGlyphHeight * scale;
switch (rotation) {
case Rotation::None:
return {extent, height};
case Rotation::Clockwise90:
case Rotation::CounterClockwise90:
return {height, extent};
}
return {extent, height};
}
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
if (text.empty())
return 0;
@@ -51,11 +103,22 @@ inline int measureText(std::string_view text, int scale = 1, int letterSpacing =
template<typename Framebuffer>
inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true,
int letterSpacing = 1) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on);
cursor += (kGlyphWidth + letterSpacing) * scale;
int letterSpacing = 1, Rotation rotation = Rotation::None) {
if (text.empty())
return;
const int advance = (kGlyphWidth + letterSpacing) * scale;
if (rotation == Rotation::None) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
cursor += advance;
}
} else {
int cursor = y;
for (char ch: text) {
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
cursor += advance;
}
}
}

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,71 +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]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : 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

@@ -12,68 +12,34 @@ namespace cardboy::sdk {
class AppSystem {
public:
explicit AppSystem(AppContext context);
~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();
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{};
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

@@ -0,0 +1,20 @@
#pragma once
#include <cstddef>
namespace cardboy::sdk {
class FramebufferHooks {
public:
using PreSendHook = void (*)(void* framebuffer, void* userData);
static void setPreSendHook(PreSendHook hook, void* userData);
static void clearPreSendHook();
static void invokePreSend(void* framebuffer);
private:
static PreSendHook _hook;
static void* _userData;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,15 @@
#pragma once
#include "cardboy/sdk/services.hpp"
namespace cardboy::sdk {
struct PersistentSettings {
bool mute = false;
bool autoLightSleep = false;
};
PersistentSettings loadPersistentSettings(Services* services);
void savePersistentSettings(Services* services, const PersistentSettings& settings);
} // namespace cardboy::sdk

View File

@@ -0,0 +1,89 @@
#pragma once
#include "cardboy/sdk/input_state.hpp"
#include "cardboy/sdk/services.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
namespace cardboy::sdk {
class StatusBar {
public:
static StatusBar& instance();
void setServices(Services* services) { _services = services; }
void setEnabled(bool value);
void toggle();
[[nodiscard]] bool isEnabled() const { return _enabled; }
void setCurrentAppName(std::string_view name);
[[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous);
template<typename Framebuffer>
void renderIfEnabled(Framebuffer& fb) {
if (!_enabled)
return;
renderBar(fb);
}
private:
StatusBar() = default;
template<typename Framebuffer>
void renderBar(Framebuffer& fb) {
const int width = fb.width();
if (width <= 0)
return;
const int barHeight = font16x8::kGlyphHeight + 2;
const int fillHeight = std::min(barHeight, fb.height());
if (fillHeight <= 0)
return;
const std::string leftText = prepareLeftText(width);
const std::string rightText = prepareRightText();
for (int y = 0; y < fillHeight; ++y) {
for (int x = 0; x < width; ++x)
fb.drawPixel(x, y, true);
}
const int textY = 1;
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
if (bottomSeparatorY < fillHeight) {
for (int x = 0; x < width; ++x)
fb.drawPixel(x, bottomSeparatorY, (x % 2) != 0);
}
const int leftX = 2;
if (!leftText.empty())
font16x8::drawText(fb, leftX, textY, leftText, 1, false, 1);
if (!rightText.empty()) {
int rightWidth = font16x8::measureText(rightText, 1, 1);
int rightX = width - rightWidth - 2;
const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6;
if (rightX < minRightX)
rightX = std::max(minRightX, width / 2);
if (rightX < width)
font16x8::drawText(fb, rightX, textY, rightText, 1, false, 1);
}
}
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
[[nodiscard]] std::string prepareRightText() const;
bool _enabled = false;
Services* _services = nullptr;
std::string _appName{};
};
} // namespace cardboy::sdk

View File

@@ -1,231 +1,145 @@
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/framebuffer_hooks.hpp"
#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;
}
constexpr std::uint32_t kIdlePollMs = 16;
template<typename Framebuffer>
void statusBarPreSendHook(void* framebuffer, void* userData) {
auto* fb = static_cast<Framebuffer*>(framebuffer);
auto* status = static_cast<StatusBar*>(userData);
if (fb && status)
status->renderIfEnabled(*fb);
}
} // 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);
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();
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) {
events.clear();
const std::uint32_t now = context.clock.millis();
processDueTimers(now, events);
if (auto* hooks = _context.loopHooks())
hooks->onLoopIteration();
const InputState inputNow = context.input.readState();
if (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;
AppEvent event;
auto event_opt = _context.eventBus()->pop(_currentTimeout);
if (!event_opt) {
event = AppEvent{_context.clock.millis(), AppTimeoutEvent{}};
} else {
event = *event_opt;
}
for (const auto& evt: events) {
dispatchEvent(evt);
if (handlePendingSwitchRequest())
break;
if (const auto* btn = event.button()) {
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(btn->current, btn->previous);
if (consumedByStatusToggle) {
continue;
}
}
const std::uint32_t waitBase = context.clock.millis();
std::uint32_t waitMs = nextTimerDueMs(waitBase);
if (waitMs == 0)
_currentTimeout = _current->handleEvent(event);
if (_context._pendingSwitch) {
handlePendingSwitchRequest();
continue;
if (waitMs == std::numeric_limits<std::uint32_t>::max())
waitMs = kIdlePollMs;
else
waitMs = std::min(waitMs, kIdlePollMs);
if (waitMs > 0)
context.clock.sleep_ms(waitMs);
}
}
}
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);
return record.id;
}
void AppSystem::cancelTimer(AppTimerHandle handle) {
auto* timer = findTimer(handle);
if (timer)
timer->active = false;
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
void AppSystem::cancelAllTimers() {
for (auto& timer: timers) {
if (timer.generation == currentGeneration)
timer.active = false;
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
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() {
++currentGeneration;
timers.clear();
}
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);
}
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

@@ -0,0 +1,23 @@
#include "cardboy/sdk/framebuffer_hooks.hpp"
namespace cardboy::sdk {
FramebufferHooks::PreSendHook FramebufferHooks::_hook = nullptr;
void* FramebufferHooks::_userData = nullptr;
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
_hook = hook;
_userData = userData;
}
void FramebufferHooks::clearPreSendHook() {
_hook = nullptr;
_userData = nullptr;
}
void FramebufferHooks::invokePreSend(void* framebuffer) {
if (_hook)
_hook(framebuffer, _userData);
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,40 @@
#include "cardboy/sdk/persistent_settings.hpp"
#include <cstdint>
#include <string_view>
namespace cardboy::sdk {
namespace {
constexpr std::string_view kNamespace = "settings";
constexpr std::string_view kMuteKey = "mute";
constexpr std::string_view kAutoLightSleepKey = "autosleep";
[[nodiscard]] std::uint32_t boolToStorage(bool value) { return value ? 1U : 0U; }
[[nodiscard]] bool storageToBool(std::uint32_t value) { return value != 0U; }
} // namespace
PersistentSettings loadPersistentSettings(Services* services) {
PersistentSettings settings{};
if (!services || !services->storage)
return settings;
std::uint32_t raw = 0;
if (services->storage->readUint32(kNamespace, kMuteKey, raw))
settings.mute = storageToBool(raw);
if (services->storage->readUint32(kNamespace, kAutoLightSleepKey, raw))
settings.autoLightSleep = storageToBool(raw);
return settings;
}
void savePersistentSettings(Services* services, const PersistentSettings& settings) {
if (!services || !services->storage)
return;
services->storage->writeUint32(kNamespace, kMuteKey, boolToStorage(settings.mute));
services->storage->writeUint32(kNamespace, kAutoLightSleepKey, boolToStorage(settings.autoLightSleep));
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,71 @@
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
namespace cardboy::sdk {
StatusBar& StatusBar::instance() {
static StatusBar bar;
return bar;
}
void StatusBar::setEnabled(bool value) { _enabled = value; }
void StatusBar::toggle() {
_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(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
}
bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) {
const bool comboNow = current.start && current.select && current.up;
const bool comboPrev = previous.start && previous.select && previous.up;
if (comboNow && !comboPrev) {
toggle();
return true;
}
return false;
}
std::string StatusBar::prepareLeftText(int displayWidth) const {
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();
return text;
}
std::string StatusBar::prepareRightText() const {
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 percentage = _services->battery->percentage();
char buf[64];
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 (!right.empty())
right.append(" ");
right.append("MUTE");
}
return right;
}
} // namespace cardboy::sdk

View File

@@ -1,9 +1,13 @@
#include "cardboy/apps/clock_app.hpp"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/lockscreen_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/desktop_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include <cstdio>
#include <exception>
@@ -18,9 +22,17 @@ int main() {
context.services = &runtime.serviceRegistry();
cardboy::sdk::AppSystem system(context);
const cardboy::sdk::PersistentSettings persistentSettings =
cardboy::sdk::loadPersistentSettings(context.getServices());
if (auto* buzzer = context.buzzer())
buzzer->setMuted(persistentSettings.mute);
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createLockscreenAppFactory());
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createGameboyAppFactory());
system.registerApp(apps::createSnakeAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.run();

View File

@@ -0,0 +1,21 @@
add_library(cardboy_utils INTERFACE)
option(CARDBOY_MORE_CHECKS "More checks" OFF)
set_target_properties(cardboy_utils PROPERTIES
EXPORT_NAME utils
)
target_include_directories(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
if(CARDBOY_MORE_CHECKS)
target_compile_definitions(cardboy_utils INTERFACE CARDBOY_MORE_CHECKS=1)
endif()
target_sources(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/utils/utils.hpp
)

View File

@@ -0,0 +1,59 @@
//
// Created by Stepan Usatiuk on 11.10.2025.
//
#ifndef CARDBOY_SDK_UTILS_HPP
#define CARDBOY_SDK_UTILS_HPP
#ifndef CARDBOY_MORE_CHECKS
#define CARDBOY_MORE_CHECKS 0
#endif
#if CARDBOY_MORE_CHECKS
#include <cstdio>
#include <cstdlib>
// Fails the program with a message. Internal use.
#define CARDBOY__CHECK_FAIL_IMPL(expr_str, file, line, func, msg_opt) \
do { \
std::fprintf(stderr, \
"CARDBOY_CHECK failed: %s\n at %s:%d in %s%s%s\n", \
(expr_str), (file), (line), (func), \
((msg_opt) ? "\n message: " : ""), \
((msg_opt) ? (msg_opt) : "")); \
std::fflush(stderr); \
std::abort(); \
} while (0)
// Runtime check that is active only when CARDBOY_MORE_CHECKS != 0.
// Evaluates the expression exactly once.
#define CARDBOY_CHECK(expr) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, NULL); \
} \
} while (0)
// Same as CARDBOY_CHECK but allows providing a custom C-string message.
#define CARDBOY_CHECK_MSG(expr, msg) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, (msg));\
} \
} while (0)
// Execute arbitrary code only when checks are enabled.
#define CARDBOY_CHECK_CODE(code) \
do { \
code; \
} while (0)
#else
// Checks compiled out when CARDBOY_MORE_CHECKS == 0.
#define CARDBOY_CHECK(expr) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_MSG(expr, _) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_CODE(code) do { } while (0)
#endif
#endif // CARDBOY_SDK_UTILS_HPP

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"
@@ -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
@@ -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