diff --git a/.gitignore b/.gitignore index 0e4fe5f..c22b212 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,4 @@ -################################################################################ -# This .gitignore file was automatically created by Microsoft(R) Visual Studio. -################################################################################ - -/.vs +.vs /exe/.vs/ectest_app /kmdf/.vs/ectest_kmdf /exe/ARM64 @@ -10,3 +6,5 @@ /lib/eclib/ARM64 /kmdf/ARM64 /rust/target +/rust/mock-bin-src/target +/rust/mock-bin diff --git a/lib/eclib.c b/lib/eclib.c index 65783bb..388a422 100644 --- a/lib/eclib.c +++ b/lib/eclib.c @@ -279,7 +279,7 @@ VOID CleanupNotification() // Cancel any pending IO if(g_notify.handle) { while(g_notify.in_progress) { - CancelIo(g_notify.handle); + CancelIoEx(g_notify.handle, NULL); SleepConditionVariableCS(&g_notify.cv, &g_notify.lock, INFINITE); } CloseHandle(g_notify.handle); @@ -301,7 +301,10 @@ VOID CleanupNotification() * UINT32 event - The event code to wait for (0 for any event). * * Returns: - * UINT32 - The event code received, or 0 if none. + * UINT32 - The event code received, or 0 if none (or error). + * + * Errors: + * ERROR_INVALID_HANDLE - Notifications are not initialized thus the handle is invalid. */ ECLIB_API UINT32 WaitForNotification(UINT32 event) @@ -310,14 +313,18 @@ UINT32 WaitForNotification(UINT32 event) UINT32 ievent = 0; NotificationRsp_t notify_response = {0}; NotificationReq_t notify_request = {0}; - - // Make sure Initialization has been done - if(g_notify.handle == INVALID_HANDLE_VALUE) { - return 0; - } + BOOL aborted = FALSE; // Loop until we get event we are looking for for(;;) { + // Make sure Initialization has been done + // This is checked every iteration because CleanupNotification may have been called which + // destroys the critical section, and attempting to enter it below is undefined behavior + if(g_notify.handle == INVALID_HANDLE_VALUE) { + SetLastError(ERROR_INVALID_HANDLE); + return 0; + } + // There could be many calls into this function, only first call calls into KMDF driver // Subsequent calls just wait for the event to be set by the KMDF driver EnterCriticalSection(&g_notify.lock); @@ -339,19 +346,33 @@ UINT32 WaitForNotification(UINT32 event) ) == TRUE ) { g_notify.event = notify_response.lastevent; + // Tricky race condition where Cleanup cancels the IO call and we beat it back to the top + // where we set in_progress to true again, then Cleanup calls cancel again but we haven't + // entered IoControl call yet, but then we get there and Cleanup is sleeping but now we + // are stuck in IoControl and can't return to Wake it again resulting in deadlock. + // So we explicitly check if we returned due to being cancelled and bail out of the loop to prevent this. + } else if(GetLastError() == ERROR_OPERATION_ABORTED) { + aborted = TRUE; } else { g_notify.event = 0; } + // Enter critical section here to ensure wake is caught by Cleanup + EnterCriticalSection(&g_notify.lock); g_notify.in_progress = FALSE; WakeAllConditionVariable(&g_notify.cv); } else { // Wait for notification to be set - LeaveCriticalSection(&g_notify.lock); - SleepConditionVariableCS(&g_notify.cv, &g_notify.lock, INFINITE); + // Loop for spurious wakeups + while(g_notify.in_progress) { + SleepConditionVariableCS(&g_notify.cv, &g_notify.lock, INFINITE); + } } - if(event == 0 || g_notify.event == event) { + // Regardless of branch we are always in a critical section at this point so leave it + LeaveCriticalSection(&g_notify.lock); + + if(aborted || event == 0 || g_notify.event == event) { ievent = g_notify.event; break; } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 171837a..b686b74 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -8,7 +8,7 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "gimli 0.31.1", ] [[package]] @@ -32,6 +32,94 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alterable_logger" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1f292171444480aedbc1b7d1a11192d1b6e48f26154cf4635481c58b9b419f" +dependencies = [ + "arc-swap", + "log", + "once_cell", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "autocfg" version = "1.5.0" @@ -48,7 +136,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -59,6 +147,18 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cassowary" version = "0.3.0" @@ -74,12 +174,92 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbor-edn" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b43829b1f353168aa8593c2e4ef6e71a81d6749fe09edfc33849f890c69278" +dependencies = [ + "chrono", + "data-encoding", + "data-encoding-macro", + "encoding_rs", + "hex", + "hexfloat2", + "num-bigint", + "num-traits", + "peg", +] + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "color-eyre" version = "0.6.5" @@ -107,6 +287,22 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "compact_str" version = "0.8.1" @@ -121,6 +317,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.28.1" @@ -181,15 +383,104 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "defmt-decoder" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b563d700380314b45fa0ddfbf2ea998a80c64b7d88ab42b0b1fba774c4da66" +dependencies = [ + "alterable_logger", + "anyhow", + "byteorder", + "cbor-edn", + "colored", + "defmt-json-schema", + "defmt-parser", + "dissimilar", + "gimli 0.29.0", + "log", + "nom", + "object 0.35.0", + "regex", + "ryu", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "defmt-json-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b04d228e57a61cf385d86bc8980bb41b47c6fc0eace90592668df97b2dad6a" +dependencies = [ + "log", + "serde", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dissimilar" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" + [[package]] name = "ec_demo" version = "0.1.0" dependencies = [ + "clap", "color-eyre", "crossterm", + "defmt-decoder", "env_logger", "log", "ratatui", + "rzcobs", + "self_cell", "strum 0.27.2", "tui-input", "uuid", @@ -201,6 +492,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -240,6 +540,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fnv" version = "1.0.7" @@ -252,6 +558,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + [[package]] name = "gimli" version = "0.31.1" @@ -281,12 +597,48 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexfloat2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befe65164a090041cdf6e0d21a0ec3198d856fbfe2b76e324a073e790bb49f8c" + [[package]] name = "humantime" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -329,6 +681,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -344,6 +702,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -377,6 +745,9 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "serde", +] [[package]] name = "lru" @@ -393,6 +764,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -414,6 +791,59 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.36.7" @@ -429,6 +859,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "owo-colors" version = "4.2.2" @@ -464,12 +900,45 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -578,12 +1047,56 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "rzcobs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af5759e4bc4858ce928073d0ebe29c82d18c04a0023bf2f312500a412f23939" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -593,6 +1106,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" @@ -629,6 +1148,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -704,6 +1229,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -713,6 +1258,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -799,6 +1375,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" @@ -817,6 +1399,64 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -848,12 +1488,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a5ae733..9f199ec 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -18,12 +18,16 @@ log = "0.4" env_logger = "0.10" tui-input = "0.14.0" uuid = { version = "1.17.0", default-features = false } +defmt-decoder = "1.0.0" +rzcobs = { version = "0.1.2", optional = true } +clap = { version = "4.5.46", features = ["derive"] } +self_cell = "1.2.0" [features] -mock = [] +mock = ["dep:rzcobs"] [lints.clippy] -suspicious = "forbid" -correctness = "forbid" -perf = "forbid" -style = "forbid" +suspicious = "deny" +correctness = "deny" +perf = "deny" +style = "deny" diff --git a/rust/mock-bin-src/.cargo/config.toml b/rust/mock-bin-src/.cargo/config.toml new file mode 100644 index 0000000..be465b3 --- /dev/null +++ b/rust/mock-bin-src/.cargo/config.toml @@ -0,0 +1,10 @@ +[build] +target = "thumbv8m.main-none-eabihf" +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "link-arg=--nmagic", +] + +[env] +DEFMT_LOG = "trace" \ No newline at end of file diff --git a/rust/mock-bin-src/Cargo.lock b/rust/mock-bin-src/Cargo.lock new file mode 100644 index 0000000..9f0987f --- /dev/null +++ b/rust/mock-bin-src/Cargo.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cortex-m" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" +dependencies = [ + "bare-metal", + "bitfield", + "critical-section", + "embedded-hal", + "volatile-register", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d4dec46b34c299ccf6b036717ae0fce602faa4f4fe816d9013b9a7c9f5ba6" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "mock-bin" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] diff --git a/rust/mock-bin-src/Cargo.toml b/rust/mock-bin-src/Cargo.toml new file mode 100644 index 0000000..b640eeb --- /dev/null +++ b/rust/mock-bin-src/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mock-bin" +version = "0.1.0" +edition = "2024" + +[dependencies] +cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] } +cortex-m-rt = "0.7.5" +defmt = "1.0.1" diff --git a/rust/mock-bin-src/README.md b/rust/mock-bin-src/README.md new file mode 100644 index 0000000..714e468 --- /dev/null +++ b/rust/mock-bin-src/README.md @@ -0,0 +1,8 @@ +# mock-bin +A minimal defmt mock-bin for TUI testing + +## Build +`cargo build --release` + +## Use +Copy `target/release/thumbv8m.main-none-eabihf/mock-bin` to the root of `/rust` \ No newline at end of file diff --git a/rust/mock-bin-src/memory.x b/rust/mock-bin-src/memory.x new file mode 100644 index 0000000..a9fb89e --- /dev/null +++ b/rust/mock-bin-src/memory.x @@ -0,0 +1,5 @@ +MEMORY +{ + FLASH : ORIGIN = 0x08000000, LENGTH = 512K + RAM : ORIGIN = 0x20000000, LENGTH = 128K +} \ No newline at end of file diff --git a/rust/mock-bin-src/src/main.rs b/rust/mock-bin-src/src/main.rs new file mode 100644 index 0000000..0f10956 --- /dev/null +++ b/rust/mock-bin-src/src/main.rs @@ -0,0 +1,35 @@ +//! Simple mock binary that when built produces an ELF with a `.defmt` section containing the log strings below +//! If this changes, mock.rs will likely need changing since it depends on an specific version of the ELF +#![no_std] +#![no_main] + +// Need this to satisfy defmt but don't actually care about logging anywhere +#[defmt::global_logger] +struct MockLogger; + +unsafe impl defmt::Logger for MockLogger { + fn acquire() {} + unsafe fn write(_bytes: &[u8]) {} + unsafe fn release() {} + unsafe fn flush() {} +} + +defmt::timestamp!("{=u64:us}", 0); + +#[cortex_m_rt::entry] +fn main() -> ! { + defmt::trace!("This is a trace defmt log"); + defmt::debug!("This is a debug defmt log"); + defmt::info!( + "This is a really long log message. Really really really long. Its length should be measured in light-years. Not characters. It will wrap around on all monitors not of cosmic scale. Who needs to log something this long anyway? Who knows. But someone will. Therefore we must be prepared." + ); + defmt::info!("This is a log message with a newline.\nSee? I'm on a newline now!"); + defmt::warn!("This is a warn defmt log"); + defmt::error!("This is a error defmt log"); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/rust/src/acpi.rs b/rust/src/acpi.rs index 037316b..a51cc8f 100644 --- a/rust/src/acpi.rs +++ b/rust/src/acpi.rs @@ -372,4 +372,15 @@ impl Source for Acpi { let _ = Acpi::evaluate("\\_SB.ECT0.TBTP", Some(&[AcpiMethodArgument::Int(trippoint)]))?; Ok(()) } + + fn get_dbg(&self) -> Result> { + let data = Acpi::evaluate("\\_SB.ECT0.DMSG", None)?; + + // Should be expecting a single output argument: A complete defmt frame in raw bytes + if data.count != 1 { + Err(eyre!("DMSG unrecognized output")) + } else { + Ok(data.arguments[0].data.clone()) + } + } } diff --git a/rust/src/app.rs b/rust/src/app.rs index bf52b14..f7d595f 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -1,3 +1,5 @@ +use crate::debug::Debug; +use crate::notifications::Notifications; use crate::rtc::Rtc; use crate::thermal::Thermal; use crate::ucsi::Ucsi; @@ -5,10 +7,11 @@ use crate::{Source, battery::Battery}; use color_eyre::Result; +use clap::Parser; use ratatui::{ DefaultTerminal, buffer::Buffer, - crossterm::event::{self, Event, KeyCode, KeyEventKind}, + crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, style::{Color, Stylize, palette::tailwind}, symbols, @@ -16,6 +19,7 @@ use ratatui::{ widgets::{Block, Padding, Tabs, Widget}, }; +use std::borrow::Cow; use std::marker::PhantomData; use std::{ cell::RefCell, @@ -29,7 +33,7 @@ use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; /// Internal trait to be implemented by modules (or Tabs). pub(crate) trait Module { /// The module's title. - fn title(&self) -> &'static str; + fn title(&self) -> Cow<'static, str>; /// Update the module. fn update(&mut self); @@ -38,7 +42,7 @@ pub(crate) trait Module { fn handle_event(&mut self, evt: &Event); /// Render the module. - fn render(&self, area: Rect, buf: &mut Buffer); + fn render(&mut self, area: Rect, buf: &mut Buffer); } #[derive(Default, Clone, Copy, PartialEq, Eq)] @@ -51,6 +55,8 @@ enum AppState { #[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, PartialEq, Eq, PartialOrd, Ord, Hash)] enum SelectedTab { #[default] + #[strum(to_string = "Debug")] + TabDebug, #[strum(to_string = "Battery")] TabBattery, #[strum(to_string = "Thermal")] @@ -61,6 +67,20 @@ enum SelectedTab { TabUCSI, } +#[derive(Parser)] +#[command( + name = "ODP End-to-End Demo", + version, + about = "TUI application demonstrating Host-to-EC communication using Rust-based ODP components" +)] +pub struct AppArgs { + #[arg( + long, + help = "Path to ELF file containing debug symbols and defmt strings currently running on EC" + )] + bin: Option, +} + /// The main application which holds the state and logic of the application. pub struct App { state: AppState, @@ -71,12 +91,13 @@ pub struct App { impl App { /// Construct a new instance of [`App`]. - pub fn new(source: S) -> Self { + pub fn new(source: S, args: AppArgs, notifications: &Notifications) -> Self { let mut modules: BTreeMap> = BTreeMap::new(); let source = Rc::new(RefCell::new(source)); let thermal_source = Rc::clone(&source); let battery_source = Rc::clone(&source); + let debug_source = Rc::clone(&source); modules.insert( SelectedTab::TabThermal, @@ -88,6 +109,14 @@ impl App { SelectedTab::TabBattery, Box::new(Battery::new(battery_source.borrow().clone())), ); + modules.insert( + SelectedTab::TabDebug, + Box::new(Debug::new( + debug_source.borrow().clone(), + args.bin.as_ref().map(std::path::PathBuf::from), + notifications, + )), + ); Self { state: Default::default(), @@ -103,7 +132,9 @@ impl App { let mut last_tick = Instant::now(); while self.state == AppState::Running { - terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; + terminal.draw(|frame| { + frame.render_widget(&mut self, frame.area()); + })?; // Adjust timeout to account for delay from handling input let timeout = tick_rate.saturating_sub(last_tick.elapsed()); @@ -127,9 +158,10 @@ impl App { if let Event::Key(key) = evt { if key.kind == KeyEventKind::Press { match key.code { - KeyCode::Char('l') | KeyCode::Right => self.next_tab(), - KeyCode::Char('h') | KeyCode::Left => self.previous_tab(), - KeyCode::Char('q') | KeyCode::Esc => self.quit(), + // Check for shift modifier so tabs can still make use of Shift+Arrow Key + KeyCode::Right if !key.modifiers.contains(KeyModifiers::SHIFT) => self.next_tab(), + KeyCode::Left if !key.modifiers.contains(KeyModifiers::SHIFT) => self.previous_tab(), + KeyCode::Esc => self.quit(), // Let the current tab handle event in this case _ => self.handle_tab_event(&evt), @@ -176,8 +208,8 @@ impl App { .render(area, buf); } - fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) { - let module = self.modules.get(&self.selected_tab).expect("Tab must exist"); + fn render_selected_tab(&mut self, area: Rect, buf: &mut Buffer) { + let module = self.modules.get_mut(&self.selected_tab).expect("Tab must exist"); let block = self.selected_tab.block().title(module.title()); let inner = block.inner(area); @@ -186,7 +218,7 @@ impl App { } } -impl Widget for &App { +impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { use Constraint::{Length, Min}; let vertical = Layout::vertical([Length(1), Min(0), Length(1)]); @@ -229,7 +261,7 @@ fn render_title(area: Rect, buf: &mut Buffer) { } fn render_footer(area: Rect, buf: &mut Buffer) { - Line::raw("◄ ► to change tab | Press q to quit") + Line::raw("◄ ► to change tab | Press ESC to quit") .centered() .render(area, buf); } @@ -253,6 +285,7 @@ impl SelectedTab { const fn palette(self) -> tailwind::Palette { match self { + Self::TabDebug => tailwind::GRAY, Self::TabBattery => tailwind::BLUE, Self::TabThermal => tailwind::EMERALD, Self::TabRTC => tailwind::INDIGO, diff --git a/rust/src/battery.rs b/rust/src/battery.rs index ede1778..8496913 100644 --- a/rust/src/battery.rs +++ b/rust/src/battery.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::Source; use crate::app::Module; use crate::common; @@ -198,7 +200,6 @@ impl Default for BatteryState { } } -#[derive(Default)] pub struct Battery { bst_data: BstData, bix_data: BixData, @@ -209,8 +210,8 @@ pub struct Battery { } impl Module for Battery { - fn title(&self) -> &'static str { - "Battery Information" + fn title(&self) -> Cow<'static, str> { + "Battery Information".into() } fn update(&mut self) { @@ -234,7 +235,7 @@ impl Module for Battery { } } - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&mut self, area: Rect, buf: &mut Buffer) { let [info_area, charge_area] = common::area_split(area, Direction::Horizontal, 80, 20); self.render_info(info_area, buf); self.render_battery(charge_area, buf); @@ -443,7 +444,7 @@ impl Battery { vec![Line::raw(format!( "Current: {} {}", self.state.btp, - self.bix_data.power_unit.as_capacity_str() + self.bix_data.power_unit.as_capacity_str(), ))] } diff --git a/rust/src/common.rs b/rust/src/common.rs index 52bd26c..9aa58a6 100644 --- a/rust/src/common.rs +++ b/rust/src/common.rs @@ -13,7 +13,7 @@ pub struct SampleBuf { samples: VecDeque, } -impl + Copy, const N: usize> SampleBuf { +impl SampleBuf { // Insert a sample into the buffer and evict the oldest if full pub fn insert(&mut self, sample: T) { self.samples.push_back(sample); @@ -22,7 +22,17 @@ impl + Copy, const N: usize> SampleBuf { } } - // Converts the buffer into a format that ratatui can use + pub fn len(&self) -> usize { + self.samples.len() + } + + pub fn is_empty(&self) -> bool { + self.samples.is_empty() + } +} + +impl + Copy, const N: usize> SampleBuf { + // Converts the buffer into a format that ratatui can use for charts // Probably more efficent way than copying but buffer is small and only called once a second pub fn get(&self) -> Vec<(f64, f64)> { self.samples @@ -33,6 +43,13 @@ impl + Copy, const N: usize> SampleBuf { } } +impl SampleBuf { + // Some ratatui methods need a owned vec of data and unfortunately don't accept a ref + pub fn as_vec(&self) -> Vec { + self.samples.clone().into() + } +} + // Properties for rendering a graph pub struct Graph { pub title: String, @@ -58,10 +75,15 @@ pub fn area_split(area: Rect, direction: Direction, first: u16, second: u16) -> Layout::default() .direction(direction) .constraints([Constraint::Percentage(first), Constraint::Percentage(second)]) - .split(area) - .as_ref() - .try_into() - .unwrap() + .areas(area) +} + +// Splits an area in a direction with given constraints +pub fn area_split_constrained(area: Rect, direction: Direction, first: Constraint, second: Constraint) -> [Rect; 2] { + Layout::default() + .direction(direction) + .constraints([first, second]) + .areas(area) } // Create a wrapping title block diff --git a/rust/src/debug.rs b/rust/src/debug.rs new file mode 100644 index 0000000..9fd9d41 --- /dev/null +++ b/rust/src/debug.rs @@ -0,0 +1,438 @@ +use crate::Source; +use crate::app::Module; +use crate::common; +use crate::notifications; +use clap::{Parser, Subcommand}; +use color_eyre::eyre::Result; +use color_eyre::eyre::eyre; +use crossterm::event::{KeyCode, KeyEventKind}; +use defmt_decoder::{DecodeError, Frame, StreamDecoder, Table}; +use ratatui::{ + buffer::Buffer, + crossterm::event::Event, + layout::{Constraint, Direction, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}, +}; +use std::borrow::Cow; +use std::fs; +use std::path::PathBuf; +use tui_input::{Input, backend::crossterm::EventHandler}; + +type ReadFrameResult = Result>>>; +type DefmtDecoder<'a> = Box; + +const MAX_LOGS: usize = 1000; + +#[derive(Parser)] +#[command(name = "dbg-cmd", disable_help_subcommand = true)] +struct Cmd { + #[command(subcommand)] + action: Action, +} + +#[derive(Subcommand)] +enum Action { + Attach { path: String }, + Detach, + Help, +} + +#[derive(Default)] +struct CmdHandler { + input: Input, +} + +impl CmdHandler { + fn parse(&mut self, line: String) -> Result { + // TODO: Will likely need to check if the command is something that should be passed to debug service + // As in, should differentiate between commands that affect the TUI vs affect the debug service + let tokens = line.split_whitespace(); + Ok(Cmd::try_parse_from(std::iter::once("dbg-cmd").chain(tokens)) + .map_err(|_| eyre!("Invalid command"))? + .action) + } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let width = area.width.max(3) - 3; + let scroll = self.input.visual_scroll(width as usize); + + let input = Paragraph::new(self.input.value()) + .style(Style::default()) + .scroll((0, scroll as u16)) + .block(Block::bordered().title("Command ")); + input.render(area, buf); + } +} + +// The defmt_decoder API requires stream_decoder to hold a reference to Table +// The stream_decoder is stateful so needs to be stored alongside Table in struct +// This creates a self-referential struct which is tricky hence the use of self_cell +self_cell::self_cell! { + struct DefmtDecoderCell { + owner: Table, + #[not_covariant] + dependent: DefmtDecoder, + } +} + +struct DefmtHandler { + bin_name: String, + decoder: DefmtDecoderCell, +} + +impl DefmtHandler { + pub fn new(elf_path: PathBuf) -> Result { + let bin_name = elf_path + .file_name() + .ok_or(eyre!("No file name found in ELF path"))? + .to_str() + .ok_or(eyre!("Invalid ELF path"))? + .to_owned(); + let elf = fs::read(elf_path).map_err(|_| eyre!("Failed to read ELF"))?; + + let table = Table::parse(&elf) + .map_err(|e| eyre!(e))? + .ok_or(eyre!("ELF contains no `.defmt` section"))?; + let decoder = DefmtDecoderCell::new(table, |table| table.new_stream_decoder()); + + Ok(Self { bin_name, decoder }) + } + + fn level_color(level: &str) -> Color { + match level { + "TRACE" => Color::Gray, + "DEBUG" => Color::White, + "INFO" => Color::Green, + "WARN" => Color::Yellow, + "ERROR" => Color::Red, + _ => Color::Black, + } + } + + // Unfortunately, the provided color formatter by defmt_decoder doesn't play nicely with Ratatui + // Hence the need for this manual formatting with color + fn frame2lines(f: &Frame) -> Vec> { + let msg = format!("{} ", f.display_message()); + let ts = f + .display_timestamp() + .map_or_else(|| " ".to_string(), |ts| format!("{ts} ")); + let ts_len = ts.len(); + let level = f + .level() + .map_or_else(|| " ".to_string(), |level| level.as_str().to_uppercase()); + + // Have to match over the string since the `Level` enum type is not re-exported + let level_color = Self::level_color(level.as_str()); + + let ts = Span::raw(ts); + let level = Span::styled(format!("{level:<7}"), Style::default().fg(level_color)); + + // A log can be multiple lines, but ratatui won't automatically display a newline + // Hence the need to manually split the log and create a `Line` for each + let msg: Vec> = msg.lines().map(|m| Span::raw(m.to_owned())).collect(); + + // The first line will always contain timestamp, level, and first line of log + let mut lines = vec![Line::from(vec![ts, level, msg[0].clone()])]; + + // If there are additional lines in the log, add them here + // We also align it with the first line of the log, just looks nicer + for span in msg.iter().skip(1) { + lines.push(Line::raw(format!("{:pad$}{span}", "", pad = ts_len + 7))); + } + lines + } + + fn read_log(&mut self, raw: Vec) -> ReadFrameResult { + self.decoder.with_dependent_mut(|_, d| d.received(&raw)); + + // TODO: May want to keep looping until reach EOF since we could receive multiple frames since last update + // However current debug service appears to guarantee only a single full frame will be sent at a time + match self.decoder.with_dependent_mut(|_, d| d.decode()) { + Ok(f) => Ok(Some(Self::frame2lines(&f))), + Err(DecodeError::UnexpectedEof) => Ok(None), + Err(DecodeError::Malformed) => Err(eyre!("Received malformed defmt packet")), + } + } +} + +struct ScrollState { + bar: ScrollbarState, + pos: usize, + size: u16, +} + +impl Default for ScrollState { + fn default() -> Self { + Self { + bar: Default::default(), + pos: 0, + size: u16::MAX, + } + } +} + +#[derive(Default)] +struct LogView { + y_scroll: ScrollState, + x_scroll: ScrollState, + max_log_len: usize, + logs: common::SampleBuf, MAX_LOGS>, +} + +impl LogView { + // Updates cached logs with newly read frame + fn log_frame(&mut self, frame: ReadFrameResult) { + match frame { + // If a full frame was received, log it + Ok(Some(log)) => { + let lines = log.len(); + for line in log { + let len = format!("{line}").len(); + self.max_log_len = std::cmp::max(self.max_log_len, len); + self.logs.insert(line); + } + self.update_scroll(lines); + } + // Unless it was an error + // TODO: Handle recovery? + Err(e) => { + self.log_meta(e); + } + // But if was unexpected EOF, just do nothing until we get the full frame + _ => {} + } + } + + fn log_meta(&mut self, msg: impl std::fmt::Display) { + self.logs + .insert(Line::styled(format!("<{msg}>"), Style::default().fg(Color::Cyan))); + self.update_scroll(1); + } + + fn scroll_up(&mut self) { + self.y_scroll.pos = self.y_scroll.pos.saturating_sub(1); + self.y_scroll.bar.prev(); + } + + fn scroll_down(&mut self) { + if self.logs.len() > self.y_scroll.size as usize { + self.y_scroll.pos = self + .y_scroll + .pos + .saturating_add(1) + .clamp(0, self.logs.len() - self.y_scroll.size as usize); + self.y_scroll.bar.next(); + } + } + + fn scroll_left(&mut self) { + self.x_scroll.pos = self.x_scroll.pos.saturating_sub(1); + self.x_scroll.bar.prev(); + } + + fn scroll_right(&mut self) { + if self.max_log_len > self.x_scroll.size as usize { + self.x_scroll.pos = self + .x_scroll + .pos + .saturating_add(1) + .clamp(0, self.max_log_len - self.x_scroll.size as usize); + self.x_scroll.bar.next(); + } + } + + // Updates log pane scroll state + fn update_scroll(&mut self, new_lines: usize) { + // Adjust the length of the horizontal scroll bar if a log doesn't fit in the window + if self.max_log_len > self.x_scroll.size as usize { + self.x_scroll.bar = self + .x_scroll + .bar + .content_length(self.max_log_len - self.x_scroll.size as usize); + } + + // Adjust the length of the vertical scroll bar if the number of logs doesn't fit in the window + if self.logs.len() > self.y_scroll.size as usize { + let height = self.logs.len() - self.y_scroll.size as usize; + self.y_scroll.bar = self.y_scroll.bar.content_length(height); + + // If we are currently scrolled to the bottom, stay scrolled to the bottom as new logs come in + if self.y_scroll.pos == height.saturating_sub(new_lines) { + self.y_scroll.bar = self.y_scroll.bar.position(height); + self.y_scroll.pos = height; + } + } + } + + fn display_help(&mut self) { + let help_lines: [&'static str; 4] = [ + "Commands supported:", + "help (Display help)", + "attach (Attach an ELF file to view defmt logs)", + "detach (Detach ELF)", + ]; + + for line in help_lines { + self.logs.insert(Line::raw(line)); + } + self.update_scroll(4); + } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + // Separate this from paragraph because we need to know the inner area for proper log scrolling + let b = common::title_block("Logs (Use Shift + ◄ ▲ ▼ ► to scroll)", 1, Color::White); + self.y_scroll.size = b.inner(area).height; + self.x_scroll.size = b.inner(area).width; + + Paragraph::new(self.logs.as_vec()) + .scroll((self.y_scroll.pos as u16, self.x_scroll.pos as u16)) + .block(b) + .render(area, buf); + + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("▲")) + .end_symbol(Some("▼")) + .render(area, buf, &mut self.y_scroll.bar); + Scrollbar::new(ScrollbarOrientation::HorizontalBottom) + .begin_symbol(Some("◄")) + .end_symbol(Some("►")) + .thumb_symbol("🬋") + .render(area, buf, &mut self.x_scroll.bar); + } +} + +pub struct Debug { + // Currently source is unused by main thread, but keeping it for ease of use in future + source: S, + log_view: LogView, + defmt: Option, + cmd_handler: CmdHandler, + event_rx: notifications::EventRx>>, +} + +impl Module for Debug { + fn title(&self) -> Cow<'static, str> { + format!( + "Debug Information ({})", + self.defmt.as_ref().map(|d| d.bin_name.as_str()).unwrap_or("None") + ) + .into() + } + + fn update(&mut self) { + if let Some(defmt) = &mut self.defmt { + while let Some(data) = self.event_rx.receive() { + match data { + Ok(raw) => { + let frame = defmt.read_log(raw); + self.log_view.log_frame(frame); + } + Err(e) => self.log_view.log_meta(e), + } + } + } + } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + // Give logs area as much room as possible + let [logs_area, cmd_area] = + common::area_split_constrained(area, Direction::Vertical, Constraint::Min(0), Constraint::Max(3)); + + self.log_view.render(logs_area, buf); + self.cmd_handler.render(cmd_area, buf); + } + + fn handle_event(&mut self, evt: &Event) { + if let Event::Key(key) = evt + && key.kind == KeyEventKind::Press + { + match key.code { + KeyCode::Up => self.log_view.scroll_up(), + KeyCode::Down => self.log_view.scroll_down(), + KeyCode::Left => self.log_view.scroll_left(), + KeyCode::Right => self.log_view.scroll_right(), + KeyCode::Enter => { + let str = self.cmd_handler.input.value_and_reset(); + self.handle_cmd(str); + } + _ => { + let _ = self.cmd_handler.input.handle_event(evt); + } + } + } + } +} + +impl Debug { + pub fn new(source: S, elf_path: Option, notifications: ¬ifications::Notifications) -> Self { + // Sources must ensure they are thread-safe + // Currently mock and ACPI are thread-safe + let src = source.clone(); + + // Reads the raw defmt frame from source every time notification is received and stores in buffer + // Previously, the event receiver just queued up bools when notifications were received so tabs could poll that + // But, not particularly effective for debug tab since the debug service will only send a new notification after the previous has been acknowledged + // This resulted in the debug tab only getting a single debug frame once a second which is too slow + // So instead the event receiver thread itself will call `get_dbg` as notifications come in and store the raw frames in a buffer + // The debug tab can then just process every raw frame once a second and push all those to the log viewer + // This allows for a more real-time approach of receiving logs + let event_rx = + notifications.event_receiver(notifications::Event::DbgFrameAvailable, move |_event| src.get_dbg()); + + let mut debug = Self { + source, + log_view: Default::default(), + defmt: None, + cmd_handler: Default::default(), + event_rx, + }; + + if let Some(elf_path) = elf_path { + debug.attach_elf(elf_path); + } else { + debug.detach_elf(); + + #[cfg(feature = "mock")] + debug.log_view.log_meta("Try running the command `attach mock-bin`"); + } + + debug + } + + fn handle_cmd(&mut self, str: String) { + match self.cmd_handler.parse(str) { + Ok(action) => match action { + Action::Attach { path } => self.attach_elf(PathBuf::from(path)), + Action::Detach => self.detach_elf(), + Action::Help => self.log_view.display_help(), + }, + Err(e) => self.log_view.log_meta(e), + } + } + + fn attach_elf(&mut self, elf_path: PathBuf) { + match DefmtHandler::new(elf_path) { + Ok(defmt) => { + self.log_view.log_meta(format!("Attached ELF: {}", defmt.bin_name)); + self.defmt = Some(defmt); + self.event_rx.start(); + + // Initial read to kick off debug service (since we would've missed last notification) + let _ = self.source.get_dbg(); + } + Err(e) => { + self.defmt = None; + self.log_view.log_meta(format!("Failed to attach ELF: {}", e)); + } + } + } + + fn detach_elf(&mut self) { + self.defmt = None; + self.log_view + .log_meta("No ELF attached so debug logs are not available"); + self.event_rx.stop(); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3832492..f6aca2d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -9,13 +9,15 @@ pub mod mock; pub mod app; pub mod battery; pub mod common; +pub mod debug; +pub mod notifications; pub mod rtc; pub mod thermal; pub mod ucsi; pub mod widgets; /// Trait implemented by all data sources -pub trait Source: Clone { +pub trait Source: Clone + Send + 'static { /// Get current temperature fn get_temperature(&self) -> Result; @@ -42,6 +44,9 @@ pub trait Source: Clone { /// Set battery trippoint fn set_btp(&self, trippoint: u32) -> Result<()>; + + /// Get raw debug data for further processing + fn get_dbg(&self) -> Result>; } pub enum Threshold { diff --git a/rust/src/main.rs b/rust/src/main.rs index ad2434e..8c6a7ef 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,15 +1,19 @@ +use clap::Parser; use color_eyre::Result; -use ec_demo::app::App; +use ec_demo::app::{App, AppArgs}; +use ec_demo::notifications::Notifications; fn main() -> Result<()> { color_eyre::install()?; let terminal = ratatui::init(); #[cfg(not(feature = "mock"))] - let source = ec_demo::acpi::Acpi::default(); + let source = ec_demo::acpi::Acpi::new(); #[cfg(feature = "mock")] let source = ec_demo::mock::Mock::default(); - App::new(source).run(terminal) + let args = AppArgs::parse(); + let notifications = Notifications::new()?; + App::new(source, args, ¬ifications).run(terminal) } diff --git a/rust/src/mock.rs b/rust/src/mock.rs index 754e872..4dbcb39 100644 --- a/rust/src/mock.rs +++ b/rust/src/mock.rs @@ -2,13 +2,26 @@ use crate::{Source, Threshold, common}; use color_eyre::Result; use std::sync::{ Mutex, OnceLock, - atomic::Ordering, - atomic::{AtomicI64, AtomicU32}, + atomic::{AtomicI64, AtomicU16, AtomicU32, AtomicU64, Ordering}, }; static SET_RPM: AtomicI64 = AtomicI64::new(-1); static SAMPLE: OnceLock> = OnceLock::new(); +// Produces a fake "on-the-wire" byte representation of a defmt call that matches format expected by mock-bin +// Index is equal to the address of the log string in the `.defmt` section of mock-bin ELF +// Mainly used for testing defmt decoding +fn mock_defmt_wire(index: u16, timestamp: u64) -> Vec { + let mut buf = Vec::new(); + buf.extend(index.to_le_bytes()); + buf.extend(timestamp.to_le_bytes()); + let mut buf = rzcobs::encode(&buf); + + // End frame delimeter + buf.push(0x00); + buf +} + #[derive(Default, Copy, Clone)] pub struct Mock {} @@ -141,4 +154,21 @@ impl Source for Mock { // Do nothing for mock Ok(()) } + + fn get_dbg(&self) -> Result> { + const DEFMT_START: u16 = 1; + const DEFMT_END: u16 = 6; + static DEFMT_IDX: AtomicU16 = AtomicU16::new(DEFMT_START); + static TIMESTAMP: AtomicU64 = AtomicU64::new(0); + + let frame_idx = DEFMT_IDX.fetch_add(1, Ordering::Relaxed); + let timestamp = TIMESTAMP.fetch_add(100000, Ordering::Relaxed); + let frame = mock_defmt_wire(frame_idx, timestamp); + + let _ = DEFMT_IDX.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |idx| { + (idx > DEFMT_END).then_some(DEFMT_START) + }); + + Ok(frame) + } } diff --git a/rust/src/notifications.rs b/rust/src/notifications.rs new file mode 100644 index 0000000..e5624f2 --- /dev/null +++ b/rust/src/notifications.rs @@ -0,0 +1,175 @@ +use color_eyre::{Result, eyre::eyre}; +use std::sync::{atomic, mpsc}; + +#[cfg(not(feature = "mock"))] +unsafe extern "C" { + fn InitializeNotification() -> i32; + fn WaitForNotification(event: u32) -> u32; + fn CleanupNotification(); +} + +#[cfg(feature = "mock")] +use mock::*; +#[cfg(feature = "mock")] +#[allow(non_snake_case)] +mod mock { + pub(super) unsafe fn InitializeNotification() -> i32 { + // Do nothing for mock + 0 + } + + pub(super) unsafe fn WaitForNotification(event: u32) -> u32 { + // Just wait for a little bit then return the event that was passed in + std::thread::sleep(std::time::Duration::from_millis(500)); + event + } + + pub(super) unsafe fn CleanupNotification() { + // Do nothing for mock + } +} + +const RX_BUF_SZ: usize = 128; + +/// A notification event +#[derive(Debug, Copy, Clone)] +pub enum Event { + Any, + DbgFrameAvailable, +} + +// Eventually would want to make this configurable to support multiple platforms +// But for now hardcode values +impl From for u32 { + fn from(event: Event) -> Self { + match event { + Event::Any => 0, + Event::DbgFrameAvailable => 20, + } + } +} + +impl TryFrom for Event { + type Error = color_eyre::Report; + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Any), + 20 => Ok(Self::DbgFrameAvailable), + _ => Err(eyre!("Unknown event received")), + } + } +} + +pub struct EventRx { + rx: std::sync::mpsc::Receiver, + signal_with_guard: std::sync::Arc<(std::sync::Mutex, std::sync::Condvar)>, +} + +impl EventRx { + /// Start the event receiver + pub fn start(&mut self) { + let (guard, signal) = &*self.signal_with_guard; + *guard.lock().expect("Guard must not be poisoned") = true; + signal.notify_one(); + } + + /// Stop the event receiver + pub fn stop(&mut self) { + let (guard, _signal) = &*self.signal_with_guard; + *guard.lock().expect("Guard must not be poisoned") = false; + } + + /// Returns the most recent data in the rx buffer if any + pub fn receive(&self) -> Option { + match self.rx.try_recv() { + Ok(data) => Some(data), + Err(mpsc::TryRecvError::Empty) => None, + + // Choose to panic here for caller ergonomics + // This case shouldn't happen in this app and is pretty much unrecoverable + Err(mpsc::TryRecvError::Disconnected) => panic!("Polled dropped notification service"), + } + } +} + +/// Singleton notification service +static INITIALIZED: atomic::AtomicBool = atomic::AtomicBool::new(false); +pub struct Notifications; + +impl Notifications { + /// Create and initialize a new notification service. + /// + /// Returns an error if notification service instance already exists. + pub fn new() -> Result { + if INITIALIZED + .compare_exchange(false, true, atomic::Ordering::SeqCst, atomic::Ordering::SeqCst) + .is_ok() + { + // SAFETY: Only a single instance will ever exist at once + let res = unsafe { InitializeNotification() }; + if res == 0 { + Ok(Self) + } else { + INITIALIZED.store(false, atomic::Ordering::SeqCst); + Err(eyre!("Failed to initialize notification service")) + } + } else { + Err(eyre!("Only one notification service must exist at a time")) + } + } + + /// Creates an event receiver `EventRx` which spawns a thread that waits for specified event. + /// + /// This receiver will then use the provided closure to perform some action and return data whenever event is received. + /// + /// This returned data is automatically stored in a buffer which caller can access via `EventRx::receive`. + pub fn event_receiver( + &self, + event: Event, + f: impl Fn(Event) -> T + Send + 'static, + ) -> EventRx { + let (tx, rx) = mpsc::sync_channel::(RX_BUF_SZ); + let signal_with_guard = std::sync::Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new())); + let waiter = std::sync::Arc::clone(&signal_with_guard); + + std::thread::spawn(move || { + let (guard, signal) = &*waiter; + + loop { + // Check if we should still run, and if not, sleep until told to start again + { + let mut running = guard.lock().expect("Guard must not be poisoned"); + while !*running { + running = signal.wait(running).expect("Guard must not be poisoned"); + } + } + + // If we somehow receive a notification that we didn't intend, just discard it + if let Ok(event) = Self::wait_event(event) { + let data = f(event); + + // Receiver has dropped, so just end the thread silently + if tx.send(data).is_err() { + break; + } + } + } + }); + + EventRx { rx, signal_with_guard } + } + + fn wait_event(event: Event) -> Result { + // SAFETY: Driver can handle multiple threads calling simultaneously + let recv = unsafe { WaitForNotification(event.into()) }; + Event::try_from(recv) + } +} + +impl Drop for Notifications { + fn drop(&mut self) { + // SAFETY: This is only called once automatically when singleton service is dropped + unsafe { CleanupNotification() }; + INITIALIZED.store(false, atomic::Ordering::SeqCst); + } +} diff --git a/rust/src/rtc.rs b/rust/src/rtc.rs index 0827046..b7f9794 100644 --- a/rust/src/rtc.rs +++ b/rust/src/rtc.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crossterm::event::Event; use ratatui::{ buffer::Buffer, @@ -15,15 +17,15 @@ const LABEL_COLOR: Color = tailwind::SLATE.c200; pub struct Rtc {} impl Module for Rtc { - fn title(&self) -> &'static str { - "RTC Information" + fn title(&self) -> Cow<'static, str> { + "RTC Information".into() } fn update(&mut self) {} fn handle_event(&mut self, _evt: &Event) {} - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&mut self, area: Rect, buf: &mut Buffer) { let status_title = title_block("RTC Properties"); Paragraph::default().block(status_title).render(area, buf); } diff --git a/rust/src/thermal.rs b/rust/src/thermal.rs index c78b775..9eb9fa8 100644 --- a/rust/src/thermal.rs +++ b/rust/src/thermal.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::app::Module; use crate::common; use crate::{Source, Threshold}; @@ -147,8 +149,8 @@ pub struct Thermal { } impl Module for Thermal { - fn title(&self) -> &'static str { - "Thermal Information" + fn title(&self) -> Cow<'static, str> { + "Thermal Information".into() } fn update(&mut self) { @@ -157,7 +159,7 @@ impl Module for Thermal { self.t += 1; } - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&mut self, area: Rect, buf: &mut Buffer) { let [sensor_area, fan_area] = common::area_split(area, Direction::Horizontal, 50, 50); self.render_sensor(sensor_area, buf); self.render_fan(fan_area, buf); diff --git a/rust/src/ucsi.rs b/rust/src/ucsi.rs index 2716864..5f09506 100644 --- a/rust/src/ucsi.rs +++ b/rust/src/ucsi.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crossterm::event::Event; use ratatui::{ buffer::Buffer, @@ -15,15 +17,15 @@ const LABEL_COLOR: Color = tailwind::SLATE.c200; pub struct Ucsi {} impl Module for Ucsi { - fn title(&self) -> &'static str { - "UCSI Information" + fn title(&self) -> Cow<'static, str> { + "UCSI Information".into() } fn update(&mut self) {} fn handle_event(&mut self, _evt: &Event) {} - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&mut self, area: Rect, buf: &mut Buffer) { let status_title = title_block("UCSI State"); Paragraph::default().block(status_title).render(area, buf); }