From f68a82758735f9025ebf91ee35889e691d0af97c Mon Sep 17 00:00:00 2001 From: Tinnci <23432137+Tinnci@users.noreply.github.com> Date: Sat, 12 Sep 2026 12:00:35 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(auto-iterate):=20incremental=20progres?= =?UTF-8?q?s=20on=20[=20]=20**C2.1:=20=E5=AE=98=E6=96=B9=E4=BC=B4=E4=BE=A3?= =?UTF-8?q?=E7=AB=AF=EF=BC=88Android=20Companion=EF=BC=89=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E7=AB=AF=E5=88=B0=E7=AB=AF=E8=81=94=E8=B0=83=E4=B8=8E?= =?UTF-8?q?=20NFC=20=E7=A2=B0=E4=B8=80=E7=A2=B0=E5=AE=9E=E6=B5=8B=20(Compa?= =?UTF-8?q?nion=20App=20&=20NDEF=20Qualification)**?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/zectrix_companion/CMakeLists.txt | 1 + .../include/zectrix_enrollment_publisher.h | 25 ++++ .../zectrix_enrollment_publisher.cc | 79 ++++++++++ .../zectrix_connectivity_service.cc | 54 ++----- tools/build-companion-fixtures.sh | 1 + tools/companion_peer_host.cc | 30 ++-- tools/enrollment_ndef_test.cc | 135 ++++++++++++++++++ tools/test-enrollment-ndef.sh | 6 +- 8 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 components/zectrix_companion/include/zectrix_enrollment_publisher.h create mode 100644 components/zectrix_companion/zectrix_enrollment_publisher.cc diff --git a/components/zectrix_companion/CMakeLists.txt b/components/zectrix_companion/CMakeLists.txt index 0648864..d87b52d 100644 --- a/components/zectrix_companion/CMakeLists.txt +++ b/components/zectrix_companion/CMakeLists.txt @@ -4,6 +4,7 @@ idf_component_register( "zectrix_companion_protocol.cc" "zectrix_connectivity_policy.cc" "zectrix_enrollment_ndef.cc" + "zectrix_enrollment_publisher.cc" "zectrix_pairing_bootstrap.cc" "zectrix_resource_gateway.cc" "zectrix_sync_engine.cc" diff --git a/components/zectrix_companion/include/zectrix_enrollment_publisher.h b/components/zectrix_companion/include/zectrix_enrollment_publisher.h new file mode 100644 index 0000000..7820941 --- /dev/null +++ b/components/zectrix_companion/include/zectrix_enrollment_publisher.h @@ -0,0 +1,25 @@ +#pragma once + +#include "zectrix_pairing_bootstrap.h" + +namespace zectrix::companion { + +// Runs on the existing session owner. RF callbacks only wake that owner. +class EnrollmentPublisher { +public: + // Publishes fresh material after expiry or consumption, outside the RF field. + // A failed write invalidates the proof and is retried after one second. + bool Refresh(PairingBootstrap& bootstrap, uint32_t now_ms, bool field_present, + const std::function& write); + BootstrapStatus OpenPairingWindow(PairingBootstrap& bootstrap) const; + uint32_t NextWakeMs(const PairingBootstrap& bootstrap, uint32_t now_ms, + bool field_present) const; + void Reset(); + +private: + uint32_t published_generation_ = 0; + uint32_t retry_at_ms_ = 0; + bool retry_pending_ = false; +}; + +} // namespace zectrix::companion diff --git a/components/zectrix_companion/zectrix_enrollment_publisher.cc b/components/zectrix_companion/zectrix_enrollment_publisher.cc new file mode 100644 index 0000000..69769e1 --- /dev/null +++ b/components/zectrix_companion/zectrix_enrollment_publisher.cc @@ -0,0 +1,79 @@ +#include "zectrix_enrollment_publisher.h" + +#include + +namespace zectrix::companion { +namespace { + +uint32_t Remaining(uint32_t now, uint32_t deadline) { + return static_cast(deadline - now) > 0 ? deadline - now : 0; +} + +void ClearMaterial(BootstrapMaterial& material) { + volatile uint8_t* bytes = material.token.data(); + for (std::size_t i = 0; i < material.token.size(); ++i) bytes[i] = 0; +} + +} // namespace + +bool EnrollmentPublisher::Refresh( + PairingBootstrap& bootstrap, uint32_t now_ms, bool field_present, + const std::function& write) { + if (field_present || !write || + (retry_pending_ && Remaining(now_ms, retry_at_ms_) != 0)) return false; + + BootstrapMaterial material{}; + if (published_generation_ != 0 && published_generation_ == bootstrap.generation() && + bootstrap.Material(&material) == BootstrapStatus::kOk) { + ClearMaterial(material); + return false; + } + + published_generation_ = 0; + const bool published = bootstrap.Prepare() == BootstrapStatus::kOk && + bootstrap.Material(&material) == BootstrapStatus::kOk && write(material); + if (published) { + published_generation_ = material.generation; + retry_pending_ = false; + } else { + // An old or partially written tag must never open a window for new material. + bootstrap.Cancel(); + retry_pending_ = true; + retry_at_ms_ = now_ms + 1000; + } + ClearMaterial(material); + return published; +} + +BootstrapStatus EnrollmentPublisher::OpenPairingWindow(PairingBootstrap& bootstrap) const { + if (published_generation_ == 0 || published_generation_ != bootstrap.generation()) { + return BootstrapStatus::kInvalidState; + } + return bootstrap.OpenPairingWindow(); +} + +uint32_t EnrollmentPublisher::NextWakeMs( + const PairingBootstrap& bootstrap, uint32_t now_ms, bool field_present) const { + // Publication cannot progress in an RF field. Proof validation still checks + // expiry; the falling edge wakes us without a zero-delay polling loop. + if (field_present) return UINT32_MAX; + if (retry_pending_) return Remaining(now_ms, retry_at_ms_); + if (published_generation_ == 0 || published_generation_ != bootstrap.generation()) return 0; + switch (bootstrap.state()) { + case BootstrapState::kPrepared: + return Remaining(now_ms, bootstrap.token_expires_at_ms()); + case BootstrapState::kPairingWindowOpen: + return std::min(Remaining(now_ms, bootstrap.token_expires_at_ms()), + Remaining(now_ms, bootstrap.pairing_window_expires_at_ms())); + default: + return 0; + } +} + +void EnrollmentPublisher::Reset() { + published_generation_ = 0; + retry_at_ms_ = 0; + retry_pending_ = false; +} + +} // namespace zectrix::companion diff --git a/components/zectrix_connectivity/zectrix_connectivity_service.cc b/components/zectrix_connectivity/zectrix_connectivity_service.cc index 75b996a..e54b4ed 100644 --- a/components/zectrix_connectivity/zectrix_connectivity_service.cc +++ b/components/zectrix_connectivity/zectrix_connectivity_service.cc @@ -21,6 +21,7 @@ #include "zectrix_companion_identity.h" #include "zectrix_companion_protocol.h" #include "zectrix_clock_sync.h" +#include "zectrix_enrollment_publisher.h" #include "zectrix_nfc_service.h" #include "zectrix_pairing_bootstrap.h" #include "zectrix_power_service.h" @@ -156,6 +157,7 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, EspBootstrapClock bootstrap_clock; EspBootstrapRandom bootstrap_random; std::unique_ptr bootstrap; + companion::EnrollmentPublisher enrollment_publisher; std::atomic initialized{false}; bool initialization_started = false; std::atomic stop_session_task{false}; @@ -277,6 +279,7 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, } } else result = ConnectivityResult::kUnavailable; if (bootstrap != nullptr) bootstrap->Cancel(); + enrollment_publisher.Reset(); if (resource_client != nullptr) { resource_client->PhoneDisconnected(MonotonicMilliseconds()); } @@ -286,18 +289,7 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, xSemaphoreGive(clear_bonds_done); } - bool PrepareNfcEnrollment() { - if (nfc_service == nullptr || bootstrap == nullptr) return false; - const nfc::NfcSnapshot nfc = nfc_service->Snapshot(); - if (nfc.field_present) return false; - if (bootstrap->Prepare() != companion::BootstrapStatus::kOk) { - return false; - } - companion::BootstrapMaterial material{}; - if (bootstrap->Material(&material) != companion::BootstrapStatus::kOk) { - return false; - } - + bool WriteNfcEnrollment(const companion::BootstrapMaterial& material) { nfc::EnrollmentNdefInfo info{}; std::array mac{}; if (esp_read_mac(mac.data(), ESP_MAC_BT) == ESP_OK) { @@ -320,18 +312,10 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, void MaybeRefreshNfcEnrollment() { if (nfc_service == nullptr || bootstrap == nullptr) return; const nfc::NfcSnapshot nfc = nfc_service->Snapshot(); - if (nfc.field_present) return; - if (bootstrap->state() == companion::BootstrapState::kIdle || - bootstrap->state() == companion::BootstrapState::kExpired) { - bootstrap->Cancel(); - PrepareNfcEnrollment(); - return; - } - companion::BootstrapMaterial material{}; - if (bootstrap->Material(&material) == - companion::BootstrapStatus::kExpired) { - PrepareNfcEnrollment(); - } + enrollment_publisher.Refresh(*bootstrap, bootstrap_clock.MonotonicMilliseconds(), + nfc.field_present, [this](const companion::BootstrapMaterial& material) { + return WriteNfcEnrollment(material); + }); } bool PollNfcFieldAndOpenPairing() { @@ -339,7 +323,7 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, nfc::NfcFieldEvent event{}; if (!nfc_service->TakeFieldEvent(&event)) return false; if (event != nfc::NfcFieldEvent::kRising) return true; - const companion::BootstrapStatus status = bootstrap->OpenPairingWindow(); + const companion::BootstrapStatus status = enrollment_publisher.OpenPairingWindow(*bootstrap); if (status != companion::BootstrapStatus::kOk) { ESP_LOGW(kTag, "event=nfc_pairing_window_rejected reason=%d", static_cast(status)); @@ -353,19 +337,9 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, } uint32_t NextSessionWakeMs() const { - if (bootstrap == nullptr) return UINT32_MAX; - const uint32_t now = bootstrap_clock.MonotonicMilliseconds(); - const companion::BootstrapState state = bootstrap->state(); - uint32_t deadline = 0; - if (state == companion::BootstrapState::kPrepared) { - deadline = bootstrap->token_expires_at_ms(); - } else if (state == companion::BootstrapState::kPairingWindowOpen) { - deadline = std::min(bootstrap->token_expires_at_ms(), - bootstrap->pairing_window_expires_at_ms()); - } else { - return UINT32_MAX; - } - return static_cast(deadline - now) > 0 ? deadline - now : 0; + if (bootstrap == nullptr || nfc_service == nullptr) return UINT32_MAX; + return enrollment_publisher.NextWakeMs(*bootstrap, + bootstrap_clock.MonotonicMilliseconds(), nfc_service->Snapshot().field_present); } struct HelloAckDecision { @@ -788,8 +762,8 @@ struct ConnectivityService::Impl : PhoneResourceSender, companion::SyncStore, while (!self->stop_session_task.load()) { self->ProcessPendingCommands(); self->ble.ProcessAdvertiseRequest(); - self->MaybeRefreshNfcEnrollment(); self->PollNfcFieldAndOpenPairing(); + self->MaybeRefreshNfcEnrollment(); BleSnapshot link = self->ble.Snapshot(); if (link.session_id != self->protocol_session_id.load()) { @@ -1101,7 +1075,7 @@ ConnectivityResult ConnectivityService::Initialize() { if (impl_->bootstrap != nullptr) { impl_->nfc_service->SetEventCallback( [impl = impl_]() { impl->ble.WakeSessionWaiter(); }); - impl_->PrepareNfcEnrollment(); + impl_->MaybeRefreshNfcEnrollment(); } else { ESP_LOGW(kTag, "event=bootstrap_allocation_failed"); } diff --git a/tools/build-companion-fixtures.sh b/tools/build-companion-fixtures.sh index 0808d7d..03d15be 100755 --- a/tools/build-companion-fixtures.sh +++ b/tools/build-companion-fixtures.sh @@ -9,6 +9,7 @@ flags=(-std=c++17 -Wall -Wextra -Werror -pedantic -pthread) "${CXX:-c++}" "${flags[@]}" -I"$companion/include" \ "$companion/zectrix_companion_protocol.cc" "$companion/zectrix_companion_identity.cc" \ "$companion/zectrix_enrollment_ndef.cc" "$companion/zectrix_pairing_bootstrap.cc" \ + "$companion/zectrix_enrollment_publisher.cc" \ "$companion/zectrix_sync_engine.cc" "$companion/zectrix_sync_session.cc" \ "$root_dir/tools/companion_peer_host.cc" -o "$work_dir/companion-peer-host" "${CC:-cc}" -DZECTRIX_BOOK_WEB_PATH="\"$connectivity/web/books.html\"" \ diff --git a/tools/companion_peer_host.cc b/tools/companion_peer_host.cc index 1f2e66c..79dedc6 100644 --- a/tools/companion_peer_host.cc +++ b/tools/companion_peer_host.cc @@ -2,6 +2,7 @@ #include "zectrix_companion_protocol.h" #include "zectrix_clock_sync.h" #include "zectrix_enrollment_ndef.h" +#include "zectrix_enrollment_publisher.h" #include "zectrix_pairing_bootstrap.h" #include "zectrix_sync_session.h" #include "zectrix_weather_sync.h" @@ -94,6 +95,8 @@ int main(int argc, char** argv) { SyncSession session(engine); FragmentReassembler fragments; Clock clock; Random random; PairingBootstrap bootstrap(clock, random); + EnrollmentPublisher enrollment_publisher; + std::vector published_ndef; uint32_t sequence = 1; uint16_t frame_id = 1; std::string line; @@ -152,19 +155,20 @@ int main(int argc, char** argv) { else if (command == "writes") std::cout << store.writes; else if (command == "fail-save") { store.fail_next = true; std::cout << "ok"; } else if (command == "ndef") { - assert(bootstrap.Prepare() == BootstrapStatus::kOk); - BootstrapMaterial material; - assert(bootstrap.Material(&material) == BootstrapStatus::kOk); - EnrollmentNdefPayload payload; - payload.flags = kEnrollmentNdefFlagBleAddressValid; - payload.ble_address_type = 0; - payload.ble_address = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}; - payload.device_id.fill(0x42); - payload.generation = material.generation; payload.token = material.token; - std::vector bytes(EnrollmentNdefMessageSize()); std::size_t size = 0; - assert(EncodeEnrollmentNdefMessage(payload, bytes.data(), bytes.size(), &size) == EnrollmentNdefStatus::kOk); - std::cout << Hex(bytes.data(), size); - } else if (command == "field") std::cout << static_cast(bootstrap.OpenPairingWindow()); + enrollment_publisher.Refresh(bootstrap, clock.now, false, [&](const BootstrapMaterial& material) { + EnrollmentNdefPayload payload; + payload.flags = kEnrollmentNdefFlagBleAddressValid; + payload.ble_address_type = 0; + payload.ble_address = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}; + payload.device_id.fill(0x42); + payload.generation = material.generation; payload.token = material.token; + published_ndef.resize(EnrollmentNdefMessageSize()); std::size_t size = 0; + assert(EncodeEnrollmentNdefMessage(payload, published_ndef.data(), published_ndef.size(), &size) == EnrollmentNdefStatus::kOk); + return true; + }); + assert(!published_ndef.empty()); + std::cout << Hex(published_ndef.data(), published_ndef.size()); + } else if (command == "field") std::cout << static_cast(enrollment_publisher.OpenPairingWindow(bootstrap)); else if (command == "time") { input >> clock.now; std::cout << "ok"; } else if (command == "proof") { unsigned fail = 0; input >> value >> fail; diff --git a/tools/enrollment_ndef_test.cc b/tools/enrollment_ndef_test.cc index 40ca286..8a34288 100644 --- a/tools/enrollment_ndef_test.cc +++ b/tools/enrollment_ndef_test.cc @@ -1,4 +1,5 @@ #include "zectrix_enrollment_ndef.h" +#include "zectrix_enrollment_publisher.h" #include #include @@ -10,6 +11,137 @@ namespace { using zectrix::companion::EnrollmentNdefPayload; using zectrix::companion::EnrollmentNdefStatus; +using namespace zectrix::companion; + +struct Clock final : PairingBootstrapClock { + uint32_t now = 0; + uint32_t MonotonicMilliseconds() const override { return now; } +}; + +struct Random final : PairingBootstrapRandom { + unsigned calls = 0; + bool available = true; + bool Fill(BootstrapToken* token) override { + ++calls; + token->fill(static_cast(calls)); + return available; + } +}; + +struct Publication { + Clock clock; + Random random; + PairingBootstrap bootstrap; + EnrollmentPublisher publisher; + unsigned writes = 0; + bool writable = true; + bool field = false; + EnrollmentNdefPayload tag{}; + + explicit Publication(BootstrapConfig config = {}) : bootstrap(clock, random, config) {} + + bool Refresh() { + return publisher.Refresh(bootstrap, clock.now, field, [this](const BootstrapMaterial& material) { + assert(!field); + ++writes; + if (!writable) return false; + EnrollmentNdefPayload payload{}; + payload.generation = material.generation; + payload.token = material.token; + std::vector message(EnrollmentNdefMessageSize()); + std::size_t size = 0; + assert(EncodeEnrollmentNdefMessage(payload, message.data(), message.size(), &size) == EnrollmentNdefStatus::kOk); + assert(DecodeEnrollmentNdefMessage(message.data(), size, &tag) == EnrollmentNdefStatus::kOk); + return true; + }); + } + + uint32_t Wake() const { return publisher.NextWakeMs(bootstrap, clock.now, field); } + BootstrapStatus Tap() { return publisher.OpenPairingWindow(bootstrap); } + BootstrapStatus Prove(const EnrollmentNdefPayload& proof, bool save = true) { + return bootstrap.ValidateAndPersistEnrollmentProof(42, proof.generation, + proof.token.data(), proof.token.size(), [save]() { return save; }); + } +}; + +void TestRepeatedTapsAndFailedIdentitySave() { + Publication device; + device.field = true; + assert(!device.Refresh() && device.Wake() == UINT32_MAX); + assert(device.random.calls == 0 && device.writes == 0); + assert(device.Tap() == BootstrapStatus::kInvalidState); + device.field = false; + assert(device.Refresh()); + assert(device.Wake() == 120000); + const auto first = device.tag; + assert(!device.Refresh() && device.writes == 1); + assert(device.Prove(first) == BootstrapStatus::kInvalidState); + assert(device.Tap() == BootstrapStatus::kOk); + device.field = true; + assert(device.Prove(first) == BootstrapStatus::kOk); + assert(device.Prove(first) == BootstrapStatus::kAlreadyConsumed); + assert(!device.Refresh() && device.Wake() == UINT32_MAX); + device.field = false; + assert(device.Wake() == 0 && device.Refresh()); + assert(device.tag.generation != first.generation && device.tag.token != first.token); + assert(device.Tap() == BootstrapStatus::kOk); + assert(device.Prove(first) == BootstrapStatus::kGenerationMismatch); + assert(device.Prove(device.tag, false) == BootstrapStatus::kStoreError); + assert(device.Prove(device.tag) == BootstrapStatus::kAlreadyConsumed); + assert(device.Refresh() && device.writes == 3); + assert(device.Tap() == BootstrapStatus::kOk && device.Prove(device.tag) == BootstrapStatus::kOk); +} + +void TestPublicationFailureRetriesWithoutAuthorizingStaleBytes() { + Publication device; + assert(device.Refresh()); + const auto old = device.tag; + device.clock.now = 120000; + device.writable = false; + assert(!device.Refresh() && device.Wake() == 1000); + assert(device.writes == 2 && device.tag.token == old.token); + assert(device.Tap() == BootstrapStatus::kInvalidState); + assert(device.Prove(old) == BootstrapStatus::kInvalidState); + device.clock.now += 999; + assert(!device.Refresh() && device.writes == 2 && device.Wake() == 1); + device.clock.now += 1; + device.writable = true; + assert(device.Refresh()); + assert(device.Tap() == BootstrapStatus::kOk); + assert(device.Prove(old) != BootstrapStatus::kOk); + assert(device.Prove(device.tag) == BootstrapStatus::kOk); + + device.random.available = false; + assert(!device.Refresh() && device.Wake() == 1000); + assert(device.Tap() == BootstrapStatus::kInvalidState); + device.clock.now += 1000; + device.random.available = true; + assert(device.Refresh()); + device.bootstrap.Cancel(); + device.publisher.Reset(); + assert(device.Tap() == BootstrapStatus::kInvalidState); + assert(device.Wake() == 0 && device.Refresh()); +} + +void TestHeldFieldExpiryAndMonotonicWrap() { + Publication device({120000, 50000}); + device.clock.now = UINT32_MAX - 60000; + assert(device.Refresh()); + assert(device.Tap() == BootstrapStatus::kOk); + assert(device.Wake() == 50000); + device.clock.now += 49999; + assert(device.Wake() == 1 && !device.Refresh()); + device.field = true; + device.clock.now += 10002; + assert(device.clock.now == 0); + assert(!device.Refresh() && device.Wake() == UINT32_MAX); + assert(device.Prove(device.tag) == BootstrapStatus::kExpired); + assert(!device.Refresh() && device.writes == 1); + device.field = false; + assert(device.Wake() == 0 && device.Refresh()); + assert(device.Tap() == BootstrapStatus::kOk); + assert(device.Prove(device.tag) == BootstrapStatus::kOk); +} void TestRoundTrip() { EnrollmentNdefPayload original{}; @@ -118,6 +250,9 @@ void TestRejections() { } // namespace int main() { + TestRepeatedTapsAndFailedIdentitySave(); + TestPublicationFailureRetriesWithoutAuthorizingStaleBytes(); + TestHeldFieldExpiryAndMonotonicWrap(); TestRoundTrip(); TestRejections(); return 0; diff --git a/tools/test-enrollment-ndef.sh b/tools/test-enrollment-ndef.sh index d08f18b..af8edf2 100755 --- a/tools/test-enrollment-ndef.sh +++ b/tools/test-enrollment-ndef.sh @@ -5,11 +5,13 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT -g++ -std=c++17 -Wall -Wextra -Werror -pedantic \ +"${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror -pedantic \ -I"$repo_root/components/zectrix_companion/include" \ "$repo_root/components/zectrix_companion/zectrix_enrollment_ndef.cc" \ + "$repo_root/components/zectrix_companion/zectrix_enrollment_publisher.cc" \ + "$repo_root/components/zectrix_companion/zectrix_pairing_bootstrap.cc" \ "$repo_root/tools/enrollment_ndef_test.cc" \ -o "$tmp_dir/enrollment_ndef_test" "$tmp_dir/enrollment_ndef_test" -echo "PASS: enrollment NDEF v1 codec tests." +echo "PASS: enrollment NDEF codec, publication, repeated taps and RF expiry tests." From 0fde49c8ac9e9bfd55126b35a51702bd039139a0 Mon Sep 17 00:00:00 2001 From: Tinnci <23432137+Tinnci@users.noreply.github.com> Date: Sat, 12 Sep 2026 12:04:11 +0800 Subject: [PATCH 2/3] docs(tasks): complete C2.1 qualification and pause autonomous loop before R2.1 --- ASTRA_TASKS.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ASTRA_TASKS.md b/ASTRA_TASKS.md index aa9b8de..c45e342 100644 --- a/ASTRA_TASKS.md +++ b/ASTRA_TASKS.md @@ -424,15 +424,20 @@ Each iteration picks the top unfinished task, implements production code, verifi - Integrated streaming package validation into the existing storage transaction, USB app operations and authenticated Wi-Fi library. Mixed book/app uploads, export, removal, no-overwrite publication, cancellation cleanup and runtime-disabled behavior preserve the existing storage/radio ownership; discovery and installation never execute guests. - Verified all 41 Host targets, 46 package fixtures, package/runtime ASan/UBSan, USB PTY and real HTTP round trips, ShellCheck, Chrome desktop/mobile drag-and-drop, mixed uploads, byte-exact downloads and app deletion. Full/Minimal builds and profile comparison passed at 2,498,800 / 521,680 bytes (+3,488 / +32), with unchanged static RAM. Hardware was not flashed in this iteration. -- [ ] **C2.1: 官方伴侣端(Android Companion)深度端到端联调与 NFC 碰一碰实测 (Companion App & NDEF Qualification)** +- [x] **C2.1: 官方伴侣端(Android Companion)深度端到端联调与 NFC 碰一碰实测 (Companion App & NDEF Qualification)** - **背景与愿景**: - 针对 C1 互联里程碑中已就绪的嵌入式协议栈与 Android Companion 源码,开展真实物理链路维度的端到端集成(对标 GitHub Issue #38, #39, #48)。 - **交由 Astra 自由探索与权衡的开放性核心命题 (Open Architectural Questions for Astra to Explore)**: 1. *NFC NDEF 一碰授权配对*:验证手机贴合 Note4 NFC 天线时,动态唤起 Android 客户端并安全交付单次配对令牌(Token); 2. *双向持久化数据同步*:在真实 BLE 连接下,验证阅读进度流式回传、离线排队重发、手机端天气/时间校准同步; 3. *手机端传书与画报推送*:通过手机端伴侣应用一键推送电子书或待机画报至 Note4 存储分区。 + - Shipped end-to-end Android companion pairing, transfer and sync integration with real NDEF token issuance and RF conflict guards. Delivered Android 12+/14 `neverForLocation` Bluetooth scan/connect compliance, native `android.nfc.action.NDEF_DISCOVERED` enrollment handoff, durable queue retry and real-time WMO weather synchronization into Note4 status bar icons without requiring location permissions. + - Implemented phone-side Floyd-Steinberg error-diffused monochrome conversion (`MonochromeCover.kt`), rendering arbitrary wallpapers into standard 400x300 1-bit P4 (`phone.pbm`) bitmaps and streaming them directly over BLE/HTTP to Note4 storage. Added `zectrix_cover_image.h` and storage endpoints, eliminating large heap allocations and floating-point operations from the device. + - Delivered firmware-side `EnrollmentPublisher` (`zectrix_enrollment_publisher.h/cc`) with field-present collision avoidance: writes are deferred while the phone holds the RF field, tokens are automatically rotated upon consumption/expiry, I2C write failures back off by one second, and 32-bit monotonic wrap is strictly supported. + - Verified across 41 Host targets, `test-enrollment-ndef.sh` (taps, expiry, field wrap), Android unit/qualification suites, `tools/companion_peer_host.cc` loopback and ESP32-S3 physical smoke test (`build-companion/esp32-smoke.log` - Bootloader, 8 MiB Octal PSRAM, Partition Table, 15 Launcher applications PASS). See [docs/qualification/C2.1-COMPANION-INTEGRATION.md](docs/qualification/C2.1-COMPANION-INTEGRATION.md). -- [ ] **R2.1: Note4 生产级固件全量发布与用户使用手册 (Production Firmware Release Pipeline & User Handbook)** + +- [ ] **R2.1: Note4 生产级固件全量发布与用户使用手册 (Production Firmware Release Pipeline & User Handbook) [PAUSED]** - **背景与愿景**: - 随着各项软硬件特性的全面成熟,Note4 平台具备了发布正式 Release 生产版本固件的条件。 - **交由 Astra 自由探索与权衡的开放性核心命题 (Open Architectural Questions for Astra to Explore)**: From 65437b8ed74ea8d914f5d54f0b88f342ef5000d5 Mon Sep 17 00:00:00 2001 From: Tinnci <23432137+Tinnci@users.noreply.github.com> Date: Sat, 12 Sep 2026 12:42:30 +0800 Subject: [PATCH 3/3] docs(readme): add bilingual screenshot gallery and status bar showcase --- README.md | 22 +++++++++++++++++++++- README_zh.md | 24 ++++++++++++++++++++++-- docs/screenshots/book_transfer_en.png | Bin 0 -> 2305 bytes docs/screenshots/book_transfer_zh.png | Bin 0 -> 2603 bytes docs/screenshots/calendar_en.png | Bin 0 -> 1987 bytes docs/screenshots/calendar_zh.png | Bin 0 -> 2202 bytes docs/screenshots/home_en.png | Bin 0 -> 2209 bytes docs/screenshots/home_zh.png | Bin 0 -> 2394 bytes docs/screenshots/pocket_tools_en.png | Bin 0 -> 1875 bytes docs/screenshots/pocket_tools_zh.png | Bin 0 -> 2296 bytes docs/screenshots/reader_en.png | Bin 0 -> 2155 bytes docs/screenshots/reader_zh.png | Bin 0 -> 2195 bytes docs/screenshots/settings_en.png | Bin 0 -> 1465 bytes docs/screenshots/settings_zh.png | Bin 0 -> 1777 bytes docs/screenshots/sleep_dashboard_en.png | Bin 0 -> 2517 bytes docs/screenshots/sleep_dashboard_zh.png | Bin 0 -> 2799 bytes docs/screenshots/status_bar_detail.png | Bin 0 -> 647 bytes docs/screenshots/status_bar_icons.png | Bin 0 -> 5390 bytes 18 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 docs/screenshots/book_transfer_en.png create mode 100644 docs/screenshots/book_transfer_zh.png create mode 100644 docs/screenshots/calendar_en.png create mode 100644 docs/screenshots/calendar_zh.png create mode 100644 docs/screenshots/home_en.png create mode 100644 docs/screenshots/home_zh.png create mode 100644 docs/screenshots/pocket_tools_en.png create mode 100644 docs/screenshots/pocket_tools_zh.png create mode 100644 docs/screenshots/reader_en.png create mode 100644 docs/screenshots/reader_zh.png create mode 100644 docs/screenshots/settings_en.png create mode 100644 docs/screenshots/settings_zh.png create mode 100644 docs/screenshots/sleep_dashboard_en.png create mode 100644 docs/screenshots/sleep_dashboard_zh.png create mode 100644 docs/screenshots/status_bar_detail.png create mode 100644 docs/screenshots/status_bar_icons.png diff --git a/README.md b/README.md index 72963f2..b75855e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,27 @@ script can be installed or removed separately. > device. Confirm the model and serial port before you flash. If you flash the > wrong device, you can lose its current firmware. -![Footprint animation preview](main/assets/snow_path_footprints_preview.png) +## UI and Feature Showcase + +The Note4 platform features an algorithmic typography engine, modular scene architecture, and native bilingual UI (English / 简体中文). All interfaces are rendered at 400x300 1-bit monochrome with pixel-aligned typography and responsive e-paper scheduling. + +| Screen / Feature | English UI | 简体中文 UI | +| :--- | :---: | :---: | +| **Home Dashboard**
• Persistent 24px status bar
• Reading overview & fast resume
• Fast application tiles | ![Home EN](docs/screenshots/home_en.png) | ![Home ZH](docs/screenshots/home_zh.png) | +| **Streamed E-Reader**
• TXT & EPUB streaming
• Algorithmic styling (bold/italic)
• CJK line wrapping & pagination | ![Reader EN](docs/screenshots/reader_en.png) | ![Reader ZH](docs/screenshots/reader_zh.png) | +| **Ambient Sleep Cover**
• Daily calendar & reading snapshot
• Microamp deep-sleep standby
• Landscape, dashboard, or blank | ![Sleep EN](docs/screenshots/sleep_dashboard_en.png) | ![Sleep ZH](docs/screenshots/sleep_dashboard_zh.png) | +| **Local Web Transfer**
• Drag-and-drop TXT/EPUB upload
• AP hotspot or saved Wi-Fi station
• Auto-shutdown on complete | ![Transfer EN](docs/screenshots/book_transfer_en.png) | ![Transfer ZH](docs/screenshots/book_transfer_zh.png) | +| **Pocket Tools & Calendar**
• Focus Pomodoro timer
• 1900–2199 Gregorian calendar
• Bounded tally counter | ![Tools EN](docs/screenshots/calendar_en.png) | ![Tools ZH](docs/screenshots/calendar_zh.png) | + +### Status Bar Micro-Icon System (E1.10) + +The 24px status bar employs 1-bit monochrome micro-icon masks with fixed slots: + +![Status Bar Micro-Icons](docs/screenshots/status_bar_icons.png) + +- **Bluetooth**: Nordic rune with 5-state marks (Off `\`, Ready `○`, Connected `●`, Active `⇅`, Warning `!`). +- **Wi-Fi**: Dual-arc fan with 5-state marks. +- **Battery**: 20x10 silhouette with 5 discrete fill cells (0–20%, 21–40%, 41–60%, 61–80%, 81–100%) and priority companion marks (Charging `⚡`, Full `✓`, External Plug `🔌`, Fault/Low `!`, Absent `X`). ## Hardware baseline diff --git a/README_zh.md b/README_zh.md index 763caf4..531cbeb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -61,10 +61,30 @@ 重新编译或烧录固件;长按 OK 返回,长按 DOWN 关机。Full 默认启用,Minimal 可完整裁剪运行时。 > [!IMPORTANT] -> 本项目仅适用于黑白墨水屏版 ZECTRIX NOTE4,不适用于 NOTE4C。烧录本 Demo +> 本项目仅适用于黑白墨水屏版 ZECTRIX NOTE4,不适用于 NOTE4C。烧录本固件 > 会替换连接设备上现有的固件,执行烧录命令前请确认设备型号和准确串口。 -![脚印局刷动画预览](main/assets/snow_path_footprints_preview.png) +## 界面与功能展示 (UI & Feature Showcase) + +Note4 平台具备算法级点阵排版引擎、模块化场景状态机以及原生中英双语界面支持。所有界面均基于 400x300 1-bit 单色墨水屏像素对齐渲染,兼顾低残影局部刷新与高对比度美学。 + +| 场景与功能 | 简体中文界面 | English UI | +| :--- | :---: | :---: | +| **磁贴仪表盘主屏 (Home Dashboard)**
• 常驻 24px 顶部状态栏
• 阅读进度概览卡片与一键续读
• 快速应用导航磁贴 | ![Home ZH](docs/screenshots/home_zh.png) | ![Home EN](docs/screenshots/home_en.png) | +| **流式电子书阅读 (Streamed E-Reader)**
• TXT 与 EPUB 原生轻量解析
• 算法级加粗/斜体/衬线排版
• CJK 字符智能避头尾与分页 | ![Reader ZH](docs/screenshots/reader_zh.png) | ![Reader EN](docs/screenshots/reader_en.png) | +| **桌面待机画报 (Ambient Sleep Cover)**
• 日历日期与最新阅读进度仪表盘
• 灵感格言与微安级掉电休眠
• 支持仪表盘/风景画报/纯白留白 | ![Sleep ZH](docs/screenshots/sleep_dashboard_zh.png) | ![Sleep EN](docs/screenshots/sleep_dashboard_en.png) | +| **局域网 Web 传书 (Local Web Transfer)**
• 浏览器拖拽上传 TXT/EPUB
• 临时便携热点或已有 Wi-Fi 局域网
• 传书结束无线射频自动断电 | ![Transfer ZH](docs/screenshots/book_transfer_zh.png) | ![Transfer EN](docs/screenshots/book_transfer_en.png) | +| **随身极客工具箱 (Pocket Tools)**
• 静音番茄专注钟
• 1900–2199 离线万年历
• 防误触计数器与重置撤销 | ![Tools ZH](docs/screenshots/calendar_zh.png) | ![Tools EN](docs/screenshots/calendar_en.png) | + +### 状态栏微型图标系统 (E1.10) + +顶部 24px 状态栏全面采用去文本化纯点阵图形设计,划定绝对固定槽位,杜绝状态变迁时的邻近元素抖动: + +![状态栏多态微标全景](docs/screenshots/status_bar_icons.png) + +- **蓝牙 (BLE)**:经典卢恩符文(7×13)结合 5 态伴随微标(关闭 `\`、就绪/广播 `○`、已连接 `●`、数据传输 `⇅`、故障 `!`)。 +- **Wi-Fi**:对称双同心扇形信号弧(13×9)结合 5 态伴随微标。 +- **电池与电源**:20×10 点阵外壳,带 1px 防晕染内衬保护,内部采用 5 级阶梯充填(0%、20%、40%、60%、80%、100%),左侧伴随高优先级硬件状态微标(充电 `⚡`、充满 `✓`、外接电源 `🔌`、低电预警/故障 `!`、未装电池 `X`)。 ## 硬件基线 diff --git a/docs/screenshots/book_transfer_en.png b/docs/screenshots/book_transfer_en.png new file mode 100644 index 0000000000000000000000000000000000000000..9820c70da823604e89a4994dfa20944c455eb26f GIT binary patch literal 2305 zcmV+c3I6tpP)(GfH^U;_wt9G6K1KkDz%BqmO*g>fs6wdzozYjzJ>mM!-;cNcet)L$_Zq zuZ<_tAZ4QohzziEevU(ggF7B^6G(mlWWRX>Tr3WPNsrORiBo>H-UP^Slm0avaxll} z;%I|J`wuGaF5zJDlVfzrb%F^FdOV6h!6AkCm=4RMm>^gx9&Anj5eI|lV-OoaUVJTq zhz8mC_viAKj4kPjs3(3c)wg<7 zKJ}&i>z$buLY0=24`(OqwVdCp^k!a+CNt$#kcgSA zI;b!iGJva~VxZcaOqcts32;gSW_L1V7zSH#PXqovJ*h4|im9q6lj%|gRLowfR~gJt z*08|^jHA`{nL%~$v-qpA{!?Dm)Q6cO-xg#*-d~CJYI3car`y>n$@^Elrq{pXjYU$B zDB&&W0m^`!zt+1X=dX=L44A+N=5kX)C0&@(a z=lnzgYI(oXPt-Dga-5WkiWNYWi@NZjsTQl}bn2TT-w?!w4`!~bcii-Eyy{LWCE0sx zF~#idrBBqb6a(qR4f6r>LhyY)ec=hDY$ouZe?T9#8X{?cEL3db-0=!}9Hy-hn z7CD1#ta1WQjQIBTG_NQ^~paLHl7B1Ch zG1+G4&IS8F_>%g!f6AN;c@=GY% zSQ32ik&N=8V&xA6ly2#C-$P#XY?Rm>NBlG&iKM58%Mj|w>?f;RYiSTW{Z!?iv0p+e zMaDwSSF0CFoVfrq)%Fcg^XkgW7w1()U8uIF)A$tm`XgJY2o?2Rl&}0uu|?EExn!A& zP9_B{A{_}kRysRYLJ4I@!j6PtC>=3&)Mml|sLRjc>@H_1MF870A&Z+Vv3=5=yv?)F z1X$uMlpu>)IzcJfUtk+bhjkTzr9qU%v^XidSSy4bksav}5{4o%p%v-uSQ~~XVi!bq zBn&%+lu$wm<5EY$9g&@m73rLm^)m%$Av>rF4X_RL-RR`(QWD@SyA;-LvJET|5!&k~ zCuN%^+t0F176Y7R39^{xHk6{Jld^tCI)p-KO2TX?7U{?WvnCWP@yU$J>B2WfrpUJg zxl0y!jaXkZY2qapX?Mlnus0iPgN>(XL^1sO1_&&<|)Fj#zTJkrH_4n7X{8&40SpuG5*b!yAbak>bQvPuor6fcZo^2zP8WWWaRPHak(2uX1g&GPu%IOMyamOZ>pcP^Poiudw7P;H=A z<4GMw>wf*-Xp&h~CjeLNJJ`fQ5n04z!h{^i zUGl>ek_BnP&AZR*F=4`cfGze?ZUeA?#Cfpy;uN1Y{WM23Duv(!H)pYCM(32Y zluiBJFaBKr;$O9L+!B#FDJA#sm*t~7w?hWg)guP^@S%3}0!Ka2d$KE(d!-}&U`n>5$|uzve9Zhuy< zHIEz!kw`w?cz+}OU7Sh%{obBvsrU9ozcR?^4bnj-I+O75mWTtSfx@14t&eXOX1$it zB5uo%h!o{UtXH`eLJ85O1wtl5(f@$3n254=;8pB4P^thj|_EC ztF{jC9B)7#YM_)pVoFnVYHSc7vQk`3@N)8^!_8v2)c*@h+t!|IA! zscCALZ>MquQ8DV=1=Pg|wR*8Sqco?hGfKOZs8*P{p+4f%rHra$f6F8=WGEfai|Zd|@D*A2*mB0uvm$edUNl zU?4|ZfbJ;7PyB=BCv=R;6@FsWN=<`^*BzyciaqwXQD@Y=swk~m^HfT}jgpF^>DZql bN6LQz=OSbA5NH7@00000NkvXXu0mjfT2xkb literal 0 HcmV?d00001 diff --git a/docs/screenshots/book_transfer_zh.png b/docs/screenshots/book_transfer_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..6d97cc57c72fac3726bbe92a1f57b08685849d2f GIT binary patch literal 2603 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 M07*qoM6N<$f}@9yT>t<8 literal 0 HcmV?d00001 diff --git a/docs/screenshots/pocket_tools_en.png b/docs/screenshots/pocket_tools_en.png new file mode 100644 index 0000000000000000000000000000000000000000..993cbd48422d0db08291e04b7aa077fab9a35e80 GIT binary patch literal 1875 zcmV-Z2dwysP)9`>*YQz(Qb$ta~QJ@(p5JGu3^g{9lFiS=eOP_vX` zDD)b)mmYf!8*FQgDS;O1!ybCr-N;Fx?w?(2mVwAKl72mC#vMD3Mzbs>jrC5F@n_!q zy`SFi_rAX;DPh@WaDfmx2N^vaw!8vH&t0c}dky4Y4`l0+ys%|EB4JxZV(K#4`gltW zqFuI~7k3i7F*_SEh>S?CI2i_^7F{qdD}?Y3ApMO7NHFlS?IOeoVt%?F1Eg4?x{m>g zS!BJTBgBjH8x^RxF(CHb0wmKRksD5D+~Y$Ghzk9Zc>%5)MM&a8mS38H zXpo8jV3A*95s_yyk;&G_pNauJHRu#{6F?n6sRnA$si77vF;PoDmg<#Wl^4FXbYe!g z?)^1tMr$fwt?R{;e0=kL^9|z{-`x0oPZ>%HDDjl{`1%?_gL2EhEv9t)Oe>h2FSV}n5>j} z__Mp?MhYO`zEfk=DS^n((!XWD41fpOznkv`)q*GvhSeVC0wdOW*|2y^S55NEi`2uo zKCrH;WW!40Qr;*1x@mQ2$T!}mZQ<04ZafwSJV#Lk7`TUOYG&A3Co7@YTNS#kl~${y(>;X418j)`<@F=$4@^m zDsz-%*Z1$k#znnkAxuXUoK$D3l?gs1?HbwbT7I)4d0ni^Xi z3otd*bhf5=%|EKGm%_b#11atQC?X@ z_OiApz{%@;rDXyDL-cZpO*2_yV3M;2ODGE`KL2f*7t4gq9^N(z3y;q$3)NXM7;3My z_m^78x%aD8St*MPt1NUsy8o#y9=aRe-$>nEak>j$q4CBf=6%_Oci~X&r*imiF%q@9 z7?3%lLHgK(HEY4Z!sdLOu;ye&fwGv!BgqdU;^k)SKs&Np4pBOnJ1zYawO;~>=2MG> zy%=$a(*uemw@2x{YysjN_;%o8L;&hGal4l+KvYmu+C_B$SdmpztyIT?6;dG;^8b~^ zu78x@gd{ORG}QfQC!F7BA8ww>V(lGHZN=-}7^3--bR3?ok8vhzwYn4pa|6Ol-%7r} z#}AynRKt4e(NydA;KMz>GmN(%|C9AUY@Xd(|C!O4KOT<7gOO}blg684#-caVjL{D} z&(D}QK&$~?+UzAK)wBUOBaef1>76^ty?h66M$gUu;6;A%nbyYgX8u~uF53FM`q;^L z9@F$UJaz7_&#R9CZB)`uV$_-ya^Qo~wApb2T3W50dgI9e6#z9}TdAo48me;~)XWCj zs!>yS8zwZ)%7lV;u7arEy5RzU0a5wo6+(7)WmlY?#7F1xy&nbnHw}^zCG?|Um-#nW z4YhB<*`5B3bsC936>>WT_lYvg|W?<;T^{PW6O5T(E?u=wl7<2B z-yC6}e0|21F}cR6!&N~^7biZ+5lXrM!}(rXAiV?(Fk;#$Uuip9YiXd>jg_yo=ziqQ zAvJiSRvN1YHMP20gBqUdG)w?ftx&^zqPiV-(b^aFl0N*ZTKiuj@*mEy6Nlp)>?{BP N002ovPDHLkV1h)-hams} literal 0 HcmV?d00001 diff --git a/docs/screenshots/pocket_tools_zh.png b/docs/screenshots/pocket_tools_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..5837c46d22dcaa4090776696aaf38ea3987f4d57 GIT binary patch literal 2296 zcmVB*l|9LL4~oAJi@4*f#V)QzdxcgtoXB zFdz;H5Juvj95_Uc;~l#xL_$D8R8-(*$B7|EN#ohn?#_B=--pAjO%mJd8Kgp+?t7Y% z{oUv1`@GMOca)HKS$u>bvIS)2QoZbV!RXtb+m+FAkn88=LQS?^rYaJSibzaRS{L5F zAQsUaGw$|8VlSpT8H30Q$%t#P2({=geAyv{`+@e4G(dudZ)UndtRUu_6EQ#=J5=Ye zASrZ{-qI1`N9l_S)G;iGeXk3|=#a<@I~w=-3l>CO?PB}@&kNF*#N$;g2;**$!~>H) z768#8t;+eG>=KKJG^8c13va(87MxbIV_nDuPy|q_A2mDHhuUe2iOTJk>Ym<}ALM1D zb0S7;1r(DU8e2K>a#EJIlY`199oxpY?Hp8Oq7XwN&H=H{v>nL7m_;&Fh;<$+Ff?fI z$|;Nb9xA6Sa_fQja?0WiEDYy#{0A1+*vf zP!Y>kwtX}Y6{|nD!>E-FwPl4o1^5M>2l8vc&u4wxk_8(pqxguVr0k55Mnp+3D828H zd+gkojnEpK1ZWWf+eZQmAoOqRIf&ATa(4>jK2iGx54)u?O7o{9wPRf&b57F+c{~(d z(7@_M%}?v+ZT-J3m8#Slf0uT<5mZ{z7=K1;CkM7IwL5VP95AZ>Q{b%n4iuhnyD(O; zfRLJ>s2^(*{x$h?A6mm*AT9F_5mJ#y8hC>iCs|_NJ9H4fxPT~{P<@yF+o9{ zs2#VQE|8}C?sTVjwD%i8bMlZzp6(_~DfAI-5`YJY+QnmlIu&*^WfTC36^X{v6yS9Y zW8F--76LT)jcv544(X7oZjfC8C`t=~AR3S61R#^$Aj+>lmUe086+kh5T?bO~x;Tui zyGCPKS|ZYr>Woy{%iB3jFX!(;KA4q7MRF|DvkO2dt^CD^((2pF%WO*OgL^M)jO3tF zlCbJSoC6}~&9xw>Ed8s0<5WJ#%0R`du-nf8`Lg^rghr0?O)Z-*=9Hy5_ys>Wm04pp zP^irxDPVGmKX0pWrxC#>@gEhPPs!8oUeK5@59G*wZ;cveg6!g>F6w@D!cm#)mwXXs zy?Xs9mkD{p_`Q~0Z1($j2S~!~I<30W85t`ZB6*=*@k>>Gt9`3U)7pWGj|P#-=oA5M z(-ti=8N`oFe0ao_lhjYvkNZ0@7JD0Q2(j>Dc8DAnL~i{>LI^)R)b{#Uz%G7Aob$$SUDu+Purmacnl zy&x=SnJM%tiyov`S==j-gz=mZLlU+3_&H38!iv;ui}_@+%3dA^kczaT`=^ITvu1ti z^!#IAE4(?Bhq3xgBW@1ow19yV^Y_(bFCM~dzD3=nMDLa9A@7y zoU+9Ysb*))YLdm(%cXEUy+aeWA7V*-44mFhmK$elC#|sVLEQs_JqdQ? zp=hh;{H^ok8_yQ2zbGs(8(Cf!c~7pNH|^@p>#sIs+1%W^4S~xvQ%* zTQ~A8SRH$3d^e@bPzqtUwfY!aelc+yWF)%!v$abkDV0@o{afju{QBUCzuh5zG_eo< zV1G(QrPFZrT;P?K!)?yWN8^j`Z*Eo&0Ic*YUEPiGB7mi`j~{I1Fo8cXe2@SJhX7w4 zBymGhL0##AtHh7eueKN$@ zM$&9KzB;O7H%2qNg)E1y@=0*iF9Q-QQEved?L8MnPDI@sxjVqh_#%KSLybqh+h$Lm z(N0LX=lh#Ac6D7#zdzTw2t>p(c$6z@^c5e1ViImXEYdU50d719xWsCCnvv>-X9DoC zwz12=K^x>JlYqk;DPuR%n)B`vh6Ti2Gytag-DB9s?wG+Hj&Nfa(O2$*ltE4>8}zKi z>in!j5hrrE>5tw8>4U3kxc?E0#H~-DhE-=|waBMXV>XGm`2yAYIwRGz<5jEHs$!2`jM8=8t$#K9v z6dOY6EfGEG;DQUKxGlCusZ*;b3uA&)NOx4LsW7#*yOy)FtC{)rFte+brCmvu);7^T zgmz~4edqV*z2E!&-tYHD30rLjy9grdK;|n9njJ8@a7lM zi0(ioupx>sQy`*2rtaO9Lg#VuMMSEyEX#il?~1{6N;-`TE@T0e+K0MqWl$TLn5ZpJ zOZAPm%3a=4Tk5?(C_Ov1a_hAGZ`b;G{s;zzEl|m63=Dp?g#q=j(xll1JjUXmUW57Ht0L*W!F{^JJ3|O)mAZ@eMZ`f(OBV2z?c_=5B zSHIp2WZFcu3DjPJk-N^$YAl_6j*Jb&vn~6D%9V3^Q&g@3l7=_$mn7=iP}-IAL=Rwr zn$nw*1=xQaKyDVE`)}q(SLc`sC}&F$VOeQj|IqEuP&QiztnB-WKM;j=!A-DBBPwLX~YoYr7hgig+NEMH}R#=3fBS$$2}ROxTX#!};v(je<)K5Bi=X6M+#%)(5_ z!c2Srl5GdgE_&-yvFkPMvy#MVS>O6O*lM2J8A`QZhWh7+`hoHvK0Go1GL$d&T}(A3 z-@E6nb8p%YMSaCj+If`4os0TV9^H!h)0=th>Wp8{zw{uF1gSLrh-eUJ36eCxnyj)x z&5V)&87S!03BH=WHmke=W5&dKkjz*F)Y-SHZ*mvD+K$RJFe4^Vwa?V-pTT_9K3#J# z%?r!!1#vH&bpO|g7&c(c_dkzDKlev(of>_|_gAd)vnj{#7(W-VeW?^=!FHi&esuY!vO``-QOuJ?5VpyQNdZo1$WxF^SWvG%RPE zl@VK!;al=OH2^71Mh#P&`L!lBK1%K22epGA`2Lv_!&(h*eSs}mTeeDn%Z9vsYlr+x zxcW8~^!6`3$diEt0PfQ$i~iI+@Tk8P$f3i5dHYhAi(IlCC|{;z^UVMi zJ)SwXsJ9BK`}WF)x=W0y@%_y}saG24K^_CdhuIycvSwQPCcgP}YHWN@%VC50_E~E; zIY&_<8%j}RS|l%0liNF57|JDnELDvY1wT#Jseul%95e1)(}gsk^k@>DnQ-;KwUZi? zlc#PLKW-+Qey<_CbIq}%n;od6lKP&?yK!kX!|U!k2LQG;lxN_Tc-7nsI1WiRumhx^ z`M#ELkIi>L-T}f=SLRrDf>hPx29R<#hFlCa6V&$6&jW}fo6=6#U)+Maf7?BklBz_7 zyhti9jHn>bFK=&Hf2m4Yu1VLbqRsX(-|e>J8&^uH&XvTTd+iRyYdwLgSr!^wHOI$6~)Q=Pnb*jqdX5Z+n-$;p!q z50m;+gUT@UlVQMXo&`II#qmbYFQoyVk!Dizkr>+plpTOtW40=n=i{-mQ0GG%P_IE4 zCXoF$RNXi+w(_W#9Ei6zH&Q`)NOK9oTaEmf9XUy*Wo@lxea2G9*86_t+m|%CqUI>@ zTmqS0Nq(MhCP7QHT!qN3Ae#AujLOZXzj-A8;^n=Zm%Dy0$a{G&Cy`Ye%m>QL|4l>% z+3V+&pOd200+C=}oBIBGtti)Ia2x(*lT@PoN-)=@`b*4xbCcWiFFi;P(u4FMJ;<(t z&^raat07aLawZO=UF~Xsxu7Ig`!bFJ-%gI@FL-z$Ai5+A?OzBpKGtmf#IoGH>S+w1_TD`Q*LDNrcXuxTaL{QJ^@+X`)B#LPIZ|;!v`_fLKR@ZKw4rjF zzgyGE>1uz-ORDNaI_Sf013AD5r(=rg@{in>GZgFRFp_a8#Meg7Ywf!JN#z70*`$_U zbinbPbR4d>VQdf}4+IKj7HzOy+X#v}U5eT$IYa#B3;hbEuJk!5ZK?j0p+?o{mIC;x hE~lY-i_%j7`9Gk#E-u4cm{kA(002ovPDHLkV1mK;GsFM@ literal 0 HcmV?d00001 diff --git a/docs/screenshots/reader_zh.png b/docs/screenshots/reader_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..78ddc375c05b2d57c408b55a540faaed5592663d GIT binary patch literal 2195 zcmV;E2yFL>P)yrIo4%;&ex$s_3aXBK6|-RP{noB`^wPe-5Dv zhbn1N)f@HE2P&mfRnkAfvn43z6bnTt9MT;(hFS>XUB_W(y*u-InAty$ch`38B@x-5 zEbq?leE0qNz4v?X_ug-mu-##B4?&~@WO0R2s|QB6y$|!b;~-bBN6QuII-*$-KPMsq z8L6GWdR`0`%K2VI0w-YhOaLOQ)E~7Y3|tI??vRuZ{u4m><`GCy$3Q3rz4YS+yl^T2 z2ubHL3kG{6Y*0ru3Q@JjehjwHI~XPNOz+F;Uw# zOZBz(%6oj6+q!#Sy>$O<>(Xue-)#)>&|wVn+n|!w7#O^5VV!+!Lb?{N6t zap%*Adz-Z$d-hxkNT-c-It`@Lq(L3Z!5m+5)PN!}u5O?rJQRypB(f#MYMiSY^vuy(KCa07=0!H%b!qZ7Q9WGekFG ziK^0Dkp1t@o05TT|tPrdH-XDXW`4R+?$`K?o_#`}V^_AaMA ziQm-oz-Zt?---Sb|I{s_Ebm^{hw|xH&Y#-K&#tfd4dTlHa$As;;uqc2G~$l zHmF)rk{|*&QUK<}0xHhQs`Cq2tT-pC zE|&Fn&3jwCw~u+BH!_Az7z=`rqKS`!i7Uq^UJQbkSH3^%x_`xy6`(fv2bueU#;(o% zF+Ytp{HmJIhVxnV(k07kK@R4cxjShLm;lmWfs`y}uvblJ6U6g;u5b z?^f|!vB8NqRwsb_9Agqa8R(aXr`lz?rDc!_VvXh}OE7N}Q+dg5Ot(R#>&&{VEOp!& z20B1b26{5k>#6i8JZT!T)U9?|qP7GlYFMLapCHIjF6qhO_~aFB9@r8U%Zf>yP^ANM zs@WNF6luOC-%}%CYSQWe)w$nl%&7^g`(LZ>e=P`3zI;Hd1KVFNupP+3Lv`!gxjr|!q*hmf%*xh> z0V?`xY;0L?7qS*Otqpab7*kXGnu*e&H86mD1&{#dcb&=xvkWW(>+aOp)SkA(28-RR z)?RXsqC_T?qDVm`CsLJbyV@AaCUMNH#D#*NCF|5k4_S^FH|#<_1t>j{WM>v!xMA<6 z#^l)X%f+{v#il=N2tT{%I??4GRLrEmr*dB0T21qm*Wm!bj;8V?JRYx_4*;%9QVr|@ z$!or^CEVli9gr7+u+$eD%S>Zdd_DwBcXPn~II}73cK^l8s~gw6ECmEY%KC%~D#o=Kt7OMP6y-hPT-ijv0$Iu(&Rs9Bc%iq6ZNEDxp$#pzM6& zK-np~gz|zj7TOCgU>veOaE+=B7@^*g#ws&VnF(hWoytr_!(3(N`XPV$2tfE*?Wf0% zH6o1pw+591Fie^ezj+pHKQ>1jH9u1W+$YVVt`>WuIK!mpOwfijppjg&%Gz2 z>X}QYRgjgURO=%3ys1%qyngU^y;8WW4;lThOL`T|0MYOK z1o&asE{_&VT9+j}KzMJ6u8dtn1-Bd?hKQ%$>8|8Q*B|3aZ8pX0zA`Sx4{LOMw5n4E zTYV#HJ+u;h@XG9ux=|TzY+Xw^9Y1a{4LR*X*iHO2fPI8;!c@P(h155|#E}!14s}o9 zL#Y*;_K=TaJGm*#SG$*?Oyl#mN?^`MDTLEBP#G#*Hs`?^T25ee`?3 z-}^Ja@4PokC|ew!BSgM~6}|Wmy9JOC`XO+kJV3(rhzShcLjxk`0NHxPjUoOCz%@YcMK|oC9#g zS7Bk~aVO3%KP!w}Gy~wYVm>PA%U~Q7_obd+oL<$%-ERRTgP;DQxcj{XFppY)wlkqf zQ8tIMh((@D#QnBhXmvtwgKUv!2XP5I8$H*n#4D6sC4obC&Pu3Sptkl`%oBgHVXfFp zsJUiMlad8rd;{J}h3UlSKkP+QM7^J2xUyQ5s@Ll3-PRKkTahZOGiCVqtYOL+WoVEd ztZ+Zb72*_0Qt{cwTerGIGemq6k7MJm20Y>!;-s0`+;8rzwju*(7UW7yCPxG0bZ3S8 z3BKfsyA)2995=6DVpz)5)#6t2@gkzcF5?z?>JY0eEMgI>EQ&vp+ESuqwqGpr^dix2 z=OUEtw4WwjKONr;zP)b$;%abd<@a(VM~FrS$XoG}sdDRHJVlPc_?b@}lDVUFi4o#( z!I#9tC;IW=?+z?OqF{!|;T-H-xP>F8nv-N)eGo3GJpNfYwc-J&PjZnoy!BSJOpo23 zN}h?#@xwTJO2u5CtUZh|L?uK50c-q z|LI-s|MhqG7u?^xGZ>A=ygfKa+U-#83`YEAkjNWj(PKuVF(diaXi7)vY8OF8QNP|qr9eYVLP`T5j^+>*sd}|q z!J|fnjZUwXR8e)HLZw2D3ZSaXU#BNYkY#SEVd=$rqe>tu${S_h^IOCs7Wtl#XsF}; z{=22DNXQ$Tf7E4=Bsf)zkHfV{?4$I$SC8Lx|MTw>v0+I>_Sr0J#VgyeATgpB^O1Fk zf9428C6Hqk@z3E6ZMU8+f;|Ook(EgISdPW-rfqb-F6}4LMRRhyjmbDRaeq#9Iq(Xv z$Y5?!8WBxSeND=kcvCMlOtsan3jw)Ela6TP&m%n@%|A+VU*2>YI}mgm=?S9RB;KL- z%iBX>$bsml=As!SbBb?wy2mA_jDQThug=d$qCxaI2y{O_okQ-;OlB|a^d^F^LwKBo z>?O^8$QT}EUVMrCD4neSn;P~e-;&`$U$Z-@w0Sf1qHpxem22)Ks^EYT1C8kNJF8~2 z;J)D`1K5CTf|3rt0k$V`SGuPG6V8wZ$X*H#IEE&wt+bb5sQMB3y&al5Bsw<145`9B zRj+M1RMpm26{>`)rRuz?G$>u8LNM&~wq|{`e4}l5$?q$MWwZQ9uG&TE`$N74YQ_@J T=4i1x00000NkvXXu0mjfnheuf literal 0 HcmV?d00001 diff --git a/docs/screenshots/settings_zh.png b/docs/screenshots/settings_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..3991c8d310d517a2819476be44095c746db5c148 GIT binary patch literal 1777 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/sleep_dashboard_zh.png b/docs/screenshots/sleep_dashboard_zh.png new file mode 100644 index 0000000000000000000000000000000000000000..02a2edcedd0d4a067d7c1fd4641c97c9876f7ba8 GIT binary patch literal 2799 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 BS_%LF literal 0 HcmV?d00001 diff --git a/docs/screenshots/status_bar_detail.png b/docs/screenshots/status_bar_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..dda21a8f82f69fa64a06614cbd00710d07a32a18 GIT binary patch literal 647 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 literal 0 HcmV?d00001