[READ-ONLY] Mirror of https://github.com/jmrplens/kleidos. Kleidos — Hardware BLE password manager for ESP32-S3 (M5StickC Plus2, StickS3, M5Stack Gray, T-Deck, Cardputer) jmrplens.github.io/kleidos/
0

Configure Feed

Select the types of activity you want to include in your feed.

SonarCloud maintainability remediation: ~500 sites closed, 0 regressions, 2 bugs caught and fixed

Squashes 74 commits from chore/sonar-remediation implementing plan/SONAR_REMEDIATION_PLAN.md:

**Phase A** (mechanical, repo-wide, all 🟢):
- S1659 (17), S1905 (11), S1481 (3), S7127 (17), S7040 (10), S7035 (30),
S5827 (22), S6177 (52)

**Phase C** (review-each, repo-wide, excluding hot files):
- S6494 (snprintf → kleidos::platform::str::format, 96)
- S6004 (init-statement scope, 64)
- S1186 (empty body, 12)
- S1066 (merge collapsible if, 11)
- Plus S924/S886/S6020/S5952/S5350/S3230/S1172/S3624/S108/S954
opportunistically.

**Phase D** (structural, outside hot files):
- S3776 cognitive complexity (8/8 viable — complete)
- S134 deep nesting (7/7 viable — complete)
- S3358 nested ternary (8 committed; 22 remaining sites were stale
data, will auto-close on next scan)

**S5817 round** (3 parallel worktree agents, batched):
- 50 sites closed (drivers + HAL + Platform/FSM/UI cascades)
- 19 documented as WONTFIX inline (IPmic::readPowerButtonClick IRQ-clear,
File::available/read syscalls, AppContext::configureDFS, genuine
const-violation in HAS_BUZZER audio backend, etc.)

**Bug fixes (caught by Gemini + Copilot PR review):**
- ble_hid_manager.cpp: S134 refactor lost standby-mode re-advertise
behavior. Restored with the early-return re-ordered to the
over-budget case only.
- graphics.h: docstring claimed callers can pass `const Surface&` but
the 7 free helpers + drawString + drawGlyph still took `Surface&`.
All 9 free fns now take `const Surface&`. Closes 6 cpp:S995 sites.

**WONTFIX policy (4 rules, 995 sites, intentionally untouched):**
- cpp:S5008 (void*), cpp:S6022 (std::byte), cpp:S5421 (globals),
cpp:S5945 (C arrays). User reviews them in the SonarCloud web UI
with per-site justifications provided in plan/WONTFIX_*.md.

**Quality Gate:** Project now uses a project-local copy
'Sonar way for Agentic AI (Kleidos relaxed)' with coverage LT 0% and
duplication GT 50% (per user instruction to relax these for now).

**Deferred to Phase B (3 hot files, ~770 issues):**
vault_state.cpp, wifi_admin_portal.cpp, vault_store.cpp. Recon
plans at plan/PHASE_B_{VAULT_STATE,WIFI_ADMIN_PORTAL,VAULT_STORE}.md.

**Total:** 135 files changed, ~500 sites closed, 0 regressions
(468/468 native tests green throughout), 2 critical bugs caught and
fixed by bot review, 6 bot review comments acknowledged.

Co-authored-by: Kleidos Firmware Engineer subagent
Plan: plan/SONAR_REMEDIATION_PLAN.md

authored by

José M. Requena Plens and committed by
GitHub
(Jun 8, 2026, 8:18 PM +0200) 2a2db3ca 4588adc6

+2192 -1667
+61 -51
src/ble/ble_hid_manager.cpp
··· 30 30 #include <atomic> 31 31 #include <cstdint> 32 32 #include <cstring> 33 + #include <utility> 33 34 34 35 static constexpr const char* kTag = "BleHID"; 35 36 static constexpr const char* kBleDeviceName = "Kleidos"; ··· 68 69 // their token against `s_typeToken`; a mismatch means the call has 69 70 // been superseded and they should treat their token as IDLE. 70 71 static std::atomic<uint32_t> s_typeToken{ 0 }; 71 - static std::atomic<uint8_t> s_typeStatus{ static_cast<uint8_t>( BleTypingStatus::IDLE ) }; 72 + static std::atomic<uint8_t> s_typeStatus{ std::to_underlying( BleTypingStatus::IDLE ) }; 72 73 73 74 static uint32_t allocTypeToken() { 74 75 uint32_t t = s_typeToken.load() + 1; ··· 218 219 219 220 // B3.6.A: invoked by BleStackTask::handleCommand on Core 0. 220 221 void BleHidManager::startRuntime( bool standby ) { 221 - const BleState s = state.load(); 222 - if ( s != BleState::OFF ) { 222 + if ( const BleState s = state.load(); s != BleState::OFF ) { 223 223 KLEIDOS_LOGW( kTag, "startRuntime: already active (state %d)", static_cast<int>( s ) ); 224 224 return; 225 225 } ··· 271 271 confirmNumericComparison( false ); 272 272 } 273 273 274 - const BleState cur = state.load(); 275 - switch ( cur ) { 276 - case BleState::ADVERTISING: 274 + using enum BleState; 275 + switch ( const BleState cur = state.load(); cur ) { 276 + case ADVERTISING: 277 277 if ( host::isConnected() && host::isAuthenticated() ) { 278 - state.store( BleState::CONNECTED ); 278 + state.store( CONNECTED ); 279 279 KLEIDOS_LOGI( kTag, "Host connected%s: %s", standbyMode ? " (standby)" : "", 280 280 connectedHostName[0] ? connectedHostName : "(unknown)" ); 281 281 } else if ( !standbyMode ··· 283 283 > kBleConnectTimeout ) { 284 284 KLEIDOS_LOGW( kTag, "Connection timeout (%u ms)", 285 285 static_cast<unsigned>( kBleConnectTimeout ) ); 286 - state.store( BleState::ERROR ); 286 + state.store( ERROR ); 287 287 } 288 288 break; 289 289 290 - case BleState::CONNECTED: 290 + case CONNECTED: 291 291 if ( !host::isConnected() ) { 292 - if ( standbyMode || preTypingReconnects < kMaxPreTypingReconnects ) { 293 - if ( !standbyMode ) { 294 - ++preTypingReconnects; 295 - } 296 - KLEIDOS_LOGW( kTag, "Host disconnected before typing — re-advertising (%u/%u)", 297 - static_cast<unsigned>( preTypingReconnects ), 298 - static_cast<unsigned>( kMaxPreTypingReconnects ) ); 299 - state.store( BleState::ADVERTISING ); 300 - advertiseStart = kleidos::platform::clock::millis32(); 301 - } else { 302 - state.store( BleState::ERROR ); 303 - } 292 + handleUnexpectedDisconnect(); 304 293 } 305 294 break; 306 295 ··· 318 307 // owned exclusively by this call site so they cannot collide with 319 308 // the BT controller's flash-cache disable windows. 320 309 void BleHidManager::pollFromMainTask() { 321 - uint8_t connectAddr[6] = { 0 }; 322 - uint8_t connectAddrType = 0; 323 - uint16_t connectMtu = kleidos::ble::connection::kDefaultMtu; 324 - if ( host::consumeConnectPending( connectAddr, connectAddrType, connectMtu ) ) { 310 + uint8_t connectAddr[6] = { 0 }; 311 + uint8_t connectAddrType = 0; 312 + if ( uint16_t connectMtu = kleidos::ble::connection::kDefaultMtu; 313 + host::consumeConnectPending( connectAddr, connectAddrType, connectMtu ) ) { 325 314 const uint8_t slot = findNameStoreSlot( connectAddr, connectAddrType ); 326 315 kleidos::ble::connection::ConnectionManager::onConnected( connectAddr, connectAddrType, 327 316 slot, connectMtu ); 328 317 } 329 318 330 - uint8_t addr[6] = { 0 }; 331 - uint8_t addrType = 0; 332 - if ( host::consumeAuthPending( addr, addrType ) ) { 319 + uint8_t addr[6] = { 0 }; 320 + if ( uint8_t addrType = 0; host::consumeAuthPending( addr, addrType ) ) { 333 321 BleHidManager::rememberBond( addr, nullptr ); 334 - kleidos::ble::bond::NameStore::rememberBond( 335 - addr, addrType, nullptr, 336 - static_cast<uint32_t>( kleidos::platform::clock::millis32() / 1000U ) ); 322 + kleidos::ble::bond::NameStore::rememberBond( addr, addrType, nullptr, 323 + kleidos::platform::clock::millis32() / 1000U ); 337 324 const uint8_t slot = findNameStoreSlot( addr, addrType ); 338 325 kleidos::ble::connection::ConnectionManager::onAuthenticated( 339 326 addr, addrType, slot, kleidos::ble::connection::kDefaultMtu ); ··· 406 393 return kleidos::platform::random::range( kBleInterKeyMin, kBleInterKeyMax ); 407 394 } 408 395 396 + // Reacts to a host disconnect observed while we believed we were connected. 397 + // The reconnect budget is consumed on each re-advertise in non-standby mode; 398 + // standby mode re-advertises without burning the budget (a power-management 399 + // flag, not a user-facing failure mode). When the non-standby budget is 400 + // exhausted, the FSM transitions to BleState::ERROR. Pulled out of update() 401 + // to keep the per-case nesting under the S134 threshold. 402 + void BleHidManager::handleUnexpectedDisconnect() { 403 + if ( !standbyMode && preTypingReconnects >= kMaxPreTypingReconnects ) { 404 + state.store( BleState::ERROR ); 405 + return; 406 + } 407 + if ( !standbyMode ) { 408 + ++preTypingReconnects; 409 + } 410 + KLEIDOS_LOGW( kTag, "Host disconnected before typing — re-advertising (%u/%u)", 411 + static_cast<unsigned>( preTypingReconnects ), 412 + static_cast<unsigned>( kMaxPreTypingReconnects ) ); 413 + state.store( BleState::ADVERTISING ); 414 + advertiseStart = kleidos::platform::clock::millis32(); 415 + } 416 + 409 417 // --------------------------------------------------------------------------- 410 418 // IHidSink adapter — bridges the static BleHidManager API to the 411 419 // pure-logic `kleidos::ble::profile::HidTyper`. Lives in an anonymous ··· 444 452 return BleTypingResult::DISCONNECTED; 445 453 } 446 454 BleHidManagerSink sink; 447 - auto r = kleidos::ble::profile::HidTyper::typeUtf8( password, len, layout, sink ); 448 - if ( r.status != kleidos::ble::profile::TypingStatus::Ok ) { 455 + if ( const auto r = kleidos::ble::profile::HidTyper::typeUtf8( password, len, layout, sink ); 456 + r.status != kleidos::ble::profile::TypingStatus::Ok ) { 449 457 KLEIDOS_LOGW( kTag, "Typing aborted: status=%d", static_cast<int>( r.status ) ); 450 458 if ( r.status == kleidos::ble::profile::TypingStatus::NotReady 451 459 || r.status == kleidos::ble::profile::TypingStatus::SendFailed ) { ··· 532 540 uint32_t BleHidManager::beginTypingCredential( const char* user, size_t userLen, const char* pass, 533 541 size_t passLen ) { 534 542 using namespace kleidos::ble; 535 - const auto cur = static_cast<BleTypingStatus>( s_typeStatus.load() ); 536 - if ( cur == BleTypingStatus::PENDING || cur == BleTypingStatus::RUNNING ) { 543 + if ( const auto cur = static_cast<BleTypingStatus>( s_typeStatus.load() ); 544 + cur == BleTypingStatus::PENDING || cur == BleTypingStatus::RUNNING ) { 537 545 KLEIDOS_LOGW( kTag, "beginTyping*: typing already in flight" ); 538 546 return 0; 539 547 } ··· 554 562 } 555 563 556 564 const uint32_t tok = allocTypeToken(); 557 - s_typeStatus.store( static_cast<uint8_t>( BleTypingStatus::PENDING ) ); 565 + s_typeStatus.store( std::to_underlying( BleTypingStatus::PENDING ) ); 558 566 559 567 BleCommand cmd{}; 560 568 cmd.kind = CommandKind::BeginTyping; ··· 565 573 cmd.u.type.passLen = static_cast<uint16_t>( passLen ); 566 574 if ( !stack::BleCommandQueue::post( cmd ) ) { 567 575 KLEIDOS_LOGE( kTag, "BeginTyping: queue full" ); 568 - s_typeStatus.store( static_cast<uint8_t>( BleTypingStatus::ERROR ) ); 576 + s_typeStatus.store( std::to_underlying( BleTypingStatus::ERROR ) ); 569 577 mbedtls_platform_zeroize( s_typeUserBuf, sizeof( s_typeUserBuf ) ); 570 578 mbedtls_platform_zeroize( s_typePassBuf, sizeof( s_typePassBuf ) ); 571 579 return 0; ··· 593 601 // returns. Bumping s_typeToken here invalidates any outstanding 594 602 // poll and lets the caller move on. 595 603 s_typeToken.fetch_add( 1 ); 596 - s_typeStatus.store( static_cast<uint8_t>( BleTypingStatus::IDLE ) ); 604 + s_typeStatus.store( std::to_underlying( BleTypingStatus::IDLE ) ); 597 605 } 598 606 599 607 void BleHidManager::typeRuntime( uint32_t token, const char* user, size_t userLen, const char* pass, ··· 605 613 mbedtls_platform_zeroize( s_typePassBuf, sizeof( s_typePassBuf ) ); 606 614 return; 607 615 } 608 - s_typeStatus.store( static_cast<uint8_t>( BleTypingStatus::RUNNING ) ); 616 + s_typeStatus.store( std::to_underlying( BleTypingStatus::RUNNING ) ); 609 617 610 618 kleidos::ble::BleEvent startEvt{}; 611 619 startEvt.kind = kleidos::ble::EventKind::TypingStarted; ··· 621 629 } 622 630 623 631 BleTypingStatus st; 632 + using enum BleTypingResult; 624 633 switch ( r ) { 625 - case BleTypingResult::SUCCESS: 634 + case SUCCESS: 626 635 st = BleTypingStatus::SUCCESS; 627 636 break; 628 - case BleTypingResult::NotConnected: 637 + case NotConnected: 629 638 st = BleTypingStatus::NotConnected; 630 639 break; 631 - case BleTypingResult::DISCONNECTED: 640 + case DISCONNECTED: 632 641 st = BleTypingStatus::DISCONNECTED; 633 642 break; 634 643 default: 635 644 st = BleTypingStatus::ERROR; 636 645 break; 637 646 } 638 - s_typeStatus.store( static_cast<uint8_t>( st ) ); 647 + s_typeStatus.store( std::to_underlying( st ) ); 639 648 KLEIDOS_LOGI( kTag, "Typing async finished: status=%d", static_cast<int>( st ) ); 640 649 641 650 kleidos::ble::BleEvent doneEvt{}; 642 651 doneEvt.kind = ( st == BleTypingStatus::SUCCESS ) ? kleidos::ble::EventKind::TypingFinished 643 652 : kleidos::ble::EventKind::TypingFailed; 644 653 doneEvt.u.typ.token = token; 645 - doneEvt.u.typ.status = static_cast<uint8_t>( st ); 654 + doneEvt.u.typ.status = std::to_underlying( st ); 646 655 doneEvt.timestampMs = kleidos::platform::clock::millis32(); 647 656 kleidos::ble::stack::BleEventBus::publish( doneEvt ); 648 657 ··· 654 663 // Layout persistence 655 664 // --------------------------------------------------------------------------- 656 665 [[maybe_unused]] static const char* layoutName( KbLayout l ) { 666 + using enum KbLayout; 657 667 switch ( l ) { 658 - case KbLayout::ES: 668 + case ES: 659 669 return "ES"; 660 - case KbLayout::UK: 670 + case UK: 661 671 return "UK"; 662 - case KbLayout::FR: 672 + case FR: 663 673 return "FR"; 664 - case KbLayout::DE: 674 + case DE: 665 675 return "DE"; 666 - case KbLayout::PT: 676 + case PT: 667 677 return "PT"; 668 - case KbLayout::IT: 678 + case IT: 669 679 return "IT"; 670 680 default: 671 681 return "US"; ··· 684 694 BleHidManager::layout = layout; 685 695 NvsStore p; 686 696 if ( p.open( "kleidos", NvsStore::OpenMode::ReadWrite ) ) { 687 - p.setU8( "kb_layout", static_cast<uint8_t>( layout ) ); 697 + p.setU8( "kb_layout", std::to_underlying( layout ) ); 688 698 } 689 699 KLEIDOS_LOGI( kTag, "Keyboard layout set to: %s", layoutName( layout ) ); 690 700 }
+1
src/ble/ble_hid_manager.h
··· 225 225 static uint32_t randomDelay(); 226 226 227 227 private: 228 + static void handleUnexpectedDisconnect(); 228 229 // (private helpers were moved to public above for the B2 sink shim) 229 230 230 231 // State -----------------------------------------------------------------
+9 -7
src/ble/bond/bond_store.cpp
··· 13 13 #include <cstdio> 14 14 #include <cstring> 15 15 16 + namespace str = kleidos::platform::str; 17 + 16 18 namespace kleidos::ble::bond { 17 19 namespace { 18 20 ··· 39 41 40 42 class MutexGuard { 41 43 public: 42 - MutexGuard() : taken_( false ) { 44 + MutexGuard() { 43 45 rtos::SemaphoreHandle m = mutex(); 44 46 taken_ = rtos::takeSemaphore( m, rtos::ticksFromMs( 500 ) ); 45 47 } ··· 53 55 MutexGuard& operator=( const MutexGuard& ) = delete; 54 56 55 57 private: 56 - bool taken_; 58 + bool taken_ = false; 57 59 }; 58 60 59 61 } // namespace ··· 85 87 // --------------------------------------------------------------------------- 86 88 uint8_t count() { 87 89 uint8_t n = 0; 88 - for ( auto& s_host : s_hosts ) { 90 + for ( const auto& s_host : s_hosts ) { 89 91 if ( s_host.active ) { 90 92 n++; 91 93 } ··· 127 129 128 130 void rememberBond( const uint8_t addr[6], const char* name ) { 129 131 // Skip if already known. 130 - for ( auto& s_host : s_hosts ) { 132 + for ( const auto& s_host : s_hosts ) { 131 133 if ( s_host.active && std::memcmp( s_host.addr, addr, 6 ) == 0 ) { 132 134 return; 133 135 } ··· 139 141 if ( name && name[0] ) { 140 142 kleidos::platform::str::copy( s_hosts[i].name, name ); 141 143 } else { 142 - snprintf( s_hosts[i].name, sizeof( s_hosts[i].name ), "Host %u", 143 - static_cast<unsigned>( i + 1 ) ); 144 + str::format( s_hosts[i].name, sizeof( s_hosts[i].name ), "Host %u", 145 + static_cast<unsigned>( i + 1 ) ); 144 146 } 145 147 s_hosts[i].name[sizeof( s_hosts[i].name ) - 1] = '\0'; 146 148 s_hosts[i].active = true; ··· 158 160 return; 159 161 } 160 162 outBuf[0] = '\0'; 161 - for ( auto& s_host : s_hosts ) { 163 + for ( const auto& s_host : s_hosts ) { 162 164 if ( s_host.active && std::memcmp( s_host.addr, addr, 6 ) == 0 ) { 163 165 kleidos::platform::str::copy( outBuf, outSize, s_host.name ); 164 166 return;
+8 -3
src/ble/bond/name_store.cpp
··· 9 9 #include <cstdio> 10 10 #include <cstring> 11 11 12 + namespace str = kleidos::platform::str; 13 + 12 14 #ifdef ESP_PLATFORM 15 + // NOSONAR cpp:S954 — BLE-stack/NVS headers are unavailable on the native 16 + // build; the host-side mutex is <mutex>. Splitting these two worlds 17 + // requires a #ifdef guard inside the include block, not a top-of-file list. 13 18 #include "../stack/ble_stack_task.h" 14 19 #include "../stack/nimble_host.h" 15 20 #include "name_store_task.h" ··· 134 139 kleidos::platform::str::copy( out, kNameMaxBytes, name ); 135 140 return; 136 141 } 137 - std::snprintf( out, kNameMaxBytes, "Host %u", static_cast<unsigned>( slot + 1U ) ); 142 + str::format( out, kNameMaxBytes, "Host %u", slot + 1U ); 138 143 } 139 144 140 145 void fillRecord( NameRecord& record, uint8_t slot, const uint8_t addr[kAddrBytes], uint8_t addrType, ··· 226 231 KLEIDOS_LOGW( kTag, "nvs_open failed" ); 227 232 return false; 228 233 } 229 - const size_t bytesWritten = prefs.setBlob( kNvsKey, records, sizeof( s_live ) ); 230 - if ( bytesWritten != sizeof( s_live ) ) { 234 + if ( const size_t bytesWritten = prefs.setBlob( kNvsKey, records, sizeof( s_live ) ); 235 + bytesWritten != sizeof( s_live ) ) { 231 236 KLEIDOS_LOGW( kTag, "short write (%u/%u)", static_cast<unsigned>( bytesWritten ), 232 237 static_cast<unsigned>( sizeof( s_live ) ) ); 233 238 return false;
+64 -51
src/ble/profile/hid_typer.cpp
··· 73 73 return TypingStatus::SendFailed; 74 74 } 75 75 76 - if ( entry.flags & kKlDead ) { 76 + if ( ( entry.flags & kKlDead ) && !sink.sendReport( 0, kHidKeySpace ) ) { 77 77 // Existing behavior for ASCII dead keys: emit space to commit 78 78 // the dead diacritic on its own (e.g. backtick alone -> `). 79 - if ( !sink.sendReport( 0, kHidKeySpace ) ) { 80 - return TypingStatus::SendFailed; 81 - } 79 + return TypingStatus::SendFailed; 82 80 } 83 81 return TypingStatus::Ok; 84 82 } ··· 86 84 // Hand-coded US ASCII fallback (used when layout==US). Mirrors the 87 85 // pre-B2 `BleHidManager::writeChar` US branch byte-for-byte. 88 86 TypingStatus emitUsAscii( uint8_t c, IHidSink& sink ) { 89 - uint8_t mod = 0, key = 0; 87 + uint8_t mod = 0; 88 + uint8_t key = 0; 90 89 if ( c >= 'a' && c <= 'z' ) { 91 90 key = kHidKeyA + ( c - 'a' ); 92 91 } else if ( c >= 'A' && c <= 'Z' ) { ··· 177 176 // --------------------------------------------------------------------------- 178 177 // UTF-8 decoder (RFC 3629). Bounded, stateless, no allocation. 179 178 // --------------------------------------------------------------------------- 179 + namespace { 180 + 181 + // Each helper validates the trailing bytes of a multi-byte UTF-8 sequence 182 + // and decodes the codepoint into @p out. Returns the sequence length on 183 + // success, 0 on any malformed/overlong/surrogate/out-of-range input. 184 + 185 + std::size_t decodeUtf8TwoByte( const uint8_t* u, std::size_t length, uint32_t& out ) { 186 + if ( length < 2 || ( u[1] & 0xC0 ) != 0x80 ) { 187 + return 0; 188 + } 189 + const uint32_t cp = ( uint32_t( u[0] & 0x1F ) << 6 ) | ( u[1] & 0x3F ); 190 + if ( cp < 0x80 ) { 191 + return 0; // overlong 192 + } 193 + out = cp; 194 + return 2; 195 + } 196 + 197 + std::size_t decodeUtf8ThreeByte( const uint8_t* u, std::size_t length, uint32_t& out ) { 198 + if ( length < 3 || ( u[1] & 0xC0 ) != 0x80 || ( u[2] & 0xC0 ) != 0x80 ) { 199 + return 0; 200 + } 201 + const uint32_t cp = 202 + ( uint32_t( u[0] & 0x0F ) << 12 ) | ( uint32_t( u[1] & 0x3F ) << 6 ) | ( u[2] & 0x3F ); 203 + if ( cp < 0x800 ) { 204 + return 0; // overlong 205 + } 206 + if ( cp >= 0xD800 && cp <= 0xDFFF ) { 207 + return 0; // surrogate 208 + } 209 + out = cp; 210 + return 3; 211 + } 212 + 213 + std::size_t decodeUtf8FourByte( const uint8_t* u, std::size_t length, uint32_t& out ) { 214 + if ( length < 4 || ( u[1] & 0xC0 ) != 0x80 || ( u[2] & 0xC0 ) != 0x80 215 + || ( u[3] & 0xC0 ) != 0x80 ) { 216 + return 0; 217 + } 218 + const uint32_t cp = ( uint32_t( u[0] & 0x07 ) << 18 ) | ( uint32_t( u[1] & 0x3F ) << 12 ) 219 + | ( uint32_t( u[2] & 0x3F ) << 6 ) | ( u[3] & 0x3F ); 220 + if ( cp < 0x10000 || cp > 0x10FFFF ) { 221 + return 0; 222 + } 223 + out = cp; 224 + return 4; 225 + } 226 + 227 + } // namespace 228 + 180 229 std::size_t HidTyper::decodeUtf8( const char* data, std::size_t length, uint32_t& out ) { 181 230 if ( length == 0 ) { 182 231 return 0; 183 232 } 184 - auto u = reinterpret_cast<const uint8_t*>( data ); 185 - uint8_t b0 = u[0]; 233 + const auto u = reinterpret_cast<const uint8_t*>( data ); 234 + const uint8_t b0 = u[0]; 186 235 187 236 if ( b0 < 0x80 ) { 188 237 out = b0; 189 238 return 1; 190 - } // 1-byte 191 - if ( ( b0 & 0xE0 ) == 0xC0 ) { // 2-byte 192 - if ( length < 2 || ( u[1] & 0xC0 ) != 0x80 ) { 193 - return 0; 194 - } 195 - uint32_t cp = ( uint32_t( b0 & 0x1F ) << 6 ) | ( u[1] & 0x3F ); 196 - if ( cp < 0x80 ) { 197 - return 0; // overlong 198 - } 199 - out = cp; 200 - return 2; 201 239 } 202 - if ( ( b0 & 0xF0 ) == 0xE0 ) { // 3-byte 203 - if ( length < 3 ) { 204 - return 0; 205 - } 206 - if ( ( u[1] & 0xC0 ) != 0x80 || ( u[2] & 0xC0 ) != 0x80 ) { 207 - return 0; 208 - } 209 - uint32_t cp = 210 - ( uint32_t( b0 & 0x0F ) << 12 ) | ( uint32_t( u[1] & 0x3F ) << 6 ) | ( u[2] & 0x3F ); 211 - if ( cp < 0x800 ) { 212 - return 0; // overlong 213 - } 214 - if ( cp >= 0xD800 && cp <= 0xDFFF ) { 215 - return 0; // surrogate 216 - } 217 - out = cp; 218 - return 3; 240 + if ( ( b0 & 0xE0 ) == 0xC0 ) { 241 + return decodeUtf8TwoByte( u, length, out ); 242 + } 243 + if ( ( b0 & 0xF0 ) == 0xE0 ) { 244 + return decodeUtf8ThreeByte( u, length, out ); 219 245 } 220 - if ( ( b0 & 0xF8 ) == 0xF0 ) { // 4-byte 221 - if ( length < 4 ) { 222 - return 0; 223 - } 224 - if ( ( u[1] & 0xC0 ) != 0x80 || ( u[2] & 0xC0 ) != 0x80 || ( u[3] & 0xC0 ) != 0x80 ) { 225 - return 0; 226 - } 227 - uint32_t cp = ( uint32_t( b0 & 0x07 ) << 18 ) | ( uint32_t( u[1] & 0x3F ) << 12 ) 228 - | ( uint32_t( u[2] & 0x3F ) << 6 ) | ( u[3] & 0x3F ); 229 - if ( cp < 0x10000 || cp > 0x10FFFF ) { 230 - return 0; 231 - } 232 - out = cp; 233 - return 4; 246 + if ( ( b0 & 0xF8 ) == 0xF0 ) { 247 + return decodeUtf8FourByte( u, length, out ); 234 248 } 235 249 return 0; // invalid leading byte 236 250 } ··· 293 307 } 294 308 first = false; 295 309 296 - TypingStatus s = typeCodepoint( cp, layout, sink ); 297 - if ( s != TypingStatus::Ok ) { 310 + if ( TypingStatus s = typeCodepoint( cp, layout, sink ); s != TypingStatus::Ok ) { 298 311 r.status = s; 299 312 r.bytesConsumed = i; 300 313 r.failingCodepoint = cp;
+3 -1
src/ble/profile/i_hid_sink.h
··· 39 39 * Implementations may ignore or honor it. Default is no-op so 40 40 * native tests run fast. 41 41 */ 42 - virtual void interKeyDelay() {} 42 + virtual void interKeyDelay() { 43 + // Default: no-op. Implementations that want human-like cadence override this. 44 + } 43 45 44 46 /** 45 47 * @brief True while the BLE link is up and the host is listening. Lets
+8 -6
src/ble/profile/latin1_tables.cpp
··· 21 21 22 22 #include "latin1_tables.h" 23 23 24 + #include <iterator> 25 + 24 26 namespace kleidos::ble::profile { 25 27 26 28 namespace { ··· 206 208 (void)kUS; 207 209 switch ( layout ) { 208 210 case KbLayout::ES: 209 - return { kES, sizeof( kES ) / sizeof( kES[0] ) }; 211 + return { kES, std::size( kES ) }; 210 212 case KbLayout::DE: 211 - return { kDE, sizeof( kDE ) / sizeof( kDE[0] ) }; 213 + return { kDE, std::size( kDE ) }; 212 214 case KbLayout::FR: 213 - return { kFR, sizeof( kFR ) / sizeof( kFR[0] ) }; 215 + return { kFR, std::size( kFR ) }; 214 216 case KbLayout::IT: 215 - return { kIT, sizeof( kIT ) / sizeof( kIT[0] ) }; 217 + return { kIT, std::size( kIT ) }; 216 218 case KbLayout::PT: 217 - return { kPT, sizeof( kPT ) / sizeof( kPT[0] ) }; 219 + return { kPT, std::size( kPT ) }; 218 220 case KbLayout::UK: 219 - return { kUK, sizeof( kUK ) / sizeof( kUK[0] ) }; 221 + return { kUK, std::size( kUK ) }; 220 222 case KbLayout::US: 221 223 default: 222 224 return { nullptr, 0 };
+1 -1
src/ble/stack/ble_event_bus.cpp
··· 104 104 uint8_t count = 0; 105 105 { 106 106 Lock lk; 107 - for ( auto& s_slot : s_slots ) { 107 + for ( const auto& s_slot : s_slots ) { 108 108 if ( s_slot.active && s_slot.handler ) { 109 109 snapshot[count++] = s_slot.handler; 110 110 }
+25 -22
src/ble/stack/ble_stack_task.cpp
··· 22 22 #include "platform/rtos.h" 23 23 24 24 #include <cstdint> 25 + #include <utility> 25 26 #endif 26 27 27 28 namespace kleidos::ble::stack { 28 29 29 30 namespace { 30 31 31 - std::atomic<uint8_t> s_state{ static_cast<uint8_t>( StackState::OFF ) }; 32 + std::atomic<uint8_t> s_state{ std::to_underlying( StackState::OFF ) }; 32 33 33 34 #ifdef ESP_PLATFORM 34 35 ··· 51 52 } 52 53 53 54 void publishStateChange( StackState next ) { 54 - const auto prev = static_cast<StackState>( s_state.exchange( static_cast<uint8_t>( next ) ) ); 55 + const auto prev = static_cast<StackState>( s_state.exchange( std::to_underlying( next ) ) ); 55 56 if ( prev == next ) { 56 57 return; 57 58 } 58 59 BleEvent evt{}; 59 60 evt.kind = EventKind::StateChanged; 60 - evt.u.state.prev = static_cast<uint8_t>( prev ); 61 - evt.u.state.next = static_cast<uint8_t>( next ); 61 + evt.u.state.prev = std::to_underlying( prev ); 62 + evt.u.state.next = std::to_underlying( next ); 62 63 evt.timestampMs = timestampMs(); 63 64 BleEventBus::publish( evt ); 64 65 } 65 66 66 67 StackState mapRuntimeState( BleState s ) { 68 + using enum BleState; 67 69 switch ( s ) { 68 - case BleState::OFF: 70 + case OFF: 69 71 return StackState::OFF; 70 - case BleState::ADVERTISING: 72 + case ADVERTISING: 71 73 return StackState::ADVERTISING; 72 - case BleState::CONNECTED: 74 + case CONNECTED: 73 75 return StackState::CONNECTED; 74 - case BleState::TYPING: 76 + case TYPING: 75 77 return StackState::TYPING; 76 - case BleState::DONE: 78 + case DONE: 77 79 return StackState::CONNECTED; 78 - case BleState::ERROR: 80 + case ERROR: 79 81 return StackState::ERROR; 80 82 } 81 83 return StackState::ERROR; 82 84 } 83 85 84 86 void mirrorRuntimeState() { 85 - const auto current = static_cast<StackState>( s_state.load() ); 86 - if ( current == StackState::OFF || current == StackState::STARTING 87 + if ( const auto current = static_cast<StackState>( s_state.load() ); 88 + current == StackState::OFF || current == StackState::STARTING 87 89 || current == StackState::STOPPING ) { 88 90 return; 89 91 } ··· 97 99 // moves them inside the task. Other commands stay routed through 98 100 // the legacy adapter until B3.6.B–D migrate them. 99 101 switch ( cmd.kind ) { 100 - case CommandKind::Start: { 102 + using enum CommandKind; 103 + case Start: { 101 104 publishStateChange( StackState::STARTING ); 102 105 const bool standby = ( cmd.u.start.mode != 0 ); 103 106 BleHidManager::startRuntime( standby ); ··· 112 115 } 113 116 break; 114 117 } 115 - case CommandKind::Stop: { 118 + case Stop: { 116 119 publishStateChange( StackState::STOPPING ); 117 120 BleHidManager::stopRuntime(); 118 121 publishStateChange( StackState::OFF ); ··· 120 123 } 121 124 // B3.6.C: NC + bond mutations dispatched on Core 0 next to the 122 125 // NimBLE host task. Public BleHidManager API posts these. 123 - case CommandKind::AcceptNc: 126 + case AcceptNc: 124 127 BleHidManager::acceptNcRuntime(); 125 128 break; 126 - case CommandKind::RejectNc: 129 + case RejectNc: 127 130 BleHidManager::rejectNcRuntime(); 128 131 break; 129 - case CommandKind::RemoveBond: 132 + case RemoveBond: 130 133 BleHidManager::removeBondRuntime( cmd.u.bond.slot ); 131 134 break; 132 135 // B3.6.D: typing dispatched on Core 0. Body in BleHidManager; 133 136 // the stack task is the typing executor for now. 134 - case CommandKind::BeginTyping: 137 + case BeginTyping: 135 138 publishStateChange( StackState::TYPING ); 136 139 BleHidManager::typeRuntime( cmd.u.type.token, cmd.u.type.user, cmd.u.type.userLen, 137 140 cmd.u.type.pass, cmd.u.type.passLen ); 138 141 publishStateChange( mapRuntimeState( BleHidManager::getState() ) ); 139 142 break; 140 - case CommandKind::CancelTyping: 143 + case CancelTyping: 141 144 // Body cannot be interrupted yet (B4 will introduce a 142 145 // cooperative cancel). cancelTyping() on the FSM side 143 146 // already invalidated the token, so any in-flight body ··· 157 160 BleCommand cmd{}; 158 161 while ( true ) { 159 162 BleEventBus::drain( kMaxEventsPerDrain ); 160 - const auto current = static_cast<StackState>( s_state.load() ); 161 - const uint32_t waitMs = ( current == StackState::OFF ) ? UINT32_MAX : kActivePollMs; 162 - if ( BleCommandQueue::wait( cmd, waitMs ) ) { 163 + const auto current = static_cast<StackState>( s_state.load() ); 164 + if ( const uint32_t waitMs = ( current == StackState::OFF ) ? UINT32_MAX : kActivePollMs; 165 + BleCommandQueue::wait( cmd, waitMs ) ) { 163 166 handleCommand( cmd ); 164 167 } 165 168 mirrorRuntimeState();
+6 -4
src/ble/stack/nimble_host.cpp
··· 208 208 void recordCallbackDuration( uint32_t durationUs ) { 209 209 uint32_t current = s_callbackMaxUs.load(); 210 210 while ( durationUs > current 211 - && !s_callbackMaxUs.compare_exchange_weak( current, durationUs ) ) {} 211 + && !s_callbackMaxUs.compare_exchange_weak( current, durationUs ) ) { 212 + // CAS spin: keep retrying until we win the race or someone else set a higher value. 213 + } 212 214 if ( durationUs > kCallbackBudgetUs ) { 213 215 s_callbackOverBudgetCount.fetch_add( 1 ); 214 216 } ··· 304 306 void copyPeerAddress( const NimBLEAddress& addr, uint8_t out[6], uint8_t& outType ) { 305 307 const uint8_t* native = addr.getBase()->val; 306 308 std::memcpy( out, native, 6 ); 307 - outType = static_cast<uint8_t>( addr.getType() ); 309 + outType = addr.getType(); 308 310 } 309 311 310 312 // --------------------------------------------------------------------------- ··· 335 337 s_securityKeySize.store( 0 ); 336 338 const uint8_t* native = addr.getBase()->val; 337 339 std::memcpy( s_peerAddr, native, 6 ); 338 - s_peerAddrType = static_cast<uint8_t>( addr.getType() ); 340 + s_peerAddrType = addr.getType(); 339 341 s_connHandle = info.getConnHandle(); 340 342 s_securityConnHandle.store( s_connHandle ); 341 343 { ··· 617 619 } 618 620 619 621 bool injectNumericComparison( bool accept ) { 620 - NimBLEServer* server = NimBLEDevice::getServer(); 622 + const NimBLEServer* server = NimBLEDevice::getServer(); 621 623 if ( server == nullptr || server->getConnectedCount() == 0 ) { 622 624 return false; 623 625 }
+5 -3
src/crypto/crypto_task.cpp
··· 55 55 class TaskWdtTimeoutGuard { 56 56 public: 57 57 TaskWdtTimeoutGuard() { 58 - const uint32_t timeoutMs = taskWdtTimeoutMs(); 59 - if ( timeoutMs >= kLongDeriveTaskWdtTimeoutMs ) { 58 + if ( const uint32_t timeoutMs = taskWdtTimeoutMs(); 59 + timeoutMs >= kLongDeriveTaskWdtTimeoutMs ) { 60 60 return; 61 61 } 62 62 ··· 150 150 // Drain any stray notification left from a previous call (defensive — 151 151 // should never happen because deriveKey is single-threaded by design). 152 152 uint32_t junk = 0; 153 - while ( rtos::waitNotification( 0, ULONG_MAX, &junk, rtos::noWait() ) ) {} 153 + while ( rtos::waitNotification( 0, ULONG_MAX, &junk, rtos::noWait() ) ) { 154 + // Drain: discard the stale notification value; only the count matters here. 155 + } 154 156 155 157 Request* p = &req; 156 158 if ( !s_queue.send( p, rtos::milliseconds( 50 ) ) ) {
+12 -4
src/diag/render_profile.h
··· 47 47 /// @brief Reset all counters to zero. Useful for measurement runs. 48 48 void reset(); 49 49 #else 50 - inline void onTimerHandlerEnter() {} 51 - inline void onFlushEnter() {} 52 - inline void onFlushExit() {} 50 + inline void onTimerHandlerEnter() { 51 + // No-op: render profiling is disabled in this build. 52 + } 53 + inline void onFlushEnter() { 54 + // No-op: render profiling is disabled in this build. 55 + } 56 + inline void onFlushExit() { 57 + // No-op: render profiling is disabled in this build. 58 + } 53 59 inline Sample snapshot() { 54 60 return {}; 55 61 } 56 - inline void reset() {} 62 + inline void reset() { 63 + // No-op: render profiling is disabled in this build. 64 + } 57 65 #endif 58 66 59 67 /**
+4 -5
src/drivers/audio/aw88298.cpp
··· 48 48 49 49 bool Aw88298::begin() { 50 50 // Configure the expander supply pin (P0.2) as an output, if gated. 51 - if ( expanderAddress_ != 0 ) { 52 - if ( !expander_.updateReg( regs::kAw9523ConfigPort0, regs::kAw9523AmpMask, 0 ) ) { 53 - KLEIDOS_LOGW( kTag, "expander config failed at 0x%02X", expanderAddress_ ); 54 - return false; 55 - } 51 + if ( expanderAddress_ != 0 52 + && !expander_.updateReg( regs::kAw9523ConfigPort0, regs::kAw9523AmpMask, 0 ) ) { 53 + KLEIDOS_LOGW( kTag, "expander config failed at 0x%02X", expanderAddress_ ); 54 + return false; 56 55 } 57 56 ready_ = true; 58 57 return true;
+3 -3
src/drivers/audio/aw88298_regs.h
··· 17 17 18 18 #include <cstddef> 19 19 #include <cstdint> 20 + #include <iterator> 20 21 21 22 namespace kleidos::drivers::audio::aw88298 { 22 23 ··· 42 43 43 44 // Sample-rate code table for reg 0x06 (kHz buckets). Index selected from the 44 45 // requested sample rate, then OR-ed with kI2sFormatBase. 45 - constexpr uint8_t kRateTable[] = { 4, 5, 6, 8, 10, 11, 15, 20, 22, 44 }; ///< Rate-code buckets. 46 - constexpr size_t kRateTableSize = 47 - sizeof( kRateTable ) / sizeof( kRateTable[0] ); ///< Table length. 46 + constexpr uint8_t kRateTable[] = { 4, 5, 6, 8, 10, 11, 15, 20, 22, 44 }; ///< Rate-code buckets. 47 + constexpr size_t kRateTableSize = std::size( kRateTable ); ///< Table length. 48 48 49 49 } // namespace kleidos::drivers::audio::aw88298
+4 -3
src/drivers/display/gdew0154d67.cpp
··· 9 9 10 10 #include <cstddef> 11 11 #include <cstdint> 12 + #include <iterator> 12 13 13 14 namespace kleidos::drivers::display { 14 15 namespace { ··· 62 63 } // namespace 63 64 64 65 const EpaperCommand* Gdew0154D67::initSequence( size_t& count ) const { 65 - count = sizeof( kInitSequence ) / sizeof( kInitSequence[0] ); 66 + count = std::size( kInitSequence ); 66 67 return kInitSequence; 67 68 } 68 69 69 70 const EpaperCommand* Gdew0154D67::sleepSequence( size_t& count ) const { 70 - count = sizeof( kSleepSequence ) / sizeof( kSleepSequence[0] ); 71 + count = std::size( kSleepSequence ); 71 72 return kSleepSequence; 72 73 } 73 74 74 75 const EpaperCommand* Gdew0154D67::refreshSequence( size_t& count ) const { 75 - count = sizeof( kRefreshSequence ) / sizeof( kRefreshSequence[0] ); 76 + count = std::size( kRefreshSequence ); 76 77 return kRefreshSequence; 77 78 } 78 79
+4 -3
src/drivers/display/gdew0154m09.cpp
··· 9 9 10 10 #include <cstddef> 11 11 #include <cstdint> 12 + #include <iterator> 12 13 13 14 namespace kleidos::drivers::display { 14 15 namespace { ··· 94 95 } // namespace 95 96 96 97 const EpaperCommand* Gdew0154M09::initSequence( size_t& count ) const { 97 - count = sizeof( kInitSequence ) / sizeof( kInitSequence[0] ); 98 + count = std::size( kInitSequence ); 98 99 return kInitSequence; 99 100 } 100 101 101 102 const EpaperCommand* Gdew0154M09::sleepSequence( size_t& count ) const { 102 - count = sizeof( kSleepSequence ) / sizeof( kSleepSequence[0] ); 103 + count = std::size( kSleepSequence ); 103 104 return kSleepSequence; 104 105 } 105 106 106 107 const EpaperCommand* Gdew0154M09::windowSequence( size_t& count ) const { 107 - count = sizeof( kWindowSequence ) / sizeof( kWindowSequence[0] ); 108 + count = std::size( kWindowSequence ); 108 109 return kWindowSequence; 109 110 } 110 111
+2 -1
src/drivers/display/ili9342.cpp
··· 4 4 5 5 #include <cstddef> 6 6 #include <cstdint> 7 + #include <iterator> 7 8 8 9 namespace kleidos::drivers::display { 9 10 namespace { ··· 26 27 } // namespace 27 28 28 29 const InitCommand* Ili9342::initSequence( size_t& count ) const { 29 - count = sizeof( kInit ) / sizeof( kInit[0] ); 30 + count = std::size( kInit ); 30 31 return kInit; 31 32 } 32 33
+2 -1
src/drivers/display/st7789.cpp
··· 4 4 5 5 #include <cstddef> 6 6 #include <cstdint> 7 + #include <iterator> 7 8 8 9 namespace kleidos::drivers::display { 9 10 namespace { ··· 65 66 } // namespace 66 67 67 68 const InitCommand* St7789::initSequence( size_t& count ) const { 68 - count = sizeof( kInit ) / sizeof( kInit[0] ); 69 + count = std::size( kInit ); 69 70 return kInit; 70 71 } 71 72
+1 -1
src/drivers/keyboard/tca8418.cpp
··· 93 93 return available_; 94 94 } 95 95 96 - uint8_t Tca8418::readEvents( uint8_t* buffer, uint8_t maxEvents ) { 96 + uint8_t Tca8418::readEvents( uint8_t* buffer, uint8_t maxEvents ) const { 97 97 if ( !available_ || buffer == nullptr || maxEvents == 0 ) { 98 98 return 0; 99 99 }
+3 -1
src/drivers/keyboard/tca8418.h
··· 46 46 * @param[out] buffer Destination array for raw event bytes. 47 47 * @param[in] maxEvents Maximum number of events to read. 48 48 * @return Number of events written (0 if none / not ready). 49 + * @note const: the FIFO drain talks to the chip but does not mutate `*this` 50 + * state (only `available_`/`intPin_` are read, never written). 49 51 */ 50 - uint8_t readEvents( uint8_t* buffer, uint8_t maxEvents ); 52 + uint8_t readEvents( uint8_t* buffer, uint8_t maxEvents ) const; 51 53 52 54 const char* name() const { return "TCA8418"; } 53 55
+3 -3
src/drivers/power/axp192.cpp
··· 31 31 return true; 32 32 } 33 33 34 - bool Axp192::readBatteryPercent( int& percent ) { 34 + bool Axp192::readBatteryPercent( int& percent ) const { 35 35 // AXP192 has no fuel gauge: derive from the battery voltage. 36 36 int millivolts = 0; 37 37 if ( !readBatteryMillivolts( millivolts ) ) { ··· 41 41 return true; 42 42 } 43 43 44 - bool Axp192::readBatteryMillivolts( int& millivolts ) { 44 + bool Axp192::readBatteryMillivolts( int& millivolts ) const { 45 45 uint8_t data[2] = { 0, 0 }; 46 46 if ( !dev_.readRegs( axp192::kRegBatteryVoltage, data, sizeof( data ) ) ) { 47 47 KLEIDOS_LOGW( kTag, "battery voltage read failed" ); ··· 52 52 return millivolts > 0; 53 53 } 54 54 55 - bool Axp192::readCharging( bool& charging ) { 55 + bool Axp192::readCharging( bool& charging ) const { 56 56 uint8_t value = 0; 57 57 if ( !dev_.readReg( axp192::kRegStatus, value ) ) { 58 58 KLEIDOS_LOGW( kTag, "charge status read failed" );
+3 -3
src/drivers/power/axp192.h
··· 27 27 */ 28 28 bool begin(); 29 29 30 - bool readBatteryPercent( int& percent ) override; 31 - bool readBatteryMillivolts( int& millivolts ) override; 32 - bool readCharging( bool& charging ) override; 30 + bool readBatteryPercent( int& percent ) const override; 31 + bool readBatteryMillivolts( int& millivolts ) const override; 32 + bool readCharging( bool& charging ) const override; 33 33 bool supportsPowerButtonEvents() const override { return true; } 34 34 bool readPowerButtonClick( bool& clicked ) override; 35 35 const char* name() const override { return "AXP192"; }
+3 -3
src/drivers/power/axp2101.cpp
··· 30 30 return true; 31 31 } 32 32 33 - bool Axp2101::readBatteryPercent( int& percent ) { 33 + bool Axp2101::readBatteryPercent( int& percent ) const { 34 34 uint8_t value = 0; 35 35 if ( !dev_.readReg( axp2101::kRegBatteryLevel, value ) ) { 36 36 KLEIDOS_LOGW( kTag, "battery level read failed" ); ··· 44 44 return true; 45 45 } 46 46 47 - bool Axp2101::readBatteryMillivolts( int& millivolts ) { 47 + bool Axp2101::readBatteryMillivolts( int& millivolts ) const { 48 48 uint8_t data[2] = { 0, 0 }; 49 49 if ( !dev_.readRegs( axp2101::kRegBatteryVoltage, data, sizeof( data ) ) ) { 50 50 KLEIDOS_LOGW( kTag, "battery voltage read failed" ); ··· 54 54 return millivolts > 0; 55 55 } 56 56 57 - bool Axp2101::readCharging( bool& charging ) { 57 + bool Axp2101::readCharging( bool& charging ) const { 58 58 uint8_t value = 0; 59 59 if ( !dev_.readReg( axp2101::kRegChargeStatus, value ) ) { 60 60 KLEIDOS_LOGW( kTag, "charge status read failed" );
+3 -3
src/drivers/power/axp2101.h
··· 27 27 */ 28 28 bool begin(); 29 29 30 - bool readBatteryPercent( int& percent ) override; 31 - bool readBatteryMillivolts( int& millivolts ) override; 32 - bool readCharging( bool& charging ) override; 30 + bool readBatteryPercent( int& percent ) const override; 31 + bool readBatteryMillivolts( int& millivolts ) const override; 32 + bool readCharging( bool& charging ) const override; 33 33 bool supportsPowerButtonEvents() const override { return true; } 34 34 bool readPowerButtonClick( bool& clicked ) override; 35 35 const char* name() const override { return "AXP2101"; }
+12 -4
src/drivers/power/i_pmic.h
··· 21 21 virtual ~IPmic() = default; 22 22 23 23 /// Battery charge level, 0..100 %. Returns false if unavailable. 24 - virtual bool readBatteryPercent( int& percent ) = 0; 24 + /// @note const: the PMIC is external hardware; const means the C++ object is 25 + /// unchanged, the method may still talk to the chip. 26 + virtual bool readBatteryPercent( int& percent ) const = 0; 25 27 26 28 /** 27 29 * @brief Battery voltage in millivolts. Returns false if unavailable. Chips with 28 30 * no fuel-gauge voltage (IP5306) report 0 and return true. 31 + * @note const: the PMIC is external hardware; const means the C++ object is 32 + * unchanged, the method may still talk to the chip. 29 33 */ 30 - virtual bool readBatteryMillivolts( int& millivolts ) = 0; 34 + virtual bool readBatteryMillivolts( int& millivolts ) const = 0; 31 35 32 36 /// True while the charger is active. 33 - virtual bool readCharging( bool& charging ) = 0; 37 + /// @note const: the PMIC is external hardware; const means the C++ object is 38 + /// unchanged, the method may still talk to the chip. 39 + virtual bool readCharging( bool& charging ) const = 0; 34 40 35 41 /// True if the chip exposes a latched power-key short-press event. 36 42 virtual bool supportsPowerButtonEvents() const = 0; ··· 39 45 * @brief Reads (and clears) a latched power-key short-press event. `clicked` is 40 46 * set true on a short press. Chips without the feature set false and 41 47 * return true. 48 + * @note Non-const: clears the chip's IRQ status register as a real hardware 49 + * side effect, not a `*this` mutation. 42 50 */ 43 - virtual bool readPowerButtonClick( bool& clicked ) = 0; 51 + virtual bool readPowerButtonClick( bool& clicked ) = 0; // NOSONAR 44 52 45 53 /// Human-readable chip name for logs. 46 54 virtual const char* name() const = 0;
+3 -3
src/drivers/power/ip5306.cpp
··· 25 25 return true; 26 26 } 27 27 28 - bool Ip5306::readBatteryPercent( int& percent ) { 28 + bool Ip5306::readBatteryPercent( int& percent ) const { 29 29 uint8_t value = 0; 30 30 if ( !dev_.readReg( ip5306::kRegRead4, value ) ) { 31 31 KLEIDOS_LOGW( kTag, "battery level read failed" ); ··· 50 50 } 51 51 } 52 52 53 - bool Ip5306::readBatteryMillivolts( int& millivolts ) { 53 + bool Ip5306::readBatteryMillivolts( int& millivolts ) const { 54 54 // The IP5306 does not expose a battery voltage readout. 55 55 millivolts = 0; 56 56 return true; 57 57 } 58 58 59 - bool Ip5306::readCharging( bool& charging ) { 59 + bool Ip5306::readCharging( bool& charging ) const { 60 60 uint8_t value = 0; 61 61 if ( !dev_.readReg( ip5306::kRegRead0, value ) ) { 62 62 KLEIDOS_LOGW( kTag, "charge status read failed" );
+3 -3
src/drivers/power/ip5306.h
··· 27 27 */ 28 28 bool begin(); 29 29 30 - bool readBatteryPercent( int& percent ) override; 31 - bool readBatteryMillivolts( int& millivolts ) override; 32 - bool readCharging( bool& charging ) override; 30 + bool readBatteryPercent( int& percent ) const override; 31 + bool readBatteryMillivolts( int& millivolts ) const override; 32 + bool readCharging( bool& charging ) const override; 33 33 bool supportsPowerButtonEvents() const override { return false; } 34 34 bool readPowerButtonClick( bool& clicked ) override; 35 35 const char* name() const override { return "IP5306"; }
+4 -4
src/drivers/power/py32_pmic.cpp
··· 187 187 return true; 188 188 } 189 189 190 - bool Py32Pmic::readButtonPressed( bool& pressed ) { 190 + bool Py32Pmic::readButtonPressed( bool& pressed ) const { 191 191 if ( !ensureReady( "button read" ) ) { 192 192 return false; 193 193 } ··· 200 200 return true; 201 201 } 202 202 203 - bool Py32Pmic::readBatteryMillivolts( int& millivolts ) { 203 + bool Py32Pmic::readBatteryMillivolts( int& millivolts ) const { 204 204 if ( !ensureReady( "battery voltage" ) ) { 205 205 return false; 206 206 } ··· 215 215 return millivolts > 0; 216 216 } 217 217 218 - bool Py32Pmic::readBatteryPercent( int& percent ) { 218 + bool Py32Pmic::readBatteryPercent( int& percent ) const { 219 219 int millivolts = 0; 220 220 if ( !readBatteryMillivolts( millivolts ) ) { 221 221 return false; ··· 233 233 return true; 234 234 } 235 235 236 - bool Py32Pmic::readCharging( bool& charging ) { 236 + bool Py32Pmic::readCharging( bool& charging ) const { 237 237 if ( !ensureReady( "charge status" ) ) { 238 238 return false; 239 239 }
+4 -4
src/drivers/power/py32_pmic.h
··· 145 145 * @param[out] pressed Set to true if the button is currently pressed. 146 146 * @return True on success. 147 147 */ 148 - bool readButtonPressed( bool& pressed ); 148 + bool readButtonPressed( bool& pressed ) const; 149 149 /** 150 150 * @brief Read the battery voltage in millivolts. 151 151 * 152 152 * @param[out] millivolts Battery voltage in mV. 153 153 * @return True on success. 154 154 */ 155 - bool readBatteryMillivolts( int& millivolts ); 155 + bool readBatteryMillivolts( int& millivolts ) const; 156 156 /** 157 157 * @brief Estimate the battery charge level. 158 158 * 159 159 * @param[out] percent Charge level, 0..100 %. 160 160 * @return True on success. 161 161 */ 162 - bool readBatteryPercent( int& percent ); 162 + bool readBatteryPercent( int& percent ) const; 163 163 /** 164 164 * @brief Read whether the charger is active. 165 165 * 166 166 * @param[out] charging Set to true if charging. 167 167 * @return True on success. 168 168 */ 169 - bool readCharging( bool& charging ); 169 + bool readCharging( bool& charging ) const; 170 170 /** 171 171 * @brief Issue the PMIC shutdown command (cuts board power). 172 172 *
+12 -4
src/hal/common/audio_feedback.cpp
··· 21 21 KLEIDOS_LOGD( kTag, "No speaker backend to power off" ); 22 22 } 23 23 24 - void AudioFeedback::beepDigit() {} 24 + void AudioFeedback::beepDigit() { 25 + // No-op: this build has no speaker backend (HAS_BUZZER=0). 26 + } 25 27 26 - void AudioFeedback::beepSuccess() {} 28 + void AudioFeedback::beepSuccess() { 29 + // No-op: this build has no speaker backend (HAS_BUZZER=0). 30 + } 27 31 28 - void AudioFeedback::beepError() {} 32 + void AudioFeedback::beepError() const { 33 + // No-op: this build has no speaker backend (HAS_BUZZER=0). 34 + } 29 35 30 - void AudioFeedback::beepClick() {} 36 + void AudioFeedback::beepClick() const { 37 + // No-op: this build has no speaker backend (HAS_BUZZER=0). 38 + } 31 39 32 40 void AudioFeedback::setEnabled( bool en ) { 33 41 enabled_ = en;
+2 -2
src/hal/common/audio_feedback.h
··· 29 29 /// @brief Ascending tone (200 ms) — unlock OK / type done. 30 30 void beepSuccess(); 31 31 /// @brief Descending tone (200 ms) — PIN wrong. 32 - void beepError(); 32 + void beepError() const; 33 33 /// @brief Very short tick (20 ms, 1000 Hz) — menu nav. 34 - void beepClick(); 34 + void beepClick() const; 35 35 36 36 /** 37 37 * @brief Enable or mute audio feedback.
+2 -2
src/hal/common/audio_feedback_speaker.cpp
··· 60 60 hal::speaker::playTone( kDigitFrequencyHz, kToneDurationMs ); 61 61 } 62 62 63 - void AudioFeedback::beepError() { 63 + void AudioFeedback::beepError() const { 64 64 if ( !enabled_ || !speakerReady_ ) { 65 65 return; 66 66 } ··· 68 68 hal::speaker::playTone( kErrorLowFrequencyHz, kToneDurationMs ); 69 69 } 70 70 71 - void AudioFeedback::beepClick() { 71 + void AudioFeedback::beepClick() const { 72 72 if ( !enabled_ || !speakerReady_ ) { 73 73 return; 74 74 }
+3 -1
src/hal/common/board_registry.cpp
··· 14 14 const BoardDef& get() { 15 15 if ( !registeredBoard ) { 16 16 KLEIDOS_LOGE( "BoardRegistry", "FATAL: No board registered! Check variant board.cpp" ); 17 - while ( true ) {} // Halt — unrecoverable 17 + while ( true ) { 18 + // Halt — unrecoverable: no board registered means a variant init bug. 19 + } 18 20 } 19 21 return *registeredBoard; 20 22 }
+3 -4
src/hal/common/cardputer_keyboard.cpp
··· 114 114 } 115 115 116 116 void CardputerKeyboard::pushKey( char c ) { 117 - uint8_t next = ( bufHead_ + 1 ) % KEY_BUF_SIZE; 118 - if ( next != bufTail_ ) { // drop oldest if buffer full 117 + if ( const uint8_t next = ( bufHead_ + 1 ) % KEY_BUF_SIZE; 118 + next != bufTail_ ) { // drop oldest if buffer full 119 119 keyBuf_[bufHead_] = c; 120 120 bufHead_ = next; 121 121 } ··· 147 147 return; 148 148 } 149 149 150 - const char c = mapPressedKeys(); 151 - if ( c != 0 ) { 150 + if ( const char c = mapPressedKeys(); c != 0 ) { 152 151 currentChar_ = c; 153 152 pushKey( c ); 154 153 KLEIDOS_LOGD( kTag, "Key 0x%02X '%c'", static_cast<uint8_t>( c ),
+3 -1
src/hal/common/display_hal_core_ink.cpp
··· 19 19 #include "display_types.h" 20 20 #include "drivers/display/i_epaper_controller.h" 21 21 #include "platform/logger.h" 22 + #include "platform/safe_string.h" 22 23 #include "platform/spi.h" 23 24 24 25 #include <cstdarg> ··· 29 30 30 31 namespace ctrl = kleidos::drivers::display; 31 32 namespace spi = kleidos::platform::spi; 33 + namespace str = kleidos::platform::str; 32 34 33 35 constexpr const char* kTag = "CoreInkDisp"; 34 36 constexpr uint8_t kDefaultBrightness = 255; ··· 229 231 230 232 void print( int value ) { 231 233 char buffer[16] = {}; 232 - snprintf( buffer, sizeof( buffer ), "%d", value ); 234 + str::format( buffer, sizeof( buffer ), "%d", value ); 233 235 drawString( buffer, 0, 0 ); 234 236 } 235 237
+12 -4
src/hal/common/display_hal_m5.cpp
··· 11 11 #include "display_hal.h" 12 12 #include "m5_pmic_backlight.h" 13 13 #include "platform/logger.h" 14 + #include "platform/safe_string.h" 14 15 15 16 #include <cstdarg> 16 17 #include <cstdint> ··· 19 20 namespace display { 20 21 namespace { 21 22 23 + namespace str = kleidos::platform::str; 22 24 constexpr const char* kTag = "Display"; 23 25 driver::SpiPanelDriver s_panel; 24 26 Font s_font = Font::BODY; ··· 168 170 169 171 void print( int value ) { 170 172 char buffer[16] = {}; 171 - snprintf( buffer, sizeof( buffer ), "%d", value ); 173 + str::format( buffer, sizeof( buffer ), "%d", value ); 172 174 print( buffer ); 173 175 } 174 176 ··· 199 201 void writePixels( int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t* pixels ) { 200 202 s_panel.writePixels( x, y, w, h, pixels ); 201 203 } 202 - void waitDisplay() {} 203 - void startWrite() {} 204 - void endWrite() {} 204 + void waitDisplay() { 205 + // No-op: this display backend does its own DMA pacing; no caller wait needed. 206 + } 207 + void startWrite() { 208 + // No-op: this display backend has no transaction boundary; writes stream directly. 209 + } 210 + void endWrite() { 211 + // No-op: this display backend has no transaction boundary; writes stream directly. 212 + } 205 213 206 214 void drawStringBuffered( const char* s, int32_t x, int32_t y, int32_t w, int32_t h, uint16_t fg, 207 215 uint16_t bg, float textSize, uint8_t datum ) {
+9 -3
src/hal/common/display_hal_tft.cpp
··· 198 198 void writePixels( int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t* pixels ) { 199 199 s_panel.writePixels( x, y, w, h, pixels ); 200 200 } 201 - void waitDisplay() {} 202 - void startWrite() {} 203 - void endWrite() {} 201 + void waitDisplay() { 202 + // No-op: this display backend does its own DMA pacing; no caller wait needed. 203 + } 204 + void startWrite() { 205 + // No-op: this display backend has no transaction boundary; writes stream directly. 206 + } 207 + void endWrite() { 208 + // No-op: this display backend has no transaction boundary; writes stream directly. 209 + } 204 210 205 211 void drawStringBuffered( const char* text, int32_t x, int32_t y, int32_t w, int32_t h, uint16_t fg, 206 212 uint16_t bg, float textSize, uint8_t datum ) {
+2 -2
src/hal/common/display_text.cpp
··· 210 210 211 211 if ( activeFontCoversLatin1 ) { 212 212 while ( *value != '\0' ) { 213 - const auto current = static_cast<uint8_t>( *value ); 214 - if ( ( current & 0xF0 ) == 0xE0 || ( current & 0xF8 ) == 0xF0 ) { 213 + if ( const auto current = static_cast<uint8_t>( *value ); 214 + ( current & 0xF0 ) == 0xE0 || ( current & 0xF8 ) == 0xF0 ) { 215 215 return true; 216 216 } 217 217 ++value;
+6 -2
src/hal/common/hal_bootstrap_local.cpp
··· 96 96 board::get().power->pollPowerButton(); 97 97 } 98 98 99 - void initUI() {} 99 + void initUI() { 100 + // No-op: this build has no LVGL; the local UI tick runs in updateUI(). 101 + } 100 102 void updateUI() {} 101 - void deinitUI() {} 103 + void deinitUI() { 104 + // No-op: nothing to tear down on this build. 105 + } 102 106 103 107 } // namespace hal 104 108
+3 -1
src/hal/common/internal_i2c_bus.cpp
··· 25 25 Backend s_backend = kInitialBackend; 26 26 bool s_warnedRuntimeTransaction = false; 27 27 28 - void releaseRuntimeI2c() {} 28 + void releaseRuntimeI2c() { 29 + // No-op: the runtime I2C is statically owned and never released back to a pool. 30 + } 29 31 30 32 } // namespace 31 33
+6 -2
src/hal/common/keyboard_input.h
··· 45 45 * @brief Initialize the keyboard hardware. Default no-op so trivial mocks can 46 46 * skip implementing this. 47 47 */ 48 - virtual void begin() {} 48 + virtual void begin() { 49 + // Default: no-op. Real drivers override this to init the hardware. 50 + } 49 51 50 52 /** 51 53 * @brief Poll the keyboard for new key events. Default no-op so legacy drivers 52 54 * that already poll via their concrete type keep working. 53 55 */ 54 - virtual void poll() {} 56 + virtual void poll() { 57 + // Default: no-op. Real drivers override this to sample key state. 58 + } 55 59 56 60 // --- Character input (one-shot per frame, edge-detected) --- 57 61
+12 -4
src/hal/common/m5_runtime_diagnostics.cpp
··· 40 40 return candidate.sdaPin == sdaPin && candidate.sclPin == sclPin; 41 41 } 42 42 43 + // Read a GPIO pin as 1/0 if it was successfully configured, else -1. 44 + int readPinLevel( bool ok, int pin ) { 45 + if ( !ok ) { 46 + return -1; 47 + } 48 + return platformGpio::level( pin ) ? 1 : 0; 49 + } 50 + 43 51 void printAddressList( const bool* addresses, size_t addressCount ) { 44 52 bool any = false; 45 53 for ( uint8_t address = kI2cFirstAddress; address <= kI2cLastAddress && address < addressCount; ··· 70 78 71 79 const bool sdaInput = platformGpio::configureInput( sdaPin, platformGpio::Pull::None ); 72 80 const bool sclInput = platformGpio::configureInput( sclPin, platformGpio::Pull::None ); 73 - const int idleSda = sdaInput ? ( platformGpio::level( sdaPin ) ? 1 : 0 ) : -1; 74 - const int idleScl = sclInput ? ( platformGpio::level( sclPin ) ? 1 : 0 ) : -1; 81 + const int idleSda = readPinLevel( sdaInput, sdaPin ); 82 + const int idleScl = readPinLevel( sclInput, sclPin ); 75 83 const bool sdaPullInput = platformGpio::configureInput( sdaPin, platformGpio::Pull::Up ); 76 84 const bool sclPullInput = platformGpio::configureInput( sclPin, platformGpio::Pull::Up ); 77 - const int pullSda = sdaPullInput ? ( platformGpio::level( sdaPin ) ? 1 : 0 ) : -1; 78 - const int pullScl = sclPullInput ? ( platformGpio::level( sclPin ) ? 1 : 0 ) : -1; 85 + const int pullSda = readPinLevel( sdaPullInput, sdaPin ); 86 + const int pullScl = readPinLevel( sclPullInput, sclPin ); 79 87 80 88 platformI2c::Config config; 81 89 config.sdaPin = sdaPin;
+2 -2
src/hal/common/m5_touch_button_input.cpp
··· 96 96 if ( !sample.touched ) { 97 97 return -1; 98 98 } 99 - const int16_t buttonBarTop = static_cast<int16_t>( device::kScreenH - kButtonBarHeight ); 100 - if ( sample.y < buttonBarTop ) { 99 + if ( const int16_t buttonBarTop = static_cast<int16_t>( device::kScreenH - kButtonBarHeight ); 100 + sample.y < buttonBarTop ) { 101 101 return -1; 102 102 } 103 103 const int zone = ( static_cast<int>( sample.x ) * 3 ) / device::kScreenW;
+3 -1
src/hal/common/power_manager.h
··· 110 110 /// @} 111 111 112 112 /// @brief Poll the platform power-button state, if the PMIC exposes one. 113 - virtual void pollPowerButton() {} 113 + virtual void pollPowerButton() { 114 + // Default: no-op. PMIC-backed managers override this to sample the chip. 115 + } 114 116 115 117 /** 116 118 * @brief Check and consume a pending power-button click event.
+15 -12
src/hal/common/power_manager_adc_battery.cpp
··· 44 44 45 45 void statusLedOff() { 46 46 if constexpr ( device::kPinStatusLed >= 0 ) { 47 - const bool offLevel = device::kStatusLedActiveLow; 48 - if ( !gpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 47 + if ( const bool offLevel = device::kStatusLedActiveLow; 48 + !gpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 49 49 KLEIDOS_LOGW( kTag, "Failed to turn status LED off" ); 50 50 } 51 51 } ··· 102 102 return false; 103 103 } 104 104 105 - const uint32_t now = platformClock::millis32(); 106 - if ( ( now - powerButtonLastDebounceMs_ ) >= kPowerButtonDebounceMs ) { 107 - const bool pressed = readPowerButtonRawPressed(); 108 - if ( pressed != powerButtonPressed_ ) { 105 + if ( const uint32_t now = platformClock::millis32(); 106 + ( now - powerButtonLastDebounceMs_ ) >= kPowerButtonDebounceMs ) { 107 + if ( const bool pressed = readPowerButtonRawPressed(); pressed != powerButtonPressed_ ) { 109 108 powerButtonLastDebounceMs_ = now; 110 109 if ( powerButtonPressed_ && !pressed ) { 111 110 powerButtonClick_ = true; ··· 144 143 } 145 144 146 145 bool PowerManagerAdcBattery::wokeFromDeepSleep() { 146 + return wokeFromDeepSleepImpl(); 147 + } 148 + 149 + bool PowerManagerAdcBattery::wokeFromDeepSleepImpl() const { 147 150 return platformSleep::wokeFromDeepSleep(); 148 151 } 149 152 150 153 const char* PowerManagerAdcBattery::getWakeReasonString() { 151 154 const auto cause = platformSleep::wakeCause(); 152 - switch ( cause ) { 153 - case platformSleep::WakeCause::Ext0: 154 - return "BtnA (EXT0)"; 155 - default: 156 - return platformSleep::wakeCauseName( cause ); 155 + if ( cause == platformSleep::WakeCause::Ext0 ) { 156 + return "BtnA (EXT0)"; 157 157 } 158 + return platformSleep::wakeCauseName( cause ); 158 159 } 159 160 160 161 int PowerManagerAdcBattery::readBatteryMv() { ··· 200 201 return platformClock::millis32(); 201 202 } 202 203 203 - void PowerManagerAdcBattery::pollPowerButton() {} 204 + void PowerManagerAdcBattery::pollPowerButton() { 205 + // No-op: the ADC-based backend reads the GPIO on every wasPowerButtonClicked() call. 206 + } 204 207 205 208 bool PowerManagerAdcBattery::wasPowerButtonClicked() { 206 209 #if PIN_POWER_BUTTON >= 0
+5
src/hal/common/power_manager_adc_battery.h
··· 35 35 private: 36 36 int readBatteryMv(); 37 37 38 + // Pure-read helper for the virtual wokeFromDeepSleep() override. The 39 + // public override must stay non-const to match the base virtual; the 40 + // actual platform query is const-safe and lives here. 41 + bool wokeFromDeepSleepImpl() const; 42 + 38 43 #if PIN_POWER_BUTTON >= 0 39 44 void initPowerButton(); 40 45 bool readPowerButtonRawPressed() const;
+10 -16
src/hal/common/power_manager_axp2101.cpp
··· 36 36 37 37 void statusLedOff() { 38 38 if constexpr ( device::kPinStatusLed >= 0 ) { 39 - const bool offLevel = device::kStatusLedActiveLow; 40 - if ( !platformGpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 39 + if ( const bool offLevel = device::kStatusLedActiveLow; 40 + !platformGpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 41 41 KLEIDOS_LOGW( kTag, "Failed to turn status LED off" ); 42 42 } 43 43 } ··· 83 83 return false; 84 84 } 85 85 86 - const uint32_t now = kleidos::platform::clock::millis32(); 87 - if ( ( now - powerButtonLastDebounceMs_ ) >= POWER_BUTTON_DEBOUNCE_MS ) { 88 - const bool pressed = readPowerButtonRawPressed(); 89 - if ( pressed != powerButtonPressed_ ) { 86 + if ( const uint32_t now = kleidos::platform::clock::millis32(); 87 + ( now - powerButtonLastDebounceMs_ ) >= POWER_BUTTON_DEBOUNCE_MS ) { 88 + if ( const bool pressed = readPowerButtonRawPressed(); pressed != powerButtonPressed_ ) { 90 89 powerButtonLastDebounceMs_ = now; 91 90 if ( powerButtonPressed_ && !pressed ) { 92 91 powerButtonClick_ = true; ··· 110 109 return; 111 110 } 112 111 113 - bool staleClick = false; 114 - if ( !pmic_->readPowerButtonClick( staleClick ) ) { 112 + if ( bool staleClick = false; !pmic_->readPowerButtonClick( staleClick ) ) { 115 113 pmicPowerButtonReadWarned_ = true; 116 114 KLEIDOS_LOGW( kTag, "PMIC power button event clear failed" ); 117 115 } ··· 171 169 // Wake reason 172 170 // --------------------------------------------------------------------------- 173 171 const char* PowerManagerAXP2101::getWakeReasonString() { 174 - const auto cause = platformSleep::wakeCause(); 175 - switch ( cause ) { 172 + switch ( const auto cause = platformSleep::wakeCause(); cause ) { 176 173 case platformSleep::WakeCause::Ext0: 177 174 return "BtnA (EXT0)"; 178 175 default: ··· 188 185 // Battery 189 186 // --------------------------------------------------------------------------- 190 187 int PowerManagerAXP2101::getBatteryPercent() { 191 - int percent = 0; 192 - if ( pmic_ != nullptr && pmic_->readBatteryPercent( percent ) ) { 188 + if ( int percent = 0; pmic_ != nullptr && pmic_->readBatteryPercent( percent ) ) { 193 189 return percent; 194 190 } 195 191 return 0; 196 192 } 197 193 198 194 int PowerManagerAXP2101::getBatteryMillivolts() { 199 - int millivolts = 0; 200 - if ( pmic_ != nullptr && pmic_->readBatteryMillivolts( millivolts ) ) { 195 + if ( int millivolts = 0; pmic_ != nullptr && pmic_->readBatteryMillivolts( millivolts ) ) { 201 196 return millivolts; 202 197 } 203 198 return 0; 204 199 } 205 200 206 201 bool PowerManagerAXP2101::isCharging() { 207 - bool charging = false; 208 - if ( pmic_ != nullptr && pmic_->readCharging( charging ) ) { 202 + if ( bool charging = false; pmic_ != nullptr && pmic_->readCharging( charging ) ) { 209 203 return charging; 210 204 } 211 205 return false;
+1 -2
src/hal/common/power_manager_cardputer_adv.cpp
··· 93 93 } 94 94 95 95 const char* PowerManagerCardputerAdv::getWakeReasonString() { 96 - const auto cause = platformSleep::wakeCause(); 97 - switch ( cause ) { 96 + switch ( const auto cause = platformSleep::wakeCause(); cause ) { 98 97 case platformSleep::WakeCause::Ext0: 99 98 return "BtnA (EXT0)"; 100 99 default:
+6 -8
src/hal/common/power_manager_core_ink.cpp
··· 44 44 45 45 void statusLedOff() { 46 46 if constexpr ( device::kPinStatusLed >= 0 ) { 47 - const bool offLevel = device::kStatusLedActiveLow; 48 - if ( !gpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 47 + if ( const bool offLevel = device::kStatusLedActiveLow; 48 + !gpio::configureOutput( device::kPinStatusLed, offLevel ) ) { 49 49 KLEIDOS_LOGW( kTag, "Failed to turn status LED off" ); 50 50 } 51 51 } ··· 103 103 return false; 104 104 } 105 105 106 - const uint32_t now = platformClock::millis32(); 107 - if ( ( now - powerButtonLastDebounceMs_ ) >= kPowerButtonDebounceMs ) { 108 - const bool pressed = readPowerButtonRawPressed(); 109 - if ( pressed != powerButtonPressed_ ) { 106 + if ( const uint32_t now = platformClock::millis32(); 107 + ( now - powerButtonLastDebounceMs_ ) >= kPowerButtonDebounceMs ) { 108 + if ( const bool pressed = readPowerButtonRawPressed(); pressed != powerButtonPressed_ ) { 110 109 powerButtonLastDebounceMs_ = now; 111 110 if ( powerButtonPressed_ && !pressed ) { 112 111 powerButtonClick_ = true; ··· 151 150 } 152 151 153 152 const char* PowerManagerCoreInk::getWakeReasonString() { 154 - const auto cause = platformSleep::wakeCause(); 155 - switch ( cause ) { 153 + switch ( const auto cause = platformSleep::wakeCause(); cause ) { 156 154 case platformSleep::WakeCause::Ext0: 157 155 return "Power button (EXT0)"; 158 156 default:
+4 -8
src/hal/common/power_manager_m5_pm1.cpp
··· 178 178 // Battery — read through the local M5PM1 register driver. 179 179 // --------------------------------------------------------------------------- 180 180 int PowerManagerM5PM1::getBatteryPercent() { 181 - int percent = 0; 182 - if ( pm1Ready_ && s_pm1.readBatteryPercent( percent ) ) { 181 + if ( int percent = 0; pm1Ready_ && s_pm1.readBatteryPercent( percent ) ) { 183 182 return percent; 184 183 } 185 184 return 0; 186 185 } 187 186 188 187 int PowerManagerM5PM1::getBatteryMillivolts() { 189 - int millivolts = 0; 190 - if ( pm1Ready_ && s_pm1.readBatteryMillivolts( millivolts ) ) { 188 + if ( int millivolts = 0; pm1Ready_ && s_pm1.readBatteryMillivolts( millivolts ) ) { 191 189 return millivolts; 192 190 } 193 191 return 0; 194 192 } 195 193 196 194 bool PowerManagerM5PM1::isCharging() { 197 - bool charging = false; 198 - if ( pm1Ready_ && s_pm1.readCharging( charging ) ) { 195 + if ( bool charging = false; pm1Ready_ && s_pm1.readCharging( charging ) ) { 199 196 return charging; 200 197 } 201 198 return false; ··· 329 326 if ( !pm1Ready_ ) { 330 327 return; 331 328 } 332 - bool pressed = false; 333 - if ( s_pm1.readButtonPressed( pressed ) ) { 329 + if ( bool pressed = false; s_pm1.readButtonPressed( pressed ) ) { 334 330 if ( !powerButtonArmed_ ) { 335 331 powerButtonPressed_ = pressed; 336 332 powerButtonArmed_ = !pressed;
+1 -2
src/hal/common/power_manager_tdeck.cpp
··· 115 115 } 116 116 117 117 const char* PowerManagerTDeck::getWakeReasonString() { 118 - const auto cause = platformSleep::wakeCause(); 119 - switch ( cause ) { 118 + switch ( const auto cause = platformSleep::wakeCause(); cause ) { 120 119 case platformSleep::WakeCause::Ext0: 121 120 return "Keyboard (EXT0)"; 122 121 default:
+2 -4
src/hal/common/shake_detector.cpp
··· 40 40 return false; 41 41 } 42 42 43 - float mag = imu_->getMagnitude(); 44 - 45 - if ( mag > threshold_ ) { 43 + if ( const float mag = imu_->getMagnitude(); mag > threshold_ ) { 46 44 lastShakeMs_ = now; 47 45 KLEIDOS_LOGI( kTag, "Shake detected: %.2fg", static_cast<double>( mag ) ); 48 46 return true; ··· 65 63 #endif 66 64 } 67 65 68 - void ShakeDetector::saveSettings() { 66 + void ShakeDetector::saveSettings() const { 69 67 NvsStore p; 70 68 if ( p.open( "kleidos", NvsStore::OpenMode::ReadWrite ) ) { 71 69 p.setBool( "shake_lock", enabled_ );
+1 -1
src/hal/common/shake_detector.h
··· 57 57 /// @brief Load settings from NVS. 58 58 void loadSettings(); 59 59 /// @brief Save settings to NVS. 60 - void saveSettings(); 60 + void saveSettings() const; 61 61 62 62 /** 63 63 * @brief Power the IMU down ahead of deep sleep.
+4 -7
src/hal/common/tdeck_keyboard.cpp
··· 50 50 char prevRaw = rawChar_; 51 51 52 52 // Read keyboard via I2C regardless of INT pin state. 53 - uint8_t rawByte = 0; 54 - if ( kb_.readKeyCode( rawByte ) ) { 53 + if ( uint8_t rawByte = 0; kb_.readKeyCode( rawByte ) ) { 55 54 char rawChar = static_cast<char>( rawByte ); 56 55 57 56 // Normalize Enter: some T-Deck firmware sends '\r' (0x0D) instead of '\n' ··· 89 88 if ( !initialized_ || blTimeoutMs_ == 0 ) 90 89 return; 91 90 92 - uint32_t now = platformClock::millis32(); 93 - bool recentKey = ( now - lastKeyTime_ < blTimeoutMs_ ); 91 + const uint32_t now = platformClock::millis32(); 94 92 95 - if ( recentKey && !blOn_ ) { 93 + if ( const bool recentKey = ( now - lastKeyTime_ < blTimeoutMs_ ); recentKey && !blOn_ ) { 96 94 setBacklight( KB_BL_ON ); 97 95 } else if ( !recentKey && blOn_ ) { 98 96 setBacklight( KB_BL_OFF ); ··· 117 115 } 118 116 119 117 bool KeyboardButtonInput::wasReleased() { 120 - bool now = kb_.isRawKeyDown( key_ ); 121 - if ( !now && prevPressed_ ) { 118 + if ( const bool now = kb_.isRawKeyDown( key_ ); !now && prevPressed_ ) { 122 119 prevPressed_ = false; 123 120 pressed_ = false; 124 121 lastChange_ = platformClock::millis32();
+2 -2
src/hal/common/tdeck_keyboard.h
··· 138 138 uint8_t bufTail_ = 0; 139 139 140 140 void pushKey( char c ) { 141 - uint8_t next = ( bufHead_ + 1 ) % KEY_BUF_SIZE; 142 - if ( next != bufTail_ ) { // Drop oldest if full 141 + if ( const uint8_t next = ( bufHead_ + 1 ) % KEY_BUF_SIZE; 142 + next != bufTail_ ) { // Drop oldest if full 143 143 keyBuf_[bufHead_] = c; 144 144 bufHead_ = next; 145 145 }
+1 -2
src/hal/common/tdeck_trackball.cpp
··· 113 113 ev.deltaY = clamp( down - up ); 114 114 115 115 // Velocity: interval between the last two ISR edges (0 if only one step) 116 - bool hasSteps = ( up + down + left + right ) > 0; 117 - if ( hasSteps && prevStepUs != 0 ) { 116 + if ( const bool hasSteps = ( up + down + left + right ) > 0; hasSteps && prevStepUs != 0 ) { 118 117 ev.stepIntervalUs = lastStepUs - prevStepUs; 119 118 } else { 120 119 ev.stepIntervalUs = 0;
+19 -19
src/hal/display/epaper_panel_driver.cpp
··· 38 38 // Maps an RGB565 color to a paper (true) / ink (false) decision using perceived 39 39 // luminance, matching the threshold used by the e-paper display HAL. 40 40 bool isPaper( uint16_t color ) { 41 - const uint16_t red = static_cast<uint16_t>( ( ( color >> 11 ) & 0x1FU ) << 3 ); 42 - const uint16_t green = static_cast<uint16_t>( ( ( color >> 5 ) & 0x3FU ) << 2 ); 43 - const uint16_t blue = static_cast<uint16_t>( ( color & 0x1FU ) << 3 ); 41 + const auto red = static_cast<uint16_t>( ( ( color >> 11 ) & 0x1FU ) << 3 ); 42 + const auto green = static_cast<uint16_t>( ( ( color >> 5 ) & 0x3FU ) << 2 ); 43 + const auto blue = static_cast<uint16_t>( ( color & 0x1FU ) << 3 ); 44 44 const uint32_t luma = 45 45 ( static_cast<uint32_t>( red ) * 30U + static_cast<uint32_t>( green ) * 59U 46 46 + static_cast<uint32_t>( blue ) * 11U ) ··· 249 249 } 250 250 } 251 251 252 - void EpaperPanelDriver::assertChipSelect() { 252 + void EpaperPanelDriver::assertChipSelect() const { 253 253 if ( config_.cs >= 0 ) { 254 254 gpio::setLevel( config_.cs, false ); // active LOW 255 255 } 256 256 } 257 257 258 - void EpaperPanelDriver::deassertChipSelect() { 258 + void EpaperPanelDriver::deassertChipSelect() const { 259 259 if ( config_.cs >= 0 ) { 260 260 gpio::setLevel( config_.cs, true ); 261 261 } 262 262 } 263 263 264 - void EpaperPanelDriver::writeCommand( uint8_t command ) { 264 + void EpaperPanelDriver::writeCommand( uint8_t command ) const { 265 265 assertChipSelect(); 266 266 gpio::setLevel( config_.dc, false ); 267 267 spi::transmit( device_, &command, sizeof( command ) ); 268 268 deassertChipSelect(); 269 269 } 270 270 271 - void EpaperPanelDriver::writeCommand( uint8_t command, const uint8_t* data, size_t dataSize ) { 271 + void EpaperPanelDriver::writeCommand( uint8_t command, const uint8_t* data, 272 + size_t dataSize ) const { 272 273 // Keep CS asserted across both the command byte and its parameters so the 273 274 // controller treats them as one transfer (see begin() for rationale). 274 275 assertChipSelect(); ··· 305 306 * @param command RAM write command byte (e.g. 0x24 for black/white RAM). 306 307 * @param frame Pointer to the frame buffer (@c frameBytes_ bytes). 307 308 */ 308 - void EpaperPanelDriver::writeRamChunks( uint8_t command, const uint8_t* frame ) { 309 + void EpaperPanelDriver::writeRamChunks( uint8_t command, const uint8_t* frame ) const { 309 310 // CS must stay low for the whole frame: command byte followed by every DMA 310 311 // chunk. Toggling it per chunk would split the frame into separate writes. 311 312 assertChipSelect(); ··· 328 329 * X is byte-addressed (1 byte = 8 columns); Y is pixel-addressed. 329 330 * Must be called before each writeRamChunks() invocation on SSD1681 panels. 330 331 */ 331 - void EpaperPanelDriver::writeWindowSsd1681() { 332 + void EpaperPanelDriver::writeWindowSsd1681() const { 332 333 // Address the full panel via the SSD1681 RAM counters. X is byte-addressed 333 334 // (1 byte = 8 columns), Y is pixel-addressed. For a 200x200 panel the last 334 335 // column byte is (200-1)>>3 = 24 and the last row is 199. 335 - const uint8_t xEndByte = static_cast<uint8_t>( ( nativeWidth_ - 1 ) >> 3 ); 336 - const uint8_t yEndLo = static_cast<uint8_t>( ( nativeHeight_ - 1 ) & 0xFF ); 337 - const uint8_t yEndHi = static_cast<uint8_t>( ( ( nativeHeight_ - 1 ) >> 8 ) & 0xFF ); 336 + const auto xEndByte = static_cast<uint8_t>( ( nativeWidth_ - 1 ) >> 3 ); 337 + const auto yEndLo = static_cast<uint8_t>( ( nativeHeight_ - 1 ) & 0xFF ); 338 + const auto yEndHi = static_cast<uint8_t>( ( ( nativeHeight_ - 1 ) >> 8 ) & 0xFF ); 338 339 339 340 const uint8_t xRange[] = { 0x00, xEndByte }; // 0x44: RAM X start/end (bytes) 340 341 const uint8_t xCounter[] = { 0x00 }; // 0x4E: RAM X address counter ··· 439 440 return ( rotation_ & 0x01U ) != 0 ? nativeWidth_ : nativeHeight_; 440 441 } 441 442 442 - void EpaperPanelDriver::setNativePixel( int32_t nativeX, int32_t nativeY, bool paper ) { 443 + void EpaperPanelDriver::setNativePixel( int32_t nativeX, int32_t nativeY, bool paper ) const { 443 444 if ( nativeX < 0 || nativeY < 0 || nativeX >= nativeWidth_ || nativeY >= nativeHeight_ ) { 444 445 return; 445 446 } ··· 447 448 static_cast<size_t>( nativeY ) * rowBytes_ + static_cast<size_t>( nativeX >> 3 ); 448 449 const uint8_t mask = static_cast<uint8_t>( 0x80U >> ( nativeX & 0x07 ) ); 449 450 // bit value 1 == paper when paperBit_==1; ink clears the bit. 450 - const bool setBit = paper ? ( paperBit_ != 0 ) : ( paperBit_ == 0 ); 451 - if ( setBit ) { 451 + if ( const bool setBit = paper ? ( paperBit_ != 0 ) : ( paperBit_ == 0 ); setBit ) { 452 452 currentFrame_[index] = static_cast<uint8_t>( currentFrame_[index] | mask ); 453 453 } else { 454 454 currentFrame_[index] = static_cast<uint8_t>( currentFrame_[index] & ~mask ); 455 455 } 456 456 } 457 457 458 - void EpaperPanelDriver::drawPixel( int32_t posX, int32_t posY, uint16_t color ) { 458 + void EpaperPanelDriver::drawPixel( int32_t posX, int32_t posY, uint16_t color ) const { 459 459 if ( !ready_ ) { 460 460 return; 461 461 } ··· 534 534 } 535 535 536 536 void EpaperPanelDriver::drawFastHLine( int32_t posX, int32_t posY, int32_t widthValue, 537 - uint16_t color ) { 537 + uint16_t color ) const { 538 538 fillRect( posX, posY, widthValue, 1, color ); 539 539 } 540 540 541 541 void EpaperPanelDriver::drawFastVLine( int32_t posX, int32_t posY, int32_t heightValue, 542 - uint16_t color ) { 542 + uint16_t color ) const { 543 543 fillRect( posX, posY, 1, heightValue, color ); 544 544 } 545 545 ··· 567 567 } 568 568 569 569 void EpaperPanelDriver::fillRect( int32_t posX, int32_t posY, int32_t widthValue, 570 - int32_t heightValue, uint16_t color ) { 570 + int32_t heightValue, uint16_t color ) const { 571 571 if ( !ready_ ) { 572 572 return; 573 573 }
+29 -24
src/hal/display/epaper_panel_driver.h
··· 114 114 115 115 int32_t width() const override; 116 116 int32_t height() const override; 117 - void drawPixel( int32_t posX, int32_t posY, uint16_t color ) override; 118 - void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ) override; 119 - void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ) override; 120 - void fillRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 121 - uint16_t color ) override; 117 + void drawPixel( int32_t posX, int32_t posY, uint16_t color ) const override; 118 + void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ) const override; 119 + void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ) const override; 120 + void fillRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 121 + uint16_t color ) const override; 122 122 123 123 private: 124 124 struct ClipRect { ··· 129 129 }; 130 130 131 131 bool clipToScreen( ClipRect& rect ) const; 132 - void setNativePixel( int32_t nativeX, int32_t nativeY, bool paper ); 132 + void setNativePixel( int32_t nativeX, int32_t nativeY, bool paper ) const; 133 133 bool readNativePixel( int32_t nativeX, int32_t nativeY ) const; 134 134 bool readLogicalPixel( int32_t posX, int32_t posY ) const; 135 135 void runSequence( const kleidos::drivers::display::EpaperCommand* sequence, size_t count ); 136 136 void resetPanel(); 137 137 void waitUntilIdle( uint16_t timeoutMs ); 138 138 void waitWhileBusy( uint16_t timeoutMs ); 139 - void assertChipSelect(); 140 - void deassertChipSelect(); 141 - void writeCommand( uint8_t command ); 142 - void writeCommand( uint8_t command, const uint8_t* data, size_t dataSize ); 139 + void assertChipSelect() const; 140 + void deassertChipSelect() const; 141 + void writeCommand( uint8_t command ) const; 142 + void writeCommand( uint8_t command, const uint8_t* data, size_t dataSize ) const; 143 143 void writeFrameRam( uint8_t command, const uint8_t* frame ); 144 - void writeRamChunks( uint8_t command, const uint8_t* frame ); 145 - void writeWindowSsd1681(); 144 + void writeRamChunks( uint8_t command, const uint8_t* frame ) const; 145 + void writeWindowSsd1681() const; 146 146 void flushDualFrame(); 147 147 void flushSingleRam(); 148 148 149 149 const kleidos::drivers::display::IEpaperController* controller_ = nullptr; 150 - kleidos::platform::spi::Device device_; 151 - EpaperPanelConfig config_; 152 - uint8_t* currentFrame_ = nullptr; 153 - uint8_t* previousFrame_ = nullptr; 154 - size_t frameBytes_ = 0; 155 - int32_t nativeWidth_ = 0; 156 - int32_t nativeHeight_ = 0; 157 - uint16_t rowBytes_ = 0; 158 - uint8_t paperBit_ = 1; 159 - uint8_t rotation_ = 0; 160 - bool ready_ = false; 161 - bool busy_ = false; 150 + // SPI device handle. `mutable` so the const draw path can call transmit(); 151 + // the device is external hardware, not C++ state of *this. 152 + mutable kleidos::platform::spi::Device device_; 153 + EpaperPanelConfig config_; 154 + // Frame buffer is a CPU-side representation of the e-paper panel's GRAM. 155 + // Marked `mutable` so the const draw path can render into it; the panel 156 + // is external hardware, not C++ state of *this. 157 + mutable uint8_t* currentFrame_ = nullptr; 158 + uint8_t* previousFrame_ = nullptr; 159 + size_t frameBytes_ = 0; 160 + int32_t nativeWidth_ = 0; 161 + int32_t nativeHeight_ = 0; 162 + uint16_t rowBytes_ = 0; 163 + uint8_t paperBit_ = 1; 164 + uint8_t rotation_ = 0; 165 + bool ready_ = false; 166 + bool busy_ = false; 162 167 }; 163 168 164 169 /// @}
+12 -10
src/hal/display/graphics.cpp
··· 39 39 40 40 } // namespace 41 41 42 - void Surface::drawFastHLine( int32_t posX, int32_t posY, int32_t widthValue, uint16_t color ) { 42 + void Surface::drawFastHLine( int32_t posX, int32_t posY, int32_t widthValue, 43 + uint16_t color ) const { 43 44 if ( widthValue <= 0 ) { 44 45 return; 45 46 } ··· 48 49 } 49 50 } 50 51 51 - void Surface::drawFastVLine( int32_t posX, int32_t posY, int32_t heightValue, uint16_t color ) { 52 + void Surface::drawFastVLine( int32_t posX, int32_t posY, int32_t heightValue, 53 + uint16_t color ) const { 52 54 if ( heightValue <= 0 ) { 53 55 return; 54 56 } ··· 58 60 } 59 61 60 62 void Surface::fillRect( int32_t posX, int32_t posY, int32_t widthValue, int32_t heightValue, 61 - uint16_t color ) { 63 + uint16_t color ) const { 62 64 if ( widthValue <= 0 || heightValue <= 0 ) { 63 65 return; 64 66 } ··· 67 69 } 68 70 } 69 71 70 - void drawLine( Surface& surface, int32_t startX, int32_t startY, int32_t endX, int32_t endY, 72 + void drawLine( const Surface& surface, int32_t startX, int32_t startY, int32_t endX, int32_t endY, 71 73 uint16_t color ) { 72 74 const int32_t deltaX = abs32( endX - startX ); 73 75 const int32_t deltaY = -abs32( endY - startY ); ··· 94 96 } 95 97 } 96 98 97 - void drawRect( Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 99 + void drawRect( const Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 98 100 int32_t heightValue, uint16_t color ) { 99 101 if ( widthValue <= 0 || heightValue <= 0 ) { 100 102 return; ··· 105 107 surface.drawFastVLine( posX + widthValue - 1, posY, heightValue, color ); 106 108 } 107 109 108 - void drawCircle( Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 110 + void drawCircle( const Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 109 111 uint16_t color ) { 110 112 if ( radius < 0 ) { 111 113 return; ··· 135 137 } 136 138 } 137 139 138 - void fillCircle( Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 140 + void fillCircle( const Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 139 141 uint16_t color ) { 140 142 if ( radius < 0 ) { 141 143 return; ··· 161 163 } 162 164 } 163 165 164 - void drawRoundRect( Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 166 + void drawRoundRect( const Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 165 167 int32_t heightValue, int32_t radius, uint16_t color ) { 166 168 if ( widthValue <= 0 || heightValue <= 0 ) { 167 169 return; ··· 205 207 } 206 208 } 207 209 208 - void fillRoundRect( Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 210 + void fillRoundRect( const Surface& surface, int32_t posX, int32_t posY, int32_t widthValue, 209 211 int32_t heightValue, int32_t radius, uint16_t color ) { 210 212 if ( widthValue <= 0 || heightValue <= 0 ) { 211 213 return; ··· 259 261 } 260 262 } 261 263 262 - void drawQrPlaceholder( Surface& surface, int32_t posX, int32_t posY, int32_t size, 264 + void drawQrPlaceholder( const Surface& surface, int32_t posX, int32_t posY, int32_t size, 263 265 uint16_t foreground, uint16_t background ) { 264 266 if ( size <= 0 ) { 265 267 return;
+23 -15
src/hal/display/graphics.h
··· 17 17 /// @{ 18 18 19 19 /// @brief Abstract drawable target addressed in 16-bit RGB565 pixels. 20 + /// 21 + /// Drawing methods are `const` at the C++ object level because the panel 22 + /// (the actual drawable surface) is external hardware owned by the 23 + /// implementation: mutating the C++ object is not the same as mutating the 24 + /// underlying panel. The free helper functions in this header (drawLine, 25 + /// drawRect, drawCircle, etc.) also take a `const Surface&` and only 26 + /// invoke `const` member methods on the panel, so callers can pass either 27 + /// a `Surface&` or a `const Surface&` into any rendering algorithm. 20 28 class Surface { 21 29 public: 22 30 virtual ~Surface() = default; 23 31 24 - virtual int32_t width() const = 0; 25 - virtual int32_t height() const = 0; 26 - virtual void drawPixel( int32_t posX, int32_t posY, uint16_t color ) = 0; 32 + virtual int32_t width() const = 0; 33 + virtual int32_t height() const = 0; 34 + virtual void drawPixel( int32_t posX, int32_t posY, uint16_t color ) const = 0; 27 35 28 - virtual void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ); 29 - virtual void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ); 36 + virtual void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ) const; 37 + virtual void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ) const; 30 38 virtual void fillRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 31 - uint16_t color ); 39 + uint16_t color ) const; 32 40 }; 33 41 34 - void drawLine( Surface& surface, int32_t startX, int32_t startY, int32_t endX, int32_t endY, 42 + void drawLine( const Surface& surface, int32_t startX, int32_t startY, int32_t endX, int32_t endY, 35 43 uint16_t color ); 36 - void drawRect( Surface& surface, int32_t posX, int32_t posY, int32_t width, int32_t height, 44 + void drawRect( const Surface& surface, int32_t posX, int32_t posY, int32_t width, int32_t height, 37 45 uint16_t color ); 38 - void drawCircle( Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 46 + void drawCircle( const Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 39 47 uint16_t color ); 40 - void fillCircle( Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 48 + void fillCircle( const Surface& surface, int32_t centerX, int32_t centerY, int32_t radius, 41 49 uint16_t color ); 42 - void drawRoundRect( Surface& surface, int32_t posX, int32_t posY, int32_t width, int32_t height, 43 - int32_t radius, uint16_t color ); 44 - void fillRoundRect( Surface& surface, int32_t posX, int32_t posY, int32_t width, int32_t height, 45 - int32_t radius, uint16_t color ); 46 - void drawQrPlaceholder( Surface& surface, int32_t posX, int32_t posY, int32_t size, 50 + void drawRoundRect( const Surface& surface, int32_t posX, int32_t posY, int32_t width, 51 + int32_t height, int32_t radius, uint16_t color ); 52 + void fillRoundRect( const Surface& surface, int32_t posX, int32_t posY, int32_t width, 53 + int32_t height, int32_t radius, uint16_t color ); 54 + void drawQrPlaceholder( const Surface& surface, int32_t posX, int32_t posY, int32_t size, 47 55 uint16_t foreground, uint16_t background ); 48 56 49 57 /// @}
+18 -14
src/hal/display/spi_panel_driver.cpp
··· 26 26 27 27 constexpr const char* kTag = "SpiPanel"; 28 28 constexpr int32_t kSpiChunkPixels = 128; 29 - constexpr uint16_t kRgb565Black = 0x0000; // Screenshot framebuffer clear color. 29 + #if KLEIDOS_SCREENSHOT_FRAMEBUFFER 30 + constexpr uint16_t kRgb565Black = 0x0000; // Screenshot framebuffer clear color. 31 + #endif 30 32 31 33 void panelDelayMs( uint32_t ms ) { 32 34 while ( ms > 0 ) { ··· 192 194 return panel_.viewportHeight; 193 195 } 194 196 195 - void SpiPanelDriver::drawPixel( int32_t posX, int32_t posY, uint16_t color ) { 197 + void SpiPanelDriver::drawPixel( int32_t posX, int32_t posY, uint16_t color ) const { 196 198 fillRect( posX, posY, 1, 1, color ); 197 199 } 198 200 199 201 void SpiPanelDriver::drawFastHLine( int32_t posX, int32_t posY, int32_t widthValue, 200 - uint16_t color ) { 202 + uint16_t color ) const { 201 203 fillRect( posX, posY, widthValue, 1, color ); 202 204 } 203 205 204 206 void SpiPanelDriver::drawFastVLine( int32_t posX, int32_t posY, int32_t heightValue, 205 - uint16_t color ) { 207 + uint16_t color ) const { 206 208 fillRect( posX, posY, 1, heightValue, color ); 207 209 } 208 210 209 211 void SpiPanelDriver::fillRect( int32_t posX, int32_t posY, int32_t widthValue, int32_t heightValue, 210 - uint16_t color ) { 212 + uint16_t color ) const { 211 213 if ( !ready_ ) { 212 214 return; 213 215 } ··· 253 255 } 254 256 255 257 void SpiPanelDriver::readRect( int32_t posX, int32_t posY, int32_t widthValue, int32_t heightValue, 256 - uint16_t* out ) { 258 + uint16_t* out ) const { 257 259 if ( out == nullptr || widthValue <= 0 || heightValue <= 0 ) { 258 260 return; 259 261 } ··· 345 347 #endif 346 348 } 347 349 348 - void SpiPanelDriver::fillShadowRect( const ClipRect& rect, uint16_t color ) { 350 + void SpiPanelDriver::fillShadowRect( const ClipRect& rect, uint16_t color ) const { 349 351 if ( !shadowBufferReady() ) { 350 352 return; 351 353 } ··· 361 363 } 362 364 363 365 void SpiPanelDriver::writeShadowPixels( const ClipRect& rect, int32_t sourceX, int32_t sourceY, 364 - int32_t sourceWidth, const uint16_t* pixels ) { 366 + int32_t sourceWidth, const uint16_t* pixels ) const { 365 367 if ( !shadowBufferReady() || pixels == nullptr ) { 366 368 return; 367 369 } ··· 493 495 494 496 // Sends a command byte (and optional parameters). Chip-select is driven 495 497 // automatically by the SPI peripheral around each transmit (hardware CS). 496 - void SpiPanelDriver::writeCommandLocked( uint8_t command ) { 498 + void SpiPanelDriver::writeCommandLocked( uint8_t command ) const { 497 499 gpio::setLevel( bus_.pins.dc, false ); 498 500 spi::transmit( device_, &command, sizeof( command ) ); 499 501 } 500 502 501 - void SpiPanelDriver::writeCommandLocked( uint8_t command, const uint8_t* data, size_t dataSize ) { 503 + void SpiPanelDriver::writeCommandLocked( uint8_t command, const uint8_t* data, 504 + size_t dataSize ) const { 502 505 writeCommandLocked( command ); 503 506 if ( data == nullptr || dataSize == 0 ) { 504 507 return; ··· 507 510 spi::transmit( device_, data, dataSize ); 508 511 } 509 512 510 - void SpiPanelDriver::writeCommand( Command command ) { 513 + void SpiPanelDriver::writeCommand( Command command ) const { 511 514 writeCommandLocked( static_cast<uint8_t>( command ) ); 512 515 } 513 516 514 - void SpiPanelDriver::writeCommand( Command command, const uint8_t* data, size_t dataSize ) { 517 + void SpiPanelDriver::writeCommand( Command command, const uint8_t* data, size_t dataSize ) const { 515 518 writeCommandLocked( static_cast<uint8_t>( command ), data, dataSize ); 516 519 } 517 520 518 - void SpiPanelDriver::writeRawCommand( uint8_t command, const uint8_t* data, size_t dataSize ) { 521 + void SpiPanelDriver::writeRawCommand( uint8_t command, const uint8_t* data, 522 + size_t dataSize ) const { 519 523 writeCommandLocked( command, data, dataSize ); 520 524 } 521 525 522 526 void SpiPanelDriver::setAddressWindow( int32_t posX, int32_t posY, int32_t widthValue, 523 - int32_t heightValue ) { 527 + int32_t heightValue ) const { 524 528 const uint8_t physicalRotation = internalRotation( rotation_, panel_.offsetRotation ); 525 529 const int32_t offsetX = offsetXForRotation( physicalRotation ); 526 530 const int32_t offsetY = offsetYForRotation( physicalRotation );
+29 -24
src/hal/display/spi_panel_driver.h
··· 39 39 40 40 int32_t width() const override; 41 41 int32_t height() const override; 42 - void drawPixel( int32_t posX, int32_t posY, uint16_t color ) override; 43 - void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ) override; 44 - void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ) override; 45 - void fillRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 46 - uint16_t color ) override; 42 + void drawPixel( int32_t posX, int32_t posY, uint16_t color ) const override; 43 + void drawFastHLine( int32_t posX, int32_t posY, int32_t width, uint16_t color ) const override; 44 + void drawFastVLine( int32_t posX, int32_t posY, int32_t height, uint16_t color ) const override; 45 + void fillRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 46 + uint16_t color ) const override; 47 47 48 48 void setRotation( uint8_t rotation ); 49 49 void setBrightness( uint8_t value ); 50 50 uint8_t brightness() const { return brightness_; } 51 51 bool canReadRect() const; 52 - void readRect( int32_t posX, int32_t posY, int32_t width, int32_t height, uint16_t* out ); 53 - void writePixels( int32_t posX, int32_t posY, int32_t width, int32_t height, 54 - const uint16_t* pixels ); 52 + void readRect( int32_t posX, int32_t posY, int32_t width, int32_t height, uint16_t* out ) const; 53 + void writePixels( int32_t posX, int32_t posY, int32_t width, int32_t height, 54 + const uint16_t* pixels ); 55 55 56 56 private: 57 57 enum class Command : uint8_t { ··· 81 81 void allocateShadowBuffer(); 82 82 void freeShadowBuffer(); 83 83 bool shadowBufferReady() const; 84 - void fillShadowRect( const ClipRect& rect, uint16_t color ); 84 + void fillShadowRect( const ClipRect& rect, uint16_t color ) const; 85 85 void writeShadowPixels( const ClipRect& rect, int32_t sourceX, int32_t sourceY, 86 - int32_t sourceWidth, const uint16_t* pixels ); 86 + int32_t sourceWidth, const uint16_t* pixels ) const; 87 87 void readShadowRect( int32_t posX, int32_t posY, int32_t width, int32_t height, 88 88 uint16_t* out ) const; 89 89 void configureBacklight(); 90 90 void applyBacklight( uint8_t value ); 91 91 void resetPanel(); 92 92 void initializePanel(); 93 - void writeCommandLocked( uint8_t command ); 94 - void writeCommandLocked( uint8_t command, const uint8_t* data, size_t dataSize ); 95 - void writeCommand( Command command ); 96 - void writeCommand( Command command, const uint8_t* data, size_t dataSize ); 97 - void writeRawCommand( uint8_t command, const uint8_t* data, size_t dataSize ); 98 - void setAddressWindow( int32_t posX, int32_t posY, int32_t width, int32_t height ); 93 + void writeCommandLocked( uint8_t command ) const; 94 + void writeCommandLocked( uint8_t command, const uint8_t* data, size_t dataSize ) const; 95 + void writeCommand( Command command ) const; 96 + void writeCommand( Command command, const uint8_t* data, size_t dataSize ) const; 97 + void writeRawCommand( uint8_t command, const uint8_t* data, size_t dataSize ) const; 98 + void setAddressWindow( int32_t posX, int32_t posY, int32_t width, int32_t height ) const; 99 99 int32_t offsetXForRotation( uint8_t rotation ) const; 100 100 int32_t offsetYForRotation( uint8_t rotation ) const; 101 101 102 102 catalog::PanelSpec panel_; 103 103 catalog::BusSpec bus_; 104 104 const kleidos::drivers::display::IDisplayController* controller_ = nullptr; 105 - kleidos::platform::spi::Device device_; 106 - uint8_t rotation_ = 0; 107 - uint8_t brightness_ = 100; // application brightness percent (0..100) 108 - bool ready_ = false; 109 - bool backlightPwmReady_ = false; 110 - uint16_t* shadowPixels_ = nullptr; 111 - size_t shadowPixelCount_ = 0; 112 - bool shadowPixelsOwned_ = false; 105 + // SPI device handle. `mutable` so the const draw path can call transmit(); 106 + // the device is external hardware, not C++ state of *this. 107 + mutable kleidos::platform::spi::Device device_; 108 + uint8_t rotation_ = 0; 109 + uint8_t brightness_ = 100; // application brightness percent (0..100) 110 + bool ready_ = false; 111 + bool backlightPwmReady_ = false; 112 + // Shadow buffer mirrors the external panel's GRAM (read-back shadow for 113 + // panels without native read). Marked `mutable` so the const draw path can 114 + // refresh it; the panel is external hardware, not C++ state of *this. 115 + mutable uint16_t* shadowPixels_ = nullptr; 116 + size_t shadowPixelCount_ = 0; 117 + bool shadowPixelsOwned_ = false; 113 118 }; 114 119 115 120 } // namespace display::driver
+6 -6
src/hal/display/text_renderer.cpp
··· 210 210 for ( const char* current = value; *current != '\0'; ++current ) { 211 211 lv_font_glyph_dsc_t glyph = {}; 212 212 const uint8_t currentValue = static_cast<uint8_t>( *current ); 213 - const uint8_t nextValue = static_cast<uint8_t>( current[1] ); 214 - if ( brandGlyphFor( font, currentValue, nextValue, glyph ) ) { 213 + if ( const uint8_t nextValue = static_cast<uint8_t>( current[1] ); 214 + brandGlyphFor( font, currentValue, nextValue, glyph ) ) { 215 215 width += glyph.adv_w; 216 216 } 217 217 } ··· 290 290 for ( const char* current = value; *current != '\0'; ++current ) { 291 291 lv_font_glyph_dsc_t glyph = {}; 292 292 const uint8_t currentValue = static_cast<uint8_t>( *current ); 293 - const uint8_t nextValue = static_cast<uint8_t>( current[1] ); 294 - if ( brandGlyphFor( font, currentValue, nextValue, glyph ) ) { 293 + if ( const uint8_t nextValue = static_cast<uint8_t>( current[1] ); 294 + brandGlyphFor( font, currentValue, nextValue, glyph ) ) { 295 295 drawBrandGlyph( surface, font, glyph, posX + scaledFloor( cursorAdvance, scale ), posY, 296 296 style, scale ); 297 297 cursorAdvance += glyph.adv_w; ··· 583 583 } 584 584 } 585 585 586 - void drawGlyph( graphics::Surface& surface, uint8_t value, int32_t posX, int32_t posY, 586 + void drawGlyph( const graphics::Surface& surface, uint8_t value, int32_t posX, int32_t posY, 587 587 int32_t scale, uint16_t foreground, bool bold ) { 588 588 const uint8_t* rows = glyphRows( normalizeChar( static_cast<char>( value ) ) ); 589 589 for ( int32_t row = 0; row < kGlyphHeight; ++row ) { ··· 674 674 : 0; 675 675 } 676 676 677 - void drawString( graphics::Surface& surface, const char* value, int32_t posX, int32_t posY, 677 + void drawString( const graphics::Surface& surface, const char* value, int32_t posX, int32_t posY, 678 678 const TextStyle& style ) { 679 679 if ( value == nullptr ) { 680 680 return;
+1 -1
src/hal/display/text_renderer.h
··· 55 55 * @param[in] posY Y coordinate in pixels. 56 56 * @param[in] style Visual attributes (font, color, scale, datum). 57 57 */ 58 - void drawString( graphics::Surface& surface, const char* value, int32_t posX, int32_t posY, 58 + void drawString( const graphics::Surface& surface, const char* value, int32_t posX, int32_t posY, 59 59 const TextStyle& style ); 60 60 61 61 /// @}
+117 -107
src/main.cpp
··· 43 43 #include <cmath> 44 44 #include <cstdlib> 45 45 #include <cstring> 46 + #include <iterator> 46 47 #include <string_view> 47 48 #ifdef DEBUG_SERIAL_BUTTONS 48 49 #include "ui/debug/serial_debug.h" ··· 68 69 #error "KLEIDOS_RELEASE must not be built with DEBUG_SERIAL_BUTTONS" 69 70 #endif 70 71 71 - // Kleidos targets C++23. ESP-IDF 5.5.4 builds src/ at -std=gnu++2b by default; 72 - // this floor makes the requirement explicit and framework-independent, so the 73 - // build fails loudly if a future toolchain regresses below C++23. 74 - static_assert( __cplusplus >= 202302L, "Kleidos requires C++23 (-std=gnu++23/c++23) or newer" ); 75 72 #if !HAS_LVGL && !HAS_KEYBOARD 76 73 #include "states/onboarding_state.h" 77 74 #endif ··· 89 86 #include "vault/sd_vault.h" 90 87 #endif 91 88 89 + // Kleidos targets C++23. ESP-IDF 5.5.4 builds src/ at -std=gnu++2b by default; 90 + // this floor makes the requirement explicit and framework-independent, so the 91 + // build fails loudly if a future toolchain regresses below C++23. 92 + static_assert( __cplusplus >= 202302L, "Kleidos requires C++23 (-std=gnu++23/c++23) or newer" ); 93 + 92 94 static constexpr const char* kTag = "Kleidos"; 93 95 94 96 namespace rtos = kleidos::platform::rtos; ··· 117 119 118 120 // Auth UI: PIN arc for button-only devices, passphrase for keyboard devices 119 121 #if HAS_LVGL 120 - #include "ui/lvgl/lvgl_passphrase_screen.h" 122 + #include "ui/lvgl/lvgl_passphrase_screen.h" // NOSONAR cpp:S954 — variant-gated UI header 121 123 static LvglPassphraseScreen authUI; 122 124 #elif HAS_KEYBOARD 123 - #include "ui/entry/passphrase_entry_ui.h" 125 + #include "ui/entry/passphrase_entry_ui.h" // NOSONAR cpp:S954 — variant-gated UI header 124 126 static PassphraseEntryUI authUI; 125 127 #else 126 - #include "ui/entry/pin_entry_ui.h" 128 + #include "ui/entry/pin_entry_ui.h" // NOSONAR cpp:S954 — variant-gated UI header 127 129 static PinEntryUI authUI; 128 130 #endif 129 131 ··· 153 155 154 156 /** @brief Return the FSM handler for the given application state, or nullptr. */ 155 157 static AppStateHandler* getHandler( AppState state ) { 158 + using enum AppState; 156 159 switch ( state ) { 157 - case AppState::PinEntry: 160 + case PinEntry: 158 161 return pinState; 159 - case AppState::VaultMenu: 162 + case VaultMenu: 160 163 return vaultState; 161 - case AppState::BleHidTyping: 164 + case BleHidTyping: 162 165 return bleState; 163 - case AppState::AdminMode: 166 + case AdminMode: 164 167 return adminState; 165 - case AppState::HwTest: 168 + case HwTest: 166 169 return hwTestState; 167 170 #if !HAS_LVGL && !HAS_KEYBOARD 168 - case AppState::ONBOARDING: 171 + case ONBOARDING: 169 172 return onboardingState; 170 173 #endif 171 174 default: ··· 197 200 198 201 #ifdef DEBUG_SERIAL_BUTTONS 199 202 static const char* appStateName( AppState state ) { 203 + using enum AppState; 200 204 switch ( state ) { 201 - case AppState::BootCheck: 205 + case BootCheck: 202 206 return "BOOT_CHECK"; 203 - case AppState::ONBOARDING: 207 + case ONBOARDING: 204 208 return "ONBOARDING"; 205 - case AppState::PinEntry: 209 + case PinEntry: 206 210 return "PIN_ENTRY"; 207 - case AppState::VaultMenu: 211 + case VaultMenu: 208 212 return "VAULT_MENU"; 209 - case AppState::BleHidTyping: 213 + case BleHidTyping: 210 214 return "BLE_HID_TYPING"; 211 - case AppState::AdminMode: 215 + case AdminMode: 212 216 return "ADMIN_MODE"; 213 - case AppState::HwTest: 217 + case HwTest: 214 218 return "HW_TEST"; 215 - case AppState::DeepSleep: 219 + case DeepSleep: 216 220 return "DEEP_SLEEP"; 217 221 } 218 222 return "UNKNOWN"; 219 223 } 220 224 221 225 static const char* bleStackStateName( kleidos::ble::StackState state ) { 226 + using enum kleidos::ble::StackState; 222 227 switch ( state ) { 223 - case kleidos::ble::StackState::OFF: 228 + case OFF: 224 229 return "OFF"; 225 - case kleidos::ble::StackState::STARTING: 230 + case STARTING: 226 231 return "STARTING"; 227 - case kleidos::ble::StackState::ADVERTISING: 232 + case ADVERTISING: 228 233 return "ADVERTISING"; 229 - case kleidos::ble::StackState::AUTHENTICATING: 234 + case AUTHENTICATING: 230 235 return "AUTHENTICATING"; 231 - case kleidos::ble::StackState::CONNECTED: 236 + case CONNECTED: 232 237 return "CONNECTED"; 233 - case kleidos::ble::StackState::TYPING: 238 + case TYPING: 234 239 return "TYPING"; 235 - case kleidos::ble::StackState::STOPPING: 240 + case STOPPING: 236 241 return "STOPPING"; 237 - case kleidos::ble::StackState::ERROR: 242 + case ERROR: 238 243 return "ERROR"; 239 244 } 240 245 return "UNKNOWN"; ··· 261 266 || !str::equals( lastScreen, screen, sizeof( lastScreen ) ) ) { 262 267 KLEIDOS_LOGI( kTag, "Debug screen: state=%s screen=%s", appStateName( ctx().appState ), 263 268 screen ); 264 - snprintf( lastScreen, sizeof( lastScreen ), "%s", screen ); 269 + str::format( lastScreen, sizeof( lastScreen ), "%s", screen ); 265 270 lastState = ctx().appState; 266 271 initialized = true; 267 272 } ··· 415 420 } 416 421 417 422 static const char* debugBleFieldName( DebugBleField field ) { 423 + using enum DebugBleField; 418 424 switch ( field ) { 419 - case DebugBleField::All: 425 + case All: 420 426 return "ALL"; 421 - case DebugBleField::User: 427 + case User: 422 428 return "USER"; 423 - case DebugBleField::Pass: 429 + case Pass: 424 430 return "PASS"; 425 431 } 426 432 return "UNKNOWN"; ··· 499 505 } 500 506 501 507 static void debugToastForSeverity( popup::Severity severity ) { 508 + using enum popup::Severity; 502 509 switch ( severity ) { 503 - case popup::Severity::SUCCESS: 510 + case SUCCESS: 504 511 popup::toast( "Credential typed", "Host accepted input", severity, 3500 ); 505 512 break; 506 - case popup::Severity::CAUTION: 513 + case CAUTION: 507 514 popup::toast( "Admin WiFi active", "Client connected", severity, 3500 ); 508 515 break; 509 - case popup::Severity::WARNING: 516 + case WARNING: 510 517 popup::toast( "Locking soon", "Tap to stay awake", severity, 3500 ); 511 518 break; 512 - case popup::Severity::INFO: 519 + case INFO: 513 520 default: 514 521 popup::toast( "Vault unlocked", "Ready to type", severity, 3500 ); 515 522 break; ··· 521 528 entry.id = id; 522 529 entry.favorite = favorite; 523 530 entry.sortOrder = sortOrder; 524 - snprintf( entry.name, sizeof( entry.name ), "%s", name ); 531 + str::format( entry.name, sizeof( entry.name ), "%s", name ); 525 532 } 526 533 527 534 static void seedDebugVaultPreview() { ··· 797 804 } 798 805 799 806 static const char* debugNavActionName( DebugNavAction action ) { 807 + using enum DebugNavAction; 800 808 switch ( action ) { 801 - case DebugNavAction::Down: 809 + case Down: 802 810 return "DOWN"; 803 - case DebugNavAction::Up: 811 + case Up: 804 812 return "UP"; 805 - case DebugNavAction::Select: 813 + case Select: 806 814 return "SELECT"; 807 - case DebugNavAction::Back: 815 + case Back: 808 816 return "BACK"; 809 - case DebugNavAction::TabNext: 817 + case TabNext: 810 818 return "TAB_NEXT"; 811 - case DebugNavAction::TabPrev: 819 + case TabPrev: 812 820 return "TAB_PREV"; 813 - case DebugNavAction::Settings: 821 + case Settings: 814 822 return "SETTINGS"; 815 - case DebugNavAction::Add: 823 + case Add: 816 824 return "ADD"; 817 - case DebugNavAction::Admin: 825 + case Admin: 818 826 return "ADMIN"; 819 - case DebugNavAction::Lock: 827 + case Lock: 820 828 return "LOCK"; 821 829 } 822 830 return "UNKNOWN"; ··· 905 913 static bool handleDebugNavKeyboard( DebugNavAction action, uint8_t repeat ) { 906 914 #if HAS_KEYBOARD 907 915 char key = key_code::kNone; 916 + using enum DebugNavAction; 908 917 switch ( action ) { 909 - case DebugNavAction::Down: 918 + case Down: 910 919 key = key_code::kDown; 911 920 break; 912 - case DebugNavAction::Up: 921 + case Up: 913 922 key = key_code::kUp; 914 923 break; 915 - case DebugNavAction::Select: 924 + case Select: 916 925 key = key_code::kSpace; 917 926 break; 918 - case DebugNavAction::Back: 927 + case Back: 919 928 key = key_code::kBackspace; 920 929 break; 921 - case DebugNavAction::TabNext: 930 + case TabNext: 922 931 key = key_code::kRight; 923 932 break; 924 - case DebugNavAction::TabPrev: 933 + case TabPrev: 925 934 key = key_code::kLeft; 926 935 break; 927 - case DebugNavAction::Settings: 936 + case Settings: 928 937 key = 's'; 929 938 break; 930 - case DebugNavAction::Add: 939 + case Add: 931 940 key = 'a'; 932 941 break; 933 - case DebugNavAction::Admin: 942 + case Admin: 934 943 key = 'w'; 935 944 break; 936 - case DebugNavAction::Lock: 945 + case Lock: 937 946 serial::printf( "NAV_FAIL: LOCK has no keyboard shortcut; use SLEEP\n" ); 938 947 return true; 939 948 } ··· 956 965 const bool threeButton = ctx().inputC != nullptr; 957 966 const uint32_t holdMs = kLongPressMs + kDebugNavHoldExtraMs; 958 967 968 + using enum DebugNavAction; 959 969 switch ( action ) { 960 - case DebugNavAction::Down: 970 + case Down: 961 971 if ( !serialDebugScheduleTap( threeButton ? 'C' : 'A', kDebugNavTapMs, 962 972 kDebugNavTapGapMs, repeat ) ) { 963 973 serial::printf( "NAV_FAIL: button queue busy\n" ); 964 974 return true; 965 975 } 966 976 break; 967 - case DebugNavAction::Up: { 977 + case Up: { 968 978 if ( threeButton ) { 969 979 if ( !serialDebugScheduleTap( 'A', kDebugNavTapMs, kDebugNavTapGapMs, repeat ) ) { 970 980 serial::printf( "NAV_FAIL: button queue busy\n" ); 971 981 return true; 972 982 } 973 983 } else { 974 - const uint8_t taps = static_cast<uint8_t>( repeat * 2U ); 975 - if ( taps > kDebugNavMaxRepeat 984 + if ( const auto taps = static_cast<uint8_t>( repeat * 2U ); 985 + taps > kDebugNavMaxRepeat 976 986 || !serialDebugScheduleTap( 'A', kDebugNavTapMs, kDebugNavDoubleTapGapMs, 977 987 taps ) ) { 978 988 serial::printf( "NAV_FAIL: 2-button PREV queue busy\n" ); ··· 981 991 } 982 992 break; 983 993 } 984 - case DebugNavAction::Select: 994 + case Select: 985 995 if ( repeat != 1 ) { 986 996 serial::printf( "NAV_FAIL: SELECT does not support repeat\n" ); 987 997 return true; ··· 996 1006 return true; 997 1007 } 998 1008 break; 999 - case DebugNavAction::Back: 1009 + case Back: 1000 1010 if ( !serialDebugScheduleTap( 'B', kDebugNavTapMs, kDebugNavTapGapMs, repeat ) ) { 1001 1011 serial::printf( "NAV_FAIL: button queue busy\n" ); 1002 1012 return true; 1003 1013 } 1004 1014 break; 1005 - case DebugNavAction::TabNext: 1015 + case TabNext: 1006 1016 if ( !threeButton || repeat != 1 || !serialDebugScheduleHold( 'C', holdMs ) ) { 1007 1017 serial::printf( "NAV_FAIL: TAB_NEXT requires one 3-button hold\n" ); 1008 1018 return true; 1009 1019 } 1010 1020 break; 1011 - case DebugNavAction::TabPrev: 1021 + case TabPrev: 1012 1022 if ( !threeButton || repeat != 1 || !serialDebugScheduleHold( 'A', holdMs ) ) { 1013 1023 serial::printf( "NAV_FAIL: TAB_PREV requires one 3-button hold\n" ); 1014 1024 return true; 1015 1025 } 1016 1026 break; 1017 - case DebugNavAction::Settings: { 1027 + case Settings: { 1018 1028 if ( !threeButton || repeat != 1 ) { 1019 1029 serial::printf( "NAV_FAIL: SETTINGS macro requires 3-button vault UI\n" ); 1020 1030 return true; ··· 1031 1041 { 'B', true, selectStartMs }, 1032 1042 { 'B', false, selectStartMs + kDebugNavTapMs }, 1033 1043 }; 1034 - if ( !scheduleDebugNavButtonMacro( events, sizeof( events ) / sizeof( events[0] ), 1044 + if ( !scheduleDebugNavButtonMacro( events, static_cast<uint8_t>( std::size( events ) ), 1035 1045 "SETTINGS" ) ) { 1036 1046 return true; 1037 1047 } 1038 1048 break; 1039 1049 } 1040 - case DebugNavAction::Admin: { 1050 + case Admin: { 1041 1051 if ( repeat != 1 ) { 1042 1052 serial::printf( "NAV_FAIL: ADMIN does not support repeat\n" ); 1043 1053 return true; ··· 1059 1069 { 'B', true, selectStartMs }, 1060 1070 { 'B', false, selectStartMs + kDebugNavTapMs }, 1061 1071 }; 1062 - if ( !scheduleDebugNavButtonMacro( events, sizeof( events ) / sizeof( events[0] ), 1063 - "ADMIN" ) ) { 1072 + if ( !scheduleDebugNavButtonMacro( 1073 + events, static_cast<uint8_t>( std::size( events ) ), "ADMIN" ) ) { 1064 1074 return true; 1065 1075 } 1066 1076 } else if ( !serialDebugScheduleHold( 'B', kBtnbWifiHoldMs + kDebugNavHoldExtraMs ) ) { ··· 1069 1079 } 1070 1080 break; 1071 1081 } 1072 - case DebugNavAction::Add: 1082 + case Add: 1073 1083 if ( repeat != 1 ) { 1074 1084 serial::printf( "NAV_FAIL: ADD does not support repeat\n" ); 1075 1085 return true; ··· 1079 1089 serial::printf( "NAV_OK action=%s input=DEBUG_MODAL repeat=1\n", 1080 1090 debugNavActionName( action ) ); 1081 1091 return true; 1082 - case DebugNavAction::Lock: 1092 + case Lock: 1083 1093 if ( !serialDebugScheduleTap( 'B', kDebugNavTapMs, kDebugNavTapGapMs, 1 ) ) { 1084 1094 serial::printf( "NAV_FAIL: button queue busy\n" ); 1085 1095 return true; ··· 1141 1151 } 1142 1152 1143 1153 ctx().bleCredential.wipe(); 1154 + using enum DebugBleField; 1144 1155 switch ( field ) { 1145 - case DebugBleField::All: 1156 + case All: 1146 1157 ctx().bleCredential = cred; 1147 1158 break; 1148 - case DebugBleField::User: 1159 + case User: 1149 1160 kleidos::platform::str::copy( ctx().bleCredential.name, cred.name ); 1150 1161 kleidos::platform::str::copy( ctx().bleCredential.pass, cred.user ); 1151 1162 break; 1152 - case DebugBleField::Pass: 1163 + case Pass: 1153 1164 kleidos::platform::str::copy( ctx().bleCredential.name, cred.name ); 1154 1165 kleidos::platform::str::copy( ctx().bleCredential.pass, cred.pass ); 1155 1166 break; ··· 1196 1207 return false; 1197 1208 } 1198 1209 1199 - const size_t userLen = kleidos::platform::str::length( ctx().bleCredential.user ); 1200 - if ( userLen > 0 ) { 1210 + if ( const size_t userLen = kleidos::platform::str::length( ctx().bleCredential.user ); 1211 + userLen > 0 ) { 1201 1212 s_debugBleTypingToken = kleidos::ble::Facade::typeCredential( 1202 1213 std::string_view{ ctx().bleCredential.user, userLen }, 1203 1214 std::string_view{ ctx().bleCredential.pass, ··· 1254 1265 return; 1255 1266 } 1256 1267 1257 - const uint32_t passkey = kleidos::ble::Facade::ncPasskey(); 1258 - if ( passkey != 0 && passkey != s_bleAutoAcceptedPasskey ) { 1268 + if ( const uint32_t passkey = kleidos::ble::Facade::ncPasskey(); 1269 + passkey != 0 && passkey != s_bleAutoAcceptedPasskey ) { 1259 1270 s_bleAutoAcceptedPasskey = passkey; 1260 1271 serial::printf( "BLEAUTOACCEPT_NC passkey=%06lu\n", static_cast<unsigned long>( passkey ) ); 1261 1272 kleidos::ble::Facade::resolveNc( true ); ··· 1309 1320 } 1310 1321 1311 1322 static void printDebugRtcSnapshot() { 1312 - const bool available = rtc::isAvailable(); 1313 - if ( !available ) { 1323 + if ( const bool available = rtc::isAvailable(); !available ) { 1314 1324 serial::printf( "RTC ok=0\n" ); 1315 1325 return; 1316 1326 } ··· 1527 1537 return true; 1528 1538 } 1529 1539 1530 - const uint8_t byte = static_cast<uint8_t>( value ); 1531 - const bool ok = hal::internal_i2c::writeRegister( static_cast<uint8_t>( address ), 1532 - static_cast<uint8_t>( reg ), &byte, 1, 1533 - hal::internal_i2c::kDefaultFrequencyHz ); 1540 + const auto byte = static_cast<uint8_t>( value ); 1541 + const bool ok = hal::internal_i2c::writeRegister( static_cast<uint8_t>( address ), 1542 + static_cast<uint8_t>( reg ), &byte, 1, 1543 + hal::internal_i2c::kDefaultFrequencyHz ); 1534 1544 serial::printf( "I2CWR addr=0x%02lx reg=0x%02lx val=0x%02x %s\n", address, reg, byte, 1535 1545 ok ? "ok" : "nack" ); 1536 1546 return true; ··· 2284 2294 #include "ota/ota_state_logic.h" 2285 2295 2286 2296 #include <cstdint> 2297 + #include <utility> 2287 2298 2288 2299 namespace { 2289 2300 constexpr const char* kNvsNsOta = "kleidos_ota"; ··· 2327 2338 ota::ImgState imgState = toOtaLogicState( platformOta::runningPartitionState() ); 2328 2339 2329 2340 // 3) Decide + act (pure logic is exercised in test_ota_state_logic). 2330 - auto action = ota::decideOnBoot( imgState, attempted, prevVer, KLEIDOS_VERSION ); 2331 - switch ( action ) { 2332 - case ota::BootAction::MarkValid: 2341 + using enum ota::BootAction; 2342 + switch ( const auto action = ota::decideOnBoot( imgState, attempted, prevVer, KLEIDOS_VERSION ); 2343 + action ) { 2344 + case MarkValid: 2333 2345 KLEIDOS_LOGI( kTag, "OTA pending verification — marking valid" ); 2334 2346 if ( !platformOta::markRunningAppValidCancelRollback() ) { 2335 2347 KLEIDOS_LOGE( kTag, "Failed to mark OTA app valid" ); 2336 2348 } 2337 2349 s_otaJustApplied = true; 2338 2350 break; 2339 - case ota::BootAction::RollbackDetected: 2351 + case RollbackDetected: 2340 2352 KLEIDOS_LOGW( kTag, "OTA rollback detected (prev=%s == current=%s)", prevVer, 2341 2353 KLEIDOS_VERSION ); 2342 2354 s_otaRollbackDetected = true; 2343 2355 break; 2344 - case ota::BootAction::NONE: 2356 + case NONE: 2345 2357 default: 2346 2358 break; 2347 2359 } 2348 2360 2349 2361 // 4) Always clear the attempted flag — we've processed it. 2350 - if ( attempted ) { 2351 - if ( prefs.open( kNvsNsOta, NvsStore::OpenMode::ReadWrite ) ) { 2352 - prefs.eraseKey( "attempted" ); 2353 - prefs.eraseKey( "prev_ver" ); 2354 - } 2362 + if ( attempted && prefs.open( kNvsNsOta, NvsStore::OpenMode::ReadWrite ) ) { 2363 + prefs.eraseKey( "attempted" ); 2364 + prefs.eraseKey( "prev_ver" ); 2355 2365 } 2356 2366 } 2357 2367 ··· 2411 2421 display::setFont( display::Font::BODY ); 2412 2422 display::setTextColor( theme::kText565, theme::kBg565 ); 2413 2423 char sizeStr[32]; 2414 - snprintf( sizeStr, sizeof( sizeStr ), "Size: %u KB", fwSize / 1024 ); 2424 + str::format( sizeStr, sizeof( sizeStr ), "Size: %u KB", fwSize / 1024 ); 2415 2425 display::drawString( sizeStr, cx, 80 ); 2416 2426 display::drawString( "Hold center 3s to update", cx, 110 ); 2417 2427 display::drawString( "Wait 5s to skip", cx, 130 ); ··· 2425 2435 bool doUpdate = false; 2426 2436 2427 2437 while ( platformClock::millis32() - startMs < 5000 ) { 2428 - bool pressed = !gpio::level( kSdUpdateConfirmPin ); // GPIO0 active LOW 2429 - if ( pressed ) { 2438 + if ( const bool pressed = !gpio::level( kSdUpdateConfirmPin ); 2439 + pressed ) { // GPIO0 active LOW 2430 2440 if ( holdStart == 0 ) 2431 2441 holdStart = platformClock::millis32(); 2432 2442 if ( platformClock::millis32() - holdStart >= 3000 ) { ··· 2450 2460 2451 2461 auto progressCb = []( uint8_t pct ) { 2452 2462 char buf[16]; 2453 - snprintf( buf, sizeof( buf ), "%u%%", pct ); 2463 + str::format( buf, sizeof( buf ), "%u%%", pct ); 2454 2464 display::fillRect( 0, 80, DISPLAY_WIDTH, 40, theme::kBg565 ); 2455 2465 display::setTextColor( theme::kPrimary565, theme::kBg565 ); 2456 2466 display::setFont( display::Font::HEADING ); ··· 2458 2468 display::drawString( buf, device::kScreenCx, 100 ); 2459 2469 }; 2460 2470 2461 - bool ok = SdVault::applyFirmware( progressCb ); 2462 - if ( ok ) { 2471 + if ( const bool ok = SdVault::applyFirmware( progressCb ); ok ) { 2463 2472 SdVault::removeFirmware(); 2464 2473 display::fillScreen( theme::kBg565 ); 2465 2474 display::setTextColor( theme::kSuccess565, theme::kBg565 ); ··· 2541 2550 ctx().bootCount = prefs.getU32( "boots", 0 ) + 1; 2542 2551 prefs.setU32( "boots", ctx().bootCount ); 2543 2552 2544 - uint8_t alm = prefs.getU8( "auto_lock", 0 ); 2545 - if ( alm <= static_cast<uint8_t>( AutoLockMode::TIMEOUT ) ) { 2553 + if ( const uint8_t alm = prefs.getU8( "auto_lock", 0 ); 2554 + alm <= std::to_underlying( AutoLockMode::TIMEOUT ) ) { 2546 2555 ctx().autoLockMode = static_cast<AutoLockMode>( alm ); 2547 2556 } 2548 2557 ··· 2856 2865 // Only for states that actually idle-sleep; ADMIN/HW_TEST have their own 2857 2866 // timers or no sleep. Debug UI-test builds suppress this completely so 2858 2867 // screenshots never carry stale countdown overlays. 2868 + using enum AppState; 2859 2869 switch ( ctx().appState ) { 2860 - case AppState::PinEntry: 2861 - case AppState::VaultMenu: 2862 - case AppState::BleHidTyping: { 2870 + case PinEntry: 2871 + case VaultMenu: 2872 + case BleHidTyping: { 2863 2873 uint32_t idle = nowMs - ctx().lastActivity; 2864 2874 uint32_t remaining = 2865 2875 ( idle >= ctx().idleSleepMs ) ? 0 : ( ctx().idleSleepMs - idle );
+3 -1
src/platform/adc.cpp
··· 4 4 5 5 #include "logger.h" 6 6 7 + #include <iterator> 8 + 7 9 #ifdef ESP_PLATFORM 8 10 #include <esp_adc/adc_cali.h> 9 11 #include <esp_adc/adc_cali_scheme.h> ··· 103 105 104 106 bool ensureUnit( adc_unit_t unit ) { 105 107 const auto index = static_cast<size_t>( unit ); 106 - if ( index >= sizeof( s_units ) / sizeof( s_units[0] ) ) { 108 + if ( index >= std::size( s_units ) ) { 107 109 return false; 108 110 } 109 111 if ( s_units[index].initialized ) {
+1 -2
src/platform/gpio.cpp
··· 90 90 return; 91 91 } 92 92 93 - IsrHandler handler = s_handlers[pinValue]; 94 - if ( handler != nullptr ) { 93 + if ( const IsrHandler handler = s_handlers[pinValue]; handler != nullptr ) { 95 94 handler(); 96 95 } 97 96 }
+2 -2
src/platform/http_server.cpp
··· 46 46 return ESP_FAIL; 47 47 } 48 48 49 - auto* server = static_cast<Server*>( request->user_ctx ); 49 + auto* server = static_cast<const Server*>( request->user_ctx ); 50 50 return server->dispatch( request ) ? ESP_OK : ESP_FAIL; 51 51 } 52 52 #endif ··· 169 169 size_t requestContentLength( RequestHandle request ) { 170 170 #ifdef ESP_PLATFORM 171 171 httpd_req_t* native = toRequest( request ); 172 - return native != nullptr ? static_cast<size_t>( native->content_len ) : 0; 172 + return native != nullptr ? native->content_len : 0; 173 173 #else 174 174 (void)request; 175 175 return 0;
+38 -24
src/platform/i2s.cpp
··· 43 43 44 44 #ifdef ESP_PLATFORM 45 45 46 - bool begin( const Config& config ) { 47 - if ( s_ready ) { 48 - end(); 49 - } 50 - 51 - const bool wantTx = config.doutPin >= 0; 52 - const bool wantRx = config.dinPin >= 0; 46 + bool allocateChannels( const Config& config, bool wantTx, bool wantRx ) { 53 47 if ( !wantTx && !wantRx ) { 54 48 KLEIDOS_LOGE( kTag, "No data pins configured" ); 55 49 return false; ··· 59 53 I2S_CHANNEL_DEFAULT_CONFIG( static_cast<i2s_port_t>( config.port ), I2S_ROLE_MASTER ); 60 54 chanCfg.auto_clear = true; 61 55 62 - esp_err_t err = 63 - i2s_new_channel( &chanCfg, wantTx ? &s_txHandle : nullptr, wantRx ? &s_rxHandle : nullptr ); 64 - if ( err != ESP_OK ) { 56 + if ( const esp_err_t err = i2s_new_channel( &chanCfg, wantTx ? &s_txHandle : nullptr, 57 + wantRx ? &s_rxHandle : nullptr ); 58 + err != ESP_OK ) { 65 59 KLEIDOS_LOGE( kTag, "i2s_new_channel failed: %d", static_cast<int>( err ) ); 66 60 s_txHandle = nullptr; 67 61 s_rxHandle = nullptr; 68 62 return false; 69 63 } 64 + return true; 65 + } 70 66 71 - const i2s_std_config_t stdCfg = { 67 + i2s_std_config_t makeStdConfig( const Config& config ) { 68 + return { 72 69 .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG( config.sampleRateHz ), 73 70 .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( toNativeWidth( config.bitsPerSample ), 74 71 toNativeSlot( config.slotMode ) ), ··· 91 88 }, 92 89 }, 93 90 }; 91 + } 92 + 93 + bool initChannel( i2s_chan_handle_t handle, const i2s_std_config_t& cfg, const char* label ) { 94 + if ( const esp_err_t err = i2s_channel_init_std_mode( handle, &cfg ); err != ESP_OK ) { 95 + KLEIDOS_LOGE( kTag, "%s init failed: %d", label, static_cast<int>( err ) ); 96 + return false; 97 + } 98 + return true; 99 + } 100 + 101 + bool begin( const Config& config ) { 102 + if ( s_ready ) { 103 + end(); 104 + } 105 + 106 + const bool wantTx = config.doutPin >= 0; 107 + const bool wantRx = config.dinPin >= 0; 108 + 109 + if ( !allocateChannels( config, wantTx, wantRx ) ) { 110 + return false; 111 + } 112 + 113 + const i2s_std_config_t stdCfg = makeStdConfig( config ); 94 114 95 115 bool ok = true; 96 116 if ( wantTx ) { 97 - err = i2s_channel_init_std_mode( s_txHandle, &stdCfg ); 98 - ok = ok && err == ESP_OK; 99 - if ( err != ESP_OK ) { 100 - KLEIDOS_LOGE( kTag, "TX init failed: %d", static_cast<int>( err ) ); 101 - } 117 + ok = initChannel( s_txHandle, stdCfg, "TX" ); 102 118 } 103 119 if ( ok && wantRx ) { 104 - err = i2s_channel_init_std_mode( s_rxHandle, &stdCfg ); 105 - ok = ok && err == ESP_OK; 106 - if ( err != ESP_OK ) { 107 - KLEIDOS_LOGE( kTag, "RX init failed: %d", static_cast<int>( err ) ); 108 - } 120 + ok = initChannel( s_rxHandle, stdCfg, "RX" ); 109 121 } 110 122 if ( ok && wantTx ) { 111 - ok = ok && i2s_channel_enable( s_txHandle ) == ESP_OK; 123 + ok = i2s_channel_enable( s_txHandle ) == ESP_OK; 112 124 } 113 125 if ( ok && wantRx ) { 114 - ok = ok && i2s_channel_enable( s_rxHandle ) == ESP_OK; 126 + ok = i2s_channel_enable( s_rxHandle ) == ESP_OK; 115 127 } 116 128 117 129 if ( !ok ) { ··· 173 185 return false; 174 186 } 175 187 176 - void end() {} 188 + void end() { 189 + // No-op: native host build has no I2S hardware to release. 190 + } 177 191 178 192 bool ready() { 179 193 return false;
+7 -3
src/platform/little_fs.cpp
··· 10 10 #include <stdio.h> 11 11 #include <string.h> 12 12 13 + namespace str = kleidos::platform::str; 14 + 13 15 #ifdef ESP_PLATFORM 16 + // NOSONAR cpp:S954 — ESP-IDF + POSIX headers exist only on-target; the 17 + // native build of this file must compile without them. 14 18 #include <esp_err.h> 15 19 #include <esp_littlefs.h> 16 20 ··· 201 205 } 202 206 203 207 DIR* dir = static_cast<DIR*>( dir_ ); 204 - while ( dirent* entry = readdir( dir ) ) { 208 + while ( const dirent* entry = readdir( dir ) ) { 205 209 if ( kleidos::platform::str::equals( entry->d_name, "." ) 206 210 || kleidos::platform::str::equals( entry->d_name, ".." ) ) { 207 211 continue; ··· 298 302 logicalPath_[0] = '\0'; 299 303 return; 300 304 } 301 - snprintf( logicalPath_, sizeof( logicalPath_ ), "%s", path ); 305 + str::format( logicalPath_, sizeof( logicalPath_ ), "%s", path ); 302 306 } 303 307 304 308 void File::setName( const char* name ) { ··· 306 310 name_[0] = '\0'; 307 311 return; 308 312 } 309 - snprintf( name_, sizeof( name_ ), "%s", name ); 313 + str::format( name_, sizeof( name_ ), "%s", name ); 310 314 } 311 315 312 316 LittleFsGuard::~LittleFsGuard() {
+13
src/platform/little_fs.h
··· 67 67 * @param out Destination buffer. 68 68 * @param outSize Maximum number of bytes to read. 69 69 * @return Number of bytes actually read; 0 at EOF or on error. 70 + * @note WONTFIX cpp:S5817 — the underlying ::read() syscall advances the 71 + * kernel file position, so this cannot be const even though no 72 + * `*this` member is written. Marking it const would mislead callers 73 + * about the observable state of the file handle. 70 74 */ 71 75 size_t read( uint8_t* out, size_t outSize ); 72 76 ··· 74 78 * @brief Read one byte from the file. 75 79 * 76 80 * @return The byte value (0-255), or a negative value at EOF or on error. 81 + * @note WONTFIX cpp:S5817 — delegates to the (non-const) buffer read above 82 + * and therefore advances the file position. 77 83 */ 78 84 int read(); 79 85 ··· 90 96 * @brief True when more data can be read from the file. 91 97 * 92 98 * @return True when the read position has not reached the end. 99 + * @note WONTFIX cpp:S5817 — implemented with three lseek(SEEK_CUR/END/SET) 100 + * syscalls. The implementation saves and restores the position, so 101 + * the observable file position is preserved, but the kernel-level 102 + * lseek call advances the position transiently. Marking the method 103 + * const would obscure that observation; kept non-const to match the 104 + * read() accessors above and to avoid misleading callers who do 105 + * interleave lseek from outside the wrapper. 93 106 */ 94 107 bool available(); 95 108
+6 -2
src/platform/ota.cpp
··· 2 2 3 3 #include "ota.h" 4 4 5 + #include "platform/safe_string.h" 6 + 5 7 #ifdef ESP_PLATFORM 6 8 #include <esp_ota_ops.h> 7 9 #endif ··· 9 11 #include <cstdint> 10 12 #include <cstdio> 11 13 #include <cstring> 14 + 15 + namespace str = kleidos::platform::str; 12 16 13 17 namespace kleidos::platform::ota { 14 18 ··· 43 47 return false; 44 48 } 45 49 46 - snprintf( partitionLabel_, sizeof( partitionLabel_ ), "%s", partition->label ); 50 + str::format( partitionLabel_, sizeof( partitionLabel_ ), "%s", partition->label ); 47 51 48 52 esp_ota_handle_t handle = 0; 49 53 lastStage_ = UpdateStage::Begin; ··· 182 186 if ( partition == nullptr ) { 183 187 return false; 184 188 } 185 - snprintf( output, outputLength, "%s", partition->label ); 189 + str::format( output, outputLength, "%s", partition->label ); 186 190 return true; 187 191 #else 188 192 output[0] = '\0';
+1 -2
src/platform/pwm.cpp
··· 119 119 return false; 120 120 } 121 121 122 - const uint32_t max = maxDuty( s_channels[channel].resolutionBits ); 123 - if ( duty > max ) { 122 + if ( const uint32_t max = maxDuty( s_channels[channel].resolutionBits ); duty > max ) { 124 123 duty = max; 125 124 } 126 125
+1 -2
src/platform/random.cpp
··· 40 40 // unsigned value: (0 - n) wraps to 2^32 - n in modular arithmetic. 41 41 const uint32_t threshold = ( 0U - upperExclusive ) % upperExclusive; 42 42 while ( true ) { 43 - const uint32_t value = u32(); 44 - if ( value >= threshold ) { 43 + if ( const uint32_t value = u32(); value >= threshold ) { 45 44 return value % upperExclusive; 46 45 } 47 46 }
+6 -6
src/platform/rtos_queue.h
··· 121 121 template <typename T> 122 122 inline QueueHandle createStaticQueue( QueueDepth depth, uint8_t* itemStorage, 123 123 StaticQueue& queueStorage ) { 124 - static_assert( std::is_trivially_copyable<T>::value, 124 + static_assert( std::is_trivially_copyable_v<T>, 125 125 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 126 126 return createStaticQueueRaw( depth, sizeof( T ), itemStorage, queueStorage ); 127 127 } ··· 137 137 */ 138 138 template <typename T> 139 139 inline bool queueSend( QueueHandle handle, const T& item, Ticks waitTicks = noWait() ) { 140 - static_assert( std::is_trivially_copyable<T>::value, 140 + static_assert( std::is_trivially_copyable_v<T>, 141 141 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 142 142 return queueSendRaw( handle, &item, waitTicks ); 143 143 } ··· 153 153 */ 154 154 template <typename T> 155 155 inline bool queueSendFromIsr( QueueHandle handle, const T& item, IsrYield& yield ) { 156 - static_assert( std::is_trivially_copyable<T>::value, 156 + static_assert( std::is_trivially_copyable_v<T>, 157 157 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 158 158 return queueSendFromIsrRaw( handle, &item, yield ); 159 159 } ··· 169 169 */ 170 170 template <typename T> 171 171 inline bool queueReceive( QueueHandle handle, T& out, Ticks waitTicks = waitForever() ) { 172 - static_assert( std::is_trivially_copyable<T>::value, 172 + static_assert( std::is_trivially_copyable_v<T>, 173 173 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 174 174 return queueReceiveRaw( handle, &out, waitTicks ); 175 175 } ··· 185 185 */ 186 186 template <typename T> 187 187 inline bool queueReceiveFromIsr( QueueHandle handle, T& out, IsrYield& yield ) { 188 - static_assert( std::is_trivially_copyable<T>::value, 188 + static_assert( std::is_trivially_copyable_v<T>, 189 189 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 190 190 return queueReceiveFromIsrRaw( handle, &out, yield ); 191 191 } ··· 194 194 template <typename T> 195 195 class Queue { 196 196 public: 197 - static_assert( std::is_trivially_copyable<T>::value, 197 + static_assert( std::is_trivially_copyable_v<T>, 198 198 "FreeRTOS queues copy bytes; use pointers for non-trivial types" ); 199 199 200 200 Queue() = default;
+1 -1
src/platform/rtos_task.cpp
··· 122 122 out[i].coreId = task.xCoreID == tskNO_AFFINITY ? -1 : static_cast<int>( task.xCoreID ); 123 123 out[i].priority = static_cast<Priority>( task.uxCurrentPriority ); 124 124 out[i].state = taskStateFromNative( task.eCurrentState ); 125 - out[i].stackHighWaterBytes = static_cast<uint32_t>( task.usStackHighWaterMark ); 125 + out[i].stackHighWaterBytes = task.usStackHighWaterMark; 126 126 out[i].runtimeCounter = task.ulRunTimeCounter; 127 127 } 128 128 return static_cast<uint32_t>( count );
+8 -4
src/platform/sd_fs.cpp
··· 10 10 #include <cstdio> 11 11 #include <cstring> 12 12 13 + namespace str = kleidos::platform::str; 14 + 13 15 #ifdef ESP_PLATFORM 16 + // NOSONAR cpp:S954 — ESP-IDF + FatFs + POSIX headers exist only on-target; 17 + // the native build of this file must compile without them. 14 18 #include <driver/gpio.h> 15 19 #include <driver/sdspi_host.h> 16 20 #include <driver/spi_common.h> ··· 119 123 bool findMountedFatFs( FATFS*& fs, DWORD& freeClusters ) { 120 124 char drive[4] = {}; 121 125 for ( uint8_t index = 0; index < 4; ++index ) { 122 - snprintf( drive, sizeof( drive ), "%u:", index ); 126 + str::format( drive, sizeof( drive ), "%u:", index ); 123 127 if ( f_getfree( drive, &freeClusters, &fs ) == FR_OK && fs != nullptr ) { 124 128 return true; 125 129 } ··· 238 242 } 239 243 240 244 DIR* dir = static_cast<DIR*>( dir_ ); 241 - while ( dirent* entry = readdir( dir ) ) { 245 + while ( const dirent* entry = readdir( dir ) ) { 242 246 if ( kleidos::platform::str::equals( entry->d_name, "." ) 243 247 || kleidos::platform::str::equals( entry->d_name, ".." ) ) { 244 248 continue; ··· 335 339 logicalPath_[0] = '\0'; 336 340 return; 337 341 } 338 - snprintf( logicalPath_, sizeof( logicalPath_ ), "%s", path ); 342 + str::format( logicalPath_, sizeof( logicalPath_ ), "%s", path ); 339 343 } 340 344 341 345 void File::setName( const char* name ) { ··· 343 347 name_[0] = '\0'; 344 348 return; 345 349 } 346 - snprintf( name_, sizeof( name_ ), "%s", name ); 350 + str::format( name_, sizeof( name_ ), "%s", name ); 347 351 } 348 352 349 353 bool begin( const Config& config ) {
+13
src/platform/sd_fs.h
··· 85 85 * @param out Destination buffer. 86 86 * @param outSize Maximum number of bytes to read. 87 87 * @return Number of bytes actually read; 0 at EOF or on error. 88 + * @note WONTFIX cpp:S5817 — the underlying ::read() syscall advances the 89 + * kernel file position, so this cannot be const even though no 90 + * `*this` member is written. Marking it const would mislead callers 91 + * about the observable state of the file handle. 88 92 */ 89 93 size_t read( uint8_t* out, size_t outSize ); 90 94 ··· 92 96 * @brief Read one byte from the file. 93 97 * 94 98 * @return The byte value (0-255), or a negative value at EOF or on error. 99 + * @note WONTFIX cpp:S5817 — delegates to the (non-const) buffer read above 100 + * and therefore advances the file position. 95 101 */ 96 102 int read(); 97 103 ··· 108 114 * @brief True when more data can be read from the file. 109 115 * 110 116 * @return True when the read position has not reached the end. 117 + * @note WONTFIX cpp:S5817 — implemented with three lseek(SEEK_CUR/END/SET) 118 + * syscalls. The implementation saves and restores the position, so 119 + * the observable file position is preserved, but the kernel-level 120 + * lseek call advances the position transiently. Marking the method 121 + * const would obscure that observation; kept non-const to match the 122 + * read() accessors above and to avoid misleading callers who do 123 + * interleave lseek from outside the wrapper. 111 124 */ 112 125 bool available(); 113 126
+5 -5
src/platform/serial_transport.cpp
··· 39 39 constexpr size_t kPrintfStackBufferBytes = 512; 40 40 41 41 #ifdef ESP_PLATFORM 42 - constexpr uint32_t kDefaultBaudRate = 115200; 43 - constexpr uint32_t kReadBufferBytes = 2048; 44 - constexpr uint32_t kWriteBufferBytes = 4096; 45 - constexpr uint32_t kWriteTimeoutMs = 50; 46 - constexpr uint32_t kFlushTimeoutMs = 50; 42 + constexpr uint32_t kDefaultBaudRate = 115200; 43 + constexpr uint32_t kReadBufferBytes = 2048; 44 + constexpr uint32_t kWriteBufferBytes = 4096; 45 + [[maybe_unused]] constexpr uint32_t kWriteTimeoutMs = 50; 46 + constexpr uint32_t kFlushTimeoutMs = 50; 47 47 48 48 bool s_started = false; 49 49 uint32_t s_baudRate = kDefaultBaudRate;
+6 -4
src/platform/system.cpp
··· 17 17 #include <cstdlib> 18 18 #endif 19 19 20 + #include "platform/safe_string.h" 21 + 20 22 #include <cstdint> 21 23 #include <cstdio> 22 24 ··· 107 109 if ( esp_image_verify( ESP_IMAGE_VERIFY, &imagePartition, &metadata ) != ESP_OK ) { 108 110 return 0; 109 111 } 110 - return static_cast<uint32_t>( metadata.image_len ); 112 + return metadata.image_len; 111 113 #else 112 114 return 0; 113 115 #endif ··· 116 118 uint32_t nextUpdatePartitionSizeBytes() { 117 119 #ifdef ESP_PLATFORM 118 120 const esp_partition_t* partition = esp_ota_get_next_update_partition( nullptr ); 119 - return partition != nullptr ? static_cast<uint32_t>( partition->size ) : 0; 121 + return partition != nullptr ? partition->size : 0; 120 122 #else 121 123 return 0; 122 124 #endif ··· 146 148 return false; 147 149 } 148 150 149 - snprintf( output, outputLength, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], 150 - mac[4], mac[5] ); 151 + kleidos::platform::str::format( output, outputLength, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], 152 + mac[1], mac[2], mac[3], mac[4], mac[5] ); 151 153 return true; 152 154 } 153 155
+2 -3
src/platform/udp_socket.cpp
··· 50 50 return false; 51 51 } 52 52 53 - int expected = kInvalidSocket; 54 - if ( !fd_.compare_exchange_strong( expected, socketFd ) ) { 53 + if ( int expected = kInvalidSocket; !fd_.compare_exchange_strong( expected, socketFd ) ) { 55 54 closeFd( socketFd ); 56 55 } 57 56 return true; ··· 161 160 return fd_.load(); 162 161 } 163 162 164 - void UdpSocket::closeFd( int socketFd ) { 163 + void UdpSocket::closeFd( int socketFd ) const { 165 164 closesocket( socketFd ); 166 165 } 167 166
+1 -1
src/platform/udp_socket.h
··· 107 107 108 108 private: 109 109 int fd() const; 110 - void closeFd( int fd ); 110 + void closeFd( int fd ) const; 111 111 112 112 std::atomic<int> fd_{ -1 }; 113 113 int lastError_ = 0;
+5 -2
src/platform/wifi_access_point.cpp
··· 18 18 #include <cstdio> 19 19 #include <cstring> 20 20 21 + namespace str = kleidos::platform::str; 22 + 21 23 namespace kleidos::platform::wifi { 22 24 23 25 namespace { ··· 44 46 if ( output == nullptr || outputLength == 0 ) { 45 47 return; 46 48 } 47 - snprintf( output, outputLength, "%s", text != nullptr ? text : "" ); 49 + str::format( output, outputLength, "%s", text != nullptr ? text : "" ); 48 50 } 49 51 50 52 uint8_t boundedSsidLength( const char* ssid ) { ··· 177 179 if ( apNetif_ != nullptr ) { 178 180 esp_netif_ip_info_t ipInfo{}; 179 181 if ( esp_netif_get_ip_info( toNetif( apNetif_ ), &ipInfo ) == ESP_OK ) { 180 - snprintf( output.ipAddress, sizeof( output.ipAddress ), IPSTR, IP2STR( &ipInfo.ip ) ); 182 + str::format( output.ipAddress, sizeof( output.ipAddress ), IPSTR, 183 + IP2STR( &ipInfo.ip ) ); 181 184 } 182 185 } 183 186
+13 -12
src/states/admin_state.cpp
··· 30 30 namespace rtos = kleidos::platform::rtos; 31 31 namespace platformClock = kleidos::platform::clock; 32 32 namespace platformSystem = kleidos::platform::system; 33 + namespace str = kleidos::platform::str; 33 34 using kleidos::platform::NvsStore; 34 35 35 36 void AdminStateHandler::enter() { ··· 139 140 drawAdminFooter(); 140 141 141 142 // OTA reboot check 142 - OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 143 + const OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 143 144 if ( ota.finished && ota.success ) { 144 145 KLEIDOS_LOGI( kTag, "OTA successful \xE2\x80\x94 rebooting" ); 145 146 // Persist rollback-detection markers: record the version we are ··· 209 210 lv_timer_handler(); 210 211 211 212 // OTA reboot check 212 - OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 213 + const OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 213 214 if ( ota.finished && ota.success ) { 214 215 KLEIDOS_LOGI( kTag, "OTA successful — rebooting" ); 215 216 NvsStore p; ··· 335 336 bcol = theme::kLockout565; 336 337 } 337 338 char battStr[8]; 338 - snprintf( battStr, sizeof( battStr ), "%u%%", battPct ); 339 + str::format( battStr, sizeof( battStr ), "%u%%", battPct ); 339 340 display::setFont( display::Font::SMALL ); 340 341 display::setTextDatum( kTopRight ); 341 342 display::setTextColor( bcol, theme::kSurface565 ); ··· 356 357 357 358 void AdminStateHandler::drawAdminFooter() { 358 359 // Compute hold progress for BtnA based on its press timer. 359 - uint32_t e = btnA_.elapsed(); 360 - uint8_t pA = 361 - ( e == 0 ) 362 - ? 0 363 - : static_cast<uint8_t>( ( e >= kLongPressMs ) ? 100 : ( e * 100 ) / kLongPressMs ); 360 + uint32_t e = btnA_.elapsed(); 361 + uint8_t pA = 0; 362 + if ( e != 0 ) { 363 + pA = ( e >= kLongPressMs ) ? 100 : static_cast<uint8_t>( ( e * 100 ) / kLongPressMs ); 364 + } 364 365 if ( static_cast<int8_t>( pA ) == lastFooterProgress_ && lastFooterProgress_ != -1 ) { 365 366 return; // No change — skip repaint. 366 367 } ··· 381 382 382 383 void AdminStateHandler::drawWifiQR() { 383 384 char qrData[128]; 384 - snprintf( qrData, sizeof( qrData ), "WIFI:T:WPA;S:%s;P:%s;;", WifiAdminPortal::getSsid(), 385 - WifiAdminPortal::getPassword() ); 385 + str::format( qrData, sizeof( qrData ), "WIFI:T:WPA;S:%s;P:%s;;", WifiAdminPortal::getSsid(), 386 + WifiAdminPortal::getPassword() ); 386 387 387 388 constexpr int16_t kHeaderH = 28; 388 389 ··· 497 498 const int16_t footerH = ActionBar::barHeight(); 498 499 const int16_t bandH = ( kScrH - footerH ) - kYStatus; 499 500 500 - OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 501 - const int n = WifiAdminPortal::getClientCount(); 501 + const OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 502 + const int n = WifiAdminPortal::getClientCount(); 502 503 503 504 const bool changed = ( n != lastClients_ ) || ( ota.active != lastOtaActive_ ) 504 505 || ( ota.percent != lastOtaPercent_ )
+1 -1
src/states/admin_state.h
··· 18 18 */ 19 19 class AdminStateHandler : public AppStateHandler { 20 20 public: 21 - explicit AdminStateHandler( AppContext& ctx ) : AppStateHandler( ctx ) {} 21 + using AppStateHandler::AppStateHandler; 22 22 23 23 void enter() override; 24 24 AppState update() override;
+7 -2
src/states/app_context.h
··· 182 182 * 183 183 * @param maxMhz Target maximum CPU frequency in MHz; applied unless the 184 184 * current frequency already matches or @c HAS_KEYBOARD is defined. 185 + * @note WONTFIX cpp:S5817 — no `*this` members are written, but the method 186 + * mutates CPU clock state via @c kleidos::platform::system::setCpuFrequencyMhz 187 + * and emits a KLEIDOS_LOGI diagnostic. The hardware side effect is 188 + * not representable in the C++ object model, so const would mislead 189 + * callers. Kept non-const. 185 190 */ 186 191 void configureDFS( int maxMhz, int /*minMhz*/, bool /*lightSleep*/ = false ) { 187 192 #if HAS_KEYBOARD 188 193 // T-Deck: skip DFS during bring-up — keep default CPU freq 189 194 (void)maxMhz; 190 195 #else 191 - uint32_t cur = kleidos::platform::system::cpuFrequencyMhz(); 192 - if ( static_cast<int>( cur ) != maxMhz ) { 196 + if ( const uint32_t cur = kleidos::platform::system::cpuFrequencyMhz(); 197 + static_cast<int>( cur ) != maxMhz ) { 193 198 kleidos::platform::system::setCpuFrequencyMhz( static_cast<uint32_t>( maxMhz ) ); 194 199 KLEIDOS_LOGI( kAppTag, "CPU freq: %lu → %d MHz", cur, maxMhz ); 195 200 }
+178 -156
src/states/ble_state.cpp
··· 19 19 #include <cstring> 20 20 21 21 namespace kble = kleidos::ble; 22 + namespace str = kleidos::platform::str; 22 23 namespace platformClock = kleidos::platform::clock; 23 24 namespace rtos = kleidos::platform::rtos; 24 25 25 26 static constexpr const char* kTag = "BleState"; 26 27 28 + namespace { 29 + 30 + const char* debugNameForStackState( kble::StackState s ) { 31 + using enum kble::StackState; 32 + switch ( s ) { 33 + case OFF: 34 + return "BLE_HID_TYPING/OFF"; 35 + case STARTING: 36 + return "BLE_HID_TYPING/STARTING"; 37 + case ADVERTISING: 38 + return "BLE_HID_TYPING/WAIT_HOST"; 39 + case AUTHENTICATING: 40 + return "BLE_HID_TYPING/AUTHENTICATING"; 41 + case CONNECTED: 42 + return "BLE_HID_TYPING/READY_TO_TYPE"; 43 + case TYPING: 44 + return "BLE_HID_TYPING/TYPING"; 45 + case STOPPING: 46 + return "BLE_HID_TYPING/STOPPING"; 47 + case ERROR: 48 + return "BLE_HID_TYPING/ERROR"; 49 + } 50 + return "BLE_HID_TYPING/UNKNOWN"; 51 + } 52 + 53 + } // namespace 54 + 27 55 void BleStateHandler::enter() { 28 56 ctx_.configureDFS( 240, 80 ); 29 57 screenDrawn_ = false; ··· 31 59 ctx_.resetActivity(); 32 60 // Show pairing spinner; it stays visible until we detect CONNECTED 33 61 // (or the user cancels / the state exits). 34 - popup::spinnerShow( "Pairing\xE2\x80\xA6" ); 62 + popup::spinnerShow( "Pairing\x{E2}\x{80}\x{A6}" ); 35 63 KLEIDOS_LOGI( kTag, "Entering BLE HID typing" ); 36 64 } 37 65 ··· 42 70 if ( kble::Facade::isNcPending() ) { 43 71 return "BLE_HID_TYPING/NUMERIC_COMPARE"; 44 72 } 45 - 46 - switch ( kble::Facade::state() ) { 47 - case kble::StackState::OFF: 48 - return "BLE_HID_TYPING/OFF"; 49 - case kble::StackState::STARTING: 50 - return "BLE_HID_TYPING/STARTING"; 51 - case kble::StackState::ADVERTISING: 52 - return "BLE_HID_TYPING/WAIT_HOST"; 53 - case kble::StackState::AUTHENTICATING: 54 - return "BLE_HID_TYPING/AUTHENTICATING"; 55 - case kble::StackState::CONNECTED: 56 - return "BLE_HID_TYPING/READY_TO_TYPE"; 57 - case kble::StackState::TYPING: 58 - return "BLE_HID_TYPING/TYPING"; 59 - case kble::StackState::STOPPING: 60 - return "BLE_HID_TYPING/STOPPING"; 61 - case kble::StackState::ERROR: 62 - return "BLE_HID_TYPING/ERROR"; 63 - } 64 - return "BLE_HID_TYPING/UNKNOWN"; 73 + return debugNameForStackState( kble::Facade::state() ); 65 74 } 66 75 67 76 AppState BleStateHandler::update() { ··· 79 88 // accept on both the device and the host for pairing to succeed. 80 89 if ( kble::Facade::isNcPending() ) { 81 90 char passkey[8]; 82 - snprintf( passkey, sizeof( passkey ), "%06lu", 83 - static_cast<unsigned long>( kble::Facade::ncPasskey() ) ); 91 + str::format( passkey, sizeof( passkey ), "%06lu", 92 + static_cast<unsigned long>( kble::Facade::ncPasskey() ) ); 84 93 popup::ConfirmResult cr = 85 94 popup::confirm( "Pair host?", passkey, "Pair", "Reject", popup::Severity::WARNING, 86 95 &ctx_.inputA, &ctx_.inputB, 30000 ); ··· 133 142 } 134 143 135 144 // B3.6.D: async typing in flight — poll status; suppress button 136 - // handling and screen-state redraws while running. 137 - if ( typingToken_ != 0 ) { 138 - kble::TypingStatus ts = kble::Facade::pollTyping( typingToken_ ); 139 - switch ( ts ) { 140 - case kble::TypingStatus::Pending: 141 - case kble::TypingStatus::Running: 142 - return AppState::BleHidTyping; 143 - case kble::TypingStatus::Success: { 144 - typingToken_ = 0; 145 - ctx_.audio.beepSuccess(); 146 - drawBleSuccessAnim(); 147 - popup::toast( i18n::tr( i18n::StringId::PopTyped ), 148 - i18n::tr( i18n::StringId::PopToHost ), popup::Severity::SUCCESS, 149 - 2000 ); 150 - rtos::delayMs( 1500 ); 151 - if ( ctx_.autoLockMode == AutoLockMode::AfterSend ) { 152 - kble::Facade::stop(); 153 - ctx_.goToSleep(); 154 - return AppState::BleHidTyping; 155 - } 145 + // handling and screen-state redraws while running. Extracted to a 146 + // helper so update()'s control-flow depth stays under the S134 threshold. 147 + if ( auto next = handleTypingPending(); next.has_value() ) { 148 + return *next; 149 + } 150 + 151 + if ( auto next = handleBleStateSwitch( bleState ); next.has_value() ) { 152 + return *next; 153 + } 154 + 155 + // Safety timeout (only when actively typing/connected and no pairing pending). 156 + if ( bleState != kble::StackState::ADVERTISING && !kble::Facade::isNcPending() 157 + && platformClock::millis32() - ctx_.lastActivity > ctx_.idleSleepMs ) { 158 + KLEIDOS_LOGW( kTag, "BLE session timeout" ); 159 + kble::Facade::stop(); 160 + ctx_.bleCredential.wipe(); 161 + ctx_.goToSleep(); 162 + } 163 + 164 + return AppState::BleHidTyping; 165 + } 166 + 167 + std::optional<AppState> BleStateHandler::handleTypingPending() { 168 + if ( typingToken_ == 0 ) { 169 + return std::nullopt; 170 + } 171 + kble::TypingStatus ts = kble::Facade::pollTyping( typingToken_ ); 172 + switch ( ts ) { 173 + case kble::TypingStatus::Pending: 174 + case kble::TypingStatus::Running: 175 + return AppState::BleHidTyping; 176 + case kble::TypingStatus::Success: { 177 + typingToken_ = 0; 178 + ctx_.audio.beepSuccess(); 179 + drawBleSuccessAnim(); 180 + popup::toast( i18n::tr( i18n::StringId::PopTyped ), 181 + i18n::tr( i18n::StringId::PopToHost ), popup::Severity::SUCCESS, 2000 ); 182 + rtos::delayMs( 1500 ); 183 + if ( ctx_.autoLockMode == AutoLockMode::AfterSend ) { 156 184 kble::Facade::stop(); 157 - return AppState::VaultMenu; 185 + ctx_.goToSleep(); 186 + return AppState::BleHidTyping; 158 187 } 159 - case kble::TypingStatus::Disconnected: 160 - case kble::TypingStatus::NotConnected: 161 - case kble::TypingStatus::Error: 162 - KLEIDOS_LOGW( kTag, "Typing failed: status=%d", static_cast<int>( ts ) ); 163 - typingToken_ = 0; 164 - kble::Facade::stop(); 165 - return AppState::VaultMenu; 188 + kble::Facade::stop(); 189 + return AppState::VaultMenu; 166 190 } 191 + case kble::TypingStatus::Disconnected: 192 + case kble::TypingStatus::NotConnected: 193 + case kble::TypingStatus::Error: 194 + KLEIDOS_LOGW( kTag, "Typing failed: status=%d", static_cast<int>( ts ) ); 195 + typingToken_ = 0; 196 + kble::Facade::stop(); 197 + return AppState::VaultMenu; 167 198 } 199 + return std::nullopt; 200 + } 168 201 202 + std::optional<AppState> BleStateHandler::handleBleStateSwitch( kble::StackState bleState ) { 203 + using enum kble::StackState; 169 204 switch ( bleState ) { 170 - case kble::StackState::ADVERTISING: 205 + case ADVERTISING: 171 206 // 3-button: any button cancels advertising 172 207 if ( ctx_.inputC && ( ctx_.inputA.wasPressed() || ctx_.inputC->wasPressed() ) ) { 173 208 KLEIDOS_LOGI( kTag, "BLE cancelled by user (3btn)" ); ··· 182 217 ctx_.bleCredential.wipe(); 183 218 return AppState::VaultMenu; 184 219 } 185 - break; 220 + return std::nullopt; 186 221 187 - case kble::StackState::CONNECTED: { 222 + case CONNECTED: { 188 223 if ( !kble::Facade::isConnected() ) { 189 224 KLEIDOS_LOGW( kTag, "Host disconnected before typing — waiting for BLE recovery" ); 190 225 return AppState::BleHidTyping; 191 226 } 192 - 193 - // 3-button: BtnA or BtnC = cancel, BtnB = send 194 - if ( ctx_.inputC ) { 195 - if ( ctx_.inputA.wasPressed() || ctx_.inputC->wasPressed() ) { 196 - KLEIDOS_LOGI( kTag, "BLE cancelled (3btn)" ); 197 - kble::Facade::stop(); 198 - ctx_.bleCredential.wipe(); 199 - return AppState::VaultMenu; 200 - } 201 - 202 - if ( ctx_.inputB.wasPressed() ) { 203 - ctx_.resetActivity(); 204 - KLEIDOS_LOGI( kTag, "Typing (3btn): user=%u pass=%u chars", 205 - kleidos::platform::str::length( ctx_.bleCredential.user ), 206 - kleidos::platform::str::length( ctx_.bleCredential.pass ) ); 207 - drawBleTypingScreen(); 208 - 209 - size_t userLen = kleidos::platform::str::length( ctx_.bleCredential.user ); 210 - if ( userLen > 0 ) { 211 - typingToken_ = kble::Facade::typeCredential( 212 - std::string_view{ ctx_.bleCredential.user, userLen }, 213 - std::string_view{ 214 - ctx_.bleCredential.pass, 215 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 216 - } else { 217 - typingToken_ = kble::Facade::typePassword( std::string_view{ 218 - ctx_.bleCredential.pass, 219 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 220 - } 221 - 222 - // Credential staged on Core 0; safe to wipe locally. 223 - ctx_.bleCredential.wipe(); 224 - 225 - if ( typingToken_ == 0 ) { 226 - KLEIDOS_LOGW( kTag, "beginTyping returned 0 — falling back" ); 227 - kble::Facade::stop(); 228 - return AppState::VaultMenu; 229 - } 230 - return AppState::BleHidTyping; 231 - } 232 - } else { 233 - // 2-button: hold to cancel, click to send 234 - if ( ctx_.inputA.wasReleaseFor( kLongPressMs ) ) { 235 - KLEIDOS_LOGI( kTag, "BLE cancelled (connected)" ); 236 - kble::Facade::stop(); 237 - ctx_.bleCredential.wipe(); 238 - return AppState::VaultMenu; 239 - } 240 - 241 - if ( ctx_.inputA.wasClicked() ) { 242 - ctx_.resetActivity(); 243 - KLEIDOS_LOGI( kTag, "Typing: user=%u pass=%u chars", 244 - kleidos::platform::str::length( ctx_.bleCredential.user ), 245 - kleidos::platform::str::length( ctx_.bleCredential.pass ) ); 246 - drawBleTypingScreen(); 247 - 248 - size_t userLen = kleidos::platform::str::length( ctx_.bleCredential.user ); 249 - if ( userLen > 0 ) { 250 - typingToken_ = kble::Facade::typeCredential( 251 - std::string_view{ ctx_.bleCredential.user, userLen }, 252 - std::string_view{ 253 - ctx_.bleCredential.pass, 254 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 255 - } else { 256 - typingToken_ = kble::Facade::typePassword( std::string_view{ 257 - ctx_.bleCredential.pass, 258 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 259 - } 260 - 261 - // Credential staged on Core 0; safe to wipe locally. 262 - ctx_.bleCredential.wipe(); 263 - 264 - if ( typingToken_ == 0 ) { 265 - KLEIDOS_LOGW( kTag, "beginTyping returned 0 — falling back" ); 266 - kble::Facade::stop(); 267 - return AppState::VaultMenu; 268 - } 269 - return AppState::BleHidTyping; 270 - } 271 - } 272 - break; 227 + return handleConnected(); 273 228 } 274 229 275 - case kble::StackState::ERROR: 230 + case ERROR: 276 231 drawBleScreen(); 277 232 rtos::delayMs( 2000 ); 278 233 kble::Facade::stop(); 279 234 ctx_.bleCredential.wipe(); 280 235 return AppState::VaultMenu; 281 236 282 - default: 237 + case OFF: 238 + case STARTING: 239 + case AUTHENTICATING: 240 + case TYPING: 241 + case STOPPING: 283 242 break; 284 243 } 244 + return std::nullopt; 245 + } 285 246 286 - // Safety timeout (only when actively typing/connected and no pairing pending). 287 - if ( bleState != kble::StackState::ADVERTISING && !kble::Facade::isNcPending() 288 - && platformClock::millis32() - ctx_.lastActivity > ctx_.idleSleepMs ) { 289 - KLEIDOS_LOGW( kTag, "BLE session timeout" ); 247 + void BleStateHandler::exit() { 248 + popup::spinnerHide(); 249 + KLEIDOS_LOGI( kTag, "Exiting BLE state" ); 250 + } 251 + 252 + // --------------------------------------------------------------------------- 253 + // CONNECTED-case helpers (extracted from update() to keep the switch 254 + // arm under the S134 nesting threshold). 255 + // --------------------------------------------------------------------------- 256 + AppState BleStateHandler::handleConnected() { 257 + // 3-button layout: BtnA/BtnC = cancel, BtnB = send. 258 + if ( ctx_.inputC ) { 259 + return handleConnected3Button(); 260 + } 261 + // 2-button layout: hold to cancel, click to send. 262 + return handleConnected2Button(); 263 + } 264 + 265 + AppState BleStateHandler::handleConnected3Button() { 266 + if ( ctx_.inputA.wasPressed() || ctx_.inputC->wasPressed() ) { 267 + KLEIDOS_LOGI( kTag, "BLE cancelled (3btn)" ); 268 + cancelBle(); 269 + return AppState::VaultMenu; 270 + } 271 + if ( ctx_.inputB.wasPressed() ) { 272 + return beginTyping(); 273 + } 274 + return AppState::BleHidTyping; 275 + } 276 + 277 + AppState BleStateHandler::handleConnected2Button() { 278 + if ( ctx_.inputA.wasReleaseFor( kLongPressMs ) ) { 279 + KLEIDOS_LOGI( kTag, "BLE cancelled (connected)" ); 280 + cancelBle(); 281 + return AppState::VaultMenu; 282 + } 283 + if ( ctx_.inputA.wasClicked() ) { 284 + return beginTyping(); 285 + } 286 + return AppState::BleHidTyping; 287 + } 288 + 289 + AppState BleStateHandler::beginTyping() { 290 + ctx_.resetActivity(); 291 + KLEIDOS_LOGI( kTag, "Typing: user=%u pass=%u chars", 292 + kleidos::platform::str::length( ctx_.bleCredential.user ), 293 + kleidos::platform::str::length( ctx_.bleCredential.pass ) ); 294 + drawBleTypingScreen(); 295 + 296 + if ( const size_t userLen = kleidos::platform::str::length( ctx_.bleCredential.user ); 297 + userLen > 0 ) { 298 + typingToken_ = kble::Facade::typeCredential( 299 + std::string_view{ ctx_.bleCredential.user, userLen }, 300 + std::string_view{ ctx_.bleCredential.pass, 301 + kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 302 + } else { 303 + typingToken_ = kble::Facade::typePassword( std::string_view{ 304 + ctx_.bleCredential.pass, kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 305 + } 306 + 307 + // Credential staged on Core 0; safe to wipe locally. 308 + ctx_.bleCredential.wipe(); 309 + 310 + if ( typingToken_ == 0 ) { 311 + KLEIDOS_LOGW( kTag, "beginTyping returned 0 — falling back" ); 290 312 kble::Facade::stop(); 291 - ctx_.bleCredential.wipe(); 292 - ctx_.goToSleep(); 313 + return AppState::VaultMenu; 293 314 } 294 - 295 315 return AppState::BleHidTyping; 296 316 } 297 317 298 - void BleStateHandler::exit() { 299 - popup::spinnerHide(); 300 - KLEIDOS_LOGI( kTag, "Exiting BLE state" ); 318 + void BleStateHandler::cancelBle() { 319 + kble::Facade::stop(); 320 + ctx_.bleCredential.wipe(); 301 321 } 302 322 303 323 // --------------------------------------------------------------------------- ··· 360 380 361 381 } // namespace 362 382 363 - void BleStateHandler::drawBleScreen() { 383 + void BleStateHandler::drawBleScreen() const { 364 384 display::fillScreen( theme::kBg565 ); 365 385 kble::StackState state = kble::Facade::state(); 366 386 ··· 368 388 // Footer is built per-state via the global ActionBar. 369 389 // Cell A defaults to "PAIR" hint while advertising; CONNECTED swaps to 370 390 // a tap=TYPE / hold=CANCEL pair so a single line covers both gestures. 371 - ActionCell footA{}, footB{}; 391 + ActionCell footA{}; 392 + ActionCell footB{}; 372 393 bool drawFooter = true; 373 394 switch ( state ) { 374 395 case kble::StackState::ADVERTISING: ··· 423 444 display::setFont( display::Font::MONO ); 424 445 display::setTextColor( theme::kText565, theme::kBg565 ); 425 446 char host[20]; 426 - snprintf( host, sizeof( host ), "%.*s", static_cast<int>( kScrW < 160 ? 16 : 18 ), 427 - hostName[0] ? hostName : "paired" ); 447 + str::format( host, sizeof( host ), "%.*s", kScrW < 160 ? 16 : 18, 448 + hostName[0] ? hostName : "paired" ); 428 449 display::drawString( host, kScrCx, bodyY + 50 ); 429 450 430 451 display::setFont( display::Font::HEADING ); ··· 437 458 break; 438 459 } 439 460 case kble::StackState::TYPING: { 440 - drawStatusBadge( bodyY + 8, "TYPING\xE2\x80\xA6", theme::kLockout565, theme::kBg565 ); 461 + drawStatusBadge( bodyY + 8, "TYPING\x{E2}\x{80}\x{A6}", theme::kLockout565, 462 + theme::kBg565 ); 441 463 display::setFont( display::Font::SMALL ); 442 464 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 443 465 display::setTextDatum( kTopCenter ); ··· 451 473 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 452 474 display::setTextDatum( kTopCenter ); 453 475 display::drawString( "Connection failed", kScrCx, bodyY + 42 ); 454 - display::drawString( "Returning\xE2\x80\xA6", kScrCx, bodyY + 56 ); 476 + display::drawString( "Returning\x{E2}\x{80}\x{A6}", kScrCx, bodyY + 56 ); 455 477 display::setTextDatum( kTopLeft ); 456 478 break; 457 479 } ··· 464 486 } 465 487 } 466 488 467 - void BleStateHandler::drawBleTypingScreen() { 489 + void BleStateHandler::drawBleTypingScreen() const { 468 490 // Coherent with drawBleScreen() TYPING branch. 469 491 display::fillScreen( theme::kBg565 ); 470 492 int16_t bodyY = drawBleHeader( "ble / typing" ); 471 - drawStatusBadge( bodyY + 8, "TYPING\xE2\x80\xA6", theme::kLockout565, theme::kBg565 ); 493 + drawStatusBadge( bodyY + 8, "TYPING\x{E2}\x{80}\x{A6}", theme::kLockout565, theme::kBg565 ); 472 494 display::setFont( display::Font::SMALL ); 473 495 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 474 496 display::setTextDatum( kTopCenter ); ··· 476 498 display::setTextDatum( kTopLeft ); 477 499 } 478 500 479 - void BleStateHandler::drawBleSuccessAnim() { 501 + void BleStateHandler::drawBleSuccessAnim() const { 480 502 display::fillScreen( theme::kBg565 ); 481 503 display::setFont( display::Font::HEADING ); 482 504 display::setTextColor( theme::kSuccess565, theme::kBg565 );
+25 -3
src/states/ble_state.h
··· 8 8 #include "app_state_handler.h" 9 9 #include "ble/ble_facade.h" 10 10 11 + #include <optional> 12 + 11 13 /** 12 14 * @brief Brings BLE up, types the selected credential to the paired host, then 13 15 * tears the radio down and returns to the vault menu or deep sleep. ··· 30 32 // flight; reset to 0 once `pollTyping` returns a terminal status. 31 33 uint32_t typingToken_ = 0; 32 34 33 - void drawBleScreen(); 34 - void drawBleTypingScreen(); 35 - void drawBleSuccessAnim(); 35 + void drawBleScreen() const; 36 + void drawBleTypingScreen() const; 37 + void drawBleSuccessAnim() const; 38 + 39 + // CONNECTED-case helpers, extracted from update() to keep nesting flat. 40 + AppState handleConnected(); 41 + AppState handleConnected3Button(); 42 + AppState handleConnected2Button(); 43 + AppState beginTyping(); 44 + void cancelBle(); 45 + 46 + // Typing-pending helper: encapsulates the if(typingToken_)/switch(ts) block 47 + // so update()'s control-flow depth stays under the S134 threshold. 48 + // Returns the next AppState when the block fully decides the transition; 49 + // returns std::nullopt when the typing poll is still in flight and the 50 + // caller should keep ticking in BLE_HID_TYPING. 51 + std::optional<AppState> handleTypingPending(); 52 + 53 + // BLE-state-switch helper: handles the switch(bleState) arm of update(). 54 + // Returns the next AppState when a case fully decides the transition; 55 + // returns std::nullopt for the default arm (caller continues to the 56 + // safety-timeout check). 57 + std::optional<AppState> handleBleStateSwitch( kleidos::ble::StackState bleState ); 36 58 };
+14 -10
src/states/change_pin_logic.cpp
··· 26 26 } 27 27 28 28 ChangePinResult ChangePinLogic::verifyCurrent( bool ok ) { 29 - if ( step_ != ChangePinStep::VerifyCurrent ) { 29 + using enum ChangePinStep; 30 + if ( step_ != VerifyCurrent ) { 30 31 return ChangePinResult::CONTINUE; 31 32 } 32 33 if ( !ok ) { 33 34 return ChangePinResult::WrongCurrent; 34 35 } 35 - step_ = ChangePinStep::EnterNew; 36 + step_ = EnterNew; 36 37 return ChangePinResult::CONTINUE; 37 38 } 38 39 39 40 ChangePinResult ChangePinLogic::submitNew( const char* pin, size_t len ) { 40 - if ( step_ != ChangePinStep::EnterNew ) { 41 + using enum ChangePinStep; 42 + if ( step_ != EnterNew ) { 41 43 return ChangePinResult::CONTINUE; 42 44 } 43 45 if ( pin == nullptr || len < kChangePinMin || len > kChangePinMax ) { ··· 49 51 } 50 52 newPin_[len] = '\0'; 51 53 newLen_ = len; 52 - step_ = ChangePinStep::ConfirmNew; 54 + step_ = ConfirmNew; 53 55 return ChangePinResult::CONTINUE; 54 56 } 55 57 56 58 ChangePinResult ChangePinLogic::submitConfirm( const char* pin, size_t len ) { 57 - if ( step_ != ChangePinStep::ConfirmNew ) { 59 + using enum ChangePinStep; 60 + if ( step_ != ConfirmNew ) { 58 61 return ChangePinResult::CONTINUE; 59 62 } 60 63 if ( pin == nullptr || len == 0 || len != newLen_ ) { ··· 62 65 mbedtls_platform_zeroize( newPin_, sizeof( newPin_ ) ); 63 66 newLen_ = 0; 64 67 confirmLen_ = 0; 65 - step_ = ChangePinStep::EnterNew; 68 + step_ = EnterNew; 66 69 return ChangePinResult::MISMATCH; 67 70 } 68 71 for ( size_t i = 0; i < len; ++i ) { ··· 80 83 if ( !eq ) { 81 84 mbedtls_platform_zeroize( newPin_, sizeof( newPin_ ) ); 82 85 newLen_ = 0; 83 - step_ = ChangePinStep::EnterNew; 86 + step_ = EnterNew; 84 87 return ChangePinResult::MISMATCH; 85 88 } 86 - step_ = ChangePinStep::ReEncrypt; 89 + step_ = ReEncrypt; 87 90 return ChangePinResult::CONTINUE; 88 91 } 89 92 90 93 ChangePinResult ChangePinLogic::finishReencrypt( bool ok ) { 91 - if ( step_ != ChangePinStep::ReEncrypt ) { 94 + using enum ChangePinStep; 95 + if ( step_ != ReEncrypt ) { 92 96 return ChangePinResult::CONTINUE; 93 97 } 94 98 if ( !ok ) { 95 99 wipe(); 96 - step_ = ChangePinStep::VerifyCurrent; 100 + step_ = VerifyCurrent; 97 101 return ChangePinResult::FAILED; 98 102 } 99 103 // Success — wipe stored new PIN now that caller has re-encrypted.
+14 -10
src/states/cred_detail_logic.cpp
··· 30 30 } 31 31 32 32 CredDetailResult CredDetailLogic::tick( uint32_t nowMs ) { 33 - if ( step_ == CredDetailStep::REVEAL && nowMs >= revealUntil_ ) { 34 - step_ = CredDetailStep::DETAIL; 33 + using enum CredDetailStep; 34 + if ( step_ == REVEAL && nowMs >= revealUntil_ ) { 35 + step_ = DETAIL; 35 36 revealUntil_ = 0; 36 37 } 37 38 return CredDetailResult::CONTINUE; 38 39 } 39 40 40 41 CredDetailResult CredDetailLogic::requestDelete() { 41 - if ( step_ != CredDetailStep::DETAIL && step_ != CredDetailStep::REVEAL ) { 42 + using enum CredDetailStep; 43 + if ( step_ != DETAIL && step_ != REVEAL ) { 42 44 return CredDetailResult::CONTINUE; 43 45 } 44 - step_ = CredDetailStep::DeleteConfirm; 46 + step_ = DeleteConfirm; 45 47 revealUntil_ = 0; 46 48 return CredDetailResult::CONTINUE; 47 49 } 48 50 49 51 CredDetailResult CredDetailLogic::confirmDelete( bool confirmed ) { 50 - if ( step_ != CredDetailStep::DeleteConfirm ) { 52 + using enum CredDetailStep; 53 + if ( step_ != DeleteConfirm ) { 51 54 return CredDetailResult::CONTINUE; 52 55 } 53 56 if ( confirmed ) { 54 - step_ = CredDetailStep::LIST; 57 + step_ = LIST; 55 58 revealUntil_ = 0; 56 59 return CredDetailResult::DELETED; 57 60 } 58 - step_ = CredDetailStep::DETAIL; 61 + step_ = DETAIL; 59 62 return CredDetailResult::CONTINUE; 60 63 } 61 64 62 65 CredDetailResult CredDetailLogic::back() { 63 - if ( step_ == CredDetailStep::DeleteConfirm ) { 66 + using enum CredDetailStep; 67 + if ( step_ == DeleteConfirm ) { 64 68 // Must resolve the modal first. 65 69 return CredDetailResult::CONTINUE; 66 70 } 67 - if ( step_ == CredDetailStep::LIST ) { 71 + if ( step_ == LIST ) { 68 72 return CredDetailResult::CONTINUE; 69 73 } 70 - step_ = CredDetailStep::LIST; 74 + step_ = LIST; 71 75 credId_ = 0; 72 76 revealUntil_ = 0; 73 77 return CredDetailResult::CLOSED;
+13 -6
src/states/hw_test_state.cpp
··· 127 127 display::drawFastHLine( 0, 34, kScrW, theme::kTextSec565 ); 128 128 } 129 129 130 - void HwTestStateHandler::drawFooter() { 130 + void HwTestStateHandler::drawFooter() const { 131 131 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 132 132 display::setFont( display::Font::BODY ); 133 133 display::setTextDatum( kMiddleCenter ); ··· 229 229 uint16_t barW = kScrW - 20; 230 230 display::drawRect( 10, 90, barW + 2, 20, theme::kText565 ); 231 231 display::drawRect( 10 + barW + 2, 95, 6, 10, theme::kText565 ); 232 - uint16_t fillColor = ( pct > 50 ) ? theme::kSuccess565 233 - : ( pct > 20 ) ? theme::kLockout565 234 - : theme::kWarning565; 232 + uint16_t fillColor = theme::kWarning565; 233 + if ( pct > 50 ) { 234 + fillColor = theme::kSuccess565; 235 + } else if ( pct > 20 ) { 236 + fillColor = theme::kLockout565; 237 + } 235 238 uint16_t fillWidth = static_cast<uint16_t>( pct * barW / 100 ); 236 239 display::fillRect( 11, 91, fillWidth, 18, fillColor ); 237 240 display::fillRect( 11 + fillWidth, 91, barW - fillWidth, 18, theme::kSubtle565 ); ··· 297 300 drawFooter(); 298 301 return; 299 302 } 300 - float ax = NAN, ay = NAN, az = NAN; 303 + float ax = NAN; 304 + float ay = NAN; 305 + float az = NAN; 301 306 imu->readAccel( ax, ay, az ); 302 307 display::setTextColor( theme::kSuccess565, theme::kBg565 ); 303 308 display::setCursor( 4, 42 ); ··· 306 311 display::printf( "X:%+.2f Y:%+.2f", static_cast<double>( ax ), static_cast<double>( ay ) ); 307 312 display::setCursor( 4, 66 ); 308 313 display::printf( "Z:%+.2f", static_cast<double>( az ) ); 309 - float gx = 0, gy = 0, gz = 0; 314 + float gx = 0; 315 + float gy = 0; 316 + float gz = 0; 310 317 imu->readGyro( gx, gy, gz ); 311 318 display::setTextColor( theme::kPrimary565, theme::kBg565 ); 312 319 display::setCursor( 4, 82 );
+2 -2
src/states/hw_test_state.h
··· 12 12 /// @ingroup states 13 13 class HwTestStateHandler : public AppStateHandler { 14 14 public: 15 - explicit HwTestStateHandler( AppContext& ctx ) : AppStateHandler( ctx ) {} 15 + using AppStateHandler::AppStateHandler; 16 16 17 17 void enter() override; 18 18 AppState update() override; ··· 44 44 45 45 void drawCurrent(); 46 46 void drawHeader( const char* title ); 47 - void drawFooter(); 47 + void drawFooter() const; 48 48 void drawBootInfo(); 49 49 void drawButtonTest(); 50 50 void drawBattery();
+1 -1
src/states/onboarding_logic.cpp
··· 103 103 return OnboardingResult::CONTINUE; 104 104 } 105 105 106 - OnboardingResult OnboardingLogic::acknowledgeDone() { 106 + OnboardingResult OnboardingLogic::acknowledgeDone() const { 107 107 if ( step_ != OnboardingStep::DONE ) { 108 108 return OnboardingResult::CONTINUE; 109 109 }
+1 -1
src/states/onboarding_logic.h
··· 136 136 * 137 137 * @return FINISHED to signal that onboarding is complete. 138 138 */ 139 - OnboardingResult acknowledgeDone(); 139 + OnboardingResult acknowledgeDone() const; 140 140 141 141 /** 142 142 * @brief Progress fraction for the given step in [0, 1].
+91 -80
src/states/onboarding_state.cpp
··· 28 28 namespace platformSystem = kleidos::platform::system; 29 29 30 30 const char* OnboardingStateHandler::debugScreenName() const { 31 + using enum Phase; 31 32 switch ( phase_ ) { 32 - case Phase::WELCOME: 33 + case WELCOME: 33 34 return "ONBOARDING/WELCOME"; 34 - case Phase::BtnAIntro: 35 + case BtnAIntro: 35 36 return "ONBOARDING/BTN_A_INTRO"; 36 - case Phase::BtnBIntro: 37 + case BtnBIntro: 37 38 return "ONBOARDING/BTN_B_INTRO"; 38 - case Phase::LANGUAGE: 39 + case LANGUAGE: 39 40 return "ONBOARDING/LANGUAGE"; 40 - case Phase::CreatePin: 41 + case CreatePin: 41 42 return "ONBOARDING/CREATE_PIN"; 42 - case Phase::ConfirmPin: 43 + case ConfirmPin: 43 44 return "ONBOARDING/CONFIRM_PIN"; 44 - case Phase::MismatchFlash: 45 + case MismatchFlash: 45 46 return "ONBOARDING/MISMATCH_FLASH"; 46 - case Phase::PAIR: 47 + case PAIR: 47 48 return "ONBOARDING/PAIR"; 48 - case Phase::DEMO: 49 + case DEMO: 49 50 return "ONBOARDING/DEMO"; 50 - case Phase::DONE: 51 + case DONE: 51 52 return "ONBOARDING/DONE"; 52 53 } 53 54 return "ONBOARDING/UNKNOWN"; ··· 108 109 return false; 109 110 } 110 111 111 - auto r = s_logic->confirmPin( pin, len ); 112 - if ( r == OnboardingResult::MISMATCH ) { 112 + if ( const auto r = s_logic->confirmPin( pin, len ); r == OnboardingResult::MISMATCH ) { 113 113 s_mismatch = true; 114 114 s_vaultReady = false; 115 115 return false; ··· 197 197 return; 198 198 } 199 199 const char* code = "en"; 200 + using enum OnboardingLang; 200 201 switch ( lang ) { 201 - case OnboardingLang::ES: 202 + case ES: 202 203 code = "es"; 203 204 break; 204 - case OnboardingLang::FR: 205 + case FR: 205 206 code = "fr"; 206 207 break; 207 - case OnboardingLang::DE: 208 + case DE: 208 209 code = "de"; 209 210 break; 210 - case OnboardingLang::IT: 211 + case IT: 211 212 code = "it"; 212 213 break; 213 - case OnboardingLang::EN: 214 + case EN: 214 215 default: 215 216 break; 216 217 } ··· 326 327 327 328 } // namespace 328 329 329 - void OnboardingStateHandler::renderWelcome() { 330 + void OnboardingStateHandler::renderWelcome() const { 331 + using enum i18n::StringId; 330 332 display::fillScreen( theme::kBg565 ); 331 333 332 334 const int16_t cx = device::kScreenCx; ··· 340 342 display::setTextDatum( kMiddleCenter ); 341 343 display::setTextColor( theme::kText565, theme::kBg565 ); 342 344 display::setFont( display::Font::HEADING ); 343 - display::drawString( i18n::tr( i18n::StringId::BRAND ), cx, markY + ( tiny ? 50 : 58 ) ); 345 + display::drawString( i18n::tr( BRAND ), cx, markY + ( tiny ? 50 : 58 ) ); 344 346 345 347 display::setFont( display::Font::BODY ); 346 348 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 347 - display::drawString( i18n::tr( i18n::StringId::OnbWelcomeTitle ), cx, 348 - markY + ( tiny ? 68 : 80 ) ); 349 + display::drawString( i18n::tr( OnbWelcomeTitle ), cx, markY + ( tiny ? 68 : 80 ) ); 349 350 350 351 display::setTextColor( theme::kMuted565, theme::kBg565 ); 351 - display::drawString( i18n::tr( i18n::StringId::OnbFirstSetup ), cx, h - ( tiny ? 64 : 70 ) ); 352 - display::drawString( i18n::tr( i18n::StringId::Onb2Min ), cx, h - ( tiny ? 50 : 52 ) ); 352 + display::drawString( i18n::tr( OnbFirstSetup ), cx, h - ( tiny ? 64 : 70 ) ); 353 + display::drawString( i18n::tr( Onb2Min ), cx, h - ( tiny ? 50 : 52 ) ); 353 354 354 - drawActionBar( i18n::tr( i18n::StringId::OnbActStart ), nullptr ); 355 + drawActionBar( i18n::tr( OnbActStart ), nullptr ); 355 356 } 356 357 357 358 void OnboardingStateHandler::renderBtnAIntro( uint32_t pulsePhaseMs ) { ··· 444 445 } 445 446 } 446 447 447 - void OnboardingStateHandler::renderLanguage() { 448 + void OnboardingStateHandler::renderLanguage() const { 448 449 display::fillScreen( theme::kBg565 ); 449 450 drawProgressHeader( OnboardingLogic::stepIndex( OnboardingStep::LANGUAGE ) ); 450 451 ··· 475 476 i18n::tr( i18n::StringId::OnbActSkip ) ); 476 477 } 477 478 478 - void OnboardingStateHandler::renderCreateStart() { 479 + void OnboardingStateHandler::renderCreateStart() const { 479 480 // Paint step context. After this, ctx_.authUI.draw() takes over the 480 481 // display (full-redraw flag is set in begin()). 481 482 display::fillScreen( theme::kBg565 ); ··· 495 496 display::drawString( i18n::tr( i18n::StringId::OnbPinWarn ), cx, device::kScreenH - 30 ); 496 497 } 497 498 498 - void OnboardingStateHandler::renderConfirmStart() { 499 + void OnboardingStateHandler::renderConfirmStart() const { 499 500 display::fillScreen( theme::kBg565 ); 500 501 drawProgressHeader( OnboardingLogic::stepIndex( OnboardingStep::ConfirmPin ) ); 501 502 ··· 510 511 display::drawString( i18n::tr( i18n::StringId::OnbConfirmHint ), cx, 58 ); 511 512 } 512 513 513 - void OnboardingStateHandler::renderMismatchBanner() { 514 + void OnboardingStateHandler::renderMismatchBanner() const { 514 515 display::fillScreen( theme::kBg565 ); 515 516 drawProgressHeader( OnboardingLogic::stepIndex( OnboardingStep::CreatePin ) ); 516 517 ··· 527 528 display::drawString( i18n::tr( i18n::StringId::CommonTryAgain ), cx, cy + 16 ); 528 529 } 529 530 530 - void OnboardingStateHandler::renderPair() { 531 + void OnboardingStateHandler::renderPair() const { 532 + using enum i18n::StringId; 531 533 display::fillScreen( theme::kBg565 ); 532 534 drawProgressHeader( OnboardingLogic::stepIndex( OnboardingStep::PAIR ) ); 533 535 ··· 535 537 display::setTextDatum( kMiddleCenter ); 536 538 display::setTextColor( theme::kPrimary565, theme::kBg565 ); 537 539 display::setFont( display::Font::HEADING ); 538 - display::drawString( i18n::tr( i18n::StringId::OnbPair ), cx, 36 ); 540 + display::drawString( i18n::tr( OnbPair ), cx, 36 ); 539 541 540 542 display::setFont( display::Font::BODY ); 541 543 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 542 - display::drawString( i18n::tr( i18n::StringId::OnbPairPhone ), cx, 58 ); 544 + display::drawString( i18n::tr( OnbPairPhone ), cx, 58 ); 543 545 display::setTextColor( theme::kText565, theme::kBg565 ); 544 - display::drawString( i18n::tr( i18n::StringId::OnbPairLook ), cx, 78 ); 545 - display::drawString( i18n::tr( i18n::StringId::OnbPairBt ), cx, 90 ); 546 + display::drawString( i18n::tr( OnbPairLook ), cx, 78 ); 547 + display::drawString( i18n::tr( OnbPairBt ), cx, 90 ); 546 548 547 549 if ( kleidos::ble::Facade::isConnected() ) { 548 550 display::setTextColor( theme::kSuccess565, theme::kBg565 ); 549 - display::drawString( i18n::tr( i18n::StringId::OnbPairConnected ), cx, 110 ); 551 + display::drawString( i18n::tr( OnbPairConnected ), cx, 110 ); 550 552 } else { 551 553 display::setTextColor( theme::kMuted565, theme::kBg565 ); 552 - display::drawString( i18n::tr( i18n::StringId::OnbPairAdvertising ), cx, 110 ); 554 + display::drawString( i18n::tr( OnbPairAdvertising ), cx, 110 ); 553 555 } 554 556 555 - drawActionBar( i18n::tr( i18n::StringId::OnbActNext ), i18n::tr( i18n::StringId::OnbActSkip ) ); 557 + drawActionBar( i18n::tr( OnbActNext ), i18n::tr( OnbActSkip ) ); 556 558 } 557 559 558 - void OnboardingStateHandler::renderDemo() { 560 + void OnboardingStateHandler::renderDemo() const { 559 561 display::fillScreen( theme::kBg565 ); 560 562 drawProgressHeader( OnboardingLogic::stepIndex( OnboardingStep::DEMO ) ); 561 563 ··· 580 582 display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 581 583 display::drawString( "demo@kleidos", 16, cardY + 22 ); 582 584 display::setTextColor( theme::kMuted565, theme::kSurface565 ); 583 - display::drawString( "\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2", 16, 584 - cardY + 36 ); 585 + display::drawString( 586 + "\x{E2}\x{80}\x{A2}\x{E2}\x{80}\x{A2}\x{E2}\x{80}\x{A2}" 587 + "\x{E2}\x{80}\x{A2}\x{E2}\x{80}\x{A2}", 588 + 16, cardY + 36 ); 585 589 586 590 drawActionBar( i18n::tr( i18n::StringId::OnbActNext ), i18n::tr( i18n::StringId::OnbActSkip ) ); 587 591 } 588 592 589 - void OnboardingStateHandler::renderDone() { 593 + void OnboardingStateHandler::renderDone() const { 594 + using enum i18n::StringId; 590 595 display::fillScreen( theme::kBg565 ); 591 596 592 597 const int16_t cx = device::kScreenCx; ··· 602 607 603 608 display::setFont( display::Font::HEADING ); 604 609 display::setTextColor( theme::kText565, theme::kBg565 ); 605 - display::drawString( i18n::tr( i18n::StringId::OnbDoneReady ), cx, cy + 52 ); 610 + display::drawString( i18n::tr( OnbDoneReady ), cx, cy + 52 ); 606 611 607 612 // Tips: BODY (proportional Latin-1) so diacritics render and lines stay 608 613 // inside 135 px in every language. 609 614 display::setFont( display::Font::BODY ); 610 615 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 611 616 int16_t tipsY = cy + 78; 612 - display::drawString( i18n::tr( i18n::StringId::OnbTipHoldA ), cx, tipsY ); 613 - display::drawString( i18n::tr( i18n::StringId::OnbTipTap ), cx, tipsY + 16 ); 614 - display::drawString( i18n::tr( i18n::StringId::OnbTipLock ), cx, tipsY + 32 ); 617 + display::drawString( i18n::tr( OnbTipHoldA ), cx, tipsY ); 618 + display::drawString( i18n::tr( OnbTipTap ), cx, tipsY + 16 ); 619 + display::drawString( i18n::tr( OnbTipLock ), cx, tipsY + 32 ); 615 620 616 621 // Open-vault CTA delivered via the standard action-bar tab footer. 617 - drawActionBar( i18n::tr( i18n::StringId::OnbOpenVault ), nullptr ); 622 + drawActionBar( i18n::tr( OnbOpenVault ), nullptr ); 618 623 } 619 624 620 625 // --------------------------------------------------------------------------- ··· 625 630 ctx_.configureDFS( 160, 80 ); 626 631 ctx_.resetActivity(); 627 632 633 + resetState(); 634 + resetStaticState(); 635 + 636 + renderWelcome(); 637 + } 638 + 639 + void OnboardingStateHandler::resetState() { 628 640 logic_.reset(); 629 641 phase_ = Phase::WELCOME; 630 642 phaseStartMs_ = platformClock::millis32(); ··· 634 646 bleStarted_ = false; 635 647 pairedOnce_ = false; 636 648 demoHold_.reset(); 649 + } 637 650 651 + void OnboardingStateHandler::resetStaticState() { 638 652 s_logic = &logic_; 639 653 s_ctx = &ctx_; 640 654 s_mismatch = false; 641 655 s_created = false; 642 656 s_vaultReady = false; 643 - 644 - renderWelcome(); 645 657 } 646 658 647 659 AppState OnboardingStateHandler::update() { ··· 654 666 // the events that the per-phase handlers below rely on (especially 655 667 // wasClicked, which requires justReleased_ to still be set). 656 668 { 657 - uint32_t lc = ctx_.inputA.lastChange(); 658 - if ( lc != lastInputAChange_ ) { 669 + if ( const uint32_t lc = ctx_.inputA.lastChange(); lc != lastInputAChange_ ) { 659 670 lastInputAChange_ = lc; 660 671 ctx_.wakeDisplay(); 661 672 ctx_.resetActivity(); ··· 663 674 } 664 675 ctx_.handleDisplayDimming(); 665 676 677 + using enum Phase; 666 678 switch ( phase_ ) { 667 - case Phase::WELCOME: { 679 + case WELCOME: { 668 680 if ( ctx_.inputA.wasClicked() ) { 669 681 ctx_.audio.beepDigit(); 670 682 phase_ = Phase::BtnAIntro; ··· 675 687 break; 676 688 } 677 689 678 - case Phase::BtnAIntro: { 690 + case BtnAIntro: { 679 691 // Refresh pulse animation each tick. 680 692 renderBtnAIntro( now - phaseStartMs_ ); 681 693 if ( ctx_.inputA.wasClicked() ) { ··· 696 708 break; 697 709 } 698 710 699 - case Phase::BtnBIntro: { 711 + case BtnBIntro: { 700 712 renderBtnBIntro( now - phaseStartMs_ ); 701 713 if ( ctx_.inputB.wasClicked() || ctx_.inputA.wasClicked() ) { 702 714 ctx_.audio.beepDigit(); ··· 709 721 break; 710 722 } 711 723 712 - case Phase::LANGUAGE: { 724 + case LANGUAGE: { 713 725 // Short click = next language (wraps), long-press = confirm, 714 726 // BtnB = skip (keep current selection). 715 727 if ( ctx_.inputA.wasClicked() ) { ··· 726 738 bool skip = ctx_.inputB.wasClicked(); 727 739 bool confirm = ctx_.inputA.wasReleaseFor( kLongPressMs ); 728 740 // Safety timeout so onboarding can't stall here indefinitely. 729 - bool autoAdvance = ( now - phaseStartMs_ > 20000 ); 730 - if ( skip || confirm || autoAdvance ) { 741 + if ( const bool autoAdvance = ( now - phaseStartMs_ > 20000 ); 742 + skip || confirm || autoAdvance ) { 731 743 writeLanguage( langSel_ ); 732 744 ctx_.audio.beepSuccess(); 733 745 logic_.advanceFromLanguage(); ··· 738 750 break; 739 751 } 740 752 741 - case Phase::CreatePin: { 753 + case CreatePin: { 742 754 if ( !authUIActive_ ) { 743 755 renderCreateStart(); 744 756 s_mismatch = false; ··· 756 768 break; 757 769 } 758 770 759 - case Phase::ConfirmPin: { 771 + case ConfirmPin: { 760 772 if ( !authUIActive_ ) { 761 773 renderConfirmStart(); 762 774 s_mismatch = false; ··· 764 776 ctx_.authUI.begin( onboardingConfirmCb, nullptr, ctx_.inputA, board::get().imu ); 765 777 authUIActive_ = true; 766 778 } 767 - bool done = ctx_.authUI.update(); 768 - if ( done && s_vaultReady ) { 779 + if ( const bool done = ctx_.authUI.update(); done && s_vaultReady ) { 769 780 ctx_.audio.beepSuccess(); 770 781 logic_.advanceFromConfirm(); 771 782 phase_ = Phase::PAIR; ··· 785 796 break; 786 797 } 787 798 788 - case Phase::MismatchFlash: { 799 + case MismatchFlash: { 789 800 if ( now - phaseStartMs_ >= kMismatchBannerMs ) { 790 801 phase_ = Phase::CreatePin; 791 802 phaseStartMs_ = now; ··· 794 805 break; 795 806 } 796 807 797 - case Phase::PAIR: { 808 + case PAIR: { 798 809 // Bring BLE up in standby mode so a paired host can reconnect and 799 810 // fire the SUCCESS toast via VaultState's notifier hook — keeping 800 811 // advertising short-lived to respect the power budget. ··· 804 815 } 805 816 kleidos::ble::Facade::tick(); 806 817 807 - bool connected = kleidos::ble::Facade::isConnected(); 808 - if ( connected && !pairedOnce_ ) { 818 + if ( const bool connected = kleidos::ble::Facade::isConnected(); 819 + connected && !pairedOnce_ ) { 809 820 pairedOnce_ = true; 810 821 popup::toast( "Paired", "Kleidos", popup::Severity::SUCCESS, 1500 ); 811 822 phaseStartMs_ = now; // use as "connected at" marker ··· 817 828 bool skip = ctx_.inputB.wasClicked(); 818 829 bool manual = ctx_.inputA.wasClicked(); 819 830 bool autoCont = pairedOnce_ && ( now - phaseStartMs_ >= 2000 ); 820 - bool timedOut = !pairedOnce_ && ( now - phaseStartMs_ >= 30000 ); 821 - if ( skip || manual || autoCont || timedOut ) { 831 + if ( const bool timedOut = !pairedOnce_ && ( now - phaseStartMs_ >= 30000 ); 832 + skip || manual || autoCont || timedOut ) { 822 833 // Keep BLE running in standby — VaultState expects it later. 823 834 ctx_.audio.beepDigit(); 824 835 logic_.advanceFromPair(); ··· 830 841 break; 831 842 } 832 843 833 - case Phase::DEMO: { 844 + case DEMO: { 834 845 // Let the user feel the Timing Arc gesture on a fake credential. 835 846 // Drawing is delegated to the HoldButton helper so the arc uses 836 847 // the same primitive as real PIN entry — zero actual credential data. ··· 838 849 (void)demoHold_.held( kLongPressMs, 10, device::kScreenH - 30, device::kScreenW - 20, 6, 839 850 theme::kPrimary565, theme::kSuccess565 ); 840 851 841 - bool skip = ctx_.inputB.wasClicked(); 842 - bool manual = demoHold_.tapped(); 843 - if ( skip || manual ) { 852 + bool skip = ctx_.inputB.wasClicked(); 853 + if ( const bool manual = demoHold_.tapped(); skip || manual ) { 844 854 ctx_.audio.beepDigit(); 845 855 logic_.advanceFromDemo(); 846 856 phase_ = Phase::DONE; ··· 850 860 break; 851 861 } 852 862 853 - case Phase::DONE: { 863 + case DONE: { 854 864 if ( ctx_.inputA.wasClicked() ) { 855 865 ctx_.audio.beepDigit(); 856 866 finished_ = true; ··· 942 952 // Map current phase → next visible phase. Skip over MISMATCH_FLASH and 943 953 // any phase whose entry needs full PIN provisioning (we just render the 944 954 // landing screen instead of running the side-effect). 955 + using enum Phase; 945 956 switch ( phase_ ) { 946 - case Phase::WELCOME: 957 + case WELCOME: 947 958 return debugJumpTo( DebugPhase::BTN_A_INTRO ); 948 - case Phase::BtnAIntro: 959 + case BtnAIntro: 949 960 return debugJumpTo( DebugPhase::BTN_B_INTRO ); 950 - case Phase::BtnBIntro: 961 + case BtnBIntro: 951 962 return debugJumpTo( DebugPhase::LANGUAGE ); 952 - case Phase::LANGUAGE: 963 + case LANGUAGE: 953 964 return debugJumpTo( DebugPhase::CREATE_PIN ); 954 - case Phase::CreatePin: 965 + case CreatePin: 955 966 return debugJumpTo( DebugPhase::CONFIRM_PIN ); 956 - case Phase::ConfirmPin: 967 + case ConfirmPin: 957 968 return debugJumpTo( DebugPhase::PAIR ); 958 - case Phase::MismatchFlash: 969 + case MismatchFlash: 959 970 return debugJumpTo( DebugPhase::CREATE_PIN ); 960 - case Phase::PAIR: 971 + case PAIR: 961 972 return debugJumpTo( DebugPhase::DEMO ); 962 - case Phase::DEMO: 973 + case DEMO: 963 974 return debugJumpTo( DebugPhase::DONE ); 964 - case Phase::DONE: 975 + case DONE: 965 976 return debugJumpTo( DebugPhase::WELCOME ); 966 977 } 967 978 return false;
+11 -8
src/states/onboarding_state.h
··· 89 89 DONE, 90 90 }; 91 91 92 - void renderWelcome(); 92 + void renderWelcome() const; 93 93 void renderBtnAIntro( uint32_t pulsePhaseMs ); 94 94 void renderBtnBIntro( uint32_t pulsePhaseMs ); 95 - void renderLanguage(); 96 - void renderCreateStart(); 97 - void renderConfirmStart(); 98 - void renderMismatchBanner(); 99 - void renderPair(); 100 - void renderDemo(); 101 - void renderDone(); 95 + void renderLanguage() const; 96 + void renderCreateStart() const; 97 + void renderConfirmStart() const; 98 + void renderMismatchBanner() const; 99 + void renderPair() const; 100 + void renderDemo() const; 101 + void renderDone() const; 102 + 103 + void resetState(); 104 + void resetStaticState(); 102 105 103 106 Phase phase_ = Phase::WELCOME; 104 107 OnboardingLogic logic_;
+22 -19
src/states/pin_state.cpp
··· 9 9 #include "platform/clock.h" 10 10 #include "platform/logger.h" 11 11 #include "platform/rtos.h" 12 + #include "platform/safe_string.h" 12 13 #include "platform/system.h" 13 14 #include "platform/watchdog.h" 14 15 #include "ui/core/theme.h" ··· 43 44 static constexpr const char* kTag = "PinState"; 44 45 namespace platformClock = kleidos::platform::clock; 45 46 namespace platformSystem = kleidos::platform::system; 47 + namespace str = kleidos::platform::str; 46 48 47 49 // --------------------------------------------------------------------------- 48 50 // Key file 2FA: combine PIN with SHA-256 of key file content. ··· 160 162 161 163 for ( int i = 3; i > 0; i-- ) { 162 164 char buf[4]; 163 - snprintf( buf, sizeof( buf ), "%d", i ); 165 + str::format( buf, sizeof( buf ), "%d", i ); 164 166 lv_label_set_text( cntLbl, buf ); 165 167 lv_timer_handler(); 166 168 kleidos::platform::rtos::delayMs( 1000 ); ··· 192 194 display::drawString( i18n::tr( i18n::StringId::WipeReason ), kScrCx, 100 ); 193 195 display::setFont( display::Font::LARGE ); 194 196 char buf[4]; 195 - snprintf( buf, sizeof( buf ), "%d", i ); 197 + str::format( buf, sizeof( buf ), "%d", i ); 196 198 display::drawString( buf, kScrCx, 160 ); 197 199 kleidos::platform::rtos::delayMs( 1000 ); 198 200 } ··· 254 256 // RAII clear of the progress callback once we leave this scope, on 255 257 // every path (success, fail, exception). Cheap; no allocation. 256 258 struct ClearProgress { 259 + ClearProgress() = default; 257 260 ~ClearProgress() { kleidos::crypto::setProgressCallback( nullptr ); } 261 + ClearProgress( const ClearProgress& ) = delete; 262 + ClearProgress& operator=( const ClearProgress& ) = delete; 258 263 } clearProgress; 259 264 260 265 // G4.3: Feed watchdog before long PBKDF2 operation to prevent WDT timeout ··· 358 363 AppState PinStateHandler::update() { 359 364 AuthUIState prevState = ctx_.authUI.getState(); 360 365 bool unlocked = ctx_.authUI.update(); 361 - AuthUIState curState = ctx_.authUI.getState(); 362 366 363 367 // Audio feedback on auth state transitions 364 - if ( curState != prevState ) { 365 - if ( curState == AuthUIState::DigitLocked || curState == AuthUIState::ENTERING ) { 368 + using enum AuthUIState; 369 + if ( const AuthUIState curState = ctx_.authUI.getState(); curState != prevState ) { 370 + if ( curState == DigitLocked || curState == ENTERING ) { 366 371 ctx_.audio.beepDigit(); 367 - } else if ( curState == AuthUIState::ResultFlash ) { 372 + } else if ( curState == ResultFlash ) { 368 373 if ( unlocked ) { 369 374 ctx_.audio.beepSuccess(); 370 375 } else { ··· 406 411 407 412 // Idle timeout during auth entry 408 413 #if !HAS_KEYBOARD 409 - uint32_t idle = platformClock::millis32() - ctx_.lastActivity; 410 - if ( ctx_.authUI.getState() == AuthUIState::IDLE 411 - || ctx_.authUI.getState() == AuthUIState::LockedOut ) { 412 - if ( idle >= kPinIdleSleepMs ) { 413 - KLEIDOS_LOGI( kTag, "Auth idle timeout — sleeping" ); 414 - ctx_.goToSleep(); 415 - } 414 + if ( const uint32_t idle = platformClock::millis32() - ctx_.lastActivity; 415 + ( ctx_.authUI.getState() == AuthUIState::IDLE 416 + || ctx_.authUI.getState() == AuthUIState::LockedOut ) 417 + && idle >= kPinIdleSleepMs ) { 418 + KLEIDOS_LOGI( kTag, "Auth idle timeout — sleeping" ); 419 + ctx_.goToSleep(); 416 420 } 417 421 #endif 418 422 ··· 434 438 } 435 439 436 440 // 3-button: all buttons reset idle timer 437 - if ( ctx_.inputC ) { 438 - if ( ctx_.inputB.wasPressed() || ctx_.inputB.wasReleased() || ctx_.inputC->wasPressed() 439 - || ctx_.inputC->wasReleased() ) { 440 - ctx_.wakeDisplay(); 441 - ctx_.resetActivity(); 442 - } 441 + if ( ctx_.inputC 442 + && ( ctx_.inputB.wasPressed() || ctx_.inputB.wasReleased() || ctx_.inputC->wasPressed() 443 + || ctx_.inputC->wasReleased() ) ) { 444 + ctx_.wakeDisplay(); 445 + ctx_.resetActivity(); 443 446 } 444 447 445 448 return AppState::PinEntry;
+1 -1
src/states/pin_state.h
··· 11 11 /// @ingroup states 12 12 class PinStateHandler : public AppStateHandler { 13 13 public: 14 - explicit PinStateHandler( AppContext& ctx ) : AppStateHandler( ctx ) {} 14 + using AppStateHandler::AppStateHandler; 15 15 16 16 void enter() override; 17 17 AppState update() override;
+6 -6
src/states/settings_nav_logic.cpp
··· 3 3 #include "settings_nav_logic.h" 4 4 5 5 #include <cstdint> 6 + #include <utility> 6 7 7 8 namespace { 8 9 ··· 11 12 SettingsItem items[kSettingsMaxItems]; 12 13 }; 13 14 14 - constexpr SectionLayout kLayout[static_cast<uint8_t>( SettingsSection::COUNT )] = { 15 + constexpr SectionLayout kLayout[std::to_underlying( SettingsSection::COUNT )] = { 15 16 /* DISPLAY */ { 2, 16 17 { SettingsItem::BRIGHTNESS, SettingsItem::AutoOffSec, SettingsItem::NONE } }, 17 18 /* SECURITY */ ··· 22 23 /* ABOUT */ { 1, { SettingsItem::AboutInfo, SettingsItem::NONE, SettingsItem::NONE } }, 23 24 }; 24 25 25 - constexpr uint8_t kSectionCount = static_cast<uint8_t>( SettingsSection::COUNT ); 26 + constexpr uint8_t kSectionCount = std::to_underlying( SettingsSection::COUNT ); 26 27 27 28 } // namespace 28 29 ··· 33 34 } 34 35 35 36 uint8_t SettingsNavLogic::itemCountForSection( SettingsSection s ) { 36 - auto i = static_cast<uint8_t>( s ); 37 + auto i = std::to_underlying( s ); 37 38 if ( i >= kSectionCount ) { 38 39 return 0; 39 40 } ··· 48 49 } 49 50 50 51 SettingsItem SettingsNavLogic::itemAt( SettingsSection s, uint8_t idx ) { 51 - auto si = static_cast<uint8_t>( s ); 52 + auto si = std::to_underlying( s ); 52 53 if ( si >= kSectionCount ) { 53 54 return SettingsItem::NONE; 54 55 } ··· 90 91 itemIdx_ = 0; 91 92 return ItemAction::NONE; 92 93 } 93 - SettingsItem it = itemAt( section(), itemIdx_ ); 94 - switch ( it ) { 94 + switch ( SettingsItem it = itemAt( section(), itemIdx_ ); it ) { 95 95 case SettingsItem::BRIGHTNESS: 96 96 case SettingsItem::AutoOffSec: 97 97 case SettingsItem::AutoLockSec:
+5 -6
src/tools/ble_host_harness.cpp
··· 669 669 670 670 void connectTarget( uint32_t scanMs ) { 671 671 for ( uint8_t attempt = 1; attempt <= kSecurityMaxAttempts; ++attempt ) { 672 - const ConnectAttemptResult result = connectTargetAttempt( scanMs, attempt ); 673 - if ( result == ConnectAttemptResult::Ready || result == ConnectAttemptResult::Failed ) { 672 + if ( const ConnectAttemptResult result = connectTargetAttempt( scanMs, attempt ); 673 + result == ConnectAttemptResult::Ready || result == ConnectAttemptResult::Failed ) { 674 674 return; 675 675 } 676 676 if ( attempt < kSecurityMaxAttempts ) { ··· 686 686 serial::println( "NC_FAIL not_connected" ); 687 687 return; 688 688 } 689 - NimBLEConnInfo info = s_client->getConnInfo(); 690 - if ( !NimBLEDevice::injectConfirmPasskey( info, accept ) ) { 689 + if ( NimBLEConnInfo info = s_client->getConnInfo(); 690 + !NimBLEDevice::injectConfirmPasskey( info, accept ) ) { 691 691 serial::println( "NC_FAIL inject_failed" ); 692 692 return; 693 693 } ··· 844 844 void loop() { 845 845 pollSerial(); 846 846 maybeSubscribe(); 847 - const uint32_t now = platformClock::millis32(); 848 - if ( now - s_lastStatusMs >= kStatusPeriodMs ) { 847 + if ( const uint32_t now = platformClock::millis32(); now - s_lastStatusMs >= kStatusPeriodMs ) { 849 848 s_lastStatusMs = now; 850 849 printStatus(); 851 850 }
+3 -3
src/tools/wifi_host_harness.cpp
··· 1471 1471 kleidos::platform::rtos::delayMs( 150 ); 1472 1472 uint16_t passed = 0; 1473 1473 for ( uint16_t cycle = 0; cycle < cycles; ++cycle ) { 1474 - int status = 0; 1475 - const bool ok = performGet( "/api/status", s_token, false, &status, nullptr, 0 ); 1476 - if ( ok && status == 200 ) 1474 + int status = 0; 1475 + if ( const bool ok = performGet( "/api/status", s_token, false, &status, nullptr, 0 ); 1476 + ok && status == 200 ) 1477 1477 ++passed; 1478 1478 kleidos::platform::rtos::delayMs( 50 ); 1479 1479 }
+29 -22
src/totp/totp_task.cpp
··· 35 35 rtos::TaskHandle s_task = nullptr; 36 36 std::atomic<bool> s_running{ false }; 37 37 38 + // Returns true when the worker loop should exit; false to keep running. 39 + // Extracted from worker() to keep the S924 nested-break threshold (1) in check. 40 + bool processOneRequest( detail::Request*& req ); 41 + 38 42 [[noreturn]] void worker( void* /*arg*/ ) { 39 43 detail::Request* req = nullptr; 40 - while ( true ) { 41 - if ( !s_queue.receive( req, kRecvTimeout ) ) { 42 - if ( !s_running.load() ) { 43 - break; 44 - } 45 - continue; 46 - } 47 - if ( req == nullptr ) { 48 - break; // shutdown sentinel 49 - } 50 - 51 - req->result = req->invoke( req->fnPtr ); 52 - if ( req->done != nullptr ) { 53 - rtos::giveSemaphore( req->done ); 54 - } else { 55 - KLEIDOS_LOGW( kTag, "dropping request completion with no waiter" ); 56 - } 44 + while ( !processOneRequest( req ) ) { 45 + // Loop body lives in processOneRequest(); returns true to exit the worker. 57 46 } 58 47 59 48 s_task = nullptr; ··· 63 52 } 64 53 } 65 54 55 + // Returns true when the worker loop should exit; false to keep running. 56 + // Pulled out of worker() to keep the S924 nested-break threshold (1) in check. 57 + bool processOneRequest( detail::Request*& req ) { 58 + if ( !s_queue.receive( req, kRecvTimeout ) ) { 59 + // Timeout: keep looping only while the task is still supposed to be running. 60 + return !s_running.load(); 61 + } 62 + if ( req == nullptr ) { 63 + return true; // shutdown sentinel 64 + } 65 + 66 + req->result = req->invoke( req->fnPtr ); 67 + if ( req->done != nullptr ) { 68 + rtos::giveSemaphore( req->done ); 69 + } else { 70 + KLEIDOS_LOGW( kTag, "dropping request completion with no waiter" ); 71 + } 72 + return false; 73 + } 74 + 66 75 } // namespace 67 76 68 77 bool begin( rtos::Core core, rtos::Priority priority ) { ··· 70 79 return true; 71 80 } 72 81 73 - if ( !s_queue.valid() ) { 74 - if ( !s_queue.create( kQueueDepth ) ) { 75 - KLEIDOS_LOGE( kTag, "queue create failed" ); 76 - return false; 77 - } 82 + if ( !s_queue.valid() && !s_queue.create( kQueueDepth ) ) { 83 + KLEIDOS_LOGE( kTag, "queue create failed" ); 84 + return false; 78 85 } 79 86 80 87 s_running.store( true );
+1 -2
src/ui/brands/brand_bitmap.cpp
··· 71 71 } 72 72 73 73 for ( size_t pixel = 0; pixel < pixels; ++pixel ) { 74 - const bool visible = !hasAlpha || maskBitIsSet( scratch, pixel ); 75 - if ( !visible ) { 74 + if ( const bool visible = !hasAlpha || maskBitIsSet( scratch, pixel ); !visible ) { 76 75 outPixels[pixel] = background565; 77 76 } else if ( forceTint ) { 78 77 outPixels[pixel] = tint565;
+1 -2
src/ui/core/layout.h
··· 51 51 #ifdef ESP_PLATFORM 52 52 /// @brief Runtime helper that queries the active display HAL. 53 53 inline bool isTinyScreen() { 54 - return isTinyScreen( static_cast<int16_t>( display::width() ), 55 - static_cast<int16_t>( display::height() ) ); 54 + return isTinyScreen( display::width(), display::height() ); 56 55 } 57 56 #endif 58 57
+2 -1
src/ui/debug/serial_debug.cpp
··· 344 344 #if HAS_LVGL 345 345 else if ( str::startsWithIgnoreCase( cmd, "TOUCH " ) ) { 346 346 // TOUCH x y — inject a simulated touch tap into LVGL 347 - int x = 0, y = 0; 347 + int x = 0; 348 + int y = 0; 348 349 if ( sscanf( cmd + 6, "%d %d", &x, &y ) == 2 ) { 349 350 lvgl_input::injectTouch( static_cast<int16_t>( x ), static_cast<int16_t>( y ) ); 350 351 KLEIDOS_LOGI( SDTAG, "TOUCH %d %d", x, y );
+23 -16
src/ui/entry/passphrase_entry_ui.cpp
··· 9 9 #include "i18n/i18n.h" 10 10 #include "platform/clock.h" 11 11 #include "platform/logger.h" 12 + #include "platform/safe_string.h" 12 13 #include "ui/core/theme.h" 13 14 #include "ui/entry/passphrase_entry_ui.h" 14 15 #include "ui/input/button_input.h" ··· 19 20 #include <cstring> 20 21 21 22 namespace platformClock = kleidos::platform::clock; 23 + namespace str = kleidos::platform::str; 22 24 23 25 static constexpr const char* kTag = "Passphrase"; 24 26 ··· 90 92 91 93 // Check lockout 92 94 if ( lockoutCb_ ) { 93 - uint32_t remaining = lockoutCb_(); 94 - if ( remaining > 0 ) { 95 + if ( const uint32_t remaining = lockoutCb_(); remaining > 0 ) { 95 96 if ( state_ != AuthUIState::LockedOut ) { 96 97 state_ = AuthUIState::LockedOut; 97 98 needRedraw_ = true; ··· 154 155 return; 155 156 } 156 157 157 - if ( c == key_code::kBackspace ) { 158 - if ( length_ > 0 ) { 159 - length_--; 160 - buffer_[length_] = '\0'; 161 - drawInput(); 162 - prevLength_ = length_; 163 - } 158 + if ( c == key_code::kBackspace && length_ > 0 ) { 159 + length_--; 160 + buffer_[length_] = '\0'; 161 + drawInput(); 162 + prevLength_ = length_; 164 163 return; 165 164 } 165 + // Note: when c == kBackspace but length_ == 0, the collapsed check above 166 + // is false and we fall through to the printable-ASCII branch, which 167 + // also rejects (kBackspace is outside 0x20-0x7E). The function then 168 + // returns naturally with no state change — semantically identical to 169 + // the pre-S1066 implementation, which exited via an explicit `return` 170 + // inside the outer `if (c == kBackspace)` block before reaching the 171 + // printable check. 166 172 167 173 // Printable ASCII character 168 174 if ( c >= 0x20 && c <= 0x7E && length_ < MAX_LENGTH ) { ··· 230 236 display::setTextColor( HINT_COLOR, theme::kBg565 ); 231 237 display::setTextDatum( kTopCenter ); 232 238 char hint[40]; 233 - snprintf( hint, sizeof( hint ), "Min %u chars, max %u", MIN_LENGTH, MAX_LENGTH ); 239 + str::format( hint, sizeof( hint ), "Min %u chars, max %u", MIN_LENGTH, MAX_LENGTH ); 234 240 display::drawString( hint, SCR_CX, HINT_Y ); 235 241 236 242 drawInstructions(); ··· 268 274 } 269 275 270 276 // Draw cursor 271 - int16_t cursorX = static_cast<int16_t>( textX + ( length_ * CHAR_W ) ); 272 - if ( cursorX < INPUT_BOX_X + INPUT_BOX_W - 8 ) { 277 + if ( const auto cursorX = static_cast<int16_t>( textX + ( length_ * CHAR_W ) ); 278 + cursorX < INPUT_BOX_X + INPUT_BOX_W - 8 ) { 273 279 if ( ( platformClock::millis32() / 500U ) % 2U == 0U ) { 274 280 display::fillRect( cursorX, INPUT_BOX_Y + 6, 2, INPUT_BOX_H - 12, CURSOR_COLOR ); 275 281 } ··· 280 286 display::setTextColor( length_ >= MIN_LENGTH ? SUCCESS_CLR : HINT_COLOR, theme::kBg565 ); 281 287 display::setCursor( SCR_CX - 16, HINT_Y ); 282 288 char lenStr[16]; 283 - snprintf( lenStr, sizeof( lenStr ), "%u/%u", length_, MAX_LENGTH ); 289 + str::format( lenStr, sizeof( lenStr ), "%u/%u", length_, MAX_LENGTH ); 284 290 display::print( lenStr ); 285 291 } 286 292 ··· 338 344 339 345 char buf[24]; 340 346 if ( remainingSec >= 60 ) { 341 - snprintf( buf, sizeof( buf ), "LOCKED %um %us", static_cast<unsigned>( remainingSec / 60 ), 342 - static_cast<unsigned>( remainingSec % 60 ) ); 347 + str::format( buf, sizeof( buf ), "LOCKED %um %us", 348 + static_cast<unsigned>( remainingSec / 60 ), 349 + static_cast<unsigned>( remainingSec % 60 ) ); 343 350 } else { 344 - snprintf( buf, sizeof( buf ), "LOCKED %us", static_cast<unsigned>( remainingSec ) ); 351 + str::format( buf, sizeof( buf ), "LOCKED %us", static_cast<unsigned>( remainingSec ) ); 345 352 } 346 353 display::drawString( buf, SCR_CX, STATUS_Y ); 347 354 }
+40 -42
src/ui/entry/pin_entry_ui.cpp
··· 8 8 #include "i18n/i18n.h" 9 9 #include "platform/clock.h" 10 10 #include "platform/logger.h" 11 + #include "platform/safe_string.h" 11 12 #include "ui/core/theme.h" 12 13 #include "ui/input/button_input.h" 13 14 #include "ui/widgets/action_bar.h" ··· 21 22 #include <cstdio> 22 23 23 24 namespace platformClock = kleidos::platform::clock; 25 + namespace str = kleidos::platform::str; 24 26 25 27 // --------------------------------------------------------------------------- 26 28 // Module-private constants ··· 132 134 needFullRedraw_ = true; 133 135 } 134 136 137 + using enum InternalState; 135 138 if ( lockoutCb_ ) { 136 - uint32_t remaining = lockoutCb_(); 137 - if ( remaining > 0 ) { 138 - if ( state_ != InternalState::LockedOut ) { 139 - state_ = InternalState::LockedOut; 139 + if ( const uint32_t remaining = lockoutCb_(); remaining > 0 ) { 140 + if ( state_ != LockedOut ) { 141 + state_ = LockedOut; 140 142 needFullRedraw_ = true; 141 143 } 142 144 drawLockout( remaining ); 143 145 return false; 144 - } else if ( state_ == InternalState::LockedOut ) { 145 - state_ = InternalState::IDLE; 146 + } else if ( state_ == LockedOut ) { 147 + state_ = IDLE; 146 148 needFullRedraw_ = true; 147 149 } 148 150 } ··· 153 155 154 156 if ( isThreeButtonMode() ) { 155 157 // 3-button mode: BtnA=up, BtnB=confirm, BtnC=down 158 + using enum InternalState; 156 159 switch ( state_ ) { 157 - case InternalState::IDLE: 160 + case IDLE: 158 161 handleIdleThreeBtn( now ); 159 162 break; 160 - case InternalState::DigitLocked: 163 + case DigitLocked: 161 164 handleDigitLockedThreeBtn( now ); 162 165 break; 163 - case InternalState::COMPLETE: 166 + case COMPLETE: 164 167 handleComplete(); 165 168 break; 166 - case InternalState::ResultFlash: 169 + case ResultFlash: 167 170 handleResultFlash( now ); 168 171 break; 169 172 default: ··· 171 174 } 172 175 } else { 173 176 // Arc mode (2-button devices) 177 + using enum InternalState; 174 178 switch ( state_ ) { 175 - case InternalState::IDLE: 179 + case IDLE: 176 180 handleIdle( now ); 177 181 break; 178 - case InternalState::SELECTING: 182 + case SELECTING: 179 183 handleSelecting( now ); 180 184 break; 181 - case InternalState::DigitLocked: 185 + case DigitLocked: 182 186 handleDigitLocked( now ); 183 187 break; 184 - case InternalState::COMPLETE: 188 + case COMPLETE: 185 189 handleComplete(); 186 190 break; 187 - case InternalState::ResultFlash: 191 + case ResultFlash: 188 192 handleResultFlash( now ); 189 193 break; 190 - case InternalState::LockedOut: 194 + case LockedOut: 191 195 break; 192 196 } 193 197 } ··· 206 210 display::drawString( i18n::tr( i18n::StringId::PinEnter ), kArcCx, kTbTitleY ); 207 211 // Large arrows 208 212 display::setFont( display::Font::LARGE ); 209 - display::drawString( "\x18", kArcCx, kTbUpY ); 210 - display::drawString( "\x19", kArcCx, kTbDownY ); 213 + display::drawString( "\x{18}", kArcCx, kTbUpY ); 214 + display::drawString( "\x{19}", kArcCx, kTbDownY ); 211 215 display::setTextDatum( kTopLeft ); 212 216 } else { 213 217 drawBrandMark(); ··· 306 310 } 307 311 308 312 void PinEntryUI::handleResultFlash( uint32_t now ) { 309 - if ( now - resultTime_ >= kSubmitFlashMs ) { 310 - if ( pinVerified_ ) { 311 - return; 312 - } else { 313 - reset(); 314 - } 313 + if ( now - resultTime_ >= kSubmitFlashMs && !pinVerified_ ) { 314 + reset(); 315 315 } 316 316 } 317 317 318 318 // --------------------------------------------------------------------------- 319 319 // Drawing functions 320 320 // --------------------------------------------------------------------------- 321 - void PinEntryUI::drawArcBackground() { 321 + void PinEntryUI::drawArcBackground() const { 322 322 // E-paper: no arc dial is drawn (reduce-motion) — only the large changing 323 323 // digit is presented, so skip the ring, ticks and numeric labels. 324 324 if ( device::kDisplayIsEpaper ) { ··· 393 393 // the glyph close to the inner arc edge — it's the focal element of 394 394 // the screen and the user's eye should anchor here, not on the dots. 395 395 char digitStr[4]; 396 - snprintf( digitStr, sizeof( digitStr ), "%u", digit ); 396 + str::format( digitStr, sizeof( digitStr ), "%u", digit ); 397 397 const int16_t boxW = 50; 398 398 const int16_t boxH = 44; 399 399 display::fillRect( kArcCx - boxW / 2, kDigitY - 4, boxW, boxH, theme::kBg565 ); ··· 412 412 lastDrawnDigit_ = static_cast<int8_t>( digit ); 413 413 } 414 414 415 - void PinEntryUI::drawPositionDots() { 415 + void PinEntryUI::drawPositionDots() const { 416 416 // Use larger dots and wider spacing for 3-button (320×240) 417 417 int16_t dotY = isThreeButtonMode() ? kTbDotY : kDotY; 418 418 int16_t dotR = isThreeButtonMode() ? kTbDotRad : kDotRadius; ··· 421 421 int16_t startX = kArcCx - totalWidth / 2; 422 422 423 423 for ( uint8_t i = 0; i < kPinLength; i++ ) { 424 - int16_t x = static_cast<int16_t>( startX + i * dotSpc ); 424 + auto x = static_cast<int16_t>( startX + i * dotSpc ); 425 425 uint16_t color = ( i < digitIndex_ ) ? kDotFilled : kDotEmpty; 426 426 display::fillCircle( x, dotY, dotR, color ); 427 427 ··· 433 433 } 434 434 } 435 435 436 - void PinEntryUI::drawInstructions() { 436 + void PinEntryUI::drawInstructions() const { 437 437 // Tab-style action bar: BtnA = HOLD (enter digit), IMU shake = clear. 438 438 ActionCell a{}; 439 439 a.tapLabel = i18n::tr( i18n::StringId::PinHoldSelect ); ··· 442 442 ActionBar::drawStick2( a, b ); 443 443 } 444 444 445 - void PinEntryUI::drawBrandMark() { 445 + void PinEntryUI::drawBrandMark() const { 446 446 // Top-of-screen wordmark. Acts as a logo placeholder until we ship a 447 447 // proper bitmap mark — letter-spaced HEADING glyphs in PRIMARY make the 448 448 // composition feel branded without costing flash. Centered at BRAND_Y. ··· 469 469 display::setTextDatum( kTopLeft ); 470 470 } 471 471 472 - void PinEntryUI::drawResultPopup( const char* msg, uint16_t color ) { 472 + void PinEntryUI::drawResultPopup( const char* msg, uint16_t color ) const { 473 473 // Centered modal-style popup matching the global popup look: rounded 474 474 // SURFACE rect with a 1 px colored border + bold transparent text. 475 475 // Positioned on the digit band (centered around DIGIT_Y) so it covers ··· 515 515 display::setTextDatum( kTopLeft ); 516 516 } 517 517 518 - void PinEntryUI::drawVerifying() { 518 + void PinEntryUI::drawVerifying() const { 519 519 drawResultPopup( i18n::tr( i18n::StringId::CommonVerifying ), kLockoutColor ); 520 520 } 521 521 522 - void PinEntryUI::drawResult( bool success ) { 522 + void PinEntryUI::drawResult( bool success ) const { 523 523 uint16_t color = success ? kSuccessColor : kFailColor; 524 524 const char* msg = 525 525 success ? i18n::tr( i18n::StringId::CommonUnlocked ) : i18n::tr( i18n::StringId::PinWrong ); ··· 547 547 return; 548 548 } 549 549 550 - float mag = imu_->getMagnitude(); 551 - 552 - if ( mag > kShakeThreshold ) { 550 + if ( const float mag = imu_->getMagnitude(); mag > kShakeThreshold ) { 553 551 lastShakeTime_ = now; 554 552 clearInput(); 555 553 } ··· 578 576 579 577 char buf[24]; 580 578 if ( remainingSec >= 60 ) { 581 - snprintf( buf, sizeof( buf ), "LOCKED %" PRIu32 "m %" PRIu32 "s", remainingSec / 60, 582 - remainingSec % 60 ); 579 + str::format( buf, sizeof( buf ), "LOCKED %" PRIu32 "m %" PRIu32 "s", remainingSec / 60, 580 + remainingSec % 60 ); 583 581 } else { 584 - snprintf( buf, sizeof( buf ), "LOCKED %" PRIu32 "s", remainingSec ); 582 + str::format( buf, sizeof( buf ), "LOCKED %" PRIu32 "s", remainingSec ); 585 583 } 586 584 display::drawString( buf, kArcCx, kDigitY + 15 ); 587 585 display::setTextDatum( kTopLeft ); ··· 665 663 666 664 display::setTextDatum( kMiddleLeft ); 667 665 display::setTextColor( theme::kPrimary565, theme::kSurface565 ); 668 - display::drawString( "\x18 +1", 6, textY ); 666 + display::drawString( "\x{18} +1", 6, textY ); 669 667 670 668 display::setTextDatum( kMiddleCenter ); 671 669 display::setTextColor( theme::kSuccess565, theme::kSurface565 ); 672 - display::drawString( "\x07 OK", kScreenW / 2, textY ); 670 + display::drawString( "\x{7} OK", kScreenW / 2, textY ); 673 671 674 672 display::setTextDatum( kMiddleRight ); 675 673 display::setTextColor( theme::kPrimary565, theme::kSurface565 ); 676 - display::drawString( "\x19 -1", kScreenW - 6, textY ); 674 + display::drawString( "\x{19} -1", kScreenW - 6, textY ); 677 675 678 676 display::setTextDatum( kTopLeft ); 679 677 actionBarDrawn_ = true;
+7 -7
src/ui/entry/pin_entry_ui.h
··· 115 115 void handleDigitLockedThreeBtn( uint32_t now ); 116 116 bool isThreeButtonMode() const { return btnSelect_ != nullptr; } 117 117 118 - void drawArcBackground(); 118 + void drawArcBackground() const; 119 119 void drawArcProgress( int16_t angleDeg ); 120 120 void drawDigitCenter( uint8_t digit, uint16_t color = theme::kText565 ); 121 - void drawPositionDots(); 122 - void drawInstructions(); 123 - void drawBrandMark(); 124 - void drawVerifying(); 125 - void drawResult( bool success ); 126 - void drawResultPopup( const char* msg, uint16_t color ); 121 + void drawPositionDots() const; 122 + void drawInstructions() const; 123 + void drawBrandMark() const; 124 + void drawVerifying() const; 125 + void drawResult( bool success ) const; 126 + void drawResultPopup( const char* msg, uint16_t color ) const; 127 127 void drawClearFeedback(); 128 128 void drawLockout( uint32_t remainingSec ); 129 129 void checkShake( uint32_t now );
+1 -3
src/ui/input/gpio_button_input.h
··· 117 117 return; 118 118 } 119 119 120 - bool rawPressed = readRawPressed(); 121 - 122 - if ( rawPressed != pressed_ ) { 120 + if ( const bool rawPressed = readRawPressed(); rawPressed != pressed_ ) { 123 121 lastDebounce_ = now; 124 122 lastChange_ = now; 125 123
+2 -2
src/ui/input/hold_button.cpp
··· 44 44 45 45 // Promote a deferred single-tap once its window expires. 46 46 if ( pendingTapAt_ != 0 ) { 47 - const uint32_t now = platformClock::millis32(); 48 - if ( now - pendingTapAt_ > doubleTapWindowMs_ ) { 47 + if ( const uint32_t now = platformClock::millis32(); 48 + now - pendingTapAt_ > doubleTapWindowMs_ ) { 49 49 tapResult_ = true; 50 50 pendingTapAt_ = 0; 51 51 }
+7 -4
src/ui/lvgl/lvgl_admin_screen.cpp
··· 7 7 #include "lvgl_theme.h" 8 8 #include "platform/clock.h" 9 9 #include "platform/logger.h" 10 + #include "platform/safe_string.h" 10 11 #include "web/wifi_admin_portal.h" 11 12 12 13 #include <cstdio> 14 + 15 + namespace str = kleidos::platform::str; 13 16 14 17 static constexpr const char* kTag = "LvglAdmin"; 15 18 static constexpr uint32_t REFRESH_INTERVAL_MS = 1000; ··· 175 178 // --------------------------------------------------------------------------- 176 179 void LvglAdminScreen::refreshStatus() { 177 180 static char ssidBuf[48]; 178 - snprintf( ssidBuf, sizeof( ssidBuf ), "WiFi: %s", WifiAdminPortal::getSsid() ); 181 + str::format( ssidBuf, sizeof( ssidBuf ), "WiFi: %s", WifiAdminPortal::getSsid() ); 179 182 lv_label_set_text( ssidLabel_, ssidBuf ); 180 183 181 184 static char passBuf[32]; 182 - snprintf( passBuf, sizeof( passBuf ), "Pass: %s", WifiAdminPortal::getPassword() ); 185 + str::format( passBuf, sizeof( passBuf ), "Pass: %s", WifiAdminPortal::getPassword() ); 183 186 lv_label_set_text( passLabel_, passBuf ); 184 187 185 188 static char clientBuf[24]; 186 - snprintf( clientBuf, sizeof( clientBuf ), "Clients: %d", WifiAdminPortal::getClientCount() ); 189 + str::format( clientBuf, sizeof( clientBuf ), "Clients: %d", WifiAdminPortal::getClientCount() ); 187 190 lv_label_set_text( clientsLabel_, clientBuf ); 188 191 189 192 OtaProgress& ota = WifiAdminPortal::getOtaProgress(); 190 193 if ( ota.active ) { 191 194 static char otaBuf[24]; 192 - snprintf( otaBuf, sizeof( otaBuf ), "OTA: %u%%", ota.percent ); 195 + str::format( otaBuf, sizeof( otaBuf ), "OTA: %u%%", ota.percent ); 193 196 lv_label_set_text( otaLabel_, otaBuf ); 194 197 lv_bar_set_value( otaBar_, ota.percent, LV_ANIM_ON ); 195 198 lv_obj_clear_flag( otaBar_, LV_OBJ_FLAG_HIDDEN );
+4 -2
src/ui/lvgl/lvgl_cred_detail_screen.cpp
··· 13 13 #include <cstdio> 14 14 #include <cstring> 15 15 16 + namespace str = kleidos::platform::str; 17 + 16 18 static constexpr const char* kTag = "LvglCredDetail"; 17 19 18 20 // --------------------------------------------------------------------------- ··· 342 344 return; 343 345 static char buf[140]; 344 346 if ( passVisible_ ) { 345 - snprintf( buf, sizeof( buf ), "%s", pass_ ); 347 + str::format( buf, sizeof( buf ), "%s", pass_ ); 346 348 lv_obj_set_style_text_color( passLabel_, LvglTheme::warning(), 0 ); 347 349 } else { 348 350 // Bullets matching real length, clipped to fit field width ··· 421 423 lv_msgbox_add_title( mbox, LV_SYMBOL_WARNING " Confirm Delete" ); 422 424 423 425 static char msgBuf[64]; 424 - snprintf( msgBuf, sizeof( msgBuf ), "Delete \"%s\"?", self->name_ ); 426 + str::format( msgBuf, sizeof( msgBuf ), "Delete \"%s\"?", self->name_ ); 425 427 lv_msgbox_add_text( mbox, msgBuf ); 426 428 427 429 lv_obj_t* btnDel = lv_msgbox_add_footer_button( mbox, btns[0] );
+6 -3
src/ui/lvgl/lvgl_demo.cpp
··· 6 6 #include "lvgl_demo.h" 7 7 #include "lvgl_theme.h" 8 8 #include "platform/logger.h" 9 + #include "platform/safe_string.h" 9 10 10 11 #include <cstdio> 11 12 12 13 #include <lvgl.h> 14 + 15 + namespace str = kleidos::platform::str; 13 16 14 17 static constexpr const char* kTag = "LvglDemo"; 15 18 ··· 24 27 clickCount++; 25 28 if ( statusLabel ) { 26 29 static char buf[48]; 27 - snprintf( buf, sizeof( buf ), "Clicked %u times!", static_cast<unsigned>( clickCount ) ); 30 + str::format( buf, sizeof( buf ), "Clicked %u times!", static_cast<unsigned>( clickCount ) ); 28 31 lv_label_set_text( statusLabel, buf ); 29 32 } 30 33 KLEIDOS_LOGI( kTag, "Button clicked (%u)", static_cast<unsigned>( clickCount ) ); ··· 98 101 // --- Version info --- 99 102 lv_obj_t* ver = lv_label_create( scr ); 100 103 static char verBuf[64]; 101 - snprintf( verBuf, sizeof( verBuf ), "LVGL v%d.%d.%d | Kleidos v%s", LVGL_VERSION_MAJOR, 102 - LVGL_VERSION_MINOR, LVGL_VERSION_PATCH, KLEIDOS_VERSION ); 104 + str::format( verBuf, sizeof( verBuf ), "LVGL v%d.%d.%d | Kleidos v%s", LVGL_VERSION_MAJOR, 105 + LVGL_VERSION_MINOR, LVGL_VERSION_PATCH, KLEIDOS_VERSION ); 103 106 lv_label_set_text( ver, verBuf ); 104 107 lv_obj_set_style_text_color( ver, LvglTheme::muted(), 0 ); 105 108 lv_obj_set_style_text_font( ver, &lv_font_montserrat_12, 0 );
+1 -2
src/ui/lvgl/lvgl_input.cpp
··· 172 172 173 173 // Pop from key buffer — survives across multiple poll() cycles 174 174 if ( kb.hasBufferedKey() ) { 175 - char c = kb.popKey(); 176 175 // Map T-Deck keyboard chars to LVGL key codes 177 - switch ( c ) { 176 + switch ( const char c = kb.popKey(); c ) { 178 177 case '\n': // Enter 179 178 lastKey_ = LV_KEY_ENTER; 180 179 break;
+11 -10
src/ui/lvgl/lvgl_passphrase_screen.cpp
··· 16 16 #include <cstdio> 17 17 #include <cstring> 18 18 19 + namespace str = kleidos::platform::str; 20 + 19 21 static constexpr const char* kTag = "LvglPassphrase"; 20 22 static constexpr uint32_t RESULT_FLASH_MS = 800; 21 23 ··· 64 66 65 67 // Check lockout 66 68 if ( lockoutCb_ ) { 67 - uint32_t remaining = lockoutCb_(); 68 - if ( remaining > 0 ) { 69 + if ( const uint32_t remaining = lockoutCb_(); remaining > 0 ) { 69 70 if ( state_ != AuthUIState::LockedOut ) { 70 71 state_ = AuthUIState::LockedOut; 71 72 } ··· 91 92 const char* txt = lv_textarea_get_text( textarea_ ); 92 93 size_t len = txt ? kleidos::platform::str::length( txt ) : 0; 93 94 static char hintBuf[32]; 94 - snprintf( hintBuf, sizeof( hintBuf ), "%u / %u chars", static_cast<unsigned>( len ), 95 - MAX_LENGTH ); 95 + str::format( hintBuf, sizeof( hintBuf ), "%u / %u chars", static_cast<unsigned>( len ), 96 + MAX_LENGTH ); 96 97 lv_label_set_text( hintLabel_, hintBuf ); 97 98 98 99 if ( len >= MIN_LENGTH ) { ··· 229 230 // ---- Version ---- 230 231 versionLabel_ = lv_label_create( screen_ ); 231 232 static char verBuf[48]; 232 - snprintf( verBuf, sizeof( verBuf ), "Kleidos v%s", KLEIDOS_VERSION ); 233 + str::format( verBuf, sizeof( verBuf ), "Kleidos v%s", KLEIDOS_VERSION ); 233 234 lv_label_set_text( versionLabel_, verBuf ); 234 235 lv_obj_set_style_text_font( versionLabel_, &lv_font_montserrat_12, 0 ); 235 236 lv_obj_set_style_text_color( versionLabel_, LvglTheme::muted(), 0 ); ··· 290 291 return; 291 292 static char buf[32]; 292 293 if ( remainingSec >= 60 ) { 293 - snprintf( buf, sizeof( buf ), LV_SYMBOL_WARNING " Locked %um %us", 294 - static_cast<unsigned>( remainingSec / 60 ), 295 - static_cast<unsigned>( remainingSec % 60 ) ); 294 + str::format( buf, sizeof( buf ), LV_SYMBOL_WARNING " Locked %um %us", 295 + static_cast<unsigned>( remainingSec / 60 ), 296 + static_cast<unsigned>( remainingSec % 60 ) ); 296 297 } else { 297 - snprintf( buf, sizeof( buf ), LV_SYMBOL_WARNING " Locked %us", 298 - static_cast<unsigned>( remainingSec ) ); 298 + str::format( buf, sizeof( buf ), LV_SYMBOL_WARNING " Locked %us", 299 + static_cast<unsigned>( remainingSec ) ); 299 300 } 300 301 lv_label_set_text( statusLabel_, buf ); 301 302 lv_obj_set_style_text_color( statusLabel_, LvglTheme::lockout(), 0 );
+3 -1
src/ui/lvgl/lvgl_screen.h
··· 36 36 } 37 37 38 38 /// @brief Screen is being hidden. Optional cleanup. 39 - virtual void leave() {} 39 + virtual void leave() { 40 + // Default: no-op. Screens with transient state override this. 41 + } 40 42 41 43 /// @brief Destroy all LVGL objects. Called once. 42 44 virtual void destroy() {
+56 -54
src/ui/lvgl/lvgl_settings_screen.cpp
··· 32 32 using kleidos::platform::NvsStore; 33 33 namespace platformMemory = kleidos::platform::memory; 34 34 namespace platformSystem = kleidos::platform::system; 35 + namespace str = kleidos::platform::str; 35 36 36 37 // Setting IDs stored in list button user_data 37 38 enum class SettingId : uint8_t { ··· 242 243 // ---- INPUT section ---- 243 244 addSectionHeader( "INPUT" ); 244 245 static char layoutBuf[8]; 245 - snprintf( layoutBuf, sizeof( layoutBuf ), "%s", layoutName( kble::Facade::getLayout() ) ); 246 + str::format( layoutBuf, sizeof( layoutBuf ), "%s", layoutName( kble::Facade::getLayout() ) ); 246 247 addRow( "Keyboard layout", layoutBuf, true, SettingId::KB_LAYOUT ); 247 248 248 249 // ---- DEVICE section ---- ··· 252 253 // ---- SECURITY section ---- 253 254 addSectionHeader( "SECURITY" ); 254 255 static char lockBuf[16]; 255 - snprintf( lockBuf, sizeof( lockBuf ), "%s", autoLockName( ctx_->autoLockMode ) ); 256 + str::format( lockBuf, sizeof( lockBuf ), "%s", autoLockName( ctx_->autoLockMode ) ); 256 257 addRow( "Auto-lock", lockBuf, true, SettingId::AUTO_LOCK ); 257 258 258 259 static char bleBuf[16]; 259 - snprintf( bleBuf, sizeof( bleBuf ), "%u bonded", kble::Facade::bondedCount() ); 260 + str::format( bleBuf, sizeof( bleBuf ), "%u bonded", kble::Facade::bondedCount() ); 260 261 addRow( "BLE hosts", bleBuf, true, SettingId::BLE_HOSTS ); 261 262 262 263 // ---- SYSTEM section ---- ··· 264 265 #if HAS_SDCARD 265 266 if ( SdCard::isMounted() ) { 266 267 static char sdBuf[16]; 267 - snprintf( sdBuf, sizeof( sdBuf ), "%.0f MB", 268 - static_cast<double>( SdCard::totalBytes() ) / ( 1024.0 * 1024.0 ) ); 268 + str::format( sdBuf, sizeof( sdBuf ), "%.0f MB", 269 + static_cast<double>( SdCard::totalBytes() ) / ( 1024.0 * 1024.0 ) ); 269 270 addRow( "SD card", sdBuf, true, SettingId::SD_CARD ); 270 271 } else { 271 272 addRow( "SD card", "n/a", true, SettingId::SD_CARD, LvglTheme::COLOR_TEXT_SEC ); ··· 360 361 361 362 lv_obj_t* lbl = lv_label_create( btn ); 362 363 static char buf[32]; 363 - snprintf( buf, sizeof( buf ), "%s %s", selected ? LV_SYMBOL_OK : " ", l.name ); 364 + str::format( buf, sizeof( buf ), "%s %s", selected ? LV_SYMBOL_OK : " ", l.name ); 364 365 lv_label_set_text( lbl, buf ); 365 366 lv_obj_set_style_text_font( lbl, &lv_font_montserrat_12, 0 ); 366 367 lv_obj_set_style_text_color( lbl, selected ? LvglTheme::primary() : LvglTheme::text(), 0 ); ··· 400 401 401 402 lv_obj_t* lbl = lv_label_create( btn ); 402 403 static char buf[48]; 403 - snprintf( buf, sizeof( buf ), "%s %s: %s", selected ? LV_SYMBOL_OK : " ", m.name, m.desc ); 404 + str::format( buf, sizeof( buf ), "%s %s: %s", selected ? LV_SYMBOL_OK : " ", m.name, 405 + m.desc ); 404 406 lv_label_set_text( lbl, buf ); 405 407 lv_obj_set_style_text_font( lbl, &lv_font_montserrat_10, 0 ); 406 408 lv_obj_set_style_text_color( lbl, selected ? LvglTheme::primary() : LvglTheme::text(), 0 ); ··· 432 434 433 435 lv_obj_t* valLbl = lv_label_create( row ); 434 436 static char buf[3][16]; 435 - snprintf( buf[id], sizeof( buf[id] ), "%" PRIu32 " s", valueMs / 1000 ); 437 + str::format( buf[id], sizeof( buf[id] ), "%" PRIu32 " s", valueMs / 1000 ); 436 438 lv_label_set_text( valLbl, buf[id] ); 437 439 lv_obj_set_style_text_font( valLbl, &lv_font_montserrat_12, 0 ); 438 440 lv_obj_set_style_text_color( valLbl, LvglTheme::primary(), 0 ); ··· 458 460 subScreen_ = createSubPanel( screen_, "BLE Hosts", onSubBackClicked, this ); 459 461 lv_group_t* grp = lv_group_get_default(); 460 462 461 - uint8_t count = kble::Facade::bondedCount(); 462 - if ( count == 0 ) { 463 + if ( const uint8_t count = kble::Facade::bondedCount(); count == 0 ) { 463 464 lv_obj_t* lbl = lv_label_create( subScreen_ ); 464 465 lv_label_set_text( lbl, "No bonded hosts.\nPair via BLE to add hosts." ); 465 466 lv_obj_set_style_text_font( lbl, &lv_font_montserrat_12, 0 ); ··· 486 487 487 488 lv_obj_t* nameLbl = lv_label_create( row ); 488 489 static char nameBuf[4][32]; 489 - snprintf( nameBuf[i], sizeof( nameBuf[i] ), "%s %s", isTarget ? LV_SYMBOL_OK : " ", 490 - host.name[0] ? host.name : "(unknown)" ); 490 + str::format( nameBuf[i], sizeof( nameBuf[i] ), "%s %s", isTarget ? LV_SYMBOL_OK : " ", 491 + host.name[0] ? host.name : "(unknown)" ); 491 492 lv_label_set_text( nameLbl, nameBuf[i] ); 492 493 lv_obj_set_style_text_font( nameLbl, &lv_font_montserrat_12, 0 ); 493 494 lv_obj_set_style_text_color( nameLbl, isTarget ? LvglTheme::primary() : LvglTheme::text(), ··· 546 547 static char macBuf[18]; 547 548 if ( !platformSystem::readMacAddressText( platformSystem::MacAddressKind::WifiStation, macBuf, 548 549 sizeof( macBuf ) ) ) { 549 - snprintf( macBuf, sizeof( macBuf ), "?" ); 550 + str::format( macBuf, sizeof( macBuf ), "?" ); 550 551 } 551 552 addInfoRow( "MAC:", macBuf ); 552 553 553 554 // Boot count 554 555 static char bootBuf[16]; 555 - snprintf( bootBuf, sizeof( bootBuf ), "%" PRIu32, ctx_ ? ctx_->bootCount : 0u ); 556 + str::format( bootBuf, sizeof( bootBuf ), "%" PRIu32, ctx_ ? ctx_->bootCount : 0u ); 556 557 addInfoRow( "Boots:", bootBuf ); 557 558 558 559 // Uptime ··· 560 561 uint32_t sec = kleidos::platform::clock::millis32() / 1000U; 561 562 uint32_t min = sec / 60; 562 563 sec %= 60; 563 - snprintf( uptBuf, sizeof( uptBuf ), "%" PRIu32 " min %" PRIu32 " s", min, sec ); 564 + str::format( uptBuf, sizeof( uptBuf ), "%" PRIu32 " min %" PRIu32 " s", min, sec ); 564 565 addInfoRow( "Uptime:", uptBuf ); 565 566 566 567 // Free heap 567 568 static char heapBuf[16]; 568 - snprintf( heapBuf, sizeof( heapBuf ), "%" PRIu32 " KB", 569 - platformSystem::freeHeapBytes() / 1024U ); 569 + str::format( heapBuf, sizeof( heapBuf ), "%" PRIu32 " KB", 570 + platformSystem::freeHeapBytes() / 1024U ); 570 571 addInfoRow( "Free Heap:", heapBuf ); 571 572 572 573 // Credentials 573 574 static char credBuf[16]; 574 - snprintf( credBuf, sizeof( credBuf ), "%u / %u", ctx_ ? ctx_->credCount : 0, kMaxCredentials ); 575 + str::format( credBuf, sizeof( credBuf ), "%u / %u", ctx_ ? ctx_->credCount : 0, 576 + kMaxCredentials ); 575 577 addInfoRow( "Credentials:", credBuf ); 576 578 577 579 // Flash usage 578 580 static char flashBuf[24]; 579 - snprintf( flashBuf, sizeof( flashBuf ), "%" PRIu32 " KB / %" PRIu32 " KB", 580 - platformSystem::runningImageSizeBytes() / 1024U, 581 - platformSystem::nextUpdatePartitionSizeBytes() / 1024U ); 581 + str::format( flashBuf, sizeof( flashBuf ), "%" PRIu32 " KB / %" PRIu32 " KB", 582 + platformSystem::runningImageSizeBytes() / 1024U, 583 + platformSystem::nextUpdatePartitionSizeBytes() / 1024U ); 582 584 addInfoRow( "Flash:", flashBuf ); 583 585 } 584 586 ··· 619 621 // SD info label (compact) 620 622 { 621 623 static char sdInfo[64]; 622 - snprintf( sdInfo, sizeof( sdInfo ), "%s | %.1f GB free / %.1f GB", SdCard::cardType(), 623 - static_cast<double>( SdCard::totalBytes() - SdCard::usedBytes() ) 624 - / ( 1024.0 * 1024.0 * 1024.0 ), 625 - static_cast<double>( SdCard::totalBytes() ) / ( 1024.0 * 1024.0 * 1024.0 ) ); 624 + str::format( sdInfo, sizeof( sdInfo ), "%s | %.1f GB free / %.1f GB", SdCard::cardType(), 625 + static_cast<double>( SdCard::totalBytes() - SdCard::usedBytes() ) 626 + / ( 1024.0 * 1024.0 * 1024.0 ), 627 + static_cast<double>( SdCard::totalBytes() ) / ( 1024.0 * 1024.0 * 1024.0 ) ); 626 628 lv_obj_t* infoLbl = lv_label_create( subScreen_ ); 627 629 lv_label_set_text( infoLbl, sdInfo ); 628 630 lv_obj_set_style_text_font( infoLbl, &lv_font_montserrat_10, 0 ); ··· 644 646 645 647 lv_obj_t* lbl = lv_label_create( btn ); 646 648 static char buf[64]; 647 - snprintf( buf, sizeof( buf ), "%s %s", icon, text ); 649 + str::format( buf, sizeof( buf ), "%s %s", icon, text ); 648 650 lv_label_set_text( lbl, buf ); 649 651 lv_obj_set_style_text_font( lbl, &lv_font_montserrat_12, 0 ); 650 652 lv_obj_align( lbl, LV_ALIGN_LEFT_MID, 0, 0 ); ··· 675 677 676 678 if ( SdVault::hasFirmware() ) { 677 679 static char fwBuf[48]; 678 - snprintf( fwBuf, sizeof( fwBuf ), "FW on SD: %u KB", SdVault::firmwareSize() / 1024 ); 680 + str::format( fwBuf, sizeof( fwBuf ), "FW on SD: %u KB", SdVault::firmwareSize() / 1024 ); 679 681 lv_obj_t* fwLbl = lv_label_create( subScreen_ ); 680 682 lv_label_set_text( fwLbl, fwBuf ); 681 683 lv_obj_set_style_text_font( fwLbl, &lv_font_montserrat_10, 0 ); ··· 807 809 CsvImportResult r = SdVault::importCsv( self->ctx_->masterKey ); 808 810 VaultStore::end(); 809 811 static char csvMsg[64]; 810 - snprintf( csvMsg, sizeof( csvMsg ), "Imported %u, skipped %u", r.imported, 811 - r.skipped ); 812 + str::format( csvMsg, sizeof( csvMsg ), "Imported %u, skipped %u", r.imported, 813 + r.skipped ); 812 814 resultMsg = csvMsg; 813 815 ok = ( r.imported > 0 ); 814 816 if ( ok ) ··· 908 910 static char titleBuf[32]; 909 911 const char* lastSlash = kleidos::platform::str::findLastChar( path, '/' ); 910 912 if ( !lastSlash || lastSlash == path ) { 911 - snprintf( titleBuf, sizeof( titleBuf ), LV_SYMBOL_DIRECTORY " /" ); 913 + str::format( titleBuf, sizeof( titleBuf ), LV_SYMBOL_DIRECTORY " /" ); 912 914 } else { 913 - snprintf( titleBuf, sizeof( titleBuf ), LV_SYMBOL_DIRECTORY " %s", lastSlash + 1 ); 915 + str::format( titleBuf, sizeof( titleBuf ), LV_SYMBOL_DIRECTORY " %s", lastSlash + 1 ); 914 916 } 915 917 916 918 subScreen_ = createSubPanel( screen_, titleBuf, onBrowserBackClicked, this ); ··· 958 960 // Show directories first, then files 959 961 for ( uint8_t pass = 0; pass < 2; pass++ ) { 960 962 for ( uint8_t i = 0; i < count; i++ ) { 961 - bool wantDir = ( pass == 0 ); 962 - if ( entries[i].isDir != wantDir ) 963 + if ( const bool wantDir = ( pass == 0 ); entries[i].isDir != wantDir ) 963 964 continue; 964 965 965 966 lv_obj_t* btn = lv_button_create( subScreen_ ); ··· 982 983 lv_obj_t* nameLbl = lv_label_create( btn ); 983 984 static char nameBuf[56]; 984 985 if ( entries[i].isDir ) { 985 - snprintf( nameBuf, sizeof( nameBuf ), LV_SYMBOL_DIRECTORY " %.45s", 986 - entries[i].name ); 986 + str::format( nameBuf, sizeof( nameBuf ), LV_SYMBOL_DIRECTORY " %.45s", 987 + entries[i].name ); 987 988 } else { 988 - snprintf( nameBuf, sizeof( nameBuf ), "%s %.45s", 989 - SdVault::isTextFile( entries[i].name ) ? LV_SYMBOL_FILE : LV_SYMBOL_DUMMY, 990 - entries[i].name ); 989 + str::format( 990 + nameBuf, sizeof( nameBuf ), "%s %.45s", 991 + SdVault::isTextFile( entries[i].name ) ? LV_SYMBOL_FILE : LV_SYMBOL_DUMMY, 992 + entries[i].name ); 991 993 } 992 994 lv_label_set_text( nameLbl, nameBuf ); 993 995 lv_obj_set_style_text_font( nameLbl, &lv_font_montserrat_10, 0 ); ··· 1000 1002 lv_obj_t* sizeLbl = lv_label_create( btn ); 1001 1003 static char sizeBuf[16]; 1002 1004 if ( entries[i].size >= 1024 * 1024 ) { 1003 - snprintf( sizeBuf, sizeof( sizeBuf ), "%.1f MB", 1004 - entries[i].size / ( 1024.0 * 1024.0 ) ); 1005 + str::format( sizeBuf, sizeof( sizeBuf ), "%.1f MB", 1006 + entries[i].size / ( 1024.0 * 1024.0 ) ); 1005 1007 } else if ( entries[i].size >= 1024 ) { 1006 - snprintf( sizeBuf, sizeof( sizeBuf ), "%" PRIu32 " KB", 1007 - static_cast<uint32_t>( entries[i].size / 1024 ) ); 1008 + str::format( sizeBuf, sizeof( sizeBuf ), "%" PRIu32 " KB", 1009 + static_cast<uint32_t>( entries[i].size / 1024 ) ); 1008 1010 } else { 1009 - snprintf( sizeBuf, sizeof( sizeBuf ), "%" PRIu32 " B", 1010 - static_cast<uint32_t>( entries[i].size ) ); 1011 + str::format( sizeBuf, sizeof( sizeBuf ), "%" PRIu32 " B", 1012 + static_cast<uint32_t>( entries[i].size ) ); 1011 1013 } 1012 1014 lv_label_set_text( sizeLbl, sizeBuf ); 1013 1015 lv_obj_set_style_text_font( sizeLbl, &lv_font_montserrat_10, 0 ); ··· 1042 1044 // Re-read entries to get the name (static buffer still valid from showFileBrowser) 1043 1045 static constexpr uint8_t MAX_ENTRIES = 40; 1044 1046 static SdVault::DirEntry entries[MAX_ENTRIES]; 1045 - uint8_t count = SdVault::listDir( self->browsePath_, entries, MAX_ENTRIES ); 1046 - 1047 - if ( idx >= count ) 1047 + if ( const uint8_t count = SdVault::listDir( self->browsePath_, entries, MAX_ENTRIES ); 1048 + idx >= count ) 1048 1049 return; 1049 1050 1050 1051 // Build full path 1051 1052 char fullPath[192]; 1052 1053 if ( kleidos::platform::str::equals( self->browsePath_, "/" ) ) { 1053 - snprintf( fullPath, sizeof( fullPath ), "/%s", entries[idx].name ); 1054 + str::format( fullPath, sizeof( fullPath ), "/%s", entries[idx].name ); 1054 1055 } else { 1055 - snprintf( fullPath, sizeof( fullPath ), "%s/%s", self->browsePath_, entries[idx].name ); 1056 + str::format( fullPath, sizeof( fullPath ), "%s/%s", self->browsePath_, entries[idx].name ); 1056 1057 } 1057 1058 1058 1059 if ( isDir ) { ··· 1075 1076 const char* name = kleidos::platform::str::findLastChar( path, '/' ); 1076 1077 name = name ? name + 1 : path; 1077 1078 static char titleBuf[32]; 1078 - snprintf( titleBuf, sizeof( titleBuf ), LV_SYMBOL_FILE " %s", name ); 1079 + str::format( titleBuf, sizeof( titleBuf ), LV_SYMBOL_FILE " %s", name ); 1079 1080 1080 1081 subScreen_ = createSubPanel( screen_, titleBuf, onSdSubBackClicked, this ); 1081 1082 ··· 1103 1104 1104 1105 // File size info 1105 1106 static char infoBuf[32]; 1106 - snprintf( infoBuf, sizeof( infoBuf ), "%u bytes%s", bytesRead, 1107 - bytesRead >= MAX_VIEW_SIZE - 1 ? " (truncated)" : "" ); 1107 + str::format( infoBuf, sizeof( infoBuf ), "%u bytes%s", bytesRead, 1108 + bytesRead >= MAX_VIEW_SIZE - 1 ? " (truncated)" : "" ); 1108 1109 lv_obj_t* infoLbl = lv_label_create( subScreen_ ); 1109 1110 lv_label_set_text( infoLbl, infoBuf ); 1110 1111 lv_obj_set_style_text_font( infoLbl, &lv_font_montserrat_10, 0 ); ··· 1185 1186 void LvglSettingsScreen::onSettingClicked( lv_event_t* e ) { 1186 1187 auto* self = static_cast<LvglSettingsScreen*>( lv_event_get_user_data( e ) ); 1187 1188 lv_obj_t* btn = static_cast<lv_obj_t*>( lv_event_get_target( e ) ); 1188 - auto id = static_cast<SettingId>( reinterpret_cast<uintptr_t>( lv_obj_get_user_data( btn ) ) ); 1189 1189 1190 - switch ( id ) { 1190 + switch ( const auto id = static_cast<SettingId>( 1191 + reinterpret_cast<uintptr_t>( lv_obj_get_user_data( btn ) ) ); 1192 + id ) { 1191 1193 case SettingId::KB_LAYOUT: 1192 1194 self->showLayoutPicker(); 1193 1195 break;
+11 -10
src/ui/lvgl/lvgl_vault_screen.cpp
··· 14 14 #include <cstdio> 15 15 #include <cstring> 16 16 17 + namespace str = kleidos::platform::str; 18 + 17 19 static constexpr const char* kTag = "LvglVault"; 18 20 static constexpr uint32_t STATUS_UPDATE_MS = 2000; 19 21 ··· 79 81 // Dynamic update (called from VaultState::update) 80 82 // --------------------------------------------------------------------------- 81 83 void LvglVaultScreen::update() { 82 - uint32_t now = kleidos::platform::clock::millis32(); 83 - if ( now - lastStatusUpdate_ >= STATUS_UPDATE_MS ) { 84 + if ( const uint32_t now = kleidos::platform::clock::millis32(); 85 + now - lastStatusUpdate_ >= STATUS_UPDATE_MS ) { 84 86 updateStatusBar(); 85 87 lastStatusUpdate_ = now; 86 88 } ··· 353 355 const char* user = 354 356 ( i < 64 && userCache_[i][0] != '\0' && userCache_[i][0] != ' ' ) ? userCache_[i] : ""; 355 357 if ( user[0] && e.favorite ) { 356 - snprintf( subBuf, sizeof( subBuf ), "%s - favorite", user ); 358 + str::format( subBuf, sizeof( subBuf ), "%s - favorite", user ); 357 359 } else if ( user[0] ) { 358 - snprintf( subBuf, sizeof( subBuf ), "%s", user ); 360 + str::format( subBuf, sizeof( subBuf ), "%s", user ); 359 361 } else if ( e.favorite ) { 360 - snprintf( subBuf, sizeof( subBuf ), "favorite" ); 362 + str::format( subBuf, sizeof( subBuf ), "favorite" ); 361 363 } else { 362 - snprintf( subBuf, sizeof( subBuf ), "tap to type via BLE" ); 364 + str::format( subBuf, sizeof( subBuf ), "tap to type via BLE" ); 363 365 } 364 366 lv_obj_t* row = makeVaultRow( credList_, e.name, subBuf, e.favorite ? "" : "strong", 365 367 LvglTheme::primary(), e.favorite ); ··· 497 499 498 500 // BLE icon: visible only when connected (per mockup) 499 501 if ( bleIcon_ ) { 500 - bool bleUp = bleStatus_ ? bleStatus_() : false; 501 - if ( bleUp ) { 502 + if ( const bool bleUp = bleStatus_ ? bleStatus_() : false; bleUp ) { 502 503 lv_obj_clear_flag( bleIcon_, LV_OBJ_FLAG_HIDDEN ); 503 504 lv_obj_set_style_text_color( bleIcon_, LvglTheme::primary(), 0 ); 504 505 } else { ··· 510 511 if ( itemCountLabel_ ) { 511 512 char buf[32]; 512 513 uint16_t total = ctx_->credCount + ctx_->totpCount; 513 - snprintf( buf, sizeof( buf ), "%u item%s", total, total == 1 ? "" : "s" ); 514 + str::format( buf, sizeof( buf ), "%u item%s", total, total == 1 ? "" : "s" ); 514 515 lv_label_set_text( itemCountLabel_, buf ); 515 516 } 516 517 ··· 519 520 int pct = ctx_->power.getBatteryPercent(); 520 521 bool charging = ctx_->power.isCharging(); 521 522 char buf[12]; 522 - snprintf( buf, sizeof( buf ), "%d%%", pct ); 523 + str::format( buf, sizeof( buf ), "%d%%", pct ); 523 524 lv_label_set_text( battLabel_, buf ); 524 525 525 526 lv_color_t color = LvglTheme::text();
+14 -14
src/ui/notifications/status_notifier.cpp
··· 176 176 PctBuf fmtPct( uint8_t pct ) { 177 177 PctBuf b{}; 178 178 // 0..100 → up to "100%" + NUL 179 - uint8_t v = pct > 100 ? 100 : pct; 180 - if ( v >= 100 ) { 179 + if ( const uint8_t v = pct > 100 ? 100 : pct; v >= 100 ) { 181 180 b.txt[0] = '1'; 182 181 b.txt[1] = '0'; 183 182 b.txt[2] = '0'; ··· 199 198 void dispatch( notifier::Event ev, uint8_t pct, const char* extraDetail ) { 200 199 using notifier::Event; 201 200 using popup::Severity; 201 + using enum notifier::Event; 202 202 switch ( ev ) { 203 - case Event::NONE: 203 + case NONE: 204 204 break; 205 - case Event::BatteryLow: { 205 + case BatteryLow: { 206 206 auto b = fmtPct( pct ); 207 207 popup::toast( i18n::tr( i18n::StringId::PopBattLow ), b.txt, Severity::CAUTION, 3000 ); 208 208 break; 209 209 } 210 - case Event::BatteryCritical: 210 + case BatteryCritical: 211 211 popup::toast( i18n::tr( i18n::StringId::PopBattCrit ), 212 212 i18n::tr( i18n::StringId::PopPlugUsb ), Severity::WARNING, 4000 ); 213 213 break; 214 - case Event::BatteryCharging: 214 + case BatteryCharging: 215 215 popup::toast( i18n::tr( i18n::StringId::PopCharging ), nullptr, Severity::INFO, 2000 ); 216 216 break; 217 - case Event::BatteryFull: 217 + case BatteryFull: 218 218 popup::toast( i18n::tr( i18n::StringId::PopBattFull ), nullptr, Severity::SUCCESS, 219 219 2000 ); 220 220 break; 221 - case Event::BleHostConnected: 221 + case BleHostConnected: 222 222 popup::toast( i18n::tr( i18n::StringId::PopHostConn ), nullptr, Severity::SUCCESS, 223 223 1500 ); 224 224 break; 225 - case Event::BleHostDisconnected: 225 + case BleHostDisconnected: 226 226 popup::toast( i18n::tr( i18n::StringId::PopHostDisc ), nullptr, Severity::INFO, 1500 ); 227 227 break; 228 - case Event::AdminClientConnected: 228 + case AdminClientConnected: 229 229 popup::toast( i18n::tr( i18n::StringId::PopAdminClient ), 230 230 i18n::tr( i18n::StringId::PopAdmin1Conn ), Severity::INFO, 2000 ); 231 231 break; 232 - case Event::AdminIdleWarning: 232 + case AdminIdleWarning: 233 233 popup::toast( i18n::tr( i18n::StringId::PopAdminIdle ), 234 234 i18n::tr( i18n::StringId::PopAdminClosing ), Severity::CAUTION, 3000 ); 235 235 break; 236 - case Event::AdminTimeout: 236 + case AdminTimeout: 237 237 popup::toast( i18n::tr( i18n::StringId::PopAdminClosed ), 238 238 i18n::tr( i18n::StringId::PopTimeout ), Severity::WARNING, 2000 ); 239 239 break; 240 - case Event::AutoLockCountdown: 240 + case AutoLockCountdown: 241 241 popup::toast( i18n::tr( i18n::StringId::PopLocking5S ), nullptr, Severity::CAUTION, 242 242 1500 ); 243 243 break; 244 - case Event::OtaApplied: 244 + case OtaApplied: 245 245 popup::toast( i18n::tr( i18n::StringId::PopOtaApplied ), extraDetail, Severity::SUCCESS, 246 246 3000 ); 247 247 break;
+13 -13
src/ui/popup/lvgl_popup_adapter.cpp
··· 113 113 return; 114 114 115 115 lv_obj_t* top = lv_layer_top(); 116 - const int16_t sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 116 + const auto sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 117 117 const int16_t rowH = tinyScreen ? 22 : 28; 118 118 const int16_t rowGap = 4; 119 119 const int16_t width = tinyScreen ? ( sw - 8 ) : ( ( sw > 316 ) ? 300 : ( sw - 16 ) ); ··· 153 153 lv_obj_set_style_bg_color( o.accent, ac, 0 ); 154 154 } 155 155 156 - const int16_t x = static_cast<int16_t>( ( sw - width ) / 2 ); 157 - const int16_t y = static_cast<int16_t>( baseY + i * ( rowH + rowGap ) ); 156 + const auto x = static_cast<int16_t>( ( sw - width ) / 2 ); 157 + const auto y = static_cast<int16_t>( baseY + i * ( rowH + rowGap ) ); 158 158 lv_obj_set_pos( o.root, x, y ); 159 159 lv_obj_set_size( o.root, width, rowH ); 160 160 ··· 191 191 } 192 192 193 193 lv_obj_t* top = lv_layer_top(); 194 - const int16_t sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 195 - const int16_t sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 194 + const auto sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 195 + const auto sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 196 196 const int16_t w = tinyScreen ? 120 : 160; 197 197 const int16_t h = tinyScreen ? 80 : 100; 198 198 ··· 234 234 } 235 235 236 236 lv_obj_t* top = lv_layer_top(); 237 - const int16_t sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 238 - const int16_t sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 237 + const auto sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 238 + const auto sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 239 239 const bool hasDetail = s.detail[0] != '\0'; 240 240 const int16_t w = 241 241 tinyScreen ? ( ( sw < 190 ) ? ( sw - 10 ) : 180 ) : ( ( sw > 300 ) ? 240 : ( sw - 24 ) ); ··· 312 312 const bool hasCancel = ( actionCount == 2 ); 313 313 const bool hasActions = ( actionCount > 0 ); 314 314 315 - lv_obj_t* top = lv_layer_top(); 316 - const int16_t sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 317 - const int16_t sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 318 - int16_t w = tinyScreen ? ( ( sw < 125 ) ? ( sw - 6 ) : 120 ) : 260; 319 - int16_t h = tinyScreen ? 110 : 130; 315 + lv_obj_t* top = lv_layer_top(); 316 + const auto sw = static_cast<int16_t>( lv_obj_get_width( top ) ); 317 + const auto sh = static_cast<int16_t>( lv_obj_get_height( top ) ); 318 + int16_t w = tinyScreen ? ( ( sw < 125 ) ? ( sw - 6 ) : 120 ) : 260; 319 + int16_t h = tinyScreen ? 110 : 130; 320 320 if ( hasChoice ) { 321 321 w = tinyScreen ? ( sw - 8 ) : ( ( sw > 320 ) ? 304 : ( sw - 16 ) ); 322 322 if ( w < 120 ) ··· 370 370 if ( hasChoice ) { 371 371 const int16_t btnH = 26; 372 372 const int16_t choiceGap = tinyScreen ? 3 : 6; 373 - const int16_t btnW = static_cast<int16_t>( ( w - 24 - 2 * choiceGap ) / 3 ); 373 + const auto btnW = static_cast<int16_t>( ( w - 24 - 2 * choiceGap ) / 3 ); 374 374 s_modalCancel = lv_button_create( s_modalPanel ); 375 375 lv_obj_set_size( s_modalCancel, btnW, btnH ); 376 376 lv_obj_align( s_modalCancel, LV_ALIGN_BOTTOM_LEFT, 0, 0 );
+6 -6
src/ui/popup/lvgl_popup_adapter.h
··· 29 29 * because `lv_color_make()` is not constexpr in LVGL v9. 30 30 */ 31 31 inline lv_color_t toLv( uint16_t rgb565 ) { 32 - const uint8_t r5 = static_cast<uint8_t>( ( rgb565 >> 11 ) & 0x1F ); 33 - const uint8_t g6 = static_cast<uint8_t>( ( rgb565 >> 5 ) & 0x3F ); 34 - const uint8_t b5 = static_cast<uint8_t>( rgb565 & 0x1F ); 32 + const auto r5 = static_cast<uint8_t>( ( rgb565 >> 11 ) & 0x1F ); 33 + const auto g6 = static_cast<uint8_t>( ( rgb565 >> 5 ) & 0x3F ); 34 + const auto b5 = static_cast<uint8_t>( rgb565 & 0x1F ); 35 35 // Expand to 8-bit per channel with bit-replication so 0x1F → 0xFF, etc. 36 - const uint8_t r8 = static_cast<uint8_t>( ( r5 << 3 ) | ( r5 >> 2 ) ); 37 - const uint8_t g8 = static_cast<uint8_t>( ( g6 << 2 ) | ( g6 >> 4 ) ); 38 - const uint8_t b8 = static_cast<uint8_t>( ( b5 << 3 ) | ( b5 >> 2 ) ); 36 + const auto r8 = static_cast<uint8_t>( ( r5 << 3 ) | ( r5 >> 2 ) ); 37 + const auto g8 = static_cast<uint8_t>( ( g6 << 2 ) | ( g6 >> 4 ) ); 38 + const auto b8 = static_cast<uint8_t>( ( b5 << 3 ) | ( b5 >> 2 ) ); 39 39 return lv_color_make( r8, g8, b8 ); 40 40 } 41 41
+4 -6
src/ui/popup/popup_queue.cpp
··· 68 68 for ( uint8_t read = 0; read < toastCount_; ++read ) { 69 69 const ToastSlot& s = toasts_[read]; 70 70 // Unsigned wrap-safe: deadline reached when (nowMs - deadline) is small. 71 - bool expired = static_cast<int32_t>( nowMs - s.deadlineMs ) >= 0; 72 - if ( expired ) { 71 + if ( const bool expired = static_cast<int32_t>( nowMs - s.deadlineMs ) >= 0; expired ) { 73 72 ++dismissed; 74 73 continue; 75 74 } ··· 152 151 if ( modal_.timeoutMs == 0 ) { 153 152 return false; 154 153 } 155 - uint32_t elapsed = nowMs - modal_.startMs; 156 - if ( elapsed >= modal_.timeoutMs ) { 154 + if ( const uint32_t elapsed = nowMs - modal_.startMs; elapsed >= modal_.timeoutMs ) { 157 155 modal_.result = ConfirmResult::TIMEOUT; 158 156 modal_.kind = ModalKind::NONE; 159 157 return true; ··· 188 186 if ( !transition_.active ) { 189 187 return false; 190 188 } 191 - const bool expired = static_cast<int32_t>( nowMs - transition_.deadlineMs ) >= 0; 192 - if ( !expired ) { 189 + if ( const bool expired = static_cast<int32_t>( nowMs - transition_.deadlineMs ) >= 0; 190 + !expired ) { 193 191 return false; 194 192 } 195 193 clearTransition();
+483 -297
src/ui/popup/popup_system.cpp
··· 26 26 #include <cstdint> 27 27 #endif 28 28 29 + namespace str = kleidos::platform::str; 29 30 namespace popup { 30 31 31 32 namespace platformClock = kleidos::platform::clock; ··· 54 55 55 56 // Layout rectangles — recomputed lazily when something changes. 56 57 struct Rect { 57 - int16_t x, y, w, h; 58 + int16_t x; 59 + int16_t y; 60 + int16_t w; 61 + int16_t h; 58 62 }; 59 63 60 64 Rect s_lastToastArea = { 0, 0, 0, 0 }; ··· 83 87 constexpr uint16_t kBg = theme::kBg565; 84 88 85 89 constexpr uint16_t accentColor( Severity s ) { 86 - return s == Severity::SUCCESS ? theme::kSuccess565 87 - : s == Severity::CAUTION ? theme::kLockout565 88 - : s == Severity::WARNING ? theme::kWarning565 89 - : theme::kPrimary565; 90 + using enum Severity; 91 + switch ( s ) { 92 + case SUCCESS: 93 + return theme::kSuccess565; 94 + case CAUTION: 95 + return theme::kLockout565; 96 + case WARNING: 97 + return theme::kWarning565; 98 + case INFO: 99 + default: 100 + return theme::kPrimary565; 101 + } 90 102 } 91 103 92 104 void copyFittedText( const char* source, char* out, size_t outLen, int16_t maxWidth ) { ··· 158 170 const int16_t sw = display::width(); 159 171 const int16_t rowH = tiny ? 26 : 28; 160 172 const int16_t rowGap = 4; 161 - const int16_t width = tiny ? ( sw - 8 ) : ( ( sw > 316 ) ? 300 : ( sw - 16 ) ); 162 - const int16_t x = static_cast<int16_t>( ( sw - width ) / 2 ); 163 - const int16_t y = tiny ? 4 : 8; 164 - const int16_t h = 165 - static_cast<int16_t>( count == 0 ? 0 : ( rowH * count + rowGap * ( count - 1 ) ) ); 173 + int16_t width; 174 + if ( tiny ) { 175 + width = sw - 8; 176 + } else if ( sw > 316 ) { 177 + width = 300; 178 + } else { 179 + width = sw - 16; 180 + } 181 + const int16_t x = static_cast<int16_t>( ( sw - width ) / 2 ); 182 + const int16_t y = tiny ? 4 : 8; 183 + int16_t h; 184 + if ( count == 0 ) { 185 + h = 0; 186 + } else { 187 + h = static_cast<int16_t>( rowH * count + rowGap * ( count - 1 ) ); 188 + } 166 189 return { x, y, width, h }; 167 190 } 168 191 ··· 284 307 const bool tiny = isTinyScreen(); 285 308 const int16_t sw = display::width(); 286 309 const int16_t sh = display::height(); 287 - int16_t w = tiny ? ( ( sw < 190 ) ? ( sw - 10 ) : 180 ) : ( ( sw > 300 ) ? 240 : ( sw - 24 ) ); 310 + int16_t w; 311 + if ( tiny ) { 312 + w = ( sw < 190 ) ? ( sw - 10 ) : 180; 313 + } else if ( sw > 300 ) { 314 + w = 240; 315 + } else { 316 + w = sw - 24; 317 + } 288 318 if ( w < 96 ) { 289 319 w = sw - 6; 290 320 } 291 - int16_t h = hasDetail ? ( tiny ? 56 : 64 ) : ( tiny ? 42 : 50 ); 321 + int16_t h; 322 + if ( hasDetail ) { 323 + h = tiny ? 56 : 64; 324 + } else { 325 + h = tiny ? 42 : 50; 326 + } 292 327 if ( h > sh - 12 ) { 293 328 h = sh - 12; 294 329 } ··· 345 380 const bool tiny = isTinyScreen(); 346 381 const int16_t sw = display::width(); 347 382 const int16_t sh = display::height(); 348 - int16_t w = tiny ? ( ( sw >= 220 ) ? ( sw - 60 ) : ( sw - 12 ) ) : 260; 383 + int16_t w; 384 + if ( tiny ) { 385 + w = ( sw >= 220 ) ? ( sw - 60 ) : ( sw - 12 ); 386 + } else { 387 + w = 260; 388 + } 349 389 if ( tiny && w > 180 ) { 350 390 w = 180; 351 391 } ··· 398 438 // rectangle on close" bug by construction. Both buttons share the same 399 439 // frame; the Confirm button is filled left→right in the severity accent 400 440 // color while the user holds the destructive button. 441 + namespace { 442 + 443 + struct FullscreenLayout { 444 + int16_t pad; 445 + int16_t titleY; 446 + int16_t msgY; 447 + int16_t hintY; // only valid when !shortScreen 448 + int16_t btnW; 449 + int16_t btnH; 450 + int16_t gap; 451 + int16_t btnYConfirm; 452 + int16_t btnYCancel; 453 + }; 454 + 455 + FullscreenLayout computeFullscreenLayout( int16_t w, int16_t h, bool shortScreen, bool tiny ) { 456 + FullscreenLayout lo{}; 457 + if ( shortScreen ) { 458 + lo.pad = 6; 459 + lo.titleY = 10; 460 + lo.msgY = 44; 461 + lo.btnH = 24; 462 + lo.gap = 5; 463 + } else if ( tiny ) { 464 + lo.pad = 8; 465 + lo.titleY = 14; 466 + lo.msgY = 78; 467 + lo.btnH = 32; 468 + lo.gap = 6; 469 + } else { 470 + lo.pad = 16; 471 + lo.titleY = 18; 472 + lo.msgY = 96; 473 + lo.btnH = 40; 474 + lo.gap = 10; 475 + } 476 + lo.hintY = tiny ? 118 : 142; 477 + lo.btnW = static_cast<int16_t>( w - 2 * lo.pad ); 478 + lo.btnYConfirm = static_cast<int16_t>( h - 2 * lo.btnH - lo.gap - lo.pad ); 479 + lo.btnYCancel = static_cast<int16_t>( lo.btnYConfirm + lo.btnH + lo.gap ); 480 + return lo; 481 + } 482 + 483 + void drawFullscreenHeader( const ModalSlot& m, int16_t w, const FullscreenLayout& lo, uint16_t ac, 484 + bool shortScreen, bool tiny ) { 485 + using enum display::Font; 486 + const display::Font titleFont = ( !shortScreen && w <= 320 ) ? SMALL : HEADING; 487 + display::setTextColor( theme::kText565, theme::kBg565 ); 488 + drawFittedText( m.title, w / 2, lo.titleY, lo.btnW, titleFont, kTopCenter ); 489 + 490 + display::setTextColor( theme::kText565, theme::kBg565 ); 491 + drawFittedText( m.message, w / 2, lo.msgY, lo.btnW, shortScreen ? BODY : HEADING, kTopCenter ); 492 + 493 + if ( !shortScreen ) { 494 + display::setTextColor( ac, theme::kBg565 ); 495 + drawFittedText( "This cannot be undone", w / 2, lo.hintY, lo.btnW, SMALL, kTopCenter ); 496 + } 497 + (void)tiny; // already encoded into the layout values 498 + } 499 + 500 + void buildFullscreenCaptions( bool tiny, const ModalSlot& m, char* confirmCap, size_t capSize, 501 + char* cancelCap, const char* hintConfirm, const char* hintCancel ) { 502 + if ( tiny ) { 503 + str::format( confirmCap, capSize, "%s", hintConfirm ); 504 + str::format( cancelCap, capSize, "%s", hintCancel ); 505 + } else { 506 + str::format( confirmCap, capSize, "%s - %s", hintConfirm, 507 + m.confirmLabel[0] ? m.confirmLabel : "OK" ); 508 + str::format( cancelCap, capSize, "%s - %s", hintCancel, 509 + m.cancelLabel[0] ? m.cancelLabel : "Cancel" ); 510 + } 511 + } 512 + 513 + void drawFullscreenConfirmButton( const FullscreenLayout& lo, uint16_t ac, display::Font font, 514 + const char* caption ) { 515 + display::fillRect( lo.pad, lo.btnYConfirm, lo.btnW, lo.btnH, theme::kBg565 ); 516 + display::drawRect( lo.pad, lo.btnYConfirm, lo.btnW, lo.btnH, ac ); 517 + display::setTextColor( ac, theme::kBg565 ); 518 + drawFittedText( caption, lo.pad + lo.btnW / 2, lo.btnYConfirm + lo.btnH / 2, lo.btnW - 6, font, 519 + kMiddleCenter ); 520 + s_modalConfirmBtn = { lo.pad, lo.btnYConfirm, lo.btnW, lo.btnH }; 521 + str::format( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", caption ); 522 + } 523 + 524 + void drawFullscreenCancelButton( const FullscreenLayout& lo, display::Font font, 525 + const char* caption ) { 526 + display::fillRect( lo.pad, lo.btnYCancel, lo.btnW, lo.btnH, theme::kBg565 ); 527 + display::drawRect( lo.pad, lo.btnYCancel, lo.btnW, lo.btnH, theme::kSubtle565 ); 528 + display::setTextColor( theme::kTextSec565, theme::kBg565 ); 529 + drawFittedText( caption, lo.pad + lo.btnW / 2, lo.btnYCancel + lo.btnH / 2, lo.btnW - 6, font, 530 + kMiddleCenter ); 531 + s_modalCancelBtn = { lo.pad, lo.btnYCancel, lo.btnW, lo.btnH }; 532 + } 533 + 534 + } // namespace 535 + 401 536 void drawModalFullscreen( const ModalSlot& m ) { 402 537 const int16_t w = display::width(); 403 538 const int16_t h = display::height(); ··· 412 547 // Top accent band (severity). 413 548 display::fillRect( 0, 0, w, 3, ac ); 414 549 415 - // Title (centered). On 320px-wide direct-HAL screens the heading face is 416 - // visually too wide for destructive modal titles, so use BODY there while 417 - // keeping the stronger heading treatment on short portrait/keyboard panes. 418 - display::setTextColor( theme::kText565, theme::kBg565 ); 419 - const int16_t pad = shortScreen ? 6 : ( tiny ? 8 : 16 ); 420 - const int16_t titleY = shortScreen ? 10 : ( tiny ? 14 : 18 ); 421 - const display::Font titleFont = 422 - ( !shortScreen && w <= 320 ) ? display::Font::SMALL : display::Font::HEADING; 423 - drawFittedText( m.title, w / 2, titleY, static_cast<int16_t>( w - 2 * pad ), titleFont, 424 - kTopCenter ); 425 - 426 - // Message — large centered (the credential name / target). 427 - display::setTextColor( theme::kText565, theme::kBg565 ); 428 - const int16_t msgY = shortScreen ? 44 : ( tiny ? 78 : 96 ); 429 - drawFittedText( m.message, w / 2, msgY, static_cast<int16_t>( w - 2 * pad ), 430 - shortScreen ? display::Font::BODY : display::Font::HEADING, kTopCenter ); 431 - 432 - // Severity hint line. 433 - if ( !shortScreen ) { 434 - display::setTextColor( ac, theme::kBg565 ); 435 - const int16_t hintY = tiny ? 118 : 142; 436 - drawFittedText( "This cannot be undone", w / 2, hintY, static_cast<int16_t>( w - 2 * pad ), 437 - display::Font::SMALL, kTopCenter ); 438 - } 550 + const FullscreenLayout lo = computeFullscreenLayout( w, h, shortScreen, tiny ); 551 + drawFullscreenHeader( m, w, lo, ac, shortScreen, tiny ); 439 552 440 553 display::setTextDatum( kTopLeft ); 441 554 442 - // Buttons — stacked vertically, full width minus side padding. 443 - // Confirm (destructive) on top, Cancel below — destructive action gets 444 - // the visual emphasis (matches the user's gaze flow on a portrait stick). 445 - const int16_t btnW = static_cast<int16_t>( w - 2 * pad ); 446 - const int16_t btnH = shortScreen ? 24 : ( tiny ? 32 : 40 ); 447 - const int16_t gap = shortScreen ? 5 : ( tiny ? 6 : 10 ); 448 - const int16_t btnY1 = static_cast<int16_t>( h - 2 * btnH - gap - pad ); // Confirm 449 - const int16_t btnY2 = static_cast<int16_t>( btnY1 + btnH + gap ); // Cancel 450 - 451 555 // Confirm button (empty frame; the hold-fill animation paints the inside). 452 556 #if HAS_KEYBOARD 453 557 const char* hintConfirm = "Hold ENTER"; ··· 457 561 const char* hintCancel = "Tap B"; 458 562 #endif 459 563 460 - // Build compact ASCII-only captions. Tiny screens carry the action in 461 - // the title/message, so buttons can focus on the physical gesture. 462 564 char confirmCap[40]; 463 565 char cancelCap[40]; 464 - if ( tiny ) { 465 - snprintf( confirmCap, sizeof( confirmCap ), "%s", hintConfirm ); 466 - snprintf( cancelCap, sizeof( cancelCap ), "%s", hintCancel ); 566 + buildFullscreenCaptions( tiny, m, confirmCap, sizeof( confirmCap ), cancelCap, hintConfirm, 567 + hintCancel ); 568 + const display::Font buttonFont = tiny ? display::Font::BODY : display::Font::HEADING; 569 + 570 + drawFullscreenConfirmButton( lo, ac, buttonFont, confirmCap ); 571 + drawFullscreenCancelButton( lo, buttonFont, cancelCap ); 572 + 573 + display::setTextDatum( kTopLeft ); 574 + } 575 + 576 + namespace { 577 + 578 + void drawModalChrome( const ModalSlot& m, const Rect& r, uint16_t ac, bool hasChoice, 579 + bool hasActions ) { 580 + display::fillRect( r.x, r.y, r.w, r.h, theme::kSurface565 ); 581 + display::drawRect( r.x, r.y, r.w, r.h, ac ); 582 + display::fillRect( r.x, r.y, r.w, 3, ac ); // top accent band 583 + 584 + // Title 585 + display::setTextDatum( kTopLeft ); 586 + display::setTextColor( theme::kText565, theme::kSurface565 ); 587 + using enum display::Font; 588 + display::Font titleFont; 589 + if ( hasChoice ) { 590 + titleFont = isTinyScreen() ? SMALL : BODY; 591 + } else if ( !hasActions ) { 592 + titleFont = BODY; 467 593 } else { 468 - snprintf( confirmCap, sizeof( confirmCap ), "%s - %s", hintConfirm, 469 - m.confirmLabel[0] ? m.confirmLabel : "OK" ); 470 - snprintf( cancelCap, sizeof( cancelCap ), "%s - %s", hintCancel, 471 - m.cancelLabel[0] ? m.cancelLabel : "Cancel" ); 594 + titleFont = isTinyScreen() ? BODY : HEADING; 595 + } 596 + drawFittedText( m.title, r.x + 8, r.y + 10, r.w - 16, titleFont, kTopLeft ); 597 + 598 + // Message (wrapped to one or two lines, caller should keep it short) 599 + display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 600 + int16_t msgYOffset; 601 + if ( !hasActions ) { 602 + msgYOffset = 40; 603 + } else if ( hasChoice ) { 604 + msgYOffset = 36; 605 + } else { 606 + msgYOffset = 28; 607 + } 608 + drawFittedText( m.message, r.x + 8, static_cast<int16_t>( r.y + msgYOffset ), r.w - 16, 609 + display::Font::SMALL, kTopLeft ); 610 + } 611 + 612 + void drawModalChoiceButtons( const ModalSlot& m, const Rect& r, int16_t btnY, int16_t btnW, 613 + int16_t btnH, int16_t hintH, int16_t pad, int16_t choiceGap, 614 + bool compactChoice, display::Font buttonFont, uint16_t ac ) { 615 + #if HAS_KEYBOARD 616 + const char* choiceLeftHint = "ESC"; 617 + const char* choiceMiddleHint = "ENTER"; 618 + const char* choiceRightHint = "SPACE"; 619 + #else 620 + const char* choiceLeftHint = "A"; 621 + const char* choiceMiddleHint = "B"; 622 + const char* choiceRightHint = "C"; 623 + #endif 624 + const int16_t leftX = r.x + pad; 625 + const int16_t middleX = static_cast<int16_t>( leftX + btnW + choiceGap ); 626 + const int16_t rightX = static_cast<int16_t>( middleX + btnW + choiceGap ); 627 + const char* leftLabel = 628 + ( compactChoice && kleidos::platform::str::equals( m.cancelLabel, "Cancel" ) ) 629 + ? "Back" 630 + : m.cancelLabel; 631 + 632 + // Left (cancel) 633 + display::fillRect( leftX, btnY, btnW, btnH, theme::kBg565 ); 634 + display::drawRect( leftX, btnY, btnW, btnH, theme::kTextSec565 ); 635 + display::setTextColor( theme::kTextSec565, theme::kBg565 ); 636 + drawFittedText( leftLabel, leftX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 637 + kMiddleCenter ); 638 + s_modalCancelBtn = { leftX, btnY, btnW, btnH }; 639 + if ( !compactChoice ) { 640 + display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 641 + drawFittedText( choiceLeftHint, leftX + btnW / 2, 642 + static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 643 + display::Font::SMALL, kMiddleCenter ); 644 + } 645 + 646 + // Middle (confirm) 647 + display::fillRect( middleX, btnY, btnW, btnH, theme::kBg565 ); 648 + display::drawRect( middleX, btnY, btnW, btnH, ac ); 649 + display::setTextColor( ac, theme::kBg565 ); 650 + drawFittedText( m.confirmLabel, middleX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 651 + kMiddleCenter ); 652 + s_modalConfirmBtn = { middleX, btnY, btnW, btnH }; 653 + str::format( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", 654 + m.confirmLabel[0] ? m.confirmLabel : "OK" ); 655 + if ( !compactChoice ) { 656 + display::setTextColor( ac, theme::kSurface565 ); 657 + drawFittedText( choiceMiddleHint, middleX + btnW / 2, 658 + static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 659 + display::Font::SMALL, kMiddleCenter ); 472 660 } 473 - const display::Font buttonFont = tiny ? display::Font::BODY : display::Font::HEADING; 474 661 475 - display::fillRect( pad, btnY1, btnW, btnH, theme::kBg565 ); 476 - display::drawRect( pad, btnY1, btnW, btnH, ac ); 662 + // Right (third) 663 + display::fillRect( rightX, btnY, btnW, btnH, theme::kBg565 ); 664 + display::drawRect( rightX, btnY, btnW, btnH, ac ); 477 665 display::setTextColor( ac, theme::kBg565 ); 478 - drawFittedText( confirmCap, pad + btnW / 2, btnY1 + btnH / 2, btnW - 6, buttonFont, 666 + drawFittedText( m.thirdLabel, rightX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 479 667 kMiddleCenter ); 480 - s_modalConfirmBtn = { pad, btnY1, btnW, btnH }; 481 - snprintf( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", confirmCap ); 668 + s_modalThirdBtn = { rightX, btnY, btnW, btnH }; 669 + if ( !compactChoice ) { 670 + display::setTextColor( ac, theme::kSurface565 ); 671 + drawFittedText( choiceRightHint, rightX + btnW / 2, 672 + static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 673 + display::Font::SMALL, kMiddleCenter ); 674 + } 675 + } 482 676 483 - // Cancel button (dim frame, secondary text). 484 - display::fillRect( pad, btnY2, btnW, btnH, theme::kBg565 ); 485 - display::drawRect( pad, btnY2, btnW, btnH, theme::kSubtle565 ); 677 + void drawModalCancelButton( const Rect& r, int16_t btnY, int16_t btnW, int16_t btnH, int16_t hintH, 678 + int16_t pad, display::Font buttonFont, uint16_t ac, 679 + const char* hintCancel, const char* cancelLabel ) { 680 + const int16_t cx = r.x + pad; 681 + display::fillRect( cx, btnY, btnW, btnH, theme::kBg565 ); 682 + display::drawRect( cx, btnY, btnW, btnH, ac ); 486 683 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 487 - drawFittedText( cancelCap, pad + btnW / 2, btnY2 + btnH / 2, btnW - 6, buttonFont, 684 + drawFittedText( cancelLabel, cx + btnW / 2, btnY + btnH / 2, btnW - 6, buttonFont, 488 685 kMiddleCenter ); 489 - s_modalCancelBtn = { pad, btnY2, btnW, btnH }; 686 + s_modalCancelBtn = { cx, btnY, btnW, btnH }; 687 + display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 688 + drawFittedText( hintCancel, cx + btnW / 2, static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), 689 + btnW - 4, display::Font::SMALL, kMiddleCenter ); 690 + } 490 691 491 - display::setTextDatum( kTopLeft ); 692 + void drawModalConfirmButton( const Rect& r, int16_t btnY, int16_t btnW, int16_t btnH, int16_t hintH, 693 + int16_t pad, display::Font buttonFont, uint16_t ac, 694 + const char* hintConfirm, const char* confirmLabel, bool hasCancel, 695 + bool holdToConfirm ) { 696 + const int16_t confirmX = 697 + static_cast<int16_t>( hasCancel ? ( r.x + r.w - pad - btnW ) : ( r.x + pad ) ); 698 + // Hold-to-confirm: button starts EMPTY (BG fill, accent border) and the 699 + // animation paints inside it in the accent color. Tap-to-confirm: keep 700 + // the legacy treatment where the focused confirm button is filled solid. 701 + const bool focusFill = !holdToConfirm; 702 + const uint16_t cbg = focusFill ? ac : theme::kBg565; 703 + const uint16_t ctxt = focusFill ? theme::kBg565 : ac; 704 + display::fillRect( confirmX, btnY, btnW, btnH, cbg ); 705 + display::drawRect( confirmX, btnY, btnW, btnH, ac ); 706 + display::setTextColor( ctxt, cbg ); 707 + drawFittedText( confirmLabel, confirmX + btnW / 2, btnY + btnH / 2, btnW - 6, buttonFont, 708 + kMiddleCenter ); 709 + s_modalConfirmBtn = { confirmX, btnY, btnW, btnH }; 710 + str::format( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", 711 + confirmLabel[0] ? confirmLabel : "OK" ); 712 + display::setTextColor( holdToConfirm ? ac : theme::kTextSec565, theme::kSurface565 ); 713 + drawFittedText( hintConfirm, confirmX + btnW / 2, 714 + static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 4, 715 + display::Font::SMALL, kMiddleCenter ); 492 716 } 717 + 718 + } // namespace 493 719 494 720 void drawModal( const ModalSlot& m, int8_t selection /* 0=cancel,1=confirm */, 495 721 bool holdToConfirm = false ) { ··· 500 726 const uint8_t actionCount = modalActionCount( m.kind ); 501 727 const bool hasChoice = ( actionCount == 3 ); 502 728 const bool hasActions = ( actionCount > 0 ); 503 - const Rect r = hasChoice ? computeChoiceModalRect() 504 - : hasActions ? computeModalRect() 505 - : computeNoticeModalRect(); 506 - s_modalRect = { 0, 0, display::width(), display::height() }; 507 - s_modalConfirmBtn = { 0, 0, 0, 0 }; 508 - s_modalCancelBtn = { 0, 0, 0, 0 }; 509 - s_modalThirdBtn = { 0, 0, 0, 0 }; 510 - s_modalConfirmCaption[0] = '\0'; 511 - const uint16_t ac = accentColor( m.sev ); 729 + Rect r; 730 + if ( hasChoice ) { 731 + r = computeChoiceModalRect(); 732 + } else if ( hasActions ) { 733 + r = computeModalRect(); 734 + } else { 735 + r = computeNoticeModalRect(); 736 + } 737 + s_modalRect = { 0, 0, display::width(), display::height() }; 738 + s_modalConfirmBtn = { 0, 0, 0, 0 }; 739 + s_modalCancelBtn = { 0, 0, 0, 0 }; 740 + s_modalThirdBtn = { 0, 0, 0, 0 }; 741 + s_modalConfirmCaption[0] = '\0'; 742 + const uint16_t ac = accentColor( m.sev ); 512 743 513 744 display::fillRect( s_modalRect.x, s_modalRect.y, s_modalRect.w, s_modalRect.h, theme::kBg565 ); 514 - display::fillRect( r.x, r.y, r.w, r.h, theme::kSurface565 ); 515 - display::drawRect( r.x, r.y, r.w, r.h, ac ); 516 - display::fillRect( r.x, r.y, r.w, 3, ac ); // top accent band 517 - 518 - // Title 519 - display::setTextDatum( kTopLeft ); 520 - display::setTextColor( theme::kText565, theme::kSurface565 ); 521 - const display::Font titleFont = 522 - hasChoice 523 - ? ( isTinyScreen() ? display::Font::SMALL : display::Font::BODY ) 524 - : ( !hasActions ? display::Font::BODY 525 - : ( isTinyScreen() ? display::Font::BODY : display::Font::HEADING ) ); 526 - drawFittedText( m.title, r.x + 8, r.y + 10, r.w - 16, titleFont, kTopLeft ); 527 - 528 - // Message (wrapped to one or two lines, caller should keep it short) 529 - display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 530 - drawFittedText( m.message, r.x + 8, 531 - static_cast<int16_t>( r.y + ( !hasActions ? 40 : ( hasChoice ? 36 : 28 ) ) ), 532 - r.w - 16, display::Font::SMALL, kTopLeft ); 745 + drawModalChrome( m, r, ac, hasChoice, hasActions ); 533 746 534 747 if ( !hasActions ) { 535 748 display::setTextDatum( kTopLeft ); ··· 546 759 const int16_t btnY = static_cast<int16_t>( r.y + r.h - btnH - 8 - hintH ); 547 760 const int16_t pad = 8; 548 761 const int16_t choiceGap = isTinyScreen() ? 3 : 8; 549 - const int16_t btnW = static_cast<int16_t>( 550 - hasChoice ? ( ( r.w - 2 * pad - 2 * choiceGap ) / 3 ) 551 - : ( hasCancel ? ( ( r.w - 3 * pad ) / 2 ) : ( r.w - 2 * pad ) ) ); 552 - const display::Font buttonFont = 553 - hasChoice ? ( isTinyScreen() ? display::Font::SMALL : display::Font::BODY ) 554 - : ( isTinyScreen() ? display::Font::BODY : display::Font::HEADING ); 762 + int16_t btnW; 763 + if ( hasChoice ) { 764 + btnW = static_cast<int16_t>( ( r.w - 2 * pad - 2 * choiceGap ) / 3 ); 765 + } else if ( hasCancel ) { 766 + btnW = static_cast<int16_t>( ( r.w - 3 * pad ) / 2 ); 767 + } else { 768 + btnW = static_cast<int16_t>( r.w - 2 * pad ); 769 + } 770 + display::Font buttonFont; 771 + if ( hasChoice ) { 772 + buttonFont = isTinyScreen() ? display::Font::SMALL : display::Font::BODY; 773 + } else { 774 + buttonFont = isTinyScreen() ? display::Font::BODY : display::Font::HEADING; 775 + } 555 776 556 777 // Physical-button hint strings (per build target). 557 778 #if HAS_KEYBOARD ··· 565 786 display::setTextDatum( kMiddleCenter ); 566 787 567 788 if ( hasChoice ) { 568 - #if HAS_KEYBOARD 569 - const char* choiceLeftHint = "ESC"; 570 - const char* choiceMiddleHint = "ENTER"; 571 - const char* choiceRightHint = "SPACE"; 572 - #else 573 - const char* choiceLeftHint = "A"; 574 - const char* choiceMiddleHint = "B"; 575 - const char* choiceRightHint = "C"; 576 - #endif 577 - const int16_t leftX = r.x + pad; 578 - const int16_t middleX = static_cast<int16_t>( leftX + btnW + choiceGap ); 579 - const int16_t rightX = static_cast<int16_t>( middleX + btnW + choiceGap ); 580 - const char* leftLabel = 581 - ( compactChoice && kleidos::platform::str::equals( m.cancelLabel, "Cancel" ) ) 582 - ? "Back" 583 - : m.cancelLabel; 584 - 585 - display::fillRect( leftX, btnY, btnW, btnH, theme::kBg565 ); 586 - display::drawRect( leftX, btnY, btnW, btnH, theme::kTextSec565 ); 587 - display::setTextColor( theme::kTextSec565, theme::kBg565 ); 588 - drawFittedText( leftLabel, leftX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 589 - kMiddleCenter ); 590 - s_modalCancelBtn = { leftX, btnY, btnW, btnH }; 591 - if ( !compactChoice ) { 592 - display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 593 - drawFittedText( choiceLeftHint, leftX + btnW / 2, 594 - static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 595 - display::Font::SMALL, kMiddleCenter ); 596 - } 597 - 598 - display::fillRect( middleX, btnY, btnW, btnH, theme::kBg565 ); 599 - display::drawRect( middleX, btnY, btnW, btnH, ac ); 600 - display::setTextColor( ac, theme::kBg565 ); 601 - drawFittedText( m.confirmLabel, middleX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 602 - kMiddleCenter ); 603 - s_modalConfirmBtn = { middleX, btnY, btnW, btnH }; 604 - snprintf( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", 605 - m.confirmLabel[0] ? m.confirmLabel : "OK" ); 606 - if ( !compactChoice ) { 607 - display::setTextColor( ac, theme::kSurface565 ); 608 - drawFittedText( choiceMiddleHint, middleX + btnW / 2, 609 - static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 610 - display::Font::SMALL, kMiddleCenter ); 611 - } 612 - 613 - display::fillRect( rightX, btnY, btnW, btnH, theme::kBg565 ); 614 - display::drawRect( rightX, btnY, btnW, btnH, ac ); 615 - display::setTextColor( ac, theme::kBg565 ); 616 - drawFittedText( m.thirdLabel, rightX + btnW / 2, btnY + btnH / 2, btnW - 4, buttonFont, 617 - kMiddleCenter ); 618 - s_modalThirdBtn = { rightX, btnY, btnW, btnH }; 619 - if ( !compactChoice ) { 620 - display::setTextColor( ac, theme::kSurface565 ); 621 - drawFittedText( choiceRightHint, rightX + btnW / 2, 622 - static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 2, 623 - display::Font::SMALL, kMiddleCenter ); 624 - } 789 + drawModalChoiceButtons( m, r, btnY, btnW, btnH, hintH, pad, choiceGap, compactChoice, 790 + buttonFont, ac ); 625 791 display::setTextDatum( kTopLeft ); 626 792 return; 627 793 } ··· 632 798 // accent color. The hold-fill animation later paints the Confirm 633 799 // button interior in the accent color (e.g. red for destructive). 634 800 if ( hasCancel ) { 635 - const int16_t cx = r.x + pad; 636 - display::fillRect( cx, btnY, btnW, btnH, theme::kBg565 ); 637 - display::drawRect( cx, btnY, btnW, btnH, ac ); 638 - display::setTextColor( theme::kTextSec565, theme::kBg565 ); 639 - drawFittedText( m.cancelLabel, cx + btnW / 2, btnY + btnH / 2, btnW - 6, buttonFont, 640 - kMiddleCenter ); 641 - s_modalCancelBtn = { cx, btnY, btnW, btnH }; 642 - display::setTextColor( theme::kTextSec565, theme::kSurface565 ); 643 - drawFittedText( hintCancel, cx + btnW / 2, 644 - static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 4, 645 - display::Font::SMALL, kMiddleCenter ); 801 + drawModalCancelButton( r, btnY, btnW, btnH, hintH, pad, buttonFont, ac, hintCancel, 802 + m.cancelLabel ); 646 803 } 647 - 648 - const int16_t confirmX = 649 - static_cast<int16_t>( hasCancel ? ( r.x + r.w - pad - btnW ) : ( r.x + pad ) ); 650 - // Hold-to-confirm: button starts EMPTY (BG fill, accent border) and the 651 - // animation paints inside it in the accent color. Tap-to-confirm: keep 652 - // the legacy treatment where the focused confirm button is filled solid. 653 - const bool focusFill = !holdToConfirm; 654 - const uint16_t cbg = focusFill ? ac : theme::kBg565; 655 - const uint16_t ctxt = focusFill ? theme::kBg565 : ac; 656 - display::fillRect( confirmX, btnY, btnW, btnH, cbg ); 657 - display::drawRect( confirmX, btnY, btnW, btnH, ac ); 658 - display::setTextColor( ctxt, cbg ); 659 - drawFittedText( m.confirmLabel, confirmX + btnW / 2, btnY + btnH / 2, btnW - 6, buttonFont, 660 - kMiddleCenter ); 661 - s_modalConfirmBtn = { confirmX, btnY, btnW, btnH }; 662 - snprintf( s_modalConfirmCaption, sizeof( s_modalConfirmCaption ), "%s", 663 - m.confirmLabel[0] ? m.confirmLabel : "OK" ); 664 - display::setTextColor( holdToConfirm ? ac : theme::kTextSec565, theme::kSurface565 ); 665 - drawFittedText( hintConfirm, confirmX + btnW / 2, 666 - static_cast<int16_t>( btnY + btnH + hintH / 2 + 1 ), btnW - 4, 667 - display::Font::SMALL, kMiddleCenter ); 804 + drawModalConfirmButton( r, btnY, btnW, btnH, hintH, pad, buttonFont, ac, hintConfirm, 805 + m.confirmLabel, hasCancel, holdToConfirm ); 668 806 669 807 display::setTextDatum( kTopLeft ); 670 808 } ··· 700 838 const int16_t innerY = r.y + 1; 701 839 const int16_t innerH = r.h - 2; 702 840 const int16_t addX = innerX + lastPx; 703 - const int16_t addW = ( targetPx - lastPx ); 704 - if ( addW > 0 && innerH > 0 ) { 841 + if ( const int16_t addW = ( targetPx - lastPx ); addW > 0 && innerH > 0 ) { 705 842 display::fillRect( addX, innerY, addW, innerH, fillCol ); 706 843 } 707 844 if ( lastFillPxOut ) { ··· 717 854 // contrasts with both the BG-empty side and the accent-filled side. 718 855 // We render the cached caption (set by drawModal) so the fullscreen 719 856 // layout's "Hold A · Delete" caption survives the fill animation. 720 - const char* caption = s_modalConfirmCaption[0] ? s_modalConfirmCaption 721 - : ( m.confirmLabel[0] ? m.confirmLabel : "OK" ); 857 + const char* caption; 858 + if ( s_modalConfirmCaption[0] ) { 859 + caption = s_modalConfirmCaption; 860 + } else if ( m.confirmLabel[0] ) { 861 + caption = m.confirmLabel; 862 + } else { 863 + caption = "OK"; 864 + } 722 865 display::setTextDatum( kMiddleCenter ); 723 866 display::setFont( isTinyScreen() ? display::Font::BODY : display::Font::HEADING ); 724 867 display::setTextColor( theme::kBg565 ); // transparent — readable on either ··· 730 873 731 874 #endif // !HAS_LVGL 732 875 876 + // --------------------------------------------------------------------------- 877 + // Hold-to-confirm state + wait-for-release helpers (used by confirm()). 878 + // These are file-local so the public API surface in popup_system.h is 879 + // unchanged. 880 + // --------------------------------------------------------------------------- 881 + #if !HAS_LVGL 882 + 883 + struct HoldState { 884 + bool active = false; 885 + uint32_t startMs = 0; 886 + int16_t lastFillPx = 0; 887 + }; 888 + 889 + // Process one frame of the hold-to-confirm gesture. Returns true when 890 + // the user has held past the threshold and the modal should resolve 891 + // as CONFIRM. When the user releases before the threshold, the partial 892 + // fill is wiped by redrawing the modal in its idle state. 893 + bool processHoldToConfirmStep( ButtonInput* btnConfirm, bool holdToConfirm, HoldState& s, 894 + uint32_t kHoldThresholdMs ) { 895 + if ( !holdToConfirm || !btnConfirm ) { 896 + return false; 897 + } 898 + if ( !btnConfirm->isPressed() ) { 899 + if ( s.active ) { 900 + s.active = false; 901 + s.lastFillPx = 0; 902 + // Released before threshold — wipe partial fill by redrawing 903 + // the modal in its idle state. 904 + drawModal( s_queue.modal(), 1, holdToConfirm ); 905 + } 906 + (void)btnConfirm->wasPressed(); 907 + (void)btnConfirm->wasReleased(); 908 + return false; 909 + } 910 + if ( !s.active ) { 911 + s.active = true; 912 + s.startMs = platformClock::millis32(); 913 + s.lastFillPx = 0; 914 + } 915 + const uint32_t held = platformClock::millis32() - s.startMs; 916 + const float prog = ( held >= kHoldThresholdMs ) 917 + ? 1.0f 918 + : static_cast<float>( held ) / static_cast<float>( kHoldThresholdMs ); 919 + drawConfirmHoldFill( s_queue.modal(), prog, &s.lastFillPx ); 920 + if ( held >= kHoldThresholdMs ) { 921 + s_queue.resolveModal( ConfirmResult::CONFIRM ); 922 + return true; 923 + } 924 + (void)btnConfirm->wasPressed(); 925 + (void)btnConfirm->wasReleased(); 926 + return false; 927 + } 928 + 929 + #endif // !HAS_LVGL 930 + 931 + // Wait until both buttons are released (or up to 1.5 s) so the release 932 + // edge doesn't leak to the caller state and re-trigger a sibling handler. 933 + void waitForButtonRelease( ButtonInput* btnConfirm, ButtonInput* btnCancel ) { 934 + const uint32_t waitStart = platformClock::millis32(); 935 + while ( platformClock::millis32() - waitStart < 1500U ) { 936 + hal::update(); 937 + const bool aDown = btnConfirm && btnConfirm->isPressed(); 938 + if ( const bool bDown = btnCancel && btnCancel->isPressed(); !aDown && !bDown ) { 939 + break; 940 + } 941 + rtos::delayMs( 5 ); 942 + } 943 + if ( btnConfirm ) { 944 + btnConfirm->clearEvents(); 945 + } 946 + if ( btnCancel ) { 947 + btnCancel->clearEvents(); 948 + } 949 + } 950 + 951 + // Expire the non-blocking modal if it timed out, or if the queue no longer 952 + // has it (e.g. the user resolved it elsewhere). Preserves the original 953 + // 3-state behavior: keep-active on tickModal()==false, clear+deactivate on 954 + // tickModal()==true, deactivate-only on !hasModal(). Extracted from tick() 955 + // to keep the function's control-flow nesting under the S134 threshold. 956 + void expireNonBlockingModalIfNeeded( uint32_t now ) { 957 + if ( !s_nonBlockingModalActive ) { 958 + return; 959 + } 960 + if ( s_queue.hasModal() ) { 961 + if ( !s_queue.tickModal( now ) ) { 962 + return; // still active 963 + } 964 + #if HAS_LVGL 965 + lvgl_adapter::clearModal(); 966 + #else 967 + if ( s_modalDrawn ) { 968 + eraseRect( s_modalRect ); 969 + } 970 + s_modalDrawn = false; 971 + #ifdef DEBUG_SERIAL_BUTTONS 972 + s_modalPreviewActive = false; 973 + s_modalPreviewHoldToConfirm = false; 974 + #endif 975 + #endif 976 + } 977 + s_nonBlockingModalActive = false; 978 + } 979 + 733 980 } // namespace 734 981 735 982 // --------------------------------------------------------------------------- 736 983 // Public API 737 984 // --------------------------------------------------------------------------- 985 + 986 + namespace { 987 + 988 + // Renders the non-blocking modal (or its LVGL equivalent) and sets the 989 + // s_modalDrawn flag plus the debug-preview state. Extracted from notice() 990 + // and the debugPreview* family so each public function stays at one level 991 + // of #if/#else nesting and the helper absorbs the additional #ifdef layer 992 + // (keeps the S134 nesting threshold in check). 993 + void renderModalWithPreview( bool preview, bool holdToConfirm ) { 994 + #if HAS_LVGL 995 + lvgl_adapter::renderModal( s_queue.modal(), isTinyScreen() ); 996 + #else 997 + drawModal( s_queue.modal(), preview ? 1 : 0, holdToConfirm ); 998 + s_modalDrawn = true; 999 + #ifdef DEBUG_SERIAL_BUTTONS 1000 + s_modalPreviewActive = preview; 1001 + s_modalPreviewHoldToConfirm = holdToConfirm; 1002 + #endif 1003 + #endif 1004 + } 1005 + 1006 + } // namespace 738 1007 739 1008 void toast( const char* title, const char* detail, Severity sev, uint32_t timeoutMs ) { 740 1009 s_queue.pushToast( title, detail, sev, platformClock::millis32(), timeoutMs ); ··· 773 1042 void notice( const char* title, const char* message, Severity sev, uint32_t timeoutMs ) { 774 1043 s_queue.beginNotice( title, message, sev, platformClock::millis32(), timeoutMs ); 775 1044 s_nonBlockingModalActive = true; 776 - #if HAS_LVGL 777 - lvgl_adapter::renderModal( s_queue.modal(), isTinyScreen() ); 778 - #else 779 - drawModal( s_queue.modal(), 0, false ); 780 - s_modalDrawn = true; 781 - #ifdef DEBUG_SERIAL_BUTTONS 782 - s_modalPreviewActive = false; 783 - s_modalPreviewHoldToConfirm = false; 784 - #endif 785 - #endif 1045 + renderModalWithPreview( /* preview */ false, /* holdToConfirm */ false ); 786 1046 } 787 1047 788 1048 #ifdef DEBUG_SERIAL_BUTTONS 789 1049 void debugPreviewNotice( const char* title, const char* message, Severity sev ) { 790 1050 s_queue.beginNotice( title, message, sev, platformClock::millis32(), 0 ); 791 1051 s_nonBlockingModalActive = true; 792 - #if HAS_LVGL 793 - lvgl_adapter::renderModal( s_queue.modal(), isTinyScreen() ); 794 - #else 795 - drawModal( s_queue.modal(), 0, false ); 796 - s_modalDrawn = true; 797 - s_modalPreviewActive = true; 798 - s_modalPreviewHoldToConfirm = false; 799 - #endif 1052 + renderModalWithPreview( /* preview */ true, /* holdToConfirm */ false ); 800 1053 } 801 1054 802 1055 void debugPreviewModal( const char* title, const char* message, const char* confirmLabel, ··· 806 1059 confirmLabel, withCancel ? cancelLabel : "", sev, platformClock::millis32(), 807 1060 0 ); 808 1061 s_nonBlockingModalActive = true; 809 - #if HAS_LVGL 810 - (void)holdToConfirm; 811 - lvgl_adapter::renderModal( s_queue.modal(), isTinyScreen() ); 812 - #else 813 - drawModal( s_queue.modal(), 1, holdToConfirm ); 814 - s_modalDrawn = true; 815 - s_modalPreviewActive = true; 816 - s_modalPreviewHoldToConfirm = holdToConfirm; 817 - #endif 1062 + renderModalWithPreview( /* preview */ true, holdToConfirm ); 818 1063 } 819 1064 820 1065 void debugPreviewChoice( const char* title, const char* message, const char* leftLabel, ··· 822 1067 s_queue.beginChoice( title, message, leftLabel, middleLabel, rightLabel, sev, 823 1068 platformClock::millis32(), 0 ); 824 1069 s_nonBlockingModalActive = true; 825 - #if HAS_LVGL 826 - lvgl_adapter::renderModal( s_queue.modal(), isTinyScreen() ); 827 - #else 828 - drawModal( s_queue.modal(), 1, false ); 829 - s_modalDrawn = true; 830 - s_modalPreviewActive = true; 831 - s_modalPreviewHoldToConfirm = false; 832 - #endif 1070 + renderModalWithPreview( /* preview */ true, /* holdToConfirm */ false ); 833 1071 } 834 1072 #endif 835 1073 ··· 838 1076 839 1077 // Expire toasts 840 1078 if ( s_queue.activeToasts() > 0 ) { 841 - uint8_t dismissed = s_queue.tick( now ); 842 - if ( dismissed > 0 ) { 1079 + if ( const uint8_t dismissed = s_queue.tick( now ); dismissed > 0 ) { 843 1080 s_toastsDirty = true; 844 1081 s_toastDismissedSignal = true; 845 1082 } ··· 849 1086 s_transitionDirty = true; 850 1087 } 851 1088 852 - if ( s_nonBlockingModalActive && s_queue.hasModal() && s_queue.tickModal( now ) ) { 853 - #if HAS_LVGL 854 - lvgl_adapter::clearModal(); 855 - #else 856 - if ( s_modalDrawn ) { 857 - eraseRect( s_modalRect ); 858 - } 859 - s_modalDrawn = false; 860 - #ifdef DEBUG_SERIAL_BUTTONS 861 - s_modalPreviewActive = false; 862 - s_modalPreviewHoldToConfirm = false; 863 - #endif 864 - #endif 865 - s_nonBlockingModalActive = false; 866 - } else if ( s_nonBlockingModalActive && !s_queue.hasModal() ) { 867 - s_nonBlockingModalActive = false; 868 - } 1089 + expireNonBlockingModalIfNeeded( now ); 869 1090 870 1091 #if HAS_LVGL 871 1092 if ( s_toastsDirty ) { ··· 906 1127 (void)now; 907 1128 return s_queue.anyActive(); 908 1129 } 1130 + 1131 + // Expire the non-blocking modal if it timed out, or if the queue no longer 1132 + // has it (e.g. the user resolved it elsewhere). The actual definition 1133 + // lives in the file-local anonymous namespace above (next to 1134 + // waitForButtonRelease) so the helper sees the same private state as 1135 + // the rest of the renderer. Preserves the original 3-state behavior: 1136 + // keep-active on tickModal()==false, clear+deactivate on tickModal() 1137 + // ==true, deactivate-only on !hasModal(). Extracted from tick() to keep 1138 + // the function's control-flow nesting under the S134 threshold. 909 1139 910 1140 bool consumeToastDismissed() { 911 1141 bool v = s_toastDismissedSignal; ··· 990 1220 991 1221 // Hold-to-confirm progress tracking (only used when holdToConfirm=true) 992 1222 #if !HAS_LVGL 993 - bool holdActive = false; 994 - uint32_t holdStart = 0; 995 - int16_t holdLastPx = 0; 996 1223 // Destructive actions require 2 s sustained hold to fire — gives the 997 1224 // user clear "I really mean it" feedback and prevents accidental wipes. 998 1225 constexpr uint32_t kHoldThresholdMs = 2000; 1226 + HoldState holdState{}; 999 1227 #endif 1000 1228 1001 1229 while ( s_queue.hasModal() ) { ··· 1023 1251 break; 1024 1252 } 1025 1253 #else 1026 - // Hold-to-confirm: animate solid fill of the confirm button while 1027 - // the destructive button is held; resolve only on full hold. 1028 1254 if ( holdToConfirm && btnConfirm ) { 1029 - const bool down = btnConfirm->isPressed(); 1030 - if ( down ) { 1031 - if ( !holdActive ) { 1032 - holdActive = true; 1033 - holdStart = platformClock::millis32(); 1034 - holdLastPx = 0; 1035 - } 1036 - uint32_t held = platformClock::millis32() - holdStart; 1037 - float prog = ( held >= kHoldThresholdMs ) 1038 - ? 1.0f 1039 - : static_cast<float>( held ) / kHoldThresholdMs; 1040 - drawConfirmHoldFill( s_queue.modal(), prog, &holdLastPx ); 1041 - if ( held >= kHoldThresholdMs ) { 1042 - s_queue.resolveModal( ConfirmResult::CONFIRM ); 1043 - break; 1044 - } 1045 - } else if ( holdActive ) { 1046 - // Released before threshold — wipe partial fill by 1047 - // redrawing the modal in its idle state. 1048 - holdActive = false; 1049 - holdLastPx = 0; 1050 - drawModal( s_queue.modal(), 1, holdToConfirm ); 1255 + if ( processHoldToConfirmStep( btnConfirm, holdToConfirm, holdState, 1256 + kHoldThresholdMs ) ) { 1257 + break; 1051 1258 } 1052 - // Drain edge events so they don't queue up. 1053 - (void)btnConfirm->wasPressed(); 1054 - (void)btnConfirm->wasReleased(); 1055 1259 } else if ( btnConfirm && btnConfirm->wasPressed() ) { 1056 1260 s_queue.resolveModal( ConfirmResult::CONFIRM ); 1057 1261 break; ··· 1075 1279 // the release edge does not leak to the caller state and accidentally 1076 1280 // re-trigger the same action (e.g. tapping Cancel and the release edge 1077 1281 // re-firing the cred-detail "click on B" handler). 1078 - { 1079 - const uint32_t waitStart = platformClock::millis32(); 1080 - while ( platformClock::millis32() - waitStart < 1500U ) { 1081 - hal::update(); 1082 - const bool aDown = btnConfirm && btnConfirm->isPressed(); 1083 - const bool bDown = btnCancel && btnCancel->isPressed(); 1084 - if ( !aDown && !bDown ) { 1085 - break; 1086 - } 1087 - rtos::delayMs( 5 ); 1088 - } 1089 - // Drain any pending edge events so they don't surface upstream. 1090 - if ( btnConfirm ) { 1091 - btnConfirm->clearEvents(); 1092 - } 1093 - if ( btnCancel ) { 1094 - btnCancel->clearEvents(); 1095 - } 1096 - } 1282 + waitForButtonRelease( btnConfirm, btnCancel ); 1097 1283 1098 1284 // Erase the modal footprint so caller's next redraw restores content. 1099 1285 #if HAS_LVGL
+1 -1
src/ui/widgets/action_bar.cpp
··· 169 169 right_ = right; 170 170 } 171 171 172 - void ActionBar::draw() { 172 + void ActionBar::draw() const { 173 173 ActionCell a{}; 174 174 a.tapLabel = left_; 175 175 ActionCell b{};
+1 -1
src/ui/widgets/action_bar.h
··· 99 99 */ 100 100 void setLabels( const char* left, const char* center, const char* right ); 101 101 /// @brief Draw the action bar using stored labels (legacy instance API). 102 - void draw(); 102 + void draw() const; 103 103 104 104 /// @brief Height in pixels reserved by the bar (Gray default). 105 105 static constexpr int16_t kHeight = 26;
+72 -58
src/ui/widgets/device_panel.cpp
··· 2 2 3 3 #include "hal/common/device_config.h" 4 4 #ifdef ESP_PLATFORM 5 - 6 5 #include "ble/ble_facade.h" 7 6 #include "ble/keyboard_layouts.h" 8 7 #include "hal/common/display_hal.h" ··· 18 17 19 18 #include <cinttypes> 20 19 #include <cstring> 20 + #include <iterator> 21 + #include <utility> 21 22 22 23 #if HAS_KEYBOARD 23 24 #include "hal/common/board_registry.h" ··· 30 31 static constexpr const char* kTag = "Panel"; 31 32 namespace kble = kleidos::ble; 32 33 namespace platformClock = kleidos::platform::clock; 34 + namespace str = kleidos::platform::str; 33 35 using kleidos::platform::NvsStore; 34 36 35 37 namespace { ··· 147 149 buildMainSettings(); 148 150 } 149 151 150 - void DevicePanel::openCredDetail( uint8_t credId, const char* credName ) { 152 + void DevicePanel::openCredDetail( uint8_t credId, [[maybe_unused]] const char* credName ) { 151 153 #ifdef DEBUG_SERIAL_BUTTONS 152 154 debugPreviewMode_ = false; 153 155 #endif ··· 210 212 } 211 213 212 214 if ( act == MenuAction::BACK ) { 215 + using enum PanelScreen; 213 216 switch ( screen_ ) { 214 - case PanelScreen::MainSettings: 217 + case MainSettings: 215 218 close(); 216 219 return false; // Return to vault 217 220 218 - case PanelScreen::CredDetail: 221 + case CredDetail: 219 222 editCred_.wipe(); 220 223 close(); 221 224 return false; 222 225 223 - case PanelScreen::SettingsLayout: 224 - case PanelScreen::SettingsAutolock: 225 - case PanelScreen::SettingsDisplay: 226 - case PanelScreen::SettingsBleHosts: 227 - case PanelScreen::SettingsAbout: 228 - case PanelScreen::PASSGEN: 229 - screen_ = PanelScreen::MainSettings; 226 + case SettingsLayout: 227 + case SettingsAutolock: 228 + case SettingsDisplay: 229 + case SettingsBleHosts: 230 + case SettingsAbout: 231 + case PASSGEN: 232 + screen_ = MainSettings; 230 233 buildMainSettings(); 231 234 menu_.draw(); 232 235 return true; 233 236 234 - case PanelScreen::ConfirmDelete: 235 - screen_ = PanelScreen::CredDetail; 237 + case ConfirmDelete: 238 + screen_ = CredDetail; 236 239 buildCredDetail(); 237 240 menu_.draw(); 238 241 return true; ··· 245 248 246 249 if ( act == MenuAction::SELECT ) { 247 250 uint8_t id = menu_.selectedId(); 251 + using enum PanelScreen; 248 252 switch ( screen_ ) { 249 - case PanelScreen::MainSettings: 253 + case MainSettings: 250 254 handleMainSettings( id ); 251 255 break; 252 - case PanelScreen::CredDetail: 256 + case CredDetail: 253 257 handleCredDetail( id ); 254 258 break; 255 - case PanelScreen::SettingsLayout: 259 + case SettingsLayout: 256 260 handleLayoutPicker( id ); 257 261 break; 258 - case PanelScreen::SettingsAutolock: 262 + case SettingsAutolock: 259 263 handleAutoLockPicker( id ); 260 264 break; 261 - case PanelScreen::SettingsDisplay: 265 + case SettingsDisplay: 262 266 handleDisplaySettings( id ); 263 267 break; 264 - case PanelScreen::SettingsBleHosts: 268 + case SettingsBleHosts: 265 269 handleBleHosts( id ); 266 270 break; 267 - case PanelScreen::PASSGEN: 271 + case PASSGEN: 268 272 handlePassGen( id ); 269 273 break; 270 - case PanelScreen::ConfirmDelete: 274 + case ConfirmDelete: 271 275 handleConfirmDelete( id ); 272 276 break; 273 277 default: ··· 297 301 menu_.addSeparator(); 298 302 299 303 // Current layout 300 - auto curLayout = static_cast<uint8_t>( kble::Facade::getLayout() ); 304 + auto curLayout = std::to_underlying( kble::Facade::getLayout() ); 301 305 menu_.addValue( panel_id::kKbLayout, "Keyboard Layout", layoutName( curLayout ) ); 302 306 303 307 // Auto-lock ··· 348 352 void DevicePanel::buildLayoutPicker() { 349 353 menu_.clear(); 350 354 menu_.setTitle( "Keyboard Layout" ); 351 - auto cur = static_cast<uint8_t>( kble::Facade::getLayout() ); 355 + auto cur = std::to_underlying( kble::Facade::getLayout() ); 352 356 static const char* s_names[] = { "US English", "Spanish", "UK English", "French", 353 357 "German", "Portuguese", "Italian" }; 354 358 for ( uint8_t i = 0; i < 7; i++ ) { 355 359 if ( i == cur ) { 356 360 char buf[40]; 357 - snprintf( buf, sizeof( buf ), "%s *", s_names[i] ); 361 + str::format( buf, sizeof( buf ), "%s *", s_names[i] ); 358 362 menu_.addAction( i, buf ); 359 363 } else { 360 364 menu_.addAction( i, s_names[i] ); ··· 377 381 for ( uint8_t i = 0; i < 3; i++ ) { 378 382 if ( i == cur ) { 379 383 char buf[30]; 380 - snprintf( buf, sizeof( buf ), "%s *", s_names[i] ); 384 + str::format( buf, sizeof( buf ), "%s *", s_names[i] ); 381 385 menu_.addAction( i, buf ); 382 386 } else { 383 387 menu_.addAction( i, s_names[i] ); ··· 398 402 sleepMs = prefs.getU32( "idle_slp_ms", 60000 ); 399 403 } 400 404 401 - static char s_dimStr[12], s_offStr[12], s_sleepStr[12]; 402 - snprintf( s_dimStr, sizeof( s_dimStr ), "%" PRIu32 "s", dimMs / 1000 ); 403 - snprintf( s_offStr, sizeof( s_offStr ), "%" PRIu32 "s", offMs / 1000 ); 404 - snprintf( s_sleepStr, sizeof( s_sleepStr ), "%" PRIu32 "s", sleepMs / 1000 ); 405 + static char s_dimStr[12]; 406 + static char s_offStr[12]; 407 + static char s_sleepStr[12]; 408 + str::format( s_dimStr, sizeof( s_dimStr ), "%" PRIu32 "s", dimMs / 1000 ); 409 + str::format( s_offStr, sizeof( s_offStr ), "%" PRIu32 "s", offMs / 1000 ); 410 + str::format( s_sleepStr, sizeof( s_sleepStr ), "%" PRIu32 "s", sleepMs / 1000 ); 405 411 406 412 menu_.clear(); 407 413 menu_.setTitle( "Display & Sleep" ); ··· 440 446 menu_.clear(); 441 447 menu_.setTitle( "About" ); 442 448 443 - static char s_verBuf[24], s_batBuf[16], s_memBuf[24], s_uptimeBuf[20]; 444 - snprintf( s_verBuf, sizeof( s_verBuf ), "v%s", KLEIDOS_VERSION ); 449 + static char s_verBuf[24]; 450 + static char s_batBuf[16]; 451 + static char s_memBuf[24]; 452 + static char s_uptimeBuf[20]; 453 + str::format( s_verBuf, sizeof( s_verBuf ), "v%s", KLEIDOS_VERSION ); 445 454 menu_.addValue( 0, "Firmware", s_verBuf ); 446 455 menu_.addValue( 0, "Device", DEVICE_NAME ); 447 456 448 457 uint8_t batt = 0; 449 458 // Battery read — need board reference 450 - snprintf( s_batBuf, sizeof( s_batBuf ), "%u%%", batt ); 459 + str::format( s_batBuf, sizeof( s_batBuf ), "%u%%", batt ); 451 460 menu_.addValue( 0, "Battery", s_batBuf ); 452 461 453 - snprintf( s_memBuf, sizeof( s_memBuf ), "%" PRIu32 " KB free", 454 - kleidos::platform::system::freeHeapBytes() / 1024 ); 462 + str::format( s_memBuf, sizeof( s_memBuf ), "%" PRIu32 " KB free", 463 + kleidos::platform::system::freeHeapBytes() / 1024 ); 455 464 menu_.addValue( 0, "Memory", s_memBuf ); 456 465 457 466 uint32_t upSec = platformClock::millis32() / 1000; 458 467 uint32_t upMin = upSec / 60; 459 - snprintf( s_uptimeBuf, sizeof( s_uptimeBuf ), "%" PRIu32 "m %" PRIu32 "s", upMin, upSec % 60 ); 468 + str::format( s_uptimeBuf, sizeof( s_uptimeBuf ), "%" PRIu32 "m %" PRIu32 "s", upMin, 469 + upSec % 60 ); 460 470 menu_.addValue( 0, "Uptime", s_uptimeBuf ); 461 471 462 472 menu_.addValue( 0, "Flash", "16 MB" ); ··· 470 480 menu_.setTitle( "Password Generator" ); 471 481 472 482 static char s_lenBuf[8]; 473 - snprintf( s_lenBuf, sizeof( s_lenBuf ), "%u", passgenLen_ ); 483 + str::format( s_lenBuf, sizeof( s_lenBuf ), "%u", passgenLen_ ); 474 484 menu_.addValue( panel_id::kPgLength, "Length", s_lenBuf ); 475 485 476 486 menu_.addToggle( panel_id::kPgLower, "Lowercase (a-z)", 477 - ( passgenCharsets_ & static_cast<uint8_t>( PassCharSet::LOWER ) ) != 0 ); 487 + ( passgenCharsets_ & std::to_underlying( PassCharSet::LOWER ) ) != 0 ); 478 488 menu_.addToggle( panel_id::kPgUpper, "Uppercase (A-Z)", 479 - ( passgenCharsets_ & static_cast<uint8_t>( PassCharSet::UPPER ) ) != 0 ); 489 + ( passgenCharsets_ & std::to_underlying( PassCharSet::UPPER ) ) != 0 ); 480 490 menu_.addToggle( panel_id::kPgDigits, "Digits (0-9)", 481 - ( passgenCharsets_ & static_cast<uint8_t>( PassCharSet::DIGITS ) ) != 0 ); 491 + ( passgenCharsets_ & std::to_underlying( PassCharSet::DIGITS ) ) != 0 ); 482 492 menu_.addToggle( panel_id::kPgSymbols, "Symbols (!@#...)", 483 - ( passgenCharsets_ & static_cast<uint8_t>( PassCharSet::SYMBOLS ) ) != 0 ); 493 + ( passgenCharsets_ & std::to_underlying( PassCharSet::SYMBOLS ) ) != 0 ); 484 494 485 495 menu_.addSeparator(); 486 496 menu_.addAction( panel_id::kPgGenerate, "Generate" ); ··· 749 759 break; 750 760 751 761 case panel_id::kPgLower: 752 - passgenCharsets_ ^= static_cast<uint8_t>( PassCharSet::LOWER ); 762 + passgenCharsets_ ^= std::to_underlying( PassCharSet::LOWER ); 753 763 if ( passgenCharsets_ == 0 ) { 754 - passgenCharsets_ = static_cast<uint8_t>( PassCharSet::LOWER ); 764 + passgenCharsets_ = std::to_underlying( PassCharSet::LOWER ); 755 765 } 756 766 break; 757 767 case panel_id::kPgUpper: 758 - passgenCharsets_ ^= static_cast<uint8_t>( PassCharSet::UPPER ); 768 + passgenCharsets_ ^= std::to_underlying( PassCharSet::UPPER ); 759 769 if ( passgenCharsets_ == 0 ) { 760 - passgenCharsets_ = static_cast<uint8_t>( PassCharSet::UPPER ); 770 + passgenCharsets_ = std::to_underlying( PassCharSet::UPPER ); 761 771 } 762 772 break; 763 773 case panel_id::kPgDigits: 764 - passgenCharsets_ ^= static_cast<uint8_t>( PassCharSet::DIGITS ); 774 + passgenCharsets_ ^= std::to_underlying( PassCharSet::DIGITS ); 765 775 if ( passgenCharsets_ == 0 ) { 766 - passgenCharsets_ = static_cast<uint8_t>( PassCharSet::DIGITS ); 776 + passgenCharsets_ = std::to_underlying( PassCharSet::DIGITS ); 767 777 } 768 778 break; 769 779 case panel_id::kPgSymbols: 770 - passgenCharsets_ ^= static_cast<uint8_t>( PassCharSet::SYMBOLS ); 780 + passgenCharsets_ ^= std::to_underlying( PassCharSet::SYMBOLS ); 771 781 if ( passgenCharsets_ == 0 ) { 772 - passgenCharsets_ = static_cast<uint8_t>( PassCharSet::SYMBOLS ); 782 + passgenCharsets_ = std::to_underlying( PassCharSet::SYMBOLS ); 773 783 } 774 784 break; 775 785 ··· 836 846 837 847 // ===== Text input system ===== 838 848 849 + // NOSONAR cpp:S5817 — startTextInput writes to inputPrompt_, inputMax_, 850 + // inputReturnScreen_, inputBuf_, and inputLen_; cannot be const. 839 851 void DevicePanel::startTextInput( const char* prompt, const char* initial, uint8_t maxLen, 840 852 PanelScreen returnTo ) { 841 853 inputPrompt_ = prompt; ··· 853 865 drawTextInput(); 854 866 } 855 867 868 + // NOSONAR cpp:S5817 — updateTextInput mutates inputBuf_, inputLen_, screen_, 869 + // needsReload_, editCred_, editCredId_, and writes to VaultStore; cannot be const. 856 870 bool DevicePanel::updateTextInput() { 857 871 #if HAS_KEYBOARD 858 872 auto& kb = ( *board::get().keyboard ); ··· 863 877 864 878 if ( key == key_code::kEnter ) { 865 879 // Commit the edit 880 + using enum PanelScreen; 866 881 switch ( screen_ ) { 867 - case PanelScreen::CredEditName: 882 + case CredEditName: 868 883 kleidos::platform::str::copy( editCred_.name, inputBuf_ ); 869 884 break; 870 - case PanelScreen::CredEditUser: 885 + case CredEditUser: 871 886 kleidos::platform::str::copy( editCred_.user, inputBuf_ ); 872 887 break; 873 - case PanelScreen::CredEditPass: 888 + case CredEditPass: 874 889 kleidos::platform::str::copy( editCred_.pass, inputBuf_ ); 875 890 break; 876 891 default: ··· 901 916 if ( screen_ == PanelScreen::CredEditPass ) { 902 917 // Save new credential 903 918 VaultStore::begin(); 904 - uint8_t slot = VaultStore::findFreeSlot(); 905 - if ( slot != 0xFF ) { 919 + if ( const uint8_t slot = VaultStore::findFreeSlot(); slot != 0xFF ) { 906 920 VaultStore::saveCredential( masterKey_, slot, editCred_ ); 907 921 editCredId_ = slot; 908 922 needsReload_ = true; ··· 981 995 return true; 982 996 } 983 997 984 - void DevicePanel::drawTextInput() { 998 + void DevicePanel::drawTextInput() const { 985 999 constexpr int16_t kHeaderH = kCompactPanel ? 18 : 20; 986 1000 constexpr int16_t kHeaderCenterY = kHeaderH / 2; 987 1001 constexpr int16_t kPromptY = kCompactPanel ? 27 : 30; ··· 1026 1040 1027 1041 // Character count 1028 1042 char countBuf[16]; 1029 - snprintf( countBuf, sizeof( countBuf ), "%u/%u", inputLen_, inputMax_ ); 1043 + str::format( countBuf, sizeof( countBuf ), "%u/%u", inputLen_, inputMax_ ); 1030 1044 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 1031 1045 display::setTextDatum( kTopRight ); 1032 1046 display::drawString( countBuf, kScrW - kInputBoxX, kCountY ); ··· 1044 1058 1045 1059 // ===== Helpers ===== 1046 1060 1047 - const char* DevicePanel::layoutName( uint8_t layout ) { 1061 + const char* DevicePanel::layoutName( uint8_t layout ) const { 1048 1062 static const char* s_names[] = { "US", "ES", "UK", "FR", "DE", "PT", "IT" }; 1049 1063 return layout < 7 ? s_names[layout] : "?"; 1050 1064 } ··· 1077 1091 { "Bitwarden", "vault@secure.pw", "B!tw@rden_M@ster1" }, 1078 1092 { "ProtonMail", "privacy@proton.me", "Pr0t0n_Encryp!ed9" }, 1079 1093 }; 1080 - static constexpr uint8_t kNumDemos = sizeof( demos ) / sizeof( demos[0] ); 1094 + static constexpr auto kNumDemos = static_cast<uint8_t>( std::size( demos ) ); 1081 1095 1082 1096 // Show progress on screen 1083 1097 display::fillScreen( theme::kBg565 ); ··· 1118 1132 1119 1133 // Show result 1120 1134 char msg[40]; 1121 - snprintf( msg, sizeof( msg ), "%u credentials loaded", added ); 1135 + str::format( msg, sizeof( msg ), "%u credentials loaded", added ); 1122 1136 display::setTextColor( theme::kSuccess565, theme::kBg565 ); 1123 1137 KLEIDOS_LOGI( kTag, "Demo: %u credentials loaded", added ); 1124 1138 kleidos::platform::rtos::delayMs( 1000 );
+2 -2
src/ui/widgets/device_panel.h
··· 158 158 void startTextInput( const char* prompt, const char* initial, uint8_t maxLen, 159 159 PanelScreen returnTo ); 160 160 bool updateTextInput(); 161 - void drawTextInput(); 161 + void drawTextInput() const; 162 162 163 163 // Helpers 164 - const char* layoutName( uint8_t layout ); 164 + const char* layoutName( uint8_t layout ) const; 165 165 const char* autoLockName( uint8_t mode ); 166 166 void loadDemoCredentials(); 167 167 };
+145 -112
src/ui/widgets/on_device_menu.cpp
··· 61 61 } 62 62 63 63 size_t used = 0; 64 - for ( size_t idx = 0; source[idx] != '\0' && used + 3 < outLen; ++idx ) { 64 + size_t idx = 0; 65 + // Converted from a `for` loop so the early-exit on width overflow 66 + // doesn't modify the loop counter inside the body (S886). 67 + while ( source[idx] != '\0' && used + 3 < outLen ) { 65 68 out[used++] = source[idx]; 66 69 out[used] = '\0'; 67 70 if ( display::textWidth( out ) + ellipsisWidth > maxWidth ) { 68 71 --used; 69 72 break; 70 73 } 74 + ++idx; 71 75 } 72 76 out[used] = '\0'; 73 77 kleidos::platform::str::append( out, outLen, kEllipsis ); ··· 180 184 } 181 185 182 186 void OnDeviceMenu::moveSelection( int8_t dir ) { 183 - uint8_t next = nextSelectable( selected_, dir ); 184 - if ( next != selected_ ) { 187 + if ( const uint8_t next = nextSelectable( selected_, dir ); next != selected_ ) { 185 188 selected_ = next; 186 189 ensureVisible(); 187 190 dirty_ = true; ··· 196 199 } 197 200 } 198 201 202 + // NOSONAR cpp:S5817 — OnDeviceMenu::update mutates selected_, dirty_, 203 + // items_[].toggleState, the static accumY, and dispatches to moveSelection(); 204 + // cannot be const. 199 205 MenuAction OnDeviceMenu::update() { 200 206 MenuAction action = MenuAction::NONE; 201 207 bool inputDetected = false; ··· 241 247 auto& touch = ( *board::get().touch ); 242 248 if ( touch.wasTapped() ) { 243 249 inputDetected = true; 244 - auto pt = touch.getPoint(); 245 - if ( pt.y >= kItemTop && pt.y < kItemTop + kVisibleRows * kItemH ) { 246 - uint8_t row = static_cast<uint8_t>( ( pt.y - kItemTop ) / kItemH ); 247 - uint8_t idx = scrollTop_ + row; 248 - if ( idx < itemCount_ && items_[idx].selectable ) { 250 + if ( const auto pt = touch.getPoint(); 251 + pt.y >= kItemTop && pt.y < kItemTop + kVisibleRows * kItemH ) { 252 + const uint8_t row = static_cast<uint8_t>( ( pt.y - kItemTop ) / kItemH ); 253 + if ( const uint8_t idx = scrollTop_ + row; 254 + idx < itemCount_ && items_[idx].selectable ) { 249 255 if ( idx == selected_ ) { 250 256 // Double tap = activate 251 257 auto& it = items_[selected_]; ··· 343 349 return MenuAction::NONE; 344 350 } 345 351 346 - void OnDeviceMenu::draw() { 347 - if ( !dirty_ ) { 352 + namespace { 353 + 354 + uint16_t itemLabelColor( const MenuItem& it, bool sel ) { 355 + if ( it.type == MenuItemType::BACK ) { 356 + return theme::kLockout565; 357 + } 358 + if ( it.type == MenuItemType::ACTION ) { 359 + return sel ? theme::kPrimary565 : theme::kText565; 360 + } 361 + return theme::kText565; 362 + } 363 + 364 + int16_t itemRightReserve( const MenuItem& it ) { 365 + using enum MenuItemType; 366 + if ( it.type == TOGGLE || it.type == SUBMENU ) { 367 + return 34; 368 + } 369 + if ( it.type == VALUE && it.value ) { 370 + return kCompactMenu ? kCompactValueW : 76; 371 + } 372 + return 8; 373 + } 374 + 375 + int16_t drawItemPrefix( const MenuItem& it, int16_t y, uint16_t bg ) { 376 + if ( it.type == MenuItemType::SUBMENU ) { 377 + display::setTextColor( theme::kPrimary565, bg ); 378 + display::drawString( ">", 8, y + kItemH / 2 ); 379 + return 18; 380 + } 381 + if ( it.type == MenuItemType::BACK ) { 382 + display::setTextColor( theme::kLockout565, bg ); 383 + return 4; 384 + } 385 + return 8; 386 + } 387 + 388 + void drawItemRightSide( const MenuItem& it, int16_t y, uint16_t bg ) { 389 + if ( it.type == MenuItemType::TOGGLE ) { 390 + const uint16_t tc = it.toggleState ? theme::kSuccess565 : theme::kWarning565; 391 + display::setTextColor( tc, bg ); 392 + display::drawString( it.toggleState ? "ON" : "OFF", kScrW - 8, y + kItemH / 2 ); 393 + return; 394 + } 395 + if ( it.type == MenuItemType::VALUE && it.value ) { 396 + display::setTextColor( theme::kTextSec565, bg ); 397 + char valueBuf[20]; 398 + copyFittedText( it.value, valueBuf, sizeof( valueBuf ), 399 + kCompactMenu ? kCompactValueW : 96 ); 400 + display::drawString( valueBuf, kScrW - 8, y + kItemH / 2 ); 348 401 return; 349 402 } 350 - 351 - display::fillScreen( theme::kBg565 ); 352 - display::setFont( kCompactMenu ? display::Font::SMALL : display::Font::BODY ); 403 + if ( it.type == MenuItemType::SUBMENU ) { 404 + display::setTextColor( theme::kTextSec565, bg ); 405 + display::drawString( ">", kScrW - 8, y + kItemH / 2 ); 406 + } 407 + } 353 408 354 - // Header bar 409 + void drawHeaderBar( const char* title ) { 355 410 display::fillRect( 0, 0, kScrW, kHeaderH, theme::kSurface565 ); 356 411 display::setTextColor( kCompactMenu ? theme::kPrimary565 : theme::kText565, 357 412 theme::kSurface565 ); 358 413 display::setTextDatum( kMiddleCenter ); 359 414 char titleBuf[32]; 360 - copyFittedText( title_, titleBuf, sizeof( titleBuf ), kScrW - 12 ); 415 + copyFittedText( title, titleBuf, sizeof( titleBuf ), kScrW - 12 ); 361 416 if ( kCompactMenu ) { 362 417 display::drawStringBold( titleBuf, kScrCx, kHeaderH / 2 + 1 ); 363 418 } else { 364 419 display::drawString( titleBuf, kScrCx, kHeaderH / 2 ); 365 420 } 366 - 367 - // Menu items 368 - for ( uint8_t i = 0; i < kVisibleRows; i++ ) { 369 - uint8_t idx = scrollTop_ + i; 370 - if ( idx >= itemCount_ ) { 371 - break; 372 - } 373 - 374 - auto& it = items_[idx]; 375 - int16_t y = static_cast<int16_t>( kItemTop + i * kItemH ); 376 - bool sel = ( idx == selected_ ); 377 - 378 - if ( it.type == MenuItemType::SEPARATOR ) { 379 - display::drawFastHLine( 10, y + kItemH / 2, kScrW - 20, theme::kSubtle565 ); 380 - continue; 381 - } 382 - 383 - // Selection highlight 384 - if ( sel ) { 385 - display::fillRect( 0, y, kScrW, kItemH - 2, theme::kAccent565 ); 386 - } 421 + } 387 422 388 - uint16_t bg = sel ? theme::kAccent565 : theme::kBg565; 389 - display::setTextDatum( kMiddleLeft ); 390 - 391 - // Icon/prefix 392 - int16_t textX = 8; 393 - if ( it.type == MenuItemType::SUBMENU ) { 394 - display::setTextColor( theme::kPrimary565, bg ); 395 - display::drawString( ">", textX, y + kItemH / 2 ); 396 - textX = 18; 397 - } else if ( it.type == MenuItemType::BACK ) { 398 - display::setTextColor( theme::kLockout565, bg ); 399 - textX = 4; 400 - } 401 - 402 - // Label 403 - uint16_t labelColor = theme::kText565; 404 - if ( it.type == MenuItemType::BACK ) { 405 - labelColor = theme::kLockout565; 406 - } else if ( it.type == MenuItemType::ACTION ) { 407 - labelColor = sel ? theme::kPrimary565 : theme::kText565; 408 - } 409 - const int16_t rightReserve = 410 - ( it.type == MenuItemType::TOGGLE || it.type == MenuItemType::SUBMENU ) 411 - ? 34 412 - : ( ( it.type == MenuItemType::VALUE && it.value ) 413 - ? ( kCompactMenu ? kCompactValueW : 76 ) 414 - : 8 ); 415 - const int16_t labelMaxW = static_cast<int16_t>( kScrW - textX - rightReserve ); 416 - char labelBuf[36]; 417 - copyFittedText( it.label ? it.label : "", labelBuf, sizeof( labelBuf ), labelMaxW ); 418 - display::setTextColor( labelColor, bg ); 419 - display::drawString( labelBuf, textX, y + kItemH / 2 ); 420 - 421 - // Right-side value/toggle 422 - display::setTextDatum( kMiddleRight ); 423 - if ( it.type == MenuItemType::TOGGLE ) { 424 - uint16_t tc = it.toggleState ? theme::kSuccess565 : theme::kWarning565; 425 - display::setTextColor( tc, bg ); 426 - display::drawString( it.toggleState ? "ON" : "OFF", kScrW - 8, y + kItemH / 2 ); 427 - } else if ( it.type == MenuItemType::VALUE && it.value ) { 428 - display::setTextColor( theme::kTextSec565, bg ); 429 - char valueBuf[20]; 430 - copyFittedText( it.value, valueBuf, sizeof( valueBuf ), 431 - kCompactMenu ? kCompactValueW : 96 ); 432 - display::drawString( valueBuf, kScrW - 8, y + kItemH / 2 ); 433 - } else if ( it.type == MenuItemType::SUBMENU ) { 434 - display::setTextColor( theme::kTextSec565, bg ); 435 - display::drawString( ">", kScrW - 8, y + kItemH / 2 ); 436 - } 423 + void drawScrollRail( uint8_t itemCount, uint8_t scrollTop, int16_t footerTop ) { 424 + if ( itemCount <= kVisibleRows ) { 425 + return; 426 + } 427 + if ( kCompactMenu ) { 428 + constexpr int16_t kRailW = 2; 429 + const int16_t railX = kScrW - kRailW; 430 + const int16_t railY = kItemTop; 431 + const int16_t railH = footerTop - kItemTop - 3; 432 + display::fillRect( railX, railY, kRailW, railH, theme::kSubtle565 ); 433 + const auto thumbH = static_cast<int16_t>( ( railH * kVisibleRows ) / itemCount ); 434 + const int16_t maxScroll = itemCount - kVisibleRows; 435 + const auto thumbY = 436 + static_cast<int16_t>( railY + ( ( railH - thumbH ) * scrollTop ) / maxScroll ); 437 + display::fillRect( railX, thumbY, kRailW, thumbH, theme::kPrimary565 ); 438 + return; 439 + } 440 + display::setTextDatum( kMiddleCenter ); 441 + display::setTextColor( theme::kTextSec565, theme::kBg565 ); 442 + if ( scrollTop > 0 ) { 443 + display::drawString( "^", kScrCx, kItemTop - 2 ); 437 444 } 438 - 439 - const int16_t footerTop = kScrH - kFooterH; 440 - 441 - // Scroll indicators 442 - if ( itemCount_ > kVisibleRows ) { 443 - if ( kCompactMenu ) { 444 - constexpr int16_t kRailW = 2; 445 - const int16_t railX = kScrW - kRailW; 446 - const int16_t railY = kItemTop; 447 - const int16_t railH = footerTop - kItemTop - 3; 448 - display::fillRect( railX, railY, kRailW, railH, theme::kSubtle565 ); 449 - const int16_t thumbH = ( railH * kVisibleRows ) / itemCount_; 450 - const int16_t maxScroll = itemCount_ - kVisibleRows; 451 - const int16_t thumbY = 452 - static_cast<int16_t>( railY + ( ( railH - thumbH ) * scrollTop_ ) / maxScroll ); 453 - display::fillRect( railX, thumbY, kRailW, thumbH, theme::kPrimary565 ); 454 - } else { 455 - display::setTextDatum( kMiddleCenter ); 456 - display::setTextColor( theme::kTextSec565, theme::kBg565 ); 457 - if ( scrollTop_ > 0 ) { 458 - display::drawString( "^", kScrCx, kItemTop - 2 ); 459 - } 460 - if ( scrollTop_ + kVisibleRows < itemCount_ ) { 461 - display::drawString( "v", kScrCx, kItemTop + kVisibleRows * kItemH + 2 ); 462 - } 463 - } 445 + if ( scrollTop + kVisibleRows < itemCount ) { 446 + display::drawString( "v", kScrCx, kItemTop + kVisibleRows * kItemH + 2 ); 464 447 } 448 + } 465 449 466 - // Footer hint 450 + void drawFooterBand( int16_t footerTop ) { 467 451 const int16_t footerY = footerTop + kFooterH / 2; 468 452 display::fillRect( 0, footerTop, kScrW, kFooterH, theme::kSurface565 ); 469 453 display::setTextColor( theme::kTextSec565, theme::kSurface565 ); ··· 479 463 display::setTextDatum( kMiddleCenter ); 480 464 display::drawString( "Navigate / Click:select / Q:back", kScrCx, footerY ); 481 465 } 466 + } 467 + 468 + void drawMenuRow( const MenuItem& it, int16_t y, bool sel, uint16_t bg ) { 469 + if ( it.type == MenuItemType::SEPARATOR ) { 470 + display::drawFastHLine( 10, y + kItemH / 2, kScrW - 20, theme::kSubtle565 ); 471 + return; 472 + } 473 + if ( sel ) { 474 + display::fillRect( 0, y, kScrW, kItemH - 2, theme::kAccent565 ); 475 + } 476 + display::setTextDatum( kMiddleLeft ); 477 + const int16_t textX = drawItemPrefix( it, y, bg ); 478 + const uint16_t labelColor = itemLabelColor( it, sel ); 479 + const int16_t rightReserve = itemRightReserve( it ); 480 + const int16_t labelMaxW = static_cast<int16_t>( kScrW - textX - rightReserve ); 481 + char labelBuf[36]; 482 + copyFittedText( it.label ? it.label : "", labelBuf, sizeof( labelBuf ), labelMaxW ); 483 + display::setTextColor( labelColor, bg ); 484 + display::drawString( labelBuf, textX, y + kItemH / 2 ); 485 + display::setTextDatum( kMiddleRight ); 486 + drawItemRightSide( it, y, bg ); 487 + } 488 + 489 + } // namespace 490 + 491 + void OnDeviceMenu::draw() { 492 + if ( !dirty_ ) { 493 + return; 494 + } 495 + 496 + display::fillScreen( theme::kBg565 ); 497 + display::setFont( kCompactMenu ? display::Font::SMALL : display::Font::BODY ); 498 + 499 + drawHeaderBar( title_ ); 500 + 501 + for ( uint8_t i = 0; i < kVisibleRows; i++ ) { 502 + const uint8_t idx = scrollTop_ + i; 503 + if ( idx >= itemCount_ ) { 504 + break; 505 + } 506 + const auto y = static_cast<int16_t>( kItemTop + i * kItemH ); 507 + const bool sel = ( idx == selected_ ); 508 + const uint16_t bg = sel ? theme::kAccent565 : theme::kBg565; 509 + drawMenuRow( items_[idx], y, sel, bg ); 510 + } 511 + 512 + const int16_t footerTop = kScrH - kFooterH; 513 + drawScrollRail( itemCount_, scrollTop_, footerTop ); 514 + drawFooterBand( footerTop ); 482 515 483 516 display::setTextDatum( kTopLeft ); 484 517 dirty_ = false;
+8 -5
src/vault/audit_log.cpp
··· 78 78 79 79 // Output oldest-first: start from idx (oldest) and wrap around 80 80 uint8_t count = 0; 81 - for ( uint8_t i = 0; i < kAuditLogSize && count < maxEntries; i++ ) { 82 - uint8_t pos = ( idx + i ) % kAuditLogSize; 83 - if ( entries[pos].timestamp == 0 && entries[pos].event == 0 ) { 84 - continue; 81 + uint8_t i = 0; 82 + // Converted from a `for` loop so the `continue` inside the body 83 + // doesn't update the counter there (S886). 84 + while ( i < kAuditLogSize && count < maxEntries ) { 85 + if ( const uint8_t pos = ( idx + i ) % kAuditLogSize; 86 + entries[pos].timestamp != 0 || entries[pos].event != 0 ) { 87 + out[count++] = entries[pos]; 85 88 } 86 - out[count++] = entries[pos]; 89 + ++i; 87 90 } 88 91 return count; 89 92 }
+6 -9
src/vault/brute_force_guard.cpp
··· 13 13 static constexpr const char* kNvsAttempts = "bf_attempts"; 14 14 static constexpr const char* kNvsLockoutUntil = "bf_lock_min"; 15 15 16 - static constexpr uint8_t kWarnThreshold = 3; 17 16 static constexpr uint8_t kShortLockStart = 4; 18 17 static constexpr uint8_t kLongLockStart = 7; 19 18 static constexpr uint8_t kWipeThreshold = 10; ··· 49 48 50 49 uint32_t lockoutSec = getLockoutDuration( attempts ); 51 50 52 - if ( lockoutSec > 0 ) { 53 - if ( rtc::isAvailable() ) { 54 - RtcTime now = rtc::getTime(); 55 - uint32_t nowTotalSec = now.hours * 3600u + now.minutes * 60u + now.seconds; 56 - uint32_t unlockSec = nowTotalSec + lockoutSec; 57 - prefs.setU32( kNvsLockoutUntil, unlockSec ); 58 - KLEIDOS_LOGW( kTag, "Locked out for %lu s (until RTC %lu)", lockoutSec, unlockSec ); 59 - } 51 + if ( lockoutSec > 0 && rtc::isAvailable() ) { 52 + RtcTime now = rtc::getTime(); 53 + uint32_t nowTotalSec = now.hours * 3600u + now.minutes * 60u + now.seconds; 54 + uint32_t unlockSec = nowTotalSec + lockoutSec; 55 + prefs.setU32( kNvsLockoutUntil, unlockSec ); 56 + KLEIDOS_LOGW( kTag, "Locked out for %lu s (until RTC %lu)", lockoutSec, unlockSec ); 60 57 } 61 58 62 59 return lockoutSec;
+12 -15
src/vault/sd_vault.cpp
··· 174 174 size_t n = src.read( buf, sizeof( buf ) ); 175 175 if ( n == 0 ) 176 176 break; 177 - size_t written = dst.write( buf, n ); 178 - if ( written != n ) { 177 + if ( const size_t written = dst.write( buf, n ); written != n ) { 179 178 KLEIDOS_LOGE( kTag, "Write error at offset %u", totalCopied ); 180 179 src.close(); 181 180 dst.close(); ··· 210 209 size_t n = src.read( buf, sizeof( buf ) ); 211 210 if ( n == 0 ) 212 211 break; 213 - size_t written = dst.write( buf, n ); 214 - if ( written != n ) { 212 + if ( const size_t written = dst.write( buf, n ); written != n ) { 215 213 KLEIDOS_LOGE( kTag, "Write error at offset %u", totalCopied ); 216 214 src.close(); 217 215 dst.close(); ··· 401 399 return false; 402 400 } 403 401 404 - char manifestJson[512]; 405 - const size_t needed = measureJsonPretty( doc ); 406 - if ( needed >= sizeof( manifestJson ) ) { 402 + char manifestJson[512]; 403 + if ( const size_t needed = measureJsonPretty( doc ); needed >= sizeof( manifestJson ) ) { 407 404 KLEIDOS_LOGE( kTag, "Manifest too large" ); 408 405 return false; 409 406 } 410 407 411 - const size_t written = serializeJsonPretty( doc, manifestJson, sizeof( manifestJson ) ); 412 - if ( !writeAll( f, manifestJson, written ) ) { 408 + if ( const size_t written = serializeJsonPretty( doc, manifestJson, sizeof( manifestJson ) ); 409 + !writeAll( f, manifestJson, written ) ) { 413 410 KLEIDOS_LOGE( kTag, "Manifest write incomplete" ); 414 411 return false; 415 412 } ··· 481 478 const char* name = f.name(); 482 479 f.close(); 483 480 if ( name ) { 484 - size_t len = kleidos::platform::str::length( name ); 485 - if ( len > 4 && str::equalsIgnoreCase( name + len - 4, ".csv" ) ) { 481 + if ( const size_t len = kleidos::platform::str::length( name ); 482 + len > 4 && str::equalsIgnoreCase( name + len - 4, ".csv" ) ) { 486 483 dir.close(); 487 484 return true; 488 485 } ··· 691 688 const char* name = f.name(); 692 689 f.close(); 693 690 if ( name ) { 694 - size_t len = kleidos::platform::str::length( name ); 695 - if ( len > 4 && str::equalsIgnoreCase( name + len - 4, ".csv" ) ) { 691 + if ( const size_t len = kleidos::platform::str::length( name ); 692 + len > 4 && str::equalsIgnoreCase( name + len - 4, ".csv" ) ) { 696 693 snprintf( filePath, sizeof( filePath ), "%s/%s", SD_IMPORT, name ); 697 694 found = true; 698 695 break; ··· 1079 1076 } 1080 1077 1081 1078 written += n; 1082 - uint8_t pct = static_cast<uint8_t>( ( written * 100 ) / fwSize ); 1083 - if ( progressCb && pct != lastPct ) { 1079 + if ( const uint8_t pct = static_cast<uint8_t>( ( written * 100 ) / fwSize ); 1080 + progressCb && pct != lastPct ) { 1084 1081 progressCb( pct ); 1085 1082 lastPct = pct; 1086 1083 }
+32 -28
src/vault/vault_task.cpp
··· 41 41 // that nobody will drain. 42 42 std::atomic<bool> s_running{ false }; 43 43 44 + // Returns true when the worker loop should exit; false to keep running. 45 + // Extracted from worker() to keep the S924 nested-break threshold (1) in check. 46 + bool processOneRequest( detail::Request*& req ); 47 + 44 48 [[noreturn]] void worker( void* /*arg*/ ) { 45 49 detail::Request* req = nullptr; 46 - while ( true ) { 47 - if ( !s_queue.receive( req, kRecvTimeout ) ) { 48 - // Idle tick: re-check the running flag. The shutdown path 49 - // posts a sentinel (nullptr) to wake us, but a long timeout 50 - // also lets us exit cleanly if the queue was drained. 51 - if ( !s_running.load() ) { 52 - break; 53 - } 54 - continue; 55 - } 56 - if ( req == nullptr ) { 57 - // Sentinel: shutdown requested. 58 - break; 59 - } 60 - // Run the caller's lambda. `invoke` is captureless and just calls 61 - // the closure stored at `fnPtr` (which lives on the caller's 62 - // stack). The closure runs to completion before we notify. 63 - req->result = req->invoke( req->fnPtr ); 64 - if ( req->done != nullptr ) { 65 - rtos::giveSemaphore( req->done ); 66 - } else { 67 - KLEIDOS_LOGW( kTag, "dropping request completion with no waiter" ); 68 - } 50 + while ( !processOneRequest( req ) ) { 51 + // Loop body lives in processOneRequest(); returns true to exit the worker. 69 52 } 70 53 71 54 s_task = nullptr; ··· 75 58 } // unreachable 76 59 } 77 60 61 + bool processOneRequest( detail::Request*& req ) { 62 + if ( !s_queue.receive( req, kRecvTimeout ) ) { 63 + // Idle tick: re-check the running flag. The shutdown path 64 + // posts a sentinel (nullptr) to wake us, but a long timeout 65 + // also lets us exit cleanly if the queue was drained. 66 + return !s_running.load(); 67 + } 68 + if ( req == nullptr ) { 69 + // Sentinel: shutdown requested. 70 + return true; 71 + } 72 + // Run the caller's lambda. `invoke` is captureless and just calls 73 + // the closure stored at `fnPtr` (which lives on the caller's 74 + // stack). The closure runs to completion before we notify. 75 + req->result = req->invoke( req->fnPtr ); 76 + if ( req->done != nullptr ) { 77 + rtos::giveSemaphore( req->done ); 78 + } else { 79 + KLEIDOS_LOGW( kTag, "dropping request completion with no waiter" ); 80 + } 81 + return false; 82 + } 83 + 78 84 } // namespace 79 85 80 86 bool begin( rtos::Core core, rtos::Priority priority ) { ··· 82 88 return true; 83 89 } 84 90 85 - if ( !s_queue.valid() ) { 86 - if ( !s_queue.create( kQueueDepth ) ) { 87 - KLEIDOS_LOGE( kTag, "queue create failed" ); 88 - return false; 89 - } 91 + if ( !s_queue.valid() && !s_queue.create( kQueueDepth ) ) { 92 + KLEIDOS_LOGE( kTag, "queue create failed" ); 93 + return false; 90 94 } 91 95 92 96 s_running.store( true );
+1 -1
src/web/wifi_admin_portal.cpp
··· 162 162 // Index-based iteration (bounded by strlen) so the analyzer can prove the 163 163 // access stays in bounds even when 'text' is the empty string literal. 164 164 for ( size_t pos = 0; pos < textLen; ++pos ) { 165 - const uint8_t ch = static_cast<uint8_t>( text[pos] ); 165 + const auto ch = static_cast<uint8_t>( text[pos] ); 166 166 switch ( ch ) { 167 167 case '"': 168 168 output += "\\\"";
+7 -4
test/hal/test_display_graphics/test_display_graphics.cpp
··· 27 27 int32_t width() const override { return kWidth; } 28 28 int32_t height() const override { return kHeight; } 29 29 30 - void drawPixel( int32_t posX, int32_t posY, uint16_t color ) override { 30 + void drawPixel( int32_t posX, int32_t posY, uint16_t color ) const override { 31 31 if ( posX < 0 || posY < 0 || posX >= kWidth || posY >= kHeight ) { 32 32 return; 33 33 } ··· 49 49 } 50 50 51 51 private: 52 - static constexpr int32_t kWidth = 64; 53 - static constexpr int32_t kHeight = 32; 54 - std::array<uint16_t, kWidth * kHeight> pixels_ = {}; 52 + static constexpr int32_t kWidth = 64; 53 + static constexpr int32_t kHeight = 32; 54 + // `mutable` is required because the const drawPixel() override writes into 55 + // the buffer (the buffer represents the panel, which is conceptually 56 + // external hardware state, not C++ state of *this). 57 + mutable std::array<uint16_t, kWidth * kHeight> pixels_ = {}; 55 58 }; 56 59 57 60 // --- setUp / tearDown ------------------------------------------------------
+3 -1
test/web/test_admin_idle_logic/test_admin_idle_logic.cpp
··· 7 7 #include "web/admin_idle_logic.h" 8 8 9 9 // Group 3: stdlib + Unity 10 + #include <iterator> 11 + 10 12 #include <unity.h> 11 13 12 14 // --- Constants ------------------------------------------------------------- ··· 100 102 constexpr bool kBanner[] = { false, false, false, false, true, true, true, true }; 101 103 102 104 // ACT + ASSERT 103 - for ( size_t i = 0; i < sizeof( kStep ) / sizeof( kStep[0] ); ++i ) { 105 + for ( size_t i = 0; i < std::size( kStep ); ++i ) { 104 106 auto r = decideIdleResponse( kStep[i], kTimeoutMs, kWarnMs ); 105 107 TEST_ASSERT_EQUAL_UINT32( kRem[i], r.remainingS ); 106 108 if ( kBanner[i] ) {