diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00ea938210..e2f6876b44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: CI # Run this workflow whenever the build may be affected on: push: - branches: [ main ] + branches: [ main, "voidsec-hub/**" ] paths-ignore: - 'doc/**' - '**.md' diff --git a/README.md b/README.md index 68d8779d0e..881c109c16 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,37 @@ +## // CYBERPUNK EDITION + +This fork reworks InfiniTime into a neon, hacker-con-ready build for the PineTime. + +| NetRunner | Glitch | Matrix | Badge | +|:-:|:-:|:-:|:-:| +| ![NetRunner](doc/cyberpunk/netrunner.png) | ![Glitch](doc/cyberpunk/netrunner-glitch.png) | ![Matrix](doc/cyberpunk/matrix.png) | ![Badge](doc/cyberpunk/badge.png) | +| **Launcher** | **Settings** | **Terminal** | **Intrusions** | +| ![Launcher](doc/cyberpunk/launcher.png) | ![Settings](doc/cyberpunk/settings.png) | ![Terminal](doc/cyberpunk/terminal.png) | ![Intrusions](doc/cyberpunk/intrusions.png) | + +What's different from upstream: + +- **NetRunner watch face** (new default): chromatic-aberration time with random glitch bursts plus a guaranteed glitch every minute, HUD frame lines, seconds sweep bar, link/power status, HR + steps, weather, and the Unix epoch in hex. The handle in the bottom-left corner comes from `src/Identity.h`. +- **Matrix app**: digital rain built from ASCII and Cyrillic glyphs. Tap to cycle green, cyan, and magenta palettes. +- **Badge app** (skull icon): your handle and a scannable QR code linking to your GitHub. The screen stays on while it is open. To point it somewhere else, run `python3 tools/badge/generate_badge_qr.py ` (needs `pip install segno`). +- **Intrusion log** (shield icon): every Bluetooth connection that isn't your bonded phone is logged with time, address, duration and what it tried (pairing, failed pairing). If you have a bonded phone, an unknown connection vibrates and pops up an `UNKNOWN LINK` alert. Toggle alerts on/off or wipe the log from the app. Your phone counts as trusted only when it is paired with the PIN (bonded), so pair it that way. +- **Bluetooth name** is your handle (`VOIDSEC`), set in `src/Identity.h`. Gadgetbridge only auto-detects watches whose name starts with `InfiniTime`, so pair it before flashing this firmware, or temporarily change the name back to pair. +- **Heart rate in the background**: measurement starts at boot and runs every 5 minutes by default, so you never have to open the heart rate app. If no pulse is found for 30 seconds (the watch is off your wrist or on the charger), it backs off and retries every 5 minutes, so even *Cont* mode doesn't run the sensor all day on the nightstand. Change the interval in *Settings → Heart rate*. +- **Neon theme**: cyan and magenta outlined buttons, sharp corners, dark purple surfaces, and recolored launcher tiles, lists, sliders, and switches. +- **Terminal face** recolored as a root shell. +- **Trimmed build**: Paint, Paddle, 2048, Dice, and Metronome apps plus the Analog, PineTimeStyle, Infineat, Casio, and Pride Flag faces are no longer built by default, which frees about 49 KB of flash. Re-enable any of them in `src/displayapp/apps/CMakeLists.txt`. + +### Installing with Gadgetbridge + +1. Grab `pinetime-mcuboot-app-dfu-.zip` (and `infinitime-resources-.zip` for the navigation icons) from the CI artifacts of this branch. +2. On the watch, go to *Settings → Over-the-air* and pick *Till reboot*. Updates and file uploads are refused while it is *Disabled*, and *Till reboot* shuts the door again after the update. +3. In Gadgetbridge, open the zip with *File installer* (or share it to Gadgetbridge) and install. Then install the resources zip the same way. +4. After the watch reboots, go to *Settings → Firmware* and tap *Validate*, otherwise the old firmware comes back on the next reboot. + +> If your watch already has saved settings, pick NetRunner in *Settings → Watch face*. + ## New to InfiniTime? - [Getting started with InfiniTime](doc/gettingStarted/gettingStarted-1.0.md) diff --git a/doc/cyberpunk/badge.png b/doc/cyberpunk/badge.png new file mode 100644 index 0000000000..4949b3e139 Binary files /dev/null and b/doc/cyberpunk/badge.png differ diff --git a/doc/cyberpunk/intrusions.png b/doc/cyberpunk/intrusions.png new file mode 100644 index 0000000000..e47919dc58 Binary files /dev/null and b/doc/cyberpunk/intrusions.png differ diff --git a/doc/cyberpunk/launcher.png b/doc/cyberpunk/launcher.png new file mode 100644 index 0000000000..b309ecaddd Binary files /dev/null and b/doc/cyberpunk/launcher.png differ diff --git a/doc/cyberpunk/matrix.png b/doc/cyberpunk/matrix.png new file mode 100644 index 0000000000..87fe9c6f82 Binary files /dev/null and b/doc/cyberpunk/matrix.png differ diff --git a/doc/cyberpunk/netrunner-glitch.png b/doc/cyberpunk/netrunner-glitch.png new file mode 100644 index 0000000000..12ac0fcdfd Binary files /dev/null and b/doc/cyberpunk/netrunner-glitch.png differ diff --git a/doc/cyberpunk/netrunner.png b/doc/cyberpunk/netrunner.png new file mode 100644 index 0000000000..f046a08e4b Binary files /dev/null and b/doc/cyberpunk/netrunner.png differ diff --git a/doc/cyberpunk/settings.png b/doc/cyberpunk/settings.png new file mode 100644 index 0000000000..8b474d1c48 Binary files /dev/null and b/doc/cyberpunk/settings.png differ diff --git a/doc/cyberpunk/terminal.png b/doc/cyberpunk/terminal.png new file mode 100644 index 0000000000..e6e2ed5910 Binary files /dev/null and b/doc/cyberpunk/terminal.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index faef4160da..2ee7eb5253 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -393,6 +393,9 @@ list(APPEND SOURCE_FILES displayapp/screens/Steps.cpp displayapp/screens/Timer.cpp displayapp/screens/Dice.cpp + displayapp/screens/Matrix.cpp + displayapp/screens/Badge.cpp + displayapp/screens/IntrusionLog.cpp displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp displayapp/screens/Alarm.cpp @@ -430,6 +433,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp displayapp/screens/WatchFacePrideFlag.cpp + displayapp/screens/WatchFaceNetRunner.cpp ## @@ -463,6 +467,7 @@ list(APPEND SOURCE_FILES components/ble/BatteryInformationService.cpp components/ble/FSService.cpp components/ble/ImmediateAlertService.cpp + components/ble/IntrusionLog.cpp components/ble/ServiceDiscovery.cpp components/ble/HeartRateService.cpp components/ble/MotionService.cpp @@ -533,6 +538,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/ble/BatteryInformationService.cpp components/ble/FSService.cpp components/ble/ImmediateAlertService.cpp + components/ble/IntrusionLog.cpp components/ble/ServiceDiscovery.cpp components/ble/NavigationService.cpp components/ble/HeartRateService.cpp @@ -591,6 +597,7 @@ set(INCLUDE_FILES ${CMAKE_CURRENT_BINARY_DIR}/src/Version.h BootloaderVersion.h + Identity.h logging/Logger.h logging/NrfLogger.h displayapp/DisplayApp.h @@ -618,6 +625,10 @@ set(INCLUDE_FILES displayapp/screens/Motion.h displayapp/screens/Timer.h displayapp/screens/Dice.h + displayapp/screens/Matrix.h + displayapp/screens/Badge.h + displayapp/screens/BadgeQr.h + displayapp/screens/IntrusionLog.h displayapp/screens/Alarm.h displayapp/Colors.h displayapp/widgets/Counter.h @@ -653,6 +664,7 @@ set(INCLUDE_FILES components/ble/BatteryInformationService.h components/ble/FSService.h components/ble/ImmediateAlertService.h + components/ble/IntrusionLog.h components/ble/ServiceDiscovery.h components/ble/BleClient.h components/ble/HeartRateService.h diff --git a/src/Identity.h b/src/Identity.h new file mode 100644 index 0000000000..c2aaa923af --- /dev/null +++ b/src/Identity.h @@ -0,0 +1,9 @@ +#pragma once + +namespace Pinetime { + namespace Identity { + // Your handle. Shown on the NetRunner watch face and the Badge app, and used as the Bluetooth name. + // Note: companion apps such as Gadgetbridge look for a name starting with "InfiniTime" when pairing a new watch. + static constexpr const char* handle = "VOIDSEC"; + } +} diff --git a/src/components/ble/IntrusionLog.cpp b/src/components/ble/IntrusionLog.cpp new file mode 100644 index 0000000000..468b987d6b --- /dev/null +++ b/src/components/ble/IntrusionLog.cpp @@ -0,0 +1,202 @@ +#include "components/ble/IntrusionLog.h" + +#include +#include +#include "components/fs/FS.h" + +using namespace Pinetime::Controllers; + +IntrusionLog::IntrusionLog(FS& fs) : fs {fs} { +} + +void IntrusionLog::Load() { + FileData data; + lfs_file_t file; + if (fs.FileOpen(&file, fileName, LFS_O_RDONLY) != LFS_ERR_OK) { + return; + } + const int read = fs.FileRead(&file, reinterpret_cast(&data), sizeof(data)); + fs.FileClose(&file); + if (read != sizeof(data) || data.magic != magic || data.version != fileVersion || data.count > MaxEntries || data.head >= MaxEntries) { + return; + } + enabled = data.enabled != 0; + count = data.count; + head = data.head; + entries = data.entries; +} + +void IntrusionLog::Save() { + FileData data; + taskENTER_CRITICAL(); + data.magic = magic; + data.version = fileVersion; + data.enabled = enabled ? 1 : 0; + data.count = count; + data.head = head; + data.entries = entries; + dirty = false; + taskEXIT_CRITICAL(); + + lfs_file_t file; + if (fs.FileOpen(&file, fileName, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) != LFS_ERR_OK) { + dirty = true; + return; + } + fs.FileWrite(&file, reinterpret_cast(&data), sizeof(data)); + fs.FileClose(&file); +} + +void IntrusionLog::SetEnabled(bool enable) { + enabled = enable; + dirty = true; +} + +void IntrusionLog::Clear() { + taskENTER_CRITICAL(); + count = 0; + committed = false; + dirty = true; + taskEXIT_CRITICAL(); +} + +IntrusionLog::Entry IntrusionLog::Get(size_t index) const { + Entry entry {}; + taskENTER_CRITICAL(); + if (index < count) { + entry = entries[(head + MaxEntries - index) % MaxEntries]; + } + taskEXIT_CRITICAL(); + return entry; +} + +void IntrusionLog::FormatAddress(const Entry& entry, char* buffer) { + static constexpr char hex[] = "0123456789ABCDEF"; + // NimBLE stores addresses little-endian, print them the usual way round + for (int i = 0; i < 6; i++) { + const uint8_t byte = entry.address[5 - i]; + *buffer++ = hex[byte >> 4]; + *buffer++ = hex[byte & 0x0F]; + *buffer++ = i < 5 ? ':' : '\0'; + } +} + +const char* IntrusionLog::Describe(const Entry& entry) { + if ((entry.flags & Trusted) != 0) { + return "BONDED"; + } + if ((entry.flags & EncryptFailed) != 0) { + return "PAIR FAILED"; + } + if ((entry.flags & PairAttempt) != 0) { + return "PAIR ATTEMPT"; + } + if ((entry.flags & NoBond) != 0) { + return "UNVERIFIED"; + } + return "UNKNOWN"; +} + +void IntrusionLog::Commit() { + taskENTER_CRITICAL(); + head = (head + 1) % MaxEntries; + count = std::min(count + 1, MaxEntries); + entries[head] = current; + committed = true; + dirty = true; + taskEXIT_CRITICAL(); +} + +void IntrusionLog::OnConnect(const uint8_t* address, uint8_t addressType, bool knownBond, uint32_t time) { + // Nothing to log when disabled, or when the bonded phone reconnects with its identity address + connected = enabled && !knownBond; + pending = connected; + committed = false; + if (!connected) { + return; + } + current.time = time; + current.duration = 0; + std::memcpy(current.address, address, sizeof(current.address)); + current.addressType = addressType; + current.flags = 0; + connectTick = xTaskGetTickCount(); +} + +bool IntrusionLog::OnPairingRequest() { + if (connected) { + Active().flags |= PairAttempt; + } + return false; +} + +bool IntrusionLog::OnEncryptionChange(bool success, bool bonded) { + if (!connected) { + return false; + } + if (success && bonded) { + if (pending) { + // Our own phone: forget about it + connected = false; + pending = false; + } else { + // Encrypted after it was already reported, keep the record but mark it + Active().flags |= Trusted; + dirty = true; + } + return false; + } + if (!success) { + Active().flags |= EncryptFailed; + if (pending) { + pending = false; + current.flags |= Alerted; + Commit(); + return true; + } + dirty = true; + } + return false; +} + +bool IntrusionLog::ClassifyPending(bool hasBond) { + if (!pending) { + return false; + } + pending = false; + if (!hasBond) { + current.flags |= NoBond; + } + const bool alert = hasBond || (current.flags & (EncryptFailed | PairAttempt)) != 0; + if (alert) { + current.flags |= Alerted; + } + Commit(); + return alert; +} + +bool IntrusionLog::Classify(bool hasBond) { + // Give someone typing the passkey on their phone time to finish pairing + if (pending && (current.flags & PairAttempt) != 0) { + return false; + } + return ClassifyPending(hasBond); +} + +bool IntrusionLog::OnDisconnect(bool hasBond) { + if (!connected) { + return false; + } + const uint32_t seconds = (xTaskGetTickCount() - connectTick) / configTICK_RATE_HZ; + const uint16_t duration = static_cast(std::min(std::max(seconds, 1), UINT16_MAX)); + current.duration = duration; + const bool alert = ClassifyPending(hasBond); + if (committed) { + taskENTER_CRITICAL(); + entries[head].duration = duration; + taskEXIT_CRITICAL(); + dirty = true; + } + connected = false; + return alert; +} diff --git a/src/components/ble/IntrusionLog.h b/src/components/ble/IntrusionLog.h new file mode 100644 index 0000000000..bd18a77781 --- /dev/null +++ b/src/components/ble/IntrusionLog.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Pinetime { + namespace Controllers { + class FS; + + // Records Bluetooth connections that don't come from the bonded phone. + // + // A connection is considered trusted when it encrypts with the stored bond. + // Anything else is logged, and raises an alert when a bond exists (so there is + // a known phone to compare against) or when pairing/encryption was attempted. + class IntrusionLog { + public: + static constexpr uint8_t MaxEntries = 16; + + enum Flags : uint8_t { + Trusted = 1 << 0, // Encrypted with the stored bond + EncryptFailed = 1 << 1, + PairAttempt = 1 << 2, // Asked the watch to show a pairing passkey + Alerted = 1 << 3, + NoBond = 1 << 4, // No bonded phone to compare against, can't tell friend from foe + }; + + struct Entry { + uint32_t time; // Local time, seconds since epoch + uint16_t duration; // Seconds connected, 0 while still connected + uint8_t address[6]; + uint8_t addressType; + uint8_t flags; + }; + + explicit IntrusionLog(FS& fs); + + void Load(); + void Save(); + + bool IsEnabled() const { + return enabled; + } + + void SetEnabled(bool enable); + void Clear(); + + bool IsDirty() const { + return dirty; + } + + size_t Count() const { + return count; + } + + // 0 is the newest entry + Entry Get(size_t index) const; + + // "AA:BB:CC:DD:EE:FF", buffer must hold at least 18 bytes + static void FormatAddress(const Entry& entry, char* buffer); + // Short description of what the connection did + static const char* Describe(const Entry& entry); + + // Called from the BLE stack. Return true when an alert should be raised. + void OnConnect(const uint8_t* address, uint8_t addressType, bool knownBond, uint32_t time); + bool OnPairingRequest(); + bool OnEncryptionChange(bool success, bool bonded); + bool OnDisconnect(bool hasBond); + + // Called a few seconds after the connection is established + bool Classify(bool hasBond); + + private: + static constexpr uint32_t magic = 0x4c495356; // "VSIL" + static constexpr uint8_t fileVersion = 1; + static constexpr const char* fileName = "/intrusion.dat"; + + struct FileData { + uint32_t magic; + uint8_t version; + uint8_t enabled; + uint8_t count; + uint8_t head; + std::array entries; + }; + + // Entry for the current connection: kept aside until it's classified, then committed to the log + Entry& Active() { + return committed ? entries[head] : current; + } + + bool ClassifyPending(bool hasBond); + void Commit(); + + FS& fs; + std::array entries {}; + uint8_t head = MaxEntries - 1; + uint8_t count = 0; + bool enabled = true; + bool dirty = false; + Entry current {}; + bool connected = false; // A connection is being tracked + bool pending = false; // The tracked connection isn't classified yet + bool committed = false; // The tracked connection was written to the log + TickType_t connectTick = 0; + }; + } +} diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 5059007ab9..447849fb7f 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -49,7 +49,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, heartRateService {*this, heartRateController}, motionService {*this, motionController}, fsService {systemTask, fs}, - serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { + serviceDiscovery({¤tTimeClient, &alertNotificationClient}), + intrusionLogger {fs} { } void nimble_on_reset(int reason) { @@ -132,6 +133,7 @@ void NimbleController::Init() { ASSERT(rc == 0); RestoreBond(); + intrusionLogger.Load(); StartAdvertising(); } @@ -207,6 +209,17 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { } else { connectionHandle = event->connect.conn_handle; bleController.Connect(); + + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(connectionHandle, &desc) == 0) { + union ble_store_key key; + union ble_store_value value; + memset(&key, 0, sizeof key); + key.sec.peer_addr = desc.peer_id_addr; + const bool knownBond = ble_store_read_peer_sec(&key.sec, &value.sec) == 0; + const auto now = std::chrono::duration_cast(dateTimeController.CurrentDateTime().time_since_epoch()); + intrusionLogger.OnConnect(desc.peer_ota_addr.val, desc.peer_ota_addr.type, knownBond, static_cast(now.count())); + } systemTask.PushMessage(Pinetime::System::Messages::BleConnected); // Service discovery is deferred via systemtask } @@ -221,6 +234,12 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { PersistBond(event->disconnect.conn); } + if (intrusionLogger.OnDisconnect(HasBond())) { + systemTask.PushMessage(Pinetime::System::Messages::OnIntrusion); + } else if (intrusionLogger.IsDirty()) { + systemTask.PushMessage(Pinetime::System::Messages::IntrusionLogChanged); + } + currentTimeClient.Reset(); alertNotificationClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; @@ -258,12 +277,15 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { if (desc.sec_state.bonded) { PersistBond(desc); } + intrusionLogger.OnEncryptionChange(true, desc.sec_state.bonded); NRF_LOG_INFO("new state: encrypted=%d authenticated=%d bonded=%d key_size=%d", desc.sec_state.encrypted, desc.sec_state.authenticated, desc.sec_state.bonded, desc.sec_state.key_size); + } else if (intrusionLogger.OnEncryptionChange(false, false)) { + systemTask.PushMessage(Pinetime::System::Messages::OnIntrusion); } break; @@ -281,6 +303,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { * Use the tinycrypt prng here since rand() is predictable. */ NRF_LOG_INFO("Security event : BLE_GAP_EVENT_PASSKEY_ACTION"); + intrusionLogger.OnPairingRequest(); if (event->passkey.params.action == BLE_SM_IOACT_DISP) { struct ble_sm_io pkey = {0}; pkey.action = event->passkey.params.action; @@ -480,6 +503,16 @@ void NimbleController::PersistBond(struct ble_gap_conn_desc& desc) { } } +bool NimbleController::HasBond() { + int count = 0; + ble_store_util_count(BLE_STORE_OBJ_TYPE_PEER_SEC, &count); + return count > 0; +} + +bool NimbleController::CheckIntrusion() { + return intrusionLogger.Classify(HasBond()); +} + void NimbleController::RestoreBond() { lfs_file_t file_p; union ble_store_value sec, cccd; diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 597ef0cc34..39e73bf9c3 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -1,6 +1,7 @@ #pragma once #include +#include "Identity.h" #define min // workaround: nimble's min/max macros conflict with libstdc++ #define max @@ -17,6 +18,7 @@ #include "components/ble/FSService.h" #include "components/ble/HeartRateService.h" #include "components/ble/ImmediateAlertService.h" +#include "components/ble/IntrusionLog.h" #include "components/ble/MusicService.h" #include "components/ble/NavigationService.h" #include "components/ble/ServiceDiscovery.h" @@ -71,6 +73,13 @@ namespace Pinetime { return weatherService; }; + Pinetime::Controllers::IntrusionLog& intrusionLog() { + return intrusionLogger; + }; + + // Classifies the current connection for the intrusion log, returns true if it should raise an alert + bool CheckIntrusion(); + uint16_t connHandle(); void NotifyBatteryLevel(uint8_t level); @@ -84,8 +93,9 @@ namespace Pinetime { private: void PersistBond(struct ble_gap_conn_desc& desc); void RestoreBond(); + bool HasBond(); - static constexpr const char* deviceName = "InfiniTime"; + static constexpr const char* deviceName = Pinetime::Identity::handle; Pinetime::System::SystemTask& systemTask; Ble& bleController; DateTime& dateTimeController; @@ -107,6 +117,7 @@ namespace Pinetime { MotionService motionService; FSService fsService; ServiceDiscovery serviceDiscovery; + IntrusionLog intrusionLogger; uint8_t addrType; uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index 9133d3fea1..2e7fcd9a55 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -367,7 +367,7 @@ namespace Pinetime { WeatherFormat weatherFormat = WeatherFormat::Metric; Notification notificationStatus = Notification::On; - Pinetime::Applications::WatchFace watchFace = Pinetime::Applications::WatchFace::Digital; + Pinetime::Applications::WatchFace watchFace = Pinetime::Applications::WatchFace::NetRunner; ChimesOption chimesOption = ChimesOption::None; PineTimeStyle PTS; @@ -382,7 +382,7 @@ namespace Pinetime { Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; bool dfuAndFsEnabledOnBoot = false; - uint16_t heartRateBackgroundPeriod = std::numeric_limits::max(); // Disabled by default + uint16_t heartRateBackgroundPeriod = 5 * 60; // Measure every 5 minutes by default }; SettingsData settings; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 84fa603622..043c78dbd8 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -611,7 +611,7 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio currentScreen = std::make_unique(settingsController); break; case Apps::SettingHeartRate: - currentScreen = std::make_unique(settingsController); + currentScreen = std::make_unique(settingsController, heartRateController); break; case Apps::SettingDisplay: currentScreen = std::make_unique(settingsController); diff --git a/src/displayapp/InfiniTimeTheme.cpp b/src/displayapp/InfiniTimeTheme.cpp index 6795647e1b..0247923745 100644 --- a/src/displayapp/InfiniTimeTheme.cpp +++ b/src/displayapp/InfiniTimeTheme.cpp @@ -59,7 +59,7 @@ static void basic_init() { style_init_reset(&style_box); lv_style_set_bg_opa(&style_box, LV_STATE_DEFAULT, LV_OPA_COVER); - lv_style_set_radius(&style_box, LV_STATE_DEFAULT, 10); + lv_style_set_radius(&style_box, LV_STATE_DEFAULT, 3); lv_style_set_value_font(&style_box, LV_STATE_DEFAULT, theme.font_normal); style_init_reset(&style_label_white); @@ -67,7 +67,12 @@ static void basic_init() { lv_style_set_text_color(&style_label_white, LV_STATE_DISABLED, LV_COLOR_GRAY); style_init_reset(&style_btn); - lv_style_set_radius(&style_btn, LV_STATE_DEFAULT, 10); + lv_style_set_radius(&style_btn, LV_STATE_DEFAULT, 3); + lv_style_set_border_width(&style_btn, LV_STATE_DEFAULT, 2); + lv_style_set_border_color(&style_btn, LV_STATE_DEFAULT, Colors::neonCyan); + lv_style_set_border_color(&style_btn, LV_STATE_CHECKED, Colors::neonMagenta); + lv_style_set_border_color(&style_btn, LV_STATE_PRESSED, Colors::neonYellow); + lv_style_set_border_color(&style_btn, LV_STATE_DISABLED, Colors::bgAlt); lv_style_set_bg_opa(&style_btn, LV_STATE_DEFAULT, LV_OPA_COVER); lv_style_set_bg_color(&style_btn, LV_STATE_DEFAULT, Colors::bg); lv_style_set_bg_color(&style_btn, LV_STATE_CHECKED, Colors::highlight); @@ -89,7 +94,7 @@ static void basic_init() { style_init_reset(&style_scrollbar); lv_style_set_bg_opa(&style_scrollbar, LV_STATE_DEFAULT, LV_OPA_COVER); lv_style_set_radius(&style_scrollbar, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); - lv_style_set_bg_color(&style_scrollbar, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_bg_color(&style_scrollbar, LV_STATE_DEFAULT, Colors::neonCyan); lv_style_set_size(&style_scrollbar, LV_STATE_DEFAULT, LV_HOR_RES / 80); lv_style_set_pad_right(&style_scrollbar, LV_STATE_DEFAULT, LV_HOR_RES / 60); @@ -135,15 +140,15 @@ static void basic_init() { style_init_reset(&style_slider_knob); lv_style_set_bg_opa(&style_slider_knob, LV_STATE_DEFAULT, LV_OPA_COVER); - lv_style_set_bg_color(&style_slider_knob, LV_STATE_DEFAULT, LV_COLOR_RED); - lv_style_set_border_color(&style_slider_knob, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_bg_color(&style_slider_knob, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_style_set_border_color(&style_slider_knob, LV_STATE_DEFAULT, Colors::neonCyan); lv_style_set_border_width(&style_slider_knob, LV_STATE_DEFAULT, 6); lv_style_set_radius(&style_slider_knob, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); lv_style_set_pad_all(&style_slider_knob, LV_STATE_DEFAULT, 10); lv_style_set_pad_all(&style_slider_knob, LV_STATE_PRESSED, 14); style_init_reset(&style_arc_indic); - lv_style_set_line_color(&style_arc_indic, LV_STATE_DEFAULT, Colors::lightGray); + lv_style_set_line_color(&style_arc_indic, LV_STATE_DEFAULT, Colors::neonCyan); lv_style_set_line_width(&style_arc_indic, LV_STATE_DEFAULT, LV_DPX(25)); lv_style_set_line_rounded(&style_arc_indic, LV_STATE_DEFAULT, true); @@ -156,11 +161,11 @@ static void basic_init() { lv_style_reset(&style_arc_knob); lv_style_set_radius(&style_arc_knob, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); lv_style_set_bg_opa(&style_arc_knob, LV_STATE_DEFAULT, LV_OPA_COVER); - lv_style_set_bg_color(&style_arc_knob, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_bg_color(&style_arc_knob, LV_STATE_DEFAULT, Colors::neonMagenta); lv_style_set_pad_all(&style_arc_knob, LV_STATE_DEFAULT, LV_DPX(5)); style_init_reset(&style_table_cell); - lv_style_set_border_color(&style_table_cell, LV_STATE_DEFAULT, LV_COLOR_GRAY); + lv_style_set_border_color(&style_table_cell, LV_STATE_DEFAULT, Colors::dimCyan); lv_style_set_border_width(&style_table_cell, LV_STATE_DEFAULT, 1); lv_style_set_border_side(&style_table_cell, LV_STATE_DEFAULT, LV_BORDER_SIDE_FULL); lv_style_set_pad_left(&style_table_cell, LV_STATE_DEFAULT, 5); @@ -188,7 +193,7 @@ static void basic_init() { lv_style_set_scale_end_line_width(&style_lmeter, LV_STATE_DEFAULT, LV_DPX(7)); style_init_reset(&style_chart_serie); - lv_style_set_line_color(&style_chart_serie, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_style_set_line_color(&style_chart_serie, LV_STATE_DEFAULT, Colors::neonCyan); lv_style_set_line_width(&style_chart_serie, LV_STATE_DEFAULT, 4); lv_style_set_size(&style_chart_serie, LV_STATE_DEFAULT, 4); lv_style_set_bg_opa(&style_chart_serie, LV_STATE_DEFAULT, 0); diff --git a/src/displayapp/InfiniTimeTheme.h b/src/displayapp/InfiniTimeTheme.h index 57680e8738..493a326f37 100644 --- a/src/displayapp/InfiniTimeTheme.h +++ b/src/displayapp/InfiniTimeTheme.h @@ -5,15 +5,23 @@ namespace Colors { static constexpr lv_color_t deepOrange = LV_COLOR_MAKE(0xff, 0x40, 0x0); static constexpr lv_color_t orange = LV_COLOR_MAKE(0xff, 0xb0, 0x0); - static constexpr lv_color_t green = LV_COLOR_MAKE(0x0, 0xb0, 0x0); - static constexpr lv_color_t blue = LV_COLOR_MAKE(0x0, 0x50, 0xff); + static constexpr lv_color_t green = LV_COLOR_MAKE(0x0, 0xd2, 0x6a); + static constexpr lv_color_t blue = LV_COLOR_MAKE(0x7a, 0x2c, 0xff); static constexpr lv_color_t lightGray = LV_COLOR_MAKE(0xb0, 0xb0, 0xb0); static constexpr lv_color_t gray = LV_COLOR_MAKE(0x50, 0x50, 0x50); - static constexpr lv_color_t bg = LV_COLOR_MAKE(0x5d, 0x69, 0x7e); - static constexpr lv_color_t bgAlt = LV_COLOR_MAKE(0x38, 0x38, 0x38); - static constexpr lv_color_t bgDark = LV_COLOR_MAKE(0x18, 0x18, 0x18); - static constexpr lv_color_t highlight = green; + // Cyberpunk neon palette + static constexpr lv_color_t neonCyan = LV_COLOR_MAKE(0x05, 0xd9, 0xe8); + static constexpr lv_color_t neonMagenta = LV_COLOR_MAKE(0xff, 0x2a, 0x6d); + static constexpr lv_color_t neonYellow = LV_COLOR_MAKE(0xfc, 0xee, 0x0a); + static constexpr lv_color_t neonGreen = LV_COLOR_MAKE(0x39, 0xff, 0x14); + static constexpr lv_color_t neonPurple = LV_COLOR_MAKE(0x9d, 0x4e, 0xdd); + static constexpr lv_color_t dimCyan = LV_COLOR_MAKE(0x0a, 0x4a, 0x55); + + static constexpr lv_color_t bg = LV_COLOR_MAKE(0x2d, 0x1b, 0x4e); + static constexpr lv_color_t bgAlt = LV_COLOR_MAKE(0x1a, 0x10, 0x2e); + static constexpr lv_color_t bgDark = LV_COLOR_MAKE(0x0c, 0x07, 0x14); + static constexpr lv_color_t highlight = neonMagenta; }; /** diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..0b3240aebb 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -4,6 +4,9 @@ #include "displayapp/screens/Alarm.h" #include "displayapp/screens/Dice.h" +#include "displayapp/screens/Matrix.h" +#include "displayapp/screens/Badge.h" +#include "displayapp/screens/IntrusionLog.h" #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" @@ -15,6 +18,7 @@ #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/WatchFacePrideFlag.h" +#include "displayapp/screens/WatchFaceNetRunner.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index d440b598d1..44431f95bd 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -30,6 +30,9 @@ namespace Pinetime { Steps, Dice, Weather, + Matrix, + Badge, + IntrusionLog, PassKey, QuickSettings, Settings, @@ -56,6 +59,7 @@ namespace Pinetime { Infineat, CasioStyleG7710, PrideFlag, + NetRunner, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..8d6fbdeaf9 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -1,20 +1,24 @@ if(DEFINED ENABLE_USERAPPS) set(USERAPP_TYPES ${ENABLE_USERAPPS} CACHE STRING "List of user apps to build into the firmware") else () - set(DEFAULT_USER_APP_TYPES "Apps::StopWatch") + set(DEFAULT_USER_APP_TYPES "Apps::Matrix") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Badge") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::IntrusionLog") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::StopWatch") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Alarm") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRate") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Music") - set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint") - set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle") - set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Twos") - set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") - set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather") + # Trimmed from the cyberpunk build, re-enable by uncommenting + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint") + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle") + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Twos") + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion") set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") endif () @@ -22,13 +26,15 @@ endif () if(DEFINED ENABLE_WATCHFACES) set(WATCHFACE_TYPES ${ENABLE_WATCHFACES} CACHE STRING "List of watch faces to build into the firmware") else() - set(DEFAULT_WATCHFACE_TYPES "WatchFace::Digital") - set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Analog") - set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PineTimeStyle") + set(DEFAULT_WATCHFACE_TYPES "WatchFace::NetRunner") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") - set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") - set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") - set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Digital") + # Trimmed from the cyberpunk build, re-enable by uncommenting + #set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Analog") + #set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PineTimeStyle") + #set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") + #set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + #set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 3223b76b56..f1837995c9 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -7,7 +7,7 @@ }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed" + "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed, 0xf120, 0xf54c" } ], "bpp": 2, diff --git a/src/displayapp/screens/Badge.cpp b/src/displayapp/screens/Badge.cpp new file mode 100644 index 0000000000..92503c882a --- /dev/null +++ b/src/displayapp/screens/Badge.cpp @@ -0,0 +1,41 @@ +#include "displayapp/screens/Badge.h" + +#include +#include "Identity.h" +#include "displayapp/InfiniTimeTheme.h" +#include "displayapp/screens/BadgeQr.h" + +using namespace Pinetime::Applications::Screens; + +Badge::Badge(System::SystemTask& systemTask) : wakeLock(systemTask) { + // Keep the screen on while someone is scanning the code + wakeLock.Lock(); + + lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_label_set_text_fmt(title, "// %s", Pinetime::Identity::handle); + lv_obj_align(title, nullptr, LV_ALIGN_IN_TOP_MID, 0, 4); + + const lv_coord_t qrSize = BadgeQr::image.header.w; + + lv_obj_t* frame = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_size(frame, qrSize + 8, qrSize + 8); + lv_obj_align(frame, nullptr, LV_ALIGN_CENTER, 0, 2); + lv_obj_set_style_local_bg_opa(frame, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_radius(frame, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_border_width(frame, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 2); + lv_obj_set_style_local_border_color(frame, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + + lv_obj_t* qr = lv_img_create(lv_scr_act(), nullptr); + lv_img_set_src(qr, &BadgeQr::image); + lv_obj_align(qr, frame, LV_ALIGN_CENTER, 0, 0); + + lv_obj_t* caption = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(caption, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonCyan); + lv_label_set_text_static(caption, BadgeQr::caption); + lv_obj_align(caption, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -4); +} + +Badge::~Badge() { + lv_obj_clean(lv_scr_act()); +} diff --git a/src/displayapp/screens/Badge.h b/src/displayapp/screens/Badge.h new file mode 100644 index 0000000000..024c1d6c23 --- /dev/null +++ b/src/displayapp/screens/Badge.h @@ -0,0 +1,38 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" +#include "Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class Badge : public Screen { + public: + explicit Badge(System::SystemTask& systemTask); + ~Badge() override; + + private: + System::WakeLock wakeLock; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Badge; + static constexpr const char* icon = Screens::Symbols::skull; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Badge(*controllers.systemTask); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} diff --git a/src/displayapp/screens/BadgeQr.h b/src/displayapp/screens/BadgeQr.h new file mode 100644 index 0000000000..0c11f8fc98 --- /dev/null +++ b/src/displayapp/screens/BadgeQr.h @@ -0,0 +1,191 @@ +// Generated by tools/badge/generate_badge_qr.py, do not edit by hand +// URL: https://github.com/CypherNova1337 +#pragma once + +#include + +namespace Pinetime { + namespace Applications { + namespace Screens { + namespace BadgeQr { + static constexpr const char* url = "https://github.com/CypherNova1337"; + static constexpr const char* caption = "gh/CypherNova1337"; + + static const uint8_t map[] = { + 0xe8, 0xd9, 0x05, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xe0, 0x07, 0xfe, 0x0f, + 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xe0, 0x07, 0xfe, 0x0f, + 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xe0, 0x07, 0xfe, 0x0f, + 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xe0, 0x07, 0xfe, 0x0f, + 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xff, 0xe0, 0x07, 0xfe, 0x0f, + 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, + 0x80, 0x00, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, + 0x80, 0x00, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, + 0x80, 0x00, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, + 0x80, 0x00, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, + 0x80, 0x00, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7c, 0x1f, 0x07, 0xc0, 0x0f, + 0x80, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7c, 0x1f, 0x07, 0xc0, 0x0f, + 0x80, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7c, 0x1f, 0x07, 0xc0, 0x0f, + 0x80, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7c, 0x1f, 0x07, 0xc0, 0x0f, + 0x80, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7c, 0x1f, 0x07, 0xc0, 0x0f, + 0x80, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7f, 0xe0, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7f, 0xe0, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7f, 0xe0, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7f, 0xe0, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x3e, 0x00, 0x7f, 0xe0, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0x83, 0xe0, 0x07, 0xfe, 0x0f, + 0xff, 0xe0, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0x83, 0xe0, 0x07, 0xfe, 0x0f, + 0xff, 0xe0, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0x83, 0xe0, 0x07, 0xfe, 0x0f, + 0xff, 0xe0, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0x83, 0xe0, 0x07, 0xfe, 0x0f, + 0xff, 0xe0, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0x83, 0xe0, 0x07, 0xfe, 0x0f, + 0xff, 0xe0, 0xf8, 0x3f, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x0f, 0x80, 0x00, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x0f, 0x80, 0x00, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x0f, 0x80, 0x00, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x0f, 0x80, 0x00, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x0f, 0x80, 0x00, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0xf8, 0x3e, 0x0f, + 0x83, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0xf8, 0x3e, 0x0f, + 0x83, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0xf8, 0x3e, 0x0f, + 0x83, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0xf8, 0x3e, 0x0f, + 0x83, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0xf8, 0x3e, 0x0f, + 0x83, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x00, 0xff, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xf0, + 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xf0, + 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xf0, + 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xf0, + 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xf0, + 0x7f, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0x00, 0x3e, 0x00, + 0x7c, 0x1f, 0x07, 0xfe, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0x00, 0x3e, 0x00, + 0x7c, 0x1f, 0x07, 0xfe, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0x00, 0x3e, 0x00, + 0x7c, 0x1f, 0x07, 0xfe, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0x00, 0x3e, 0x00, + 0x7c, 0x1f, 0x07, 0xfe, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0x00, 0x3e, 0x00, + 0x7c, 0x1f, 0x07, 0xfe, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x7c, 0x00, 0xf8, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0x83, 0xe0, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x7c, 0x00, 0xf8, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0x83, 0xe0, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x7c, 0x00, 0xf8, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0x83, 0xe0, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x7c, 0x00, 0xf8, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0x83, 0xe0, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x7c, 0x00, 0xf8, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0x83, 0xe0, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x0f, + 0xfc, 0x1f, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x0f, + 0xfc, 0x1f, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x0f, + 0xfc, 0x1f, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x0f, + 0xfc, 0x1f, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x00, 0x00, 0x0f, + 0xfc, 0x1f, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xff, 0xc0, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xff, 0xc0, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xff, 0xc0, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xff, 0xc0, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xff, 0xc0, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x80, 0x1f, 0x07, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x01, 0xff, + 0x83, 0xe0, 0xff, 0xfe, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x80, 0x1f, 0x07, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x01, 0xff, + 0x83, 0xe0, 0xff, 0xfe, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x80, 0x1f, 0x07, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x01, 0xff, + 0x83, 0xe0, 0xff, 0xfe, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x80, 0x1f, 0x07, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x01, 0xff, + 0x83, 0xe0, 0xff, 0xfe, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x80, 0x1f, 0x07, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x01, 0xff, + 0x83, 0xe0, 0xff, 0xfe, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x0f, 0x83, 0xe0, 0x07, 0xff, 0xf0, + 0x03, 0xff, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x0f, 0x83, 0xe0, 0x07, 0xff, 0xf0, + 0x03, 0xff, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x0f, 0x83, 0xe0, 0x07, 0xff, 0xf0, + 0x03, 0xff, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x0f, 0x83, 0xe0, 0x07, 0xff, 0xf0, + 0x03, 0xff, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x0f, 0x83, 0xe0, 0x07, 0xff, 0xf0, + 0x03, 0xff, 0x00, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0xc1, 0xff, 0x83, 0xe0, 0x07, 0xc0, 0x0f, + 0xfc, 0x00, 0x07, 0xfe, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0xc1, 0xff, 0x83, 0xe0, 0x07, 0xc0, 0x0f, + 0xfc, 0x00, 0x07, 0xfe, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0xc1, 0xff, 0x83, 0xe0, 0x07, 0xc0, 0x0f, + 0xfc, 0x00, 0x07, 0xfe, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0xc1, 0xff, 0x83, 0xe0, 0x07, 0xc0, 0x0f, + 0xfc, 0x00, 0x07, 0xfe, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x03, 0xe0, 0x07, 0xc1, 0xff, 0x83, 0xe0, 0x07, 0xc0, 0x0f, + 0xfc, 0x00, 0x07, 0xfe, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xf8, 0x3e, 0x0f, 0x80, 0x00, 0x07, 0xff, 0xf0, + 0x7c, 0x1f, 0x07, 0xc1, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xf8, 0x3e, 0x0f, 0x80, 0x00, 0x07, 0xff, 0xf0, + 0x7c, 0x1f, 0x07, 0xc1, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xf8, 0x3e, 0x0f, 0x80, 0x00, 0x07, 0xff, 0xf0, + 0x7c, 0x1f, 0x07, 0xc1, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xf8, 0x3e, 0x0f, 0x80, 0x00, 0x07, 0xff, 0xf0, + 0x7c, 0x1f, 0x07, 0xc1, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xe0, 0xf8, 0x3e, 0x0f, 0x80, 0x00, 0x07, 0xff, 0xf0, + 0x7c, 0x1f, 0x07, 0xc1, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x1f, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x1f, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x1f, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x1f, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xfc, 0x1f, 0x00, 0x00, 0x00, 0x7c, 0x1f, 0xff, 0xc1, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x7f, 0xe0, 0x07, 0xc0, 0x00, + 0x7c, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x7f, 0xe0, 0x07, 0xc0, 0x00, + 0x7c, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x7f, 0xe0, 0x07, 0xc0, 0x00, + 0x7c, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x7f, 0xe0, 0x07, 0xc0, 0x00, + 0x7c, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x7f, 0xe0, 0x07, 0xc0, 0x00, + 0x7c, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x01, 0xff, 0xff, 0xff, 0x07, 0xc0, 0x00, + 0x03, 0xe0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xfc, 0x1f, 0xf8, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xff, 0xf0, + 0x7f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xfc, 0x1f, 0xf8, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xff, 0xf0, + 0x7f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xfc, 0x1f, 0xf8, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xff, 0xf0, + 0x7f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xfc, 0x1f, 0xf8, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xff, 0xf0, + 0x7f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xfc, 0x1f, 0xf8, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xff, 0xf0, + 0x7f, 0xff, 0xff, 0xfe, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x00, + 0x03, 0xe0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x00, + 0x03, 0xe0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x00, + 0x03, 0xe0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x00, + 0x03, 0xe0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, 0x03, 0xe0, 0xff, 0xfe, 0x00, + 0x03, 0xe0, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xf8, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xf8, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xf8, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xf8, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xff, 0xf0, + 0x7f, 0xe0, 0xf8, 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x0f, + 0xff, 0xe0, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x0f, + 0xff, 0xe0, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x0f, + 0xff, 0xe0, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x0f, + 0xff, 0xe0, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0x00, 0xff, 0xc0, 0x0f, + 0xff, 0xe0, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0xfc, 0x1f, 0xff, 0xc0, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0xfc, 0x1f, 0xff, 0xc0, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0xfc, 0x1f, 0xff, 0xc0, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0xfc, 0x1f, 0xff, 0xc0, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0xfc, 0x1f, 0xff, 0xc0, 0x00, + 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0x83, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0x00, 0x01, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0x83, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0x00, 0x01, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0x83, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0x00, 0x01, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0x83, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0x00, 0x01, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x00, 0x0f, 0x83, 0xff, 0xff, 0xc0, 0x0f, + 0xff, 0xff, 0x00, 0x01, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xfe, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xfe, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xfe, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xfe, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0xff, 0xe0, 0xf8, 0x01, 0xff, 0xfc, 0x00, 0x07, 0xfe, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3e, 0x00, + 0x00, 0x00, 0xff, 0xc1, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3e, 0x00, + 0x00, 0x00, 0xff, 0xc1, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3e, 0x00, + 0x00, 0x00, 0xff, 0xc1, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3e, 0x00, + 0x00, 0x00, 0xff, 0xc1, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xf8, 0x3e, 0x00, + 0x00, 0x00, 0xff, 0xc1, 0xff, 0x83, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, + 0xfc, 0x00, 0xff, 0xfe, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + static const lv_img_dsc_t image = { + {LV_IMG_CF_INDEXED_1BIT, 0, 0, 165, 165}, + 3473, + map, + }; + } + } + } +} diff --git a/src/displayapp/screens/IntrusionLog.cpp b/src/displayapp/screens/IntrusionLog.cpp new file mode 100644 index 0000000000..8910976bd4 --- /dev/null +++ b/src/displayapp/screens/IntrusionLog.cpp @@ -0,0 +1,164 @@ +#include "displayapp/screens/IntrusionLog.h" + +#include +#include +#include "displayapp/InfiniTimeTheme.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + void ButtonEventHandler(lv_obj_t* object, lv_event_t event) { + auto* screen = static_cast(object->user_data); + screen->OnButtonEvent(object, event); + } + + lv_obj_t* CreateButton(lv_obj_t** label, const char* text, void* userData) { + lv_obj_t* button = lv_btn_create(lv_scr_act(), nullptr); + button->user_data = userData; + lv_obj_set_event_cb(button, ButtonEventHandler); + lv_obj_set_style_local_pad_all(button, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 0); + *label = lv_label_create(button, nullptr); + lv_label_set_text_static(*label, text); + return button; + } +} + +IntrusionLog::IntrusionLog(Controllers::IntrusionLog& log) : log {log} { + // The display task keeps the SPI flash awake, so this is a safe moment to persist pending changes + if (log.IsDirty()) { + log.Save(); + } + + lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_label_set_text_static(title, "// INTRUSIONS"); + lv_obj_set_pos(title, 4, 2); + + labelPage = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelPage, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonPurple); + + buttonToggle = CreateButton(&labelToggle, "", this); + lv_obj_set_size(buttonToggle, 148, 40); + lv_obj_set_pos(buttonToggle, 2, 30); + + lv_obj_t* labelWipe; + buttonWipe = CreateButton(&labelWipe, "WIPE", this); + lv_obj_set_size(buttonWipe, 82, 40); + lv_obj_set_pos(buttonWipe, 156, 30); + + labelEmpty = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(labelEmpty, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); + lv_label_set_long_mode(labelEmpty, LV_LABEL_LONG_BREAK); + lv_obj_set_width(labelEmpty, 232); + lv_label_set_text_static(labelEmpty, "No intrusions\nlogged.\n\nPair your phone\nwith the PIN to\nenable alerts."); + lv_obj_set_pos(labelEmpty, 4, 84); + + for (uint8_t i = 0; i < entriesPerPage; i++) { + const lv_coord_t y = 80 + i * 80; + labelStatus[i] = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_pos(labelStatus[i], 4, y); + labelDetails[i] = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(labelDetails[i], true); + lv_obj_set_pos(labelDetails[i], 4, y + 24); + } + + UpdateToggle(); + ShowPage(); +} + +IntrusionLog::~IntrusionLog() { + if (log.IsDirty()) { + log.Save(); + } + lv_obj_clean(lv_scr_act()); +} + +uint8_t IntrusionLog::PageCount() const { + const size_t count = log.Count(); + return count == 0 ? 1 : (count + entriesPerPage - 1) / entriesPerPage; +} + +void IntrusionLog::UpdateToggle() { + const bool enabled = log.IsEnabled(); + lv_label_set_text_static(labelToggle, enabled ? "ALERTS ON" : "ALERTS OFF"); + lv_obj_set_style_local_text_color(labelToggle, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, enabled ? Colors::neonCyan : Colors::gray); + lv_obj_set_style_local_border_color(buttonToggle, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, enabled ? Colors::neonCyan : Colors::gray); +} + +void IntrusionLog::ShowPage() { + lv_label_set_text_fmt(labelPage, "%d/%d", page + 1, PageCount()); + lv_obj_align(labelPage, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 2); + lv_obj_set_hidden(labelEmpty, log.Count() != 0); + + for (uint8_t i = 0; i < entriesPerPage; i++) { + const size_t index = page * entriesPerPage + i; + const bool visible = index < log.Count(); + lv_obj_set_hidden(labelStatus[i], !visible); + lv_obj_set_hidden(labelDetails[i], !visible); + if (!visible) { + continue; + } + + const auto entry = log.Get(index); + const bool alerted = (entry.flags & Controllers::IntrusionLog::Alerted) != 0; + lv_obj_set_style_local_text_color(labelStatus[i], + LV_LABEL_PART_MAIN, + LV_STATE_DEFAULT, + alerted ? Colors::neonMagenta : Colors::lightGray); + + char duration[8]; + if (entry.duration == 0) { + snprintf(duration, sizeof(duration), "LIVE"); + } else if (entry.duration < 60) { + snprintf(duration, sizeof(duration), "%ds", entry.duration); + } else if (entry.duration < 3600) { + snprintf(duration, sizeof(duration), "%dm", entry.duration / 60); + } else { + snprintf(duration, sizeof(duration), "%dh", entry.duration / 3600); + } + lv_label_set_text_fmt(labelStatus[i], "%-13s%5s", Controllers::IntrusionLog::Describe(entry), duration); + + const std::chrono::sys_seconds time {std::chrono::seconds {entry.time}}; + const auto days = std::chrono::floor(time); + const std::chrono::year_month_day date {days}; + const std::chrono::hh_mm_ss clock {time - days}; + + char address[18]; + Controllers::IntrusionLog::FormatAddress(entry, address); + lv_label_set_text_fmt(labelDetails[i], + "#fcee0a %02u-%02u %02d:%02d#\n#05d9e8 %s#", + static_cast(date.month()), + static_cast(date.day()), + static_cast(clock.hours().count()), + static_cast(clock.minutes().count()), + address); + } +} + +void IntrusionLog::OnButtonEvent(lv_obj_t* object, lv_event_t event) { + if (event != LV_EVENT_CLICKED) { + return; + } + if (object == buttonToggle) { + log.SetEnabled(!log.IsEnabled()); + UpdateToggle(); + } else if (object == buttonWipe) { + log.Clear(); + page = 0; + ShowPage(); + } +} + +bool IntrusionLog::OnTouchEvent(TouchEvents event) { + if (event == TouchEvents::SwipeUp && page + 1 < PageCount()) { + page++; + ShowPage(); + return true; + } + if (event == TouchEvents::SwipeDown && page > 0) { + page--; + ShowPage(); + return true; + } + return false; +} diff --git a/src/displayapp/screens/IntrusionLog.h b/src/displayapp/screens/IntrusionLog.h new file mode 100644 index 0000000000..f8ecdba18f --- /dev/null +++ b/src/displayapp/screens/IntrusionLog.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/ble/IntrusionLog.h" +#include "systemtask/SystemTask.h" +#include "Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class IntrusionLog : public Screen { + public: + explicit IntrusionLog(Controllers::IntrusionLog& log); + ~IntrusionLog() override; + + bool OnTouchEvent(TouchEvents event) override; + void OnButtonEvent(lv_obj_t* object, lv_event_t event); + + private: + static constexpr uint8_t entriesPerPage = 2; + + void UpdateToggle(); + void ShowPage(); + uint8_t PageCount() const; + + Controllers::IntrusionLog& log; + uint8_t page = 0; + + lv_obj_t* labelPage; + lv_obj_t* buttonToggle; + lv_obj_t* labelToggle; + lv_obj_t* buttonWipe; + lv_obj_t* labelEmpty; + std::array labelStatus; + std::array labelDetails; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::IntrusionLog; + static constexpr const char* icon = Screens::Symbols::shieldAlt; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::IntrusionLog(controllers.systemTask->nimble().intrusionLog()); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} diff --git a/src/displayapp/screens/List.cpp b/src/displayapp/screens/List.cpp index 264b4fc98b..029bffb403 100644 --- a/src/displayapp/screens/List.cpp +++ b/src/displayapp/screens/List.cpp @@ -44,8 +44,11 @@ List::List(uint8_t screenID, static constexpr int btnHeight = (LV_HOR_RES_MAX - ((MAXLISTITEMS - 1) * innerPad)) / MAXLISTITEMS; itemApps[i] = lv_btn_create(container, nullptr); - lv_obj_set_style_local_radius(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, btnHeight / 3); + lv_obj_set_style_local_radius(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 3); lv_obj_set_style_local_bg_color(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt); + lv_obj_set_style_local_border_side(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_BORDER_SIDE_LEFT); + lv_obj_set_style_local_border_width(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 4); + lv_obj_set_style_local_border_color(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); lv_obj_set_width(itemApps[i], LV_HOR_RES - 8); lv_obj_set_height(itemApps[i], btnHeight); lv_obj_set_event_cb(itemApps[i], ButtonEventHandler); @@ -54,7 +57,7 @@ List::List(uint8_t screenID, lv_obj_set_style_local_clip_corner(itemApps[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, true); lv_obj_t* icon = lv_label_create(itemApps[i], nullptr); - lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW); + lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonCyan); lv_label_set_text_static(icon, applications[i].icon); lv_label_set_long_mode(icon, LV_LABEL_LONG_CROP); lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER); diff --git a/src/displayapp/screens/Matrix.cpp b/src/displayapp/screens/Matrix.cpp new file mode 100644 index 0000000000..7a9a972d75 --- /dev/null +++ b/src/displayapp/screens/Matrix.cpp @@ -0,0 +1,142 @@ +#include "displayapp/screens/Matrix.h" + +#include +#include + +using namespace Pinetime::Applications::Screens; + +namespace { + // Printable ASCII plus the Cyrillic block, both of which exist in jetbrains_mono_bold_20. + // '#' is excluded because it is the recolor escape character. + constexpr char asciiGlyphs[] = "0123456789ABCDEFXZ$%&*+<>=?@"; + + char* AppendGlyph(char* out, uint16_t glyph) { + if (glyph < 0x80) { + *out++ = static_cast(glyph); + } else { + *out++ = static_cast(0xC0 | (glyph >> 6)); + *out++ = static_cast(0x80 | (glyph & 0x3F)); + } + return out; + } + + char* AppendColor(char* out, const char* color) { + *out++ = '#'; + for (uint8_t i = 0; i < 6; i++) { + *out++ = color[i]; + } + *out++ = ' '; + return out; + } +} + +Matrix::Matrix(Controllers::DateTime& dateTimeController) { + rngState = static_cast(dateTimeController.CurrentDateTime().time_since_epoch().count()) | 1; + + lv_obj_set_style_local_bg_color(lv_scr_act(), LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + + for (uint8_t i = 0; i < nbColumns; i++) { + Column& column = columns[i]; + ResetColumn(column, true); + for (auto& glyph : column.glyphs) { + glyph = RandomGlyph(); + } + column.label = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(column.label, true); + lv_obj_set_style_local_text_line_space(column.label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_pos(column.label, i * columnWidth, 0); + RenderColumn(column); + } + + taskRefresh = lv_task_create(RefreshTaskCallback, 60, LV_TASK_PRIO_MID, this); +} + +Matrix::~Matrix() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +uint32_t Matrix::NextRandom() { + // xorshift32 + rngState ^= rngState << 13; + rngState ^= rngState >> 17; + rngState ^= rngState << 5; + return rngState; +} + +uint16_t Matrix::RandomGlyph() { + const uint32_t r = NextRandom(); + if (r % 3 == 0) { + return static_cast(asciiGlyphs[(r >> 2) % (sizeof(asciiGlyphs) - 1)]); + } + return 0x410 + ((r >> 2) % 0x40); +} + +void Matrix::ResetColumn(Column& column, bool initial) { + const uint32_t r = NextRandom(); + column.trail = 4 + r % 7; + column.speed = 1 + (r >> 4) % 3; + // Start above the screen so drops enter at staggered times + column.head = initial ? -static_cast((r >> 8) % 16) : -static_cast((r >> 8) % 8); +} + +void Matrix::RenderColumn(Column& column) { + const Palette& palette = palettes[paletteIndex]; + char* out = column.text.data(); + for (int8_t row = 0; row < nbRows; row++) { + const int8_t distance = column.head - row; + if (distance < 0 || distance >= column.trail) { + *out++ = ' '; + } else { + const char* color = distance == 0 ? palette.head : (distance < 3 ? palette.bright : palette.dim); + out = AppendColor(out, color); + out = AppendGlyph(out, column.glyphs[row]); + *out++ = '#'; + } + if (row < nbRows - 1) { + *out++ = '\n'; + } + } + *out = '\0'; + lv_label_set_text_static(column.label, column.text.data()); +} + +void Matrix::Refresh() { + tick++; + for (auto& column : columns) { + bool changed = false; + + // Glyphs inside the trail flicker every now and then + if (NextRandom() % 4 == 0) { + column.glyphs[NextRandom() % nbRows] = RandomGlyph(); + changed = true; + } + + if (tick % column.speed == 0) { + column.head++; + if (column.head - column.trail >= nbRows) { + ResetColumn(column, false); + } + // The new head always gets a fresh glyph + if (column.head >= 0 && column.head < nbRows) { + column.glyphs[column.head] = RandomGlyph(); + } + changed = true; + } + + if (changed) { + RenderColumn(column); + } + } +} + +bool Matrix::OnTouchEvent(TouchEvents event) { + if (event == TouchEvents::Tap) { + paletteIndex = (paletteIndex + 1) % palettes.size(); + for (auto& column : columns) { + RenderColumn(column); + } + return true; + } + return false; +} diff --git a/src/displayapp/screens/Matrix.h b/src/displayapp/screens/Matrix.h new file mode 100644 index 0000000000..ce0e68cf36 --- /dev/null +++ b/src/displayapp/screens/Matrix.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/datetime/DateTimeController.h" +#include "Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class Matrix : public Screen { + public: + explicit Matrix(Controllers::DateTime& dateTimeController); + ~Matrix() override; + + void Refresh() override; + bool OnTouchEvent(TouchEvents event) override; + + private: + static constexpr uint8_t nbColumns = 20; + static constexpr uint8_t nbRows = 10; + static constexpr uint8_t columnWidth = 12; + static constexpr uint8_t rowHeight = 24; + // Worst case per row: "#rrggbb " + 2 byte glyph + "#\n" + static constexpr uint16_t bufferSize = nbRows * 12 + 1; + + struct Column { + int8_t head; + uint8_t trail; + uint8_t speed; + std::array glyphs; + std::array text; + lv_obj_t* label; + }; + + struct Palette { + const char* head; + const char* bright; + const char* dim; + }; + + static constexpr std::array palettes {{ + {"ffffff", "39ff14", "0b6b1a"}, + {"ffffff", "05d9e8", "0a4a55"}, + {"fcee0a", "ff2a6d", "5a1030"}, + }}; + + uint32_t NextRandom(); + uint16_t RandomGlyph(); + void ResetColumn(Column& column, bool initial); + void RenderColumn(Column& column); + + std::array columns; + uint32_t rngState; + uint8_t tick = 0; + uint8_t paletteIndex = 0; + lv_task_t* taskRefresh; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Matrix; + static constexpr const char* icon = Screens::Symbols::terminal; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Matrix(controllers.dateTimeController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 058b2d06e9..01c454bbe6 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -42,6 +42,8 @@ namespace Pinetime { static constexpr const char* sleep = "\xEE\xBD\x84"; static constexpr const char* calculator = "\xEF\x87\xAC"; static constexpr const char* backspace = "\xEF\x95\x9A"; + static constexpr const char* terminal = "\xEF\x84\xA0"; // 0xf120 + static constexpr const char* skull = "\xEF\x95\x8C"; // 0xf54c // fontawesome_weathericons.c // static constexpr const char* sun = "\xEF\x86\x85"; diff --git a/src/displayapp/screens/SystemInfo.cpp b/src/displayapp/screens/SystemInfo.cpp index 2392f3be3e..b6ab02862a 100644 --- a/src/displayapp/screens/SystemInfo.cpp +++ b/src/displayapp/screens/SystemInfo.cpp @@ -80,7 +80,7 @@ std::unique_ptr SystemInfo::CreateScreen1() { lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); lv_label_set_recolor(label, true); lv_label_set_text_fmt(label, - "#FFFF00 InfiniTime#\n\n" + "#ff2a6d InfiniTime#\n#05d9e8 // CYBERPUNK#\n" "#808080 Version# %ld.%ld.%ld\n" "#808080 Short Ref# %s\n" "#808080 Build date#\n" diff --git a/src/displayapp/screens/Tile.cpp b/src/displayapp/screens/Tile.cpp index 45f715b55a..8cdede5e07 100644 --- a/src/displayapp/screens/Tile.cpp +++ b/src/displayapp/screens/Tile.cpp @@ -69,7 +69,12 @@ Tile::Tile(uint8_t screenID, lv_obj_set_size(btnm1, LV_HOR_RES - 16, LV_VER_RES - 60); lv_obj_align(btnm1, nullptr, LV_ALIGN_CENTER, 0, 10); - lv_obj_set_style_local_radius(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, 20); + lv_obj_set_style_local_radius(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, 4); + lv_obj_set_style_local_text_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, Colors::neonCyan); + lv_obj_set_style_local_text_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_PRESSED, Colors::neonYellow); + lv_obj_set_style_local_border_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_obj_set_style_local_border_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_PRESSED, Colors::neonYellow); + lv_obj_set_style_local_border_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DISABLED, Colors::bgAlt); lv_obj_set_style_local_bg_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, Colors::bgAlt); lv_obj_set_style_local_bg_opa(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DISABLED, LV_OPA_50); lv_obj_set_style_local_bg_color(btnm1, LV_BTNMATRIX_PART_BTN, LV_STATE_DISABLED, Colors::bgDark); diff --git a/src/displayapp/screens/WatchFaceNetRunner.cpp b/src/displayapp/screens/WatchFaceNetRunner.cpp new file mode 100644 index 0000000000..160daf1876 --- /dev/null +++ b/src/displayapp/screens/WatchFaceNetRunner.cpp @@ -0,0 +1,302 @@ +#include "displayapp/screens/WatchFaceNetRunner.h" + +#include +#include "Identity.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" +#include "components/motion/MotionController.h" +#include "components/settings/Settings.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "displayapp/InfiniTimeTheme.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + lv_obj_t* CreateLabel(lv_obj_t* parent, lv_color_t color) { + lv_obj_t* label = lv_label_create(parent, nullptr); + lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + return label; + } + + lv_color_t PowerColor(int percent) { + if (percent > 50) { + return Colors::neonGreen; + } + if (percent > 20) { + return Colors::neonYellow; + } + return Colors::neonMagenta; + } +} + +WatchFaceNetRunner::WatchFaceNetRunner(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) + : currentDateTime {{}}, + dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + notificationManager {notificationManager}, + settingsController {settingsController}, + heartRateController {heartRateController}, + motionController {motionController}, + weatherService {weatherService} { + + // HUD frame lines + topLine = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(topLine, topLinePoints, 4); + lv_obj_set_style_local_line_color(topLine, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_obj_set_style_local_line_width(topLine, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 2); + + bottomLine = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(bottomLine, bottomLinePoints, 4); + lv_obj_set_style_local_line_color(bottomLine, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, Colors::neonCyan); + lv_obj_set_style_local_line_width(bottomLine, LV_LINE_PART_MAIN, LV_STATE_DEFAULT, 2); + + // Status row + labelLink = CreateLabel(lv_scr_act(), Colors::gray); + lv_obj_set_pos(labelLink, 4, 4); + + labelPower = CreateLabel(lv_scr_act(), Colors::neonGreen); + lv_obj_align(labelPower, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 4); + + // Date row + labelDate = CreateLabel(lv_scr_act(), Colors::neonYellow); + lv_obj_set_pos(labelDate, 2, 38); + + labelNotification = CreateLabel(lv_scr_act(), Colors::neonMagenta); + lv_label_set_text_static(labelNotification, "[MSG]"); + lv_obj_align(labelNotification, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 44); + lv_obj_set_hidden(labelNotification, true); + + // Time: a yellow ghost and a magenta shadow sit behind the cyan digits for the + // chromatic-aberration look. The ghost only shows up while glitching. + labelTimeGhost = CreateLabel(lv_scr_act(), Colors::neonYellow); + labelTimeShadow = CreateLabel(lv_scr_act(), Colors::neonMagenta); + labelTime = CreateLabel(lv_scr_act(), Colors::neonCyan); + for (lv_obj_t* label : {labelTimeGhost, labelTimeShadow, labelTime}) { + lv_obj_set_style_local_text_font(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_extrabold_compressed); + lv_obj_set_style_local_text_letter_space(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, -6); + lv_label_set_text_static(label, "00:00"); + } + lv_obj_set_hidden(labelTimeGhost, true); + + glitchSlice = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(glitchSlice, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_obj_set_style_local_radius(glitchSlice, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_border_width(glitchSlice, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + lv_obj_set_hidden(glitchSlice, true); + + // Seconds sweep + barSeconds = lv_bar_create(lv_scr_act(), nullptr); + lv_obj_set_size(barSeconds, 150, 6); + lv_obj_set_pos(barSeconds, 6, 138); + lv_bar_set_range(barSeconds, 0, 59); + lv_obj_set_style_local_bg_color(barSeconds, LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::dimCyan); + lv_obj_set_style_local_bg_opa(barSeconds, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_COVER); + lv_obj_set_style_local_radius(barSeconds, LV_BAR_PART_BG, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_bg_color(barSeconds, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::neonYellow); + lv_obj_set_style_local_radius(barSeconds, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, 0); + + labelSeconds = CreateLabel(lv_scr_act(), Colors::neonYellow); + lv_obj_set_pos(labelSeconds, 164, 129); + + // Biometrics and environment + labelVitals = CreateLabel(lv_scr_act(), LV_COLOR_WHITE); + lv_label_set_recolor(labelVitals, true); + lv_obj_set_pos(labelVitals, 4, 148); + + labelEnv = CreateLabel(lv_scr_act(), LV_COLOR_WHITE); + lv_label_set_recolor(labelEnv, true); + lv_obj_set_pos(labelEnv, 4, 170); + + // Footer + labelHandle = CreateLabel(lv_scr_act(), Colors::neonMagenta); + lv_label_set_text_static(labelHandle, Pinetime::Identity::handle); + lv_obj_set_pos(labelHandle, 4, 212); + + labelHex = CreateLabel(lv_scr_act(), Colors::neonPurple); + lv_obj_align(labelHex, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 212); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +WatchFaceNetRunner::~WatchFaceNetRunner() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +uint32_t WatchFaceNetRunner::NextRandom() { + // xorshift32 + rngState ^= rngState << 13; + rngState ^= rngState >> 17; + rngState ^= rngState << 5; + return rngState; +} + +void WatchFaceNetRunner::StartGlitch() { + glitchFrames = 4 + NextRandom() % 5; + lv_obj_set_hidden(labelTimeGhost, false); + lv_obj_set_hidden(glitchSlice, false); +} + +void WatchFaceNetRunner::UpdateGlitch() { + if (glitchFrames == 0) { + return; + } + glitchFrames--; + + const lv_coord_t timeX = (LV_HOR_RES - lv_obj_get_width(labelTime)) / 2; + if (glitchFrames == 0) { + lv_obj_set_hidden(labelTimeGhost, true); + lv_obj_set_hidden(glitchSlice, true); + lv_obj_set_pos(labelTime, timeX, timeY); + lv_obj_set_pos(labelTimeShadow, timeX + shadowOffset, timeY + shadowOffset); + return; + } + + auto jitter = [this](int range) { + return static_cast(static_cast(NextRandom() % (2 * range + 1)) - range); + }; + lv_obj_set_pos(labelTime, timeX + jitter(3), timeY); + lv_obj_set_pos(labelTimeShadow, timeX + shadowOffset + jitter(8), timeY + shadowOffset); + lv_obj_set_pos(labelTimeGhost, timeX + jitter(10), timeY + jitter(3)); + + lv_obj_set_style_local_bg_color(glitchSlice, + LV_OBJ_PART_MAIN, + LV_STATE_DEFAULT, + (NextRandom() & 1) != 0 ? Colors::neonMagenta : Colors::neonCyan); + lv_obj_set_size(glitchSlice, 40 + NextRandom() % 160, 2 + NextRandom() % 5); + lv_obj_set_pos(glitchSlice, NextRandom() % 120, timeY + NextRandom() % 60); +} + +void WatchFaceNetRunner::Refresh() { + UpdateGlitch(); + + notificationState = notificationManager.AreNewNotificationsAvailable(); + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + const uint8_t second = dateTimeController.Seconds(); + lv_bar_set_value(barSeconds, second, LV_ANIM_OFF); + + const auto epoch = std::chrono::duration_cast(dateTimeController.UTCDateTime().time_since_epoch()).count(); + lv_label_set_text_fmt(labelHex, "0x%08lX", static_cast(epoch)); + lv_obj_align(labelHex, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 212); + + // Blink the notification tag once per second + lv_obj_set_hidden(labelNotification, !notificationState.Get() || (second % 2) != 0); + + // Stir the RNG with the clock so every boot glitches differently, then roll for a glitch + rngState ^= static_cast(epoch); + if (rngState == 0) { + rngState = 0x1337c0de; + } + if (glitchFrames == 0 && NextRandom() % 7 == 0) { + StartGlitch(); + } + + currentMinute = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentMinute.IsUpdated()) { + uint8_t hour = dateTimeController.Hours(); + const uint8_t minute = dateTimeController.Minutes(); + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + amPm = hour < 12 ? "AM" : "PM"; + if (hour == 0) { + hour = 12; + } else if (hour > 12) { + hour -= 12; + } + } else { + amPm = ""; + } + + for (lv_obj_t* label : {labelTimeGhost, labelTimeShadow, labelTime}) { + lv_label_set_text_fmt(label, "%02d:%02d", hour, minute); + } + const lv_coord_t timeX = (LV_HOR_RES - lv_obj_get_width(labelTime)) / 2; + lv_obj_set_pos(labelTime, timeX, timeY); + lv_obj_set_pos(labelTimeShadow, timeX + shadowOffset, timeY + shadowOffset); + lv_obj_set_pos(labelTimeGhost, timeX, timeY); + + // A guaranteed glitch at the top of every minute + StartGlitch(); + } + + lv_label_set_text_fmt(labelSeconds, ":%02d%s", second, amPm); + + currentDate = std::chrono::time_point_cast(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + lv_label_set_text_fmt(labelDate, + "%s %04d.%02d.%02d", + dateTimeController.DayOfWeekShortToString(), + dateTimeController.Year(), + static_cast(dateTimeController.Month()), + dateTimeController.Day()); + } + } + + powerPresent = batteryController.IsPowerPresent(); + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) { + const int percent = batteryPercentRemaining.Get(); + lv_obj_set_style_local_text_color(labelPower, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, PowerColor(percent)); + if (batteryController.IsCharging()) { + lv_label_set_text_fmt(labelPower, "CHG %d%%", percent); + } else { + lv_label_set_text_fmt(labelPower, "PWR %d%%", percent); + } + lv_obj_align(labelPower, nullptr, LV_ALIGN_IN_TOP_RIGHT, -4, 4); + } + + bleState = bleController.IsConnected(); + bleRadioEnabled = bleController.IsRadioEnabled(); + if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { + if (!bleRadioEnabled.Get()) { + lv_label_set_text_static(labelLink, "LNK:OFF"); + lv_obj_set_style_local_text_color(labelLink, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::gray); + } else if (bleState.Get()) { + lv_label_set_text_static(labelLink, "LNK:UP"); + lv_obj_set_style_local_text_color(labelLink, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonCyan); + } else { + lv_label_set_text_static(labelLink, "LNK:--"); + lv_obj_set_style_local_text_color(labelLink, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); + } + } + + stepCount = motionController.NbSteps(); + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (stepCount.IsUpdated() || heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get() && heartbeat.Get() != 0) { + lv_label_set_text_fmt(labelVitals, "#ff2a6d HR# %03d #05d9e8 STP# %lu", heartbeat.Get(), stepCount.Get()); + } else { + lv_label_set_text_fmt(labelVitals, "#ff2a6d HR# --- #05d9e8 STP# %lu", stepCount.Get()); + } + } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + tempUnit = 'F'; + } + lv_label_set_text_fmt(labelEnv, "#9d4edd ENV# %d°%c %s", temp, tempUnit, Symbols::GetSimpleCondition(optCurrentWeather->iconId)); + } else { + lv_label_set_text_static(labelEnv, "#9d4edd ENV# NO SIGNAL"); + } + } +} diff --git a/src/displayapp/screens/WatchFaceNetRunner.h b/src/displayapp/screens/WatchFaceNetRunner.h new file mode 100644 index 0000000000..05a4520474 --- /dev/null +++ b/src/displayapp/screens/WatchFaceNetRunner.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "displayapp/screens/Screen.h" +#include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" +#include "utility/DirtyValue.h" + +namespace Pinetime { + namespace Controllers { + class Settings; + class Battery; + class Ble; + class NotificationManager; + class HeartRateController; + class MotionController; + } + + namespace Applications { + namespace Screens { + + class WatchFaceNetRunner : public Screen { + public: + WatchFaceNetRunner(Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::NotificationManager& notificationManager, + Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService); + ~WatchFaceNetRunner() override; + + void Refresh() override; + + private: + void UpdateGlitch(); + void StartGlitch(); + uint32_t NextRandom(); + + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue bleRadioEnabled {}; + Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue> currentMinute {}; + Utility::DirtyValue> currentDate {}; + Utility::DirtyValue stepCount {}; + Utility::DirtyValue heartbeat {}; + Utility::DirtyValue heartbeatRunning {}; + Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentWeather {}; + + static constexpr lv_coord_t timeY = 62; + static constexpr lv_coord_t shadowOffset = 3; + + lv_point_t topLinePoints[4] {{0, 30}, {170, 30}, {180, 38}, {240, 38}}; + lv_point_t bottomLinePoints[4] {{0, 204}, {98, 204}, {108, 196}, {240, 196}}; + + lv_obj_t* topLine; + lv_obj_t* bottomLine; + lv_obj_t* labelLink; + lv_obj_t* labelPower; + lv_obj_t* labelDate; + lv_obj_t* labelNotification; + lv_obj_t* labelTimeGhost; + lv_obj_t* labelTimeShadow; + lv_obj_t* labelTime; + lv_obj_t* barSeconds; + lv_obj_t* labelSeconds; + lv_obj_t* labelVitals; + lv_obj_t* labelEnv; + lv_obj_t* labelHandle; + lv_obj_t* labelHex; + lv_obj_t* glitchSlice; + + uint32_t rngState = 0x1337c0de; + uint8_t glitchFrames = 0; + const char* amPm = ""; + + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::NotificationManager& notificationManager; + Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; + Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; + + lv_task_t* taskRefresh; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::NetRunner; + static constexpr const char* name = "NetRunner"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceNetRunner(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} diff --git a/src/displayapp/screens/WatchFaceTerminal.cpp b/src/displayapp/screens/WatchFaceTerminal.cpp index 505f79500c..d2c1d99010 100644 --- a/src/displayapp/screens/WatchFaceTerminal.cpp +++ b/src/displayapp/screens/WatchFaceTerminal.cpp @@ -40,8 +40,8 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, notificationIcon = lv_label_create(container, nullptr); labelPrompt1 = lv_label_create(container, nullptr); - lv_obj_set_style_local_text_color(labelPrompt1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); - lv_label_set_text_static(labelPrompt1, "user@watch:~ $ now"); + lv_obj_set_style_local_text_color(labelPrompt1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_label_set_text_static(labelPrompt1, "root@voidsec:~# now"); labelTime = lv_label_create(container, nullptr); lv_label_set_recolor(labelTime, true); @@ -54,7 +54,7 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, stepValue = lv_label_create(container, nullptr); lv_label_set_recolor(stepValue, true); - lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); + lv_obj_set_style_local_text_color(stepValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonYellow); heartbeatValue = lv_label_create(container, nullptr); lv_label_set_recolor(heartbeatValue, true); @@ -66,8 +66,8 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, lv_label_set_recolor(connectState, true); labelPrompt2 = lv_label_create(container, nullptr); - lv_obj_set_style_local_text_color(labelPrompt2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); - lv_label_set_text_static(labelPrompt2, "user@watch:~ $"); + lv_obj_set_style_local_text_color(labelPrompt2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonMagenta); + lv_label_set_text_static(labelPrompt2, "root@voidsec:~#"); lv_obj_align(container, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 7); @@ -106,9 +106,9 @@ void WatchFaceTerminal::Refresh() { hour = hour - 12; ampmChar[0] = 'P'; } - lv_label_set_text_fmt(labelTime, "#ffffff [TIME]# #11cc55 %02d:%02d:%02d %s#", hour, minute, second, ampmChar); + lv_label_set_text_fmt(labelTime, "#ffffff [TIME]# #39ff14 %02d:%02d:%02d %s#", hour, minute, second, ampmChar); } else { - lv_label_set_text_fmt(labelTime, "#ffffff [TIME]# #11cc55 %02d:%02d:%02d#", hour, minute, second); + lv_label_set_text_fmt(labelTime, "#ffffff [TIME]# #39ff14 %02d:%02d:%02d#", hour, minute, second); } currentDate = std::chrono::time_point_cast(currentDateTime.Get()); @@ -116,7 +116,7 @@ void WatchFaceTerminal::Refresh() { uint16_t year = dateTimeController.Year(); Controllers::DateTime::Months month = dateTimeController.Month(); uint8_t day = dateTimeController.Day(); - lv_label_set_text_fmt(labelDate, "#ffffff [DATE]# #007fff %04d-%02d-%02d#", year, month, day); + lv_label_set_text_fmt(labelDate, "#ffffff [DATE]# #05d9e8 %04d-%02d-%02d#", year, month, day); } } @@ -161,12 +161,12 @@ void WatchFaceTerminal::Refresh() { tempUnit = 'F'; } lv_label_set_text_fmt(weather, - "#ffffff [WTHR]# #ffdd00 %d°%c %s#", + "#ffffff [WTHR]# #fcee0a %d°%c %s#", temp, tempUnit, Symbols::GetSimpleCondition(optCurrentWeather->iconId)); } else { - lv_label_set_text(weather, "#ffffff [WTHR]# #ffdd00 ---"); + lv_label_set_text(weather, "#ffffff [WTHR]# #fcee0a ---"); } } @@ -179,7 +179,7 @@ void WatchFaceTerminal::Refresh() { } else { if (bleState.Get()) { lv_label_set_text_static(connectState, "#ffffff [STAT]# Connected"); - lv_obj_set_style_local_text_color(connectState, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::blue); + lv_obj_set_style_local_text_color(connectState, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::neonCyan); } else { lv_label_set_text_static(connectState, "#ffffff [STAT]# Disconnected"); lv_obj_set_style_local_text_color(connectState, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::gray); diff --git a/src/displayapp/screens/settings/SettingHeartRate.cpp b/src/displayapp/screens/settings/SettingHeartRate.cpp index a45dc835ae..31596f0ebf 100644 --- a/src/displayapp/screens/settings/SettingHeartRate.cpp +++ b/src/displayapp/screens/settings/SettingHeartRate.cpp @@ -12,7 +12,9 @@ namespace { } } -SettingHeartRate::SettingHeartRate(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} { +SettingHeartRate::SettingHeartRate(Pinetime::Controllers::Settings& settingsController, + Pinetime::Controllers::HeartRateController& heartRateController) + : settingsController {settingsController}, heartRateController {heartRateController} { lv_obj_t* container = lv_cont_create(lv_scr_act(), nullptr); lv_obj_set_style_local_bg_opa(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); @@ -63,6 +65,11 @@ void SettingHeartRate::UpdateSelected(lv_obj_t* object, lv_event_t event) { if (object == cbOption[i]) { lv_checkbox_set_checked(cbOption[i], true); settingsController.SetHeartRateBackgroundMeasurementInterval(options[i].intervalInSeconds); + // Start measuring right away instead of waiting for the heart rate app to be opened + if (options[i].intervalInSeconds.has_value() && + heartRateController.State() == Pinetime::Controllers::HeartRateController::States::Stopped) { + heartRateController.Enable(); + } } else { lv_checkbox_set_checked(cbOption[i], false); } diff --git a/src/displayapp/screens/settings/SettingHeartRate.h b/src/displayapp/screens/settings/SettingHeartRate.h index 736f2b10d7..dfc10a1fc5 100644 --- a/src/displayapp/screens/settings/SettingHeartRate.h +++ b/src/displayapp/screens/settings/SettingHeartRate.h @@ -6,6 +6,7 @@ #include #include "components/settings/Settings.h" +#include "components/heartrate/HeartRateController.h" #include "displayapp/screens/Screen.h" namespace Pinetime { @@ -14,7 +15,7 @@ namespace Pinetime { namespace Screens { class SettingHeartRate : public Screen { public: - explicit SettingHeartRate(Pinetime::Controllers::Settings& settings); + SettingHeartRate(Pinetime::Controllers::Settings& settings, Pinetime::Controllers::HeartRateController& heartRateController); ~SettingHeartRate() override; void UpdateSelected(lv_obj_t* object, lv_event_t event); @@ -26,6 +27,7 @@ namespace Pinetime { }; Pinetime::Controllers::Settings& settingsController; + Pinetime::Controllers::HeartRateController& heartRateController; static constexpr std::array options = {{ {.intervalInSeconds = std::nullopt, .name = " Off"}, diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index fa352772f7..5cbe803202 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -9,6 +9,9 @@ using namespace Pinetime::Applications; namespace { constexpr TickType_t backgroundMeasurementTimeLimit = 30 * configTICK_RATE_HZ; + // When no pulse is found in the background, retry at most this often. + // Stops continuous mode from running the sensor all day while the watch is off the wrist. + constexpr TickType_t noPulseRetryInterval = 5 * 60 * configTICK_RATE_HZ; } std::optional HeartRateTask::BackgroundMeasurementInterval() const { @@ -16,7 +19,11 @@ std::optional HeartRateTask::BackgroundMeasurementInterval() const { if (!interval.has_value()) { return std::nullopt; } - return interval.value() * configTICK_RATE_HZ; + TickType_t ticks = interval.value() * configTICK_RATE_HZ; + if (noPulse && ticks < noPulseRetryInterval) { + return noPulseRetryInterval; + } + return ticks; } bool HeartRateTask::BackgroundMeasurementNeeded() const { @@ -185,6 +192,7 @@ void HeartRateTask::StartMeasurement() { measurementSucceeded = false; count = 0; measurementStartTime = xTaskGetTickCount(); + lastPulseTime = measurementStartTime; } void HeartRateTask::StopMeasurement() { @@ -235,6 +243,8 @@ void HeartRateTask::HandleSensorData() { } measurementSucceeded = true; valueCurrentlyShown = true; + lastPulseTime = xTaskGetTickCount(); + noPulse = false; controller.Update(Controllers::HeartRateController::States::Running, bpm); return; } @@ -253,6 +263,12 @@ void HeartRateTask::HandleSensorData() { valueCurrentlyShown = false; } if (state == States::BackgroundMeasuring) { + if (xTaskGetTickCount() - lastPulseTime > backgroundMeasurementTimeLimit) { + // No pulse for a while: back off until the watch is worn again + noPulse = true; + controller.Update(Controllers::HeartRateController::States::Running, 0); + valueCurrentlyShown = false; + } lastMeasurementTime = xTaskGetTickCount() - backgroundMeasurementTimeLimit; } else { lastMeasurementTime = xTaskGetTickCount(); diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h index e00bc4d638..399cab7bff 100644 --- a/src/heartratetask/HeartRateTask.h +++ b/src/heartratetask/HeartRateTask.h @@ -51,6 +51,10 @@ namespace Pinetime { Controllers::Ppg ppg; TickType_t lastMeasurementTime; TickType_t measurementStartTime; + // Time of the last valid reading (or the start of the measurement if none yet) + TickType_t lastPulseTime; + // Set when a background measurement found no pulse, e.g. the watch is off the wrist + bool noPulse = false; }; } diff --git a/src/systemtask/Messages.h b/src/systemtask/Messages.h index fee94bb747..2bf7d8df65 100644 --- a/src/systemtask/Messages.h +++ b/src/systemtask/Messages.h @@ -29,7 +29,9 @@ namespace Pinetime { BatteryPercentageUpdated, StartFileTransfer, StopFileTransfer, - BleRadioEnableToggle + BleRadioEnableToggle, + OnIntrusion, + IntrusionLogChanged }; } } diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 826474a068..2c433c7e2b 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -13,10 +13,13 @@ #include "drivers/SpiNorFlash.h" #include "drivers/TwiMaster.h" #include "drivers/Hrs3300.h" +#include "components/heartrate/HeartRateController.h" #include "drivers/PinMap.h" #include "main.h" #include "BootErrors.h" +#include +#include #include using namespace Pinetime::System; @@ -148,6 +151,9 @@ void SystemTask::Work() { heartRateSensor.Init(); heartRateSensor.Disable(); heartRateApp.Start(); + if (settingsController.GetHeartRateBackgroundMeasurementInterval().has_value()) { + heartRateController.Enable(); + } buttonHandler.Init(this); @@ -239,6 +245,8 @@ void SystemTask::Work() { displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); isBleDiscoveryTimerRunning = true; bleDiscoveryTimer = 5; + // Give a bonded phone 10 seconds to encrypt the link before calling it an intrusion + intrusionCheckTimer = 100; break; case Messages::BleFirmwareUpdateStarted: GoToRunning(); @@ -366,6 +374,12 @@ void SystemTask::Work() { GoToRunning(); displayApp.PushMessage(Pinetime::Applications::Display::Messages::ShowPairingKey); break; + case Messages::OnIntrusion: + RaiseIntrusionAlert(); + break; + case Messages::IntrusionLogChanged: + SaveIntrusionLog(); + break; case Messages::BleRadioEnableToggle: if (settingsController.GetBleRadioEnabled()) { nimbleController.EnableRadio(); @@ -390,6 +404,13 @@ void SystemTask::Work() { bleDiscoveryTimer--; } } + if (intrusionCheckTimer > 0 && --intrusionCheckTimer == 0) { + if (nimbleController.CheckIntrusion()) { + RaiseIntrusionAlert(); + } else { + SaveIntrusionLog(); + } + } monitor.Process(); NoInit_BackUpTime = dateTimeController.CurrentDateTime(); if (nrf_gpio_pin_read(PinMap::Button) == 0) { @@ -401,6 +422,37 @@ void SystemTask::Work() { #pragma clang diagnostic pop } +void SystemTask::SaveIntrusionLog() { + auto& log = nimbleController.intrusionLog(); + // The SPI flash is powered down while sleeping, the log gets saved on the next alert or when the app is opened + if (log.IsDirty() && state == SystemTaskState::Running) { + log.Save(); + } +} + +void SystemTask::RaiseIntrusionAlert() { + GoToRunning(); + SaveIntrusionLog(); + + auto& log = nimbleController.intrusionLog(); + if (log.Count() == 0) { + return; + } + const auto entry = log.Get(0); + char address[18]; + Controllers::IntrusionLog::FormatAddress(entry, address); + + Controllers::NotificationManager::Notification notif; + const int size = + snprintf(notif.message.data(), notif.message.size(), "UNKNOWN LINK%c%s\n%s", '\0', address, Controllers::IntrusionLog::Describe(entry)); + notif.size = std::min(size + 1, notif.message.size()); + notif.category = Controllers::NotificationManager::Categories::SimpleAlert; + notificationManager.Push(std::move(notif)); + if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On) { + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NewNotification); + } +} + void SystemTask::GoToRunning() { if (state == SystemTaskState::Running) { return; diff --git a/src/systemtask/SystemTask.h b/src/systemtask/SystemTask.h index 606ddd3492..a083182df2 100644 --- a/src/systemtask/SystemTask.h +++ b/src/systemtask/SystemTask.h @@ -132,6 +132,10 @@ namespace Pinetime { void Work(); bool isBleDiscoveryTimerRunning = false; uint8_t bleDiscoveryTimer = 0; + // Counts down in state update periods after a connection, then the connection is classified + uint8_t intrusionCheckTimer = 0; + void RaiseIntrusionAlert(); + void SaveIntrusionLog(); TimerHandle_t measureBatteryTimer; uint8_t wakeLocksHeld = 0; SystemTaskState state = SystemTaskState::Running;