From 4a38c8e2d1f0e524503efa263b64fb3bc5fffebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Thu, 27 Aug 2026 01:08:36 +0900 Subject: [PATCH 1/3] feat(arduino): support XIAO firmware and BLE input Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: sisyphus-dev-ai --- Cargo.lock | 412 +++++++++++++++--- apps/desktop/src-tauri/Cargo.toml | 4 + apps/desktop/src-tauri/src/arduino.rs | 194 +++++++++ apps/desktop/src-tauri/src/firmware.rs | 88 ++-- apps/desktop/src-tauri/src/flasher.rs | 357 ++++++++++++++- apps/desktop/src-tauri/src/lib.rs | 9 + apps/desktop/src-tauri/src/registry.rs | 48 +- .../arduino-firmware.browser.test.ts | 12 +- .../__tests__/arduino-setup.browser.test.tsx | 10 +- apps/desktop/src/lib/firmware.ts | 14 +- bun.lock | 32 +- 11 files changed, 1045 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b997933..f8ca14a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,17 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "atk" version = "0.18.2" @@ -221,13 +232,51 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2", + "objc2 0.6.4", +] + +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags 2.13.1", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.20", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", ] [[package]] @@ -260,6 +309,36 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.13.1", + "bluez-async", + "dashmap 6.2.1", + "dbus", + "futures", + "jni 0.19.0", + "jni-utils", + "log", + "objc2 0.5.2", + "objc2-core-bluetooth", + "objc2-foundation 0.2.2", + "once_cell", + "serde", + "serde_bytes", + "static_assertions", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "uuid", + "windows 0.61.3", + "windows-future 0.2.1", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -576,13 +655,13 @@ dependencies = [ "ndk-context", "num-derive", "num-traits", - "objc2", + "objc2 0.6.4", "objc2-audio-toolbox", "objc2-avf-audio", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -723,6 +802,33 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -735,11 +841,24 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ + "futures-channel", + "futures-util", "libc", "libdbus-sys", "windows-sys 0.61.2", ] +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + [[package]] name = "defmt" version = "1.1.1" @@ -850,9 +969,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -955,6 +1074,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + [[package]] name = "embed-resource" version = "3.0.11" @@ -987,9 +1112,9 @@ dependencies = [ "libc", "log", "nom", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.2", "windows 0.61.3", "x11rb 0.13.2", "xkbcommon", @@ -1118,6 +1243,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -1180,6 +1320,7 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1441,7 +1582,7 @@ checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "once_cell", "serde", @@ -1520,10 +1661,12 @@ version = "0.1.0" dependencies = [ "accessibility", "accessibility-sys", + "btleplug", "chrono", "core-foundation", "enigo", - "objc2", + "futures", + "objc2 0.6.4", "objc2-app-kit", "reqwest", "rodio", @@ -1534,8 +1677,10 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-global-shortcut", + "tokio", "unicode-normalization", "ureq", + "uuid", "windows-sys 0.61.2", "x11rb 0.14.0", ] @@ -1546,6 +1691,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.17.1" @@ -1878,6 +2029,15 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1960,6 +2120,20 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "jni" version = "0.21.1" @@ -2034,6 +2208,21 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni 0.19.0", + "log", + "once_cell", + "static_assertions", + "uuid", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -2298,10 +2487,10 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", @@ -2452,6 +2641,22 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.4" @@ -2469,9 +2674,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -2479,7 +2684,7 @@ dependencies = [ "objc2-core-image", "objc2-core-text", "objc2-core-video", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-quartz-core", ] @@ -2491,11 +2696,11 @@ checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ "bitflags 2.13.1", "libc", - "objc2", + "objc2 0.6.4", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -2504,8 +2709,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2515,8 +2720,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.1", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2526,10 +2731,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -2539,7 +2744,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2549,8 +2765,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.13.1", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2560,10 +2776,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "dispatch2", "libc", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -2574,7 +2790,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -2585,8 +2801,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2595,8 +2811,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2606,7 +2822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -2618,7 +2834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -2639,6 +2855,18 @@ dependencies = [ "cc", ] +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -2646,9 +2874,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2659,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2670,9 +2898,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -2682,8 +2910,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.1", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -2691,7 +2919,7 @@ dependencies = [ "objc2-core-image", "objc2-core-location", "objc2-core-text", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-quartz-core", "objc2-user-notifications", ] @@ -2702,8 +2930,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2713,11 +2941,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ "bitflags 2.13.1", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -3509,6 +3737,28 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror 2.0.20", + "xml", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -3752,10 +4002,10 @@ dependencies = [ "bytemuck", "js-sys", "ndk", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-quartz-core", "raw-window-handle", "redox_syscall", @@ -3797,6 +4047,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.9.0" @@ -3916,7 +4172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "core-foundation", "core-graphics 0.25.0", "crossbeam-channel", @@ -3932,9 +4188,9 @@ dependencies = [ "log", "ndk", "ndk-sys", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-ui-kit", "once_cell", "parking_lot", @@ -3988,9 +4244,9 @@ dependencies = [ "log", "mime", "muda", - "objc2", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -4121,7 +4377,7 @@ dependencies = [ "gtk", "http", "jni 0.21.1", - "objc2", + "objc2 0.6.4", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", @@ -4145,7 +4401,7 @@ dependencies = [ "http", "jni 0.21.1", "log", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "once_cell", "percent-encoding", @@ -4325,9 +4581,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -4338,6 +4606,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -4545,11 +4825,11 @@ dependencies = [ "dirs", "libappindicator", "muda", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", @@ -5031,10 +5311,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5458,7 +5738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs", @@ -5472,10 +5752,10 @@ dependencies = [ "jni 0.21.1", "libc", "ndk", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5567,6 +5847,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + [[package]] name = "yoke" version = "0.8.3" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 2dcd76b..131682b 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -29,6 +29,10 @@ reqwest = { version = "0.13.4", default-features = false, features = ["blocking" ureq = "3.4.0" sha2 = "0.11.0" unicode-normalization = "0.1.25" +btleplug = { version = "0.11", features = ["serde"] } +futures = "0.3.34" +tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread", "time"] } +uuid = "1.25.0" # 이후 단계에서 붙일 crate. 버전은 반드시 `cargo add`로 확정한다. # cargo add tauri-plugin-store # 사용자 프로필 영속화 (M3) diff --git a/apps/desktop/src-tauri/src/arduino.rs b/apps/desktop/src-tauri/src/arduino.rs index e504cc9..db9f5af 100644 --- a/apps/desktop/src-tauri/src/arduino.rs +++ b/apps/desktop/src-tauri/src/arduino.rs @@ -11,7 +11,12 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; +use btleplug::api::{Central, CharPropFlags, Manager as _, Peripheral as _, ScanFilter}; +use btleplug::platform::Manager as BleManager; +use futures::StreamExt; use serde::Serialize; +use tokio::runtime::Builder as TokioRuntimeBuilder; +use uuid::Uuid; use crate::input::{Judgement, SharedDetector}; @@ -26,6 +31,11 @@ const HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(500); const HANDSHAKE_ATTEMPTS: usize = 4; const READ_TIMEOUT: Duration = Duration::from_millis(100); const OUTPUT_QUEUE_CAPACITY: usize = 32; +const BLE_RETRY_DELAY: Duration = Duration::from_secs(2); +const BLE_SCAN_WINDOW: Duration = Duration::from_secs(2); +const HANBEON_XIAO_NAME: &str = "HanBeon XIAO"; +const NUS_SERVICE_UUID: Uuid = Uuid::from_u128(0x6e40_0001_b5a3_f393_e0a9_e50e_24dc_ca9e); +const NUS_TX_UUID: Uuid = Uuid::from_u128(0x6e40_0003_b5a3_f393_e0a9_e50e_24dc_ca9e); type ActiveOutput = Option<(u64, SyncSender)>; @@ -331,6 +341,43 @@ impl Drop for ArduinoSwitch { } } +/// BLE Nordic UART Service transport for the XIAO nRF52840. +/// +/// It is intentionally input-only today: native scan output remains available +/// whenever the USB CDC transport is connected, while a wireless switch routes +/// P/R records through the same parser and gesture detector. +pub struct BleSwitch { + shutdown: mpsc::Sender<()>, + worker: Option>, +} + +impl BleSwitch { + pub fn spawn(on_switch: S) -> Self + where + S: Fn(SwitchEvent) + Send + 'static, + { + let (shutdown, shutdown_rx) = mpsc::channel(); + let worker = thread::spawn(move || run_ble_worker(Shutdown::new(shutdown_rx), on_switch)); + Self { + shutdown, + worker: Some(worker), + } + } + + fn join_worker(&mut self) { + let _ = self.shutdown.send(()); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +impl Drop for BleSwitch { + fn drop(&mut self) { + self.join_worker(); + } +} + /// The only component currently permitted to own the Arduino serial port. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ArduinoOwner { @@ -672,6 +719,153 @@ where } } +fn run_ble_worker(shutdown: Shutdown, on_switch: S) +where + S: Fn(SwitchEvent), +{ + let runtime = match TokioRuntimeBuilder::new_multi_thread() + .enable_time() + .build() + { + Ok(runtime) => runtime, + Err(error) => { + eprintln!("[arduino] BLE runtime을 시작하지 못했습니다: {error}"); + return; + } + }; + runtime.block_on(async move { + let manager = match BleManager::new().await { + Ok(manager) => manager, + Err(error) => { + eprintln!("[arduino] BLE 어댑터를 열지 못했습니다: {error}"); + return; + } + }; + let adapters = match manager.adapters().await { + Ok(adapters) => adapters, + Err(error) => { + eprintln!("[arduino] BLE 어댑터를 찾지 못했습니다: {error}"); + return; + } + }; + let Some(adapter) = adapters.into_iter().next() else { + eprintln!("[arduino] 사용할 BLE 어댑터가 없습니다."); + return; + }; + + loop { + if shutdown.is_requested() { + return; + } + if let Err(error) = adapter + .start_scan(ScanFilter { + services: vec![NUS_SERVICE_UUID], + }) + .await + { + eprintln!("[arduino] BLE 스캔을 시작하지 못했습니다: {error}"); + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + } + tokio::time::sleep(BLE_SCAN_WINDOW).await; + let peripherals = match adapter.peripherals().await { + Ok(peripherals) => peripherals, + Err(error) => { + eprintln!("[arduino] BLE 장치 목록을 읽지 못했습니다: {error}"); + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + } + }; + let mut xiao = None; + for peripheral in peripherals { + let Ok(Some(properties)) = peripheral.properties().await else { + continue; + }; + let is_hanbeon = properties.local_name.as_deref() == Some(HANBEON_XIAO_NAME) + || properties.services.contains(&NUS_SERVICE_UUID); + if is_hanbeon { + if std::env::var("HANBEON_LOG").is_ok() { + eprintln!( + "[arduino] BLE found: name={:?}, services={:?}", + properties.local_name, properties.services + ); + } + xiao = Some(peripheral); + break; + } + } + let Some(peripheral) = xiao else { + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + }; + if !peripheral.is_connected().await.unwrap_or(false) + && peripheral.connect().await.is_err() + { + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + } + if peripheral.discover_services().await.is_err() { + let _ = peripheral.disconnect().await; + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + } + let Some(tx) = peripheral + .characteristics() + .into_iter() + .find(|characteristic| { + characteristic.uuid == NUS_TX_UUID + && characteristic.properties.contains(CharPropFlags::NOTIFY) + }) + else { + let _ = peripheral.disconnect().await; + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + }; + if peripheral.subscribe(&tx).await.is_err() { + let _ = peripheral.disconnect().await; + tokio::time::sleep(BLE_RETRY_DELAY).await; + continue; + } + let Ok(mut notifications) = peripheral.notifications().await else { + let _ = peripheral.disconnect().await; + thread::sleep(BLE_RETRY_DELAY); + continue; + }; + let mut parser = RecordParser::default(); + let mut press_state = PressState::default(); + loop { + let notification = tokio::select! { + notification = notifications.next() => notification, + () = tokio::time::sleep(Duration::from_millis(100)) => { + if shutdown.is_requested() { + break; + } + continue; + } + }; + let Some(notification) = notification else { + break; + }; + if notification.uuid != NUS_TX_UUID { + continue; + } + for record in parser.push(¬ification.value).into_iter().flatten() { + if let Some(event) = press_state.apply(record) { + if std::env::var("HANBEON_LOG").is_ok() { + eprintln!("[arduino] BLE input: {event:?}"); + } + on_switch(event); + } + } + } + if let Some(release) = press_state.disconnect() { + on_switch(release); + } + let _ = peripheral.disconnect().await; + } + }); +} + fn lifecycle_after_connection(shutdown: &Shutdown) -> Option { (!shutdown.is_requested()).then_some(Lifecycle::Reconnecting) } diff --git a/apps/desktop/src-tauri/src/firmware.rs b/apps/desktop/src-tauri/src/firmware.rs index bdb70fd..6084530 100644 --- a/apps/desktop/src-tauri/src/firmware.rs +++ b/apps/desktop/src-tauri/src/firmware.rs @@ -17,8 +17,8 @@ use tauri::{AppHandle, Emitter, Manager, State}; use crate::arduino::{ ArduinoCoordinator, BAUD_RATE, HANDSHAKE_REQUEST, HANDSHAKE_RESPONSE, InstallerExit, }; -use crate::flasher::{FlashImage, SerialIo}; -use crate::registry::{RegistryClient, RegistryError, UsbIdentity}; +use crate::flasher::{BootloaderFamily, FlashImage, SerialIo, family_for_fqbn, request_bootloader}; +use crate::registry::{RegistryClient, RegistryError, UsbIdentity, VerifiedFirmware}; pub const EVENT_FIRMWARE: &str = "arduino://firmware"; /// ATmega328P의 SPM 페이지 크기(optiboot가 한 번에 쓰는 단위). @@ -564,7 +564,7 @@ fn download_firmware( app: &AppHandle, candidate: &ArduinoCandidate, cancelled: &AtomicBool, -) -> Result { +) -> Result { ensure_not_cancelled(cancelled)?; let cache_dir = registry_cache_dir(app).map_err(InstallFailure::Download)?; let client = RegistryClient::new(cache_dir); @@ -595,20 +595,24 @@ fn download_firmware( ), other => InstallFailure::Download(other.to_string()), })?; - let image = FlashImage::from_ihex(&firmware.hex_text) - .map_err(|error| InstallFailure::Download(format!("펌웨어 해석 실패: {error}")))?; - if !image.fits_within(UNO_APPLICATION_BYTES) { - return Err(InstallFailure::Download( - "펌웨어가 Arduino Uno R3 애플리케이션 영역을 벗어납니다.".to_owned(), - )); + Ok(firmware) +} + +impl From for InstallFailure { + fn from(error: crate::flasher::FlashError) -> Self { + match error { + crate::flasher::FlashError::Cancelled => InstallFailure::Cancelled, + other => InstallFailure::Upload(other.to_string()), + } } - Ok(image) } fn flash_with_retry( app: &AppHandle, candidate: &ArduinoCandidate, image: &FlashImage, + hex_text: &str, + family: BootloaderFamily, cancelled: &AtomicBool, ) -> Result<(), InstallFailure> { let mut last_error = String::new(); @@ -626,25 +630,37 @@ fn flash_with_retry( }, ); } - let port = open_flash_port(&active_candidate.port).map_err(InstallFailure::Upload)?; - let mut flash_port = Box::new(port) as Box; - let mut written_bytes = 0usize; - let flash_result = flasher::synchronize(flash_port.as_mut(), SYNC_ATTEMPTS, cancelled) - .and_then(|()| { - flasher::program( - flash_port.as_mut(), - image, - FLASH_PAGE_BYTES, - cancelled, - &mut |written| { - // 페이지 경계마다만 기록하면 충분하다. - if written / FLASH_PAGE_BYTES != written_bytes / FLASH_PAGE_BYTES { - written_bytes = written; - } - }, - ) - }); - drop(flash_port); + let flash_result = match family { + BootloaderFamily::Stk500v1 => { + let port = open_flash_port(&active_candidate.port) + .map_err(InstallFailure::Upload)?; + let mut flash_port = Box::new(port) as Box; + let mut written_bytes = 0usize; + let result = flasher::synchronize(flash_port.as_mut(), SYNC_ATTEMPTS, cancelled) + .and_then(|()| { + flasher::program( + flash_port.as_mut(), + image, + FLASH_PAGE_BYTES, + cancelled, + &mut |written| { + // 페이지 경계마다만 기록하면 충분하다. + if written / FLASH_PAGE_BYTES != written_bytes / FLASH_PAGE_BYTES { + written_bytes = written; + } + }, + ) + }); + drop(flash_port); + result + } + BootloaderFamily::Uf2 => { + // XIAO nRF52840의 부트로더는 UF2 드라이브가 아니라 시리얼 DFU + // (adafruit-nrfutil)다. 부트로더 터치 후 nrfutil DFU 업로드. + request_bootloader(&active_candidate.port)?; + flasher::flash_serial_dfu(&active_candidate.port, hex_text) + } + }; match flash_result { Ok(()) => return Ok(()), Err(flasher::FlashError::Cancelled) => return Err(InstallFailure::Cancelled), @@ -667,7 +683,10 @@ fn install( cancelled: &AtomicBool, ) -> Result<(), InstallFailure> { ensure_not_cancelled(cancelled)?; - let image = download_firmware(app, candidate, cancelled)?; + let firmware = download_firmware(app, candidate, cancelled)?; + let image = FlashImage::from_ihex(&firmware.hex_text) + .map_err(|error| InstallFailure::Download(format!("펌웨어 해석 실패: {error}")))?; + let family = family_for_fqbn(&firmware.fqbn); ensure_not_cancelled(cancelled)?; emit( @@ -676,7 +695,14 @@ fn install( device_id: device_id.to_owned(), }, ); - flash_with_retry(app, candidate, &image, cancelled)?; + flash_with_retry( + app, + candidate, + &image, + &firmware.hex_text, + family, + cancelled, + )?; ensure_not_cancelled(cancelled)?; emit( app, diff --git a/apps/desktop/src-tauri/src/flasher.rs b/apps/desktop/src-tauri/src/flasher.rs index 4f15a47..666d965 100644 --- a/apps/desktop/src-tauri/src/flasher.rs +++ b/apps/desktop/src-tauri/src/flasher.rs @@ -9,7 +9,9 @@ //! only the thin `SerialIo` impl touches real hardware. use std::collections::BTreeMap; +use std::fs; use std::io::{self, Read, Write}; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::{Duration, Instant}; @@ -19,6 +21,9 @@ use std::time::{Duration, Instant}; pub struct FlashImage { /// Sparse page map keyed by word address (STK500 uses word addressing). pages: Vec<(u16, Vec)>, + /// Sparse byte segments keyed by absolute flash address. Unlike STK500, + /// UF2 uses byte addressing and can represent the full Intel HEX range. + byte_segments: Vec<(u32, Vec)>, } impl FlashImage { @@ -90,10 +95,13 @@ impl FlashImage { for (_, byte) in chunk { payload.push(*byte); } - image.pages.push(( - u16::try_from(byte_address / 2).map_err(|_| "주소가 범위를 벗어났습니다")?, - payload, - )); + image.byte_segments.push((byte_address, payload.clone())); + // AVR의 STK500v1 address field is a 16-bit word address. Keep its + // existing representation for that uploader, while preserving + // high-address data above for byte-addressed UF2 targets. + if let Ok(word_address) = u16::try_from(byte_address / 2) { + image.pages.push((word_address, payload)); + } start += chunk_end; } Ok(image) @@ -104,7 +112,7 @@ impl FlashImage { } pub fn total_bytes(&self) -> usize { - self.pages.iter().map(|(_, data)| data.len()).sum() + self.byte_segments.iter().map(|(_, data)| data.len()).sum() } pub fn highest_byte_address(&self) -> Option { @@ -161,6 +169,255 @@ impl FlashImage { }) .collect() } + + fn byte_segments(&self) -> &[(u32, Vec)] { + &self.byte_segments + } +} + +/// Bootloader transport selected by the board platform in its Arduino FQBN. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BootloaderFamily { + Stk500v1, + Uf2, +} + +/// Selects the bootloader transport from the registry-provided FQBN. +/// +/// Unknown platforms retain the established STK500v1 path so existing AVR +/// registry entries remain compatible; registry-supported nRF52 XIAO boards +/// explicitly select the UF2 path. +pub fn family_for_fqbn(fqbn: &str) -> BootloaderFamily { + if fqbn.starts_with("Seeeduino:nrf52") { + BootloaderFamily::Uf2 + } else { + BootloaderFamily::Stk500v1 + } +} + +const UF2_MAGIC_START0: u32 = 0x0A32_4655; +const UF2_MAGIC_START1: u32 = 0x9E5D_5157; +const UF2_FLAGS_FAMILY_ID_PRESENT: u32 = 0x0000_2000; +const UF2_PAYLOAD_SIZE: usize = 256; +const UF2_BLOCK_SIZE: usize = 512; +const UF2_MAGIC_END: u32 = 0x0AB1_6F30; + +/// Encodes an Intel HEX image as UF2 blocks for a mass-storage bootloader. +/// +/// The UF2 family ID occupies the standard header's final word when the +/// family-ID-present flag is set. Each block carries a fixed 256-byte payload +/// and targets the absolute byte address from the Intel HEX input. +pub fn encode_uf2(hex_image: &FlashImage, family_id: u32) -> Vec { + let blocks: Vec<(u32, &[u8])> = hex_image + .byte_segments() + .iter() + .flat_map(|(address, data)| { + data.chunks(UF2_PAYLOAD_SIZE) + .enumerate() + .map(move |(index, payload)| { + (*address + (index * UF2_PAYLOAD_SIZE) as u32, payload) + }) + }) + .collect(); + let total_blocks = blocks.len() as u32; + let mut output = Vec::with_capacity(blocks.len() * UF2_BLOCK_SIZE); + + for (sequence, (address, payload)) in blocks.into_iter().enumerate() { + let mut block = [0_u8; UF2_BLOCK_SIZE]; + let header = [ + UF2_MAGIC_START0, + UF2_MAGIC_START1, + UF2_FLAGS_FAMILY_ID_PRESENT, + address, + UF2_PAYLOAD_SIZE as u32, + sequence as u32, + total_blocks, + family_id, + ]; + for (index, word) in header.into_iter().enumerate() { + block[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes()); + } + block[32..32 + payload.len()].copy_from_slice(payload); + block[508..512].copy_from_slice(&UF2_MAGIC_END.to_le_bytes()); + output.extend_from_slice(&block); + } + output +} + +/// Source of mounted volumes, isolated so UF2 writing is testable without a +/// physical board. +pub trait DriveProbe { + fn volumes(&self) -> Vec; +} + +/// macOS volume probe for XIAO's TinyUSB mass-storage bootloader. +pub struct MacVolumes; + +impl DriveProbe for MacVolumes { + fn volumes(&self) -> Vec { + fs::read_dir("/Volumes") + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.is_dir() + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.to_ascii_uppercase().contains("XIAO")) + }) + .collect() + } +} + +/// UF2 mass-storage uploader parameterized by its mounted-volume probe. +pub struct Uf2Flasher

{ + probe: P, +} + +impl Uf2Flasher

{ + pub fn new(probe: P) -> Self { + Self { probe } + } + + /// Writes the UF2 file into the one connected XIAO bootloader volume. + pub fn flash(&self, uf2_bytes: &[u8]) -> Result<(), FlashError> { + let volumes = self.probe.volumes(); + let volume = match volumes.as_slice() { + [] => { + return Err(FlashError::Uf2( + "XIAO UF2 부트로더 드라이브를 찾지 못했습니다.".to_owned(), + )); + } + [volume] => volume, + _ => { + return Err(FlashError::Uf2( + "XIAO UF2 부트로더 드라이브가 여러 개 연결되어 있습니다.".to_owned(), + )); + } + }; + fs::write(volume.join("hanbeon.uf2"), uf2_bytes) + .map_err(|error| FlashError::Uf2(format!("UF2 파일을 쓰지 못했습니다: {error}"))) + } +} + +impl Uf2Flasher { + pub fn macos() -> Self { + Self::new(MacVolumes) + } +} + +/// Seeed 코어가 번들한 adafruit-nrfutil 바이너리 경로를 찾는다. +fn find_adafruit_nrfutil() -> Result { + // 우선순위: Arduino15 패키지 폴더(Seeed 코어 설치 시 자동 생성) -> PATH + let home = std::env::var("HOME").unwrap_or_default(); + let bundled = + PathBuf::from(home).join("Library/Arduino15/packages/Seeeduino/tools/adafruit-nrfutil"); + if let Ok(entries) = fs::read_dir(&bundled) { + for entry in entries.flatten() { + let macos = entry.path().join("macos/adafruit-nrfutil"); + if macos.is_file() { + return Ok(macos); + } + } + } + // PATH에서도 탐색 (CLI 환경 대비) + if let Ok(path_var) = std::env::var("PATH") { + for dir in std::env::split_paths(&path_var) { + let candidate = dir.join("adafruit-nrfutil"); + if candidate.is_file() { + return Ok(candidate); + } + } + } + Err(FlashError::Uf2( + "adafruit-nrfutil을 찾지 못했습니다. Seeed nRF52 코어를 설치해 주세요.".to_owned(), + )) +} + +/// 부트로더에 시리얼 DFU로 Intel HEX 펌웨어를 올린다. +/// +/// Seeed nRF52 코어가 사용하는 `adafruit-nrfutil` 호출과 같은 형식으로 +/// HEX를 DFU zip으로 패키징한 뒤, 1200bps 터치로 진입한 부트로더에 올린다. +pub fn flash_serial_dfu(port_name: &str, hex_text: &str) -> Result<(), FlashError> { + use std::process::Command; + + let nrfutil = find_adafruit_nrfutil()?; + let work = std::env::temp_dir().join(format!("hanbeon-dfu-{}", std::process::id())); + let _ = fs::remove_dir_all(&work); + fs::create_dir_all(&work) + .map_err(|error| FlashError::Uf2(format!("작업 폴더 생성 실패: {error}")))?; + + let hex_path = work.join("firmware.hex"); + fs::write(&hex_path, hex_text) + .map_err(|error| FlashError::Uf2(format!("HEX 쓰기 실패: {error}")))?; + + // 애플리케이션만 패키징한다. `0x0123`은 Seeed XIAO nRF52840(Sense)의 + // boards.txt에 정의된 S140 7.3.0 SoftDevice firmware ID다. + let zip_path = work.join("firmware.zip"); + let package = Command::new(&nrfutil) + .args([ + "dfu", + "genpkg", + "--dev-type", + "0x0052", + "--sd-req", + "0x0123", + "--application", + &hex_path.to_string_lossy(), + &zip_path.to_string_lossy(), + ]) + .output() + .map_err(|error| FlashError::Uf2(format!("nrfutil 실행 실패: {error}")))?; + if !package.status.success() { + return Err(FlashError::Uf2(format!( + "DFU 패키징 실패: {}", + String::from_utf8_lossy(&package.stderr).trim() + ))); + } + + // 시리얼 DFU 업로드 (부트로더 모드 상태여야 한다) + let upload = Command::new(&nrfutil) + .args([ + "dfu", + "serial", + "-pkg", + &zip_path.to_string_lossy(), + "-p", + port_name, + "-b", + "115200", + "--singlebank", + ]) + .output() + .map_err(|error| FlashError::Uf2(format!("DFU 업로드 실행 실패: {error}")))?; + let _ = fs::remove_dir_all(&work); + if !upload.status.success() { + return Err(FlashError::Uf2(format!( + "DFU 업로드 실패: {}", + String::from_utf8_lossy(&upload.stderr).trim() + ))); + } + Ok(()) +} + +pub fn request_bootloader(port_name: &str) -> Result<(), FlashError> { + // TinyUSB CDC의 DFU 진입 조건은 3단계다: (1) 115200으로 CDC 세션 성립, + // (2) 1200bps로 레이트 변경, (3) DTR을 내리며 닫기. 1200으로 바로 열면 + // line_coding 변경 이벤트가 없어 부트로더로 들어가지 않는다. + for baud_rate in [115_200, 1200] { + let port = serialport::new(port_name, baud_rate) + .timeout(Duration::from_millis(500)) + .open() + .map_err(|error| { + FlashError::Uf2(format!("부트로더 요청 포트를 열지 못했습니다: {error}")) + })?; + thread::sleep(Duration::from_millis(500)); + drop(port); + } + Ok(()) } struct IhexRecord { @@ -234,6 +491,7 @@ pub enum FlashError { Sync, Protocol(String), Io(String), + Uf2(String), Cancelled, } @@ -246,6 +504,7 @@ impl std::fmt::Display for FlashError { ), Self::Protocol(detail) => write!(f, "부트로더 응답이 잘못되었습니다: {detail}"), Self::Io(detail) => write!(f, "시리얼 통신 오류: {detail}"), + Self::Uf2(detail) => write!(f, "UF2 업로드 오류: {detail}"), Self::Cancelled => write!(f, "설치가 취소되었습니다"), } } @@ -414,8 +673,11 @@ pub fn program( mod tests { use super::*; use std::collections::VecDeque; + use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; use std::sync::mpsc; + static TEMP_DIR_SEQUENCE: AtomicUsize = AtomicUsize::new(0); + /// Scripted fake port: asserts every write matches an expectation and /// produces queued responses, like a mock server for the bootloader. struct FakePort { @@ -477,6 +739,91 @@ mod tests { const OK_RESPONSE: [u8; 2] = [STK_INSYNC, STK_OK]; const SIGNATURE_RESPONSE: [u8; 5] = [STK_INSYNC, 0x1E, 0x95, 0x0F, STK_OK]; + struct FakeDriveProbe { + volumes: Vec, + } + + impl DriveProbe for FakeDriveProbe { + fn volumes(&self) -> Vec { + self.volumes.clone() + } + } + + fn test_dir(tag: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "hanbeon-uf2-test-{tag}-{}-{}", + std::process::id(), + TEMP_DIR_SEQUENCE.fetch_add(1, AtomicOrdering::Relaxed) + )); + let _ = fs::remove_dir_all(&path); + fs::create_dir_all(&path).expect("create temporary volume"); + path + } + + fn word(block: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(block[offset..offset + 4].try_into().expect("u32 word")) + } + + #[test] + fn family_for_fqbn_selects_stk500_and_uf2() { + assert_eq!( + family_for_fqbn("arduino:avr:uno"), + BootloaderFamily::Stk500v1 + ); + assert_eq!( + family_for_fqbn("Seeeduino:nrf52:xiaonRF52840Sense"), + BootloaderFamily::Uf2 + ); + assert_eq!( + family_for_fqbn("other:platform:board"), + BootloaderFamily::Stk500v1 + ); + } + + #[test] + fn encode_uf2_has_expected_headers_and_payloads() { + let image = FlashImage::from_ihex(":020000040001F9\n:02000000AABB99\n:00000001FF\n") + .expect("parse image"); + let encoded = encode_uf2(&image, 0x621E_11BE); + assert_eq!(encoded.len(), UF2_BLOCK_SIZE); + let block = &encoded[..UF2_BLOCK_SIZE]; + assert_eq!(word(block, 0), UF2_MAGIC_START0); + assert_eq!(word(block, 4), UF2_MAGIC_START1); + assert_eq!(word(block, 8), UF2_FLAGS_FAMILY_ID_PRESENT); + assert_eq!(word(block, 12), 0x0001_0000); + assert_eq!(word(block, 16), UF2_PAYLOAD_SIZE as u32); + assert_eq!(word(block, 20), 0); + assert_eq!(word(block, 24), 1); + assert_eq!(word(block, 28), 0x621E_11BE); + assert_eq!(&block[32..34], &[0xAA, 0xBB]); + assert!(block[34..508].iter().all(|byte| *byte == 0)); + assert_eq!(word(block, 508), UF2_MAGIC_END); + } + + #[test] + fn uf2_flasher_writes_matched_volume_and_rejects_ambiguous_drives() { + let volume = test_dir("matched"); + let flasher = Uf2Flasher::new(FakeDriveProbe { + volumes: vec![volume.clone()], + }); + flasher.flash(b"UF2").expect("write UF2"); + assert_eq!( + fs::read(volume.join("hanbeon.uf2")).expect("read UF2"), + b"UF2" + ); + + let zero = Uf2Flasher::new(FakeDriveProbe { volumes: vec![] }); + assert!(matches!(zero.flash(b"UF2"), Err(FlashError::Uf2(_)))); + + let second = test_dir("second"); + let multiple = Uf2Flasher::new(FakeDriveProbe { + volumes: vec![volume.clone(), second.clone()], + }); + assert!(matches!(multiple.flash(b"UF2"), Err(FlashError::Uf2(_)))); + let _ = fs::remove_dir_all(volume); + let _ = fs::remove_dir_all(second); + } + #[test] fn ihex_parses_data_eof_and_extended_addresses() { let hex = ":100000000C9434000C9451000C9451000C94510049\n:100010000C9451000C9451000C9451000C9451001C\n:020000020001FB\n:00000001FF\n"; diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 63b4aac..ce6e2b3 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -261,6 +261,15 @@ pub fn run() { arduino::ArduinoCoordinator::new(spawn_switch) }; app.manage(native_switch); + let ble_app = app.handle().clone(); + let ble_detector = Arc::clone(&detector); + let ble_scanner = scanner.clone(); + app.manage(arduino::BleSwitch::spawn(move |event| { + arduino::route_switch_event(&ble_detector, event, Instant::now(), |judgement| { + input::announce(&ble_app, judgement); + ble_scanner.handle(&ble_app, judgement); + }); + })); app.manage(firmware::FirmwareInstaller::default()); let registered = input::register( diff --git a/apps/desktop/src-tauri/src/registry.rs b/apps/desktop/src-tauri/src/registry.rs index 88aed58..329dd24 100644 --- a/apps/desktop/src-tauri/src/registry.rs +++ b/apps/desktop/src-tauri/src/registry.rs @@ -1,4 +1,4 @@ -//! Hana Cloud (https://github.com/dev-five-git/hana-cloud) board registry client. +//! Hana Cloud (https://github.com/dev-five-git/hana-cloud) board-registry client. //! //! Downloads and verifies the root index, board manifests, and firmware files. //! The contract from the registry README is binding: @@ -604,6 +604,29 @@ mod tests { .to_vec() } + fn xiao_index() -> Vec { + br#"{ + "schemaVersion": 1, + "revision": 4, + "apps": [], + "boards": [{ + "id": "seeed.xiao-nrf52840-sense", + "name": "Seeed XIAO nRF52840 Sense", + "manifest": "boards/seeed-xiao-nrf52840-sense.json", + "sha256": "f3842b609b55f75cb00f2a2fda3e12cb2f11b0f24c9b793aa12dd8565ea9889b", + "detect": {"usb": [ + {"vid": "2886", "pid": "8045", "confidence": "exact", + "manufacturerAliases": ["Seeed Studio"], + "productAliases": ["XIAO nRF52840 Sense"]}, + {"vid": "2886", "pid": "0045", "confidence": "exact", + "manufacturerAliases": ["Seeed Studio"], + "productAliases": ["XIAO nRF52840 Sense"]} + ]} + }] + }"# + .to_vec() + } + #[test] fn normalization_matches_registry_contract_vectors() { assert_eq!(normalize_descriptor(" Arduino UNO_R3 "), "arduino-uno-r3"); @@ -645,6 +668,25 @@ mod tests { assert_eq!(matched.manifest_path, "boards/arduino-uno-r3.json"); } + #[test] + fn xiao_bootloader_and_application_usb_ids_match_exactly() { + let index = parse_index(&xiao_index()).expect("parse XIAO index"); + for pid in [0x8045, 0x0045] { + let matched = match_board( + &index, + &UsbIdentity { + vid: 0x2886, + pid, + product: Some("XIAO nRF52840 Sense".to_owned()), + manufacturer: Some("Seeed Studio".to_owned()), + }, + ) + .expect("XIAO must match"); + assert_eq!(matched.board_id, "seeed.xiao-nrf52840-sense"); + assert_eq!(matched.confidence, Confidence::Exact); + } + } + #[test] fn descriptor_disagreement_demotes_but_keeps_candidate() { let index = parse_index(&uno_index()).unwrap(); @@ -803,6 +845,10 @@ mod tests { .to_owned() }; assert_eq!(host_of(REGISTRY_BASE), "raw.githubusercontent.com"); + assert_eq!( + REGISTRY_BASE, + "https://raw.githubusercontent.com/dev-five-git/hana-cloud/main" + ); assert_ne!( host_of("https://evil.example/registry.json"), host_of(REGISTRY_BASE) diff --git a/apps/desktop/src/__tests__/arduino-firmware.browser.test.ts b/apps/desktop/src/__tests__/arduino-firmware.browser.test.ts index 95814c1..72afc4b 100644 --- a/apps/desktop/src/__tests__/arduino-firmware.browser.test.ts +++ b/apps/desktop/src/__tests__/arduino-firmware.browser.test.ts @@ -62,11 +62,11 @@ describe('Arduino firmware lifecycle contract', () => { expect(phases.every(firmwareOwnsPort)).toBe(true) }) - test('uses the requested connect copy while searching', () => { + test('uses board-neutral copy while searching', () => { expect(firmwareStatusText({ state: 'searching' })).toBe( - 'Arduino Uno를 USB로 연결해 주세요', + '지원되는 버튼 보드를 USB로 연결해 주세요', ) - expect(FIRMWARE_COPY.startTitle).toBe('한번을 Arduino 버튼과 연결해 볼게요') + expect(FIRMWARE_COPY.startTitle).toBe('한번을 버튼 보드와 연결해 볼게요') expect(FIRMWARE_COPY.startAction).toBe('시작하기') }) @@ -123,7 +123,9 @@ describe('Arduino firmware lifecycle contract', () => { }) test('maps retryable installer failures without raw port names', () => { - expect(firmwareErrorText('notFound')).toBe('Arduino를 찾지 못했습니다') + expect(firmwareErrorText('notFound')).toBe( + '지원되는 버튼 보드를 찾지 못했습니다', + ) expect(firmwareErrorText('portUnavailable')).toBe( '포트를 사용할 수 없습니다', ) @@ -151,7 +153,7 @@ describe('Arduino firmware lifecycle contract', () => { code: 'notFound', retryable: false, }), - ).toBe('Arduino를 찾지 못했습니다') + ).toBe('지원되는 버튼 보드를 찾지 못했습니다') expect(firmwareErrorText('raw stderr')).toBe( '펌웨어 설치 중 문제가 발생했습니다', ) diff --git a/apps/desktop/src/__tests__/arduino-setup.browser.test.tsx b/apps/desktop/src/__tests__/arduino-setup.browser.test.tsx index 6ac7593..65b9e67 100644 --- a/apps/desktop/src/__tests__/arduino-setup.browser.test.tsx +++ b/apps/desktop/src/__tests__/arduino-setup.browser.test.tsx @@ -267,13 +267,13 @@ describe('Arduino firmware setup screens', () => { }) expect(textOf(view.container)).toInclude( - 'Arduino Uno를 USB로 연결해 주세요', + '지원되는 버튼 보드를 USB로 연결해 주세요', ) expect( view.container.querySelector('[data-state]')?.getAttribute('data-state'), ).toBe('searching') expect(view.container.querySelector('output')?.textContent).toInclude( - 'Arduino Uno를 USB로 연결해 주세요', + '지원되는 버튼 보드를 USB로 연결해 주세요', ) }) @@ -283,7 +283,7 @@ describe('Arduino firmware setup screens', () => { candidates: [UNO], }) - expect(textOf(view.container)).toInclude('Arduino Uno를 찾았습니다') + expect(textOf(view.container)).toInclude('지원되는 버튼 보드를 찾았습니다') expect(textOf(view.container)).toInclude('Arduino Uno') expect(textOf(view.container)).toInclude('2341:0043') expect(textOf(view.container)).not.toInclude('/dev/cu.usbmodem1401') @@ -300,9 +300,7 @@ describe('Arduino firmware setup screens', () => { candidates: [UNO, UNO_TWO], }) - expect(textOf(view.container)).toInclude( - '연결할 Arduino Uno를 선택해 주세요', - ) + expect(textOf(view.container)).toInclude('연결할 버튼 보드를 선택해 주세요') expect(buttonNamed(view.container, '다음')?.hasAttribute('disabled')).toBe( true, ) diff --git a/apps/desktop/src/lib/firmware.ts b/apps/desktop/src/lib/firmware.ts index 4688a0f..3cc5add 100644 --- a/apps/desktop/src/lib/firmware.ts +++ b/apps/desktop/src/lib/firmware.ts @@ -55,15 +55,15 @@ export type FirmwareState = export const INITIAL_FIRMWARE_STATE: FirmwareState = { state: 'idle' } export const FIRMWARE_COPY = { - startTitle: '한번을 Arduino 버튼과 연결해 볼게요', + startTitle: '한번을 버튼 보드와 연결해 볼게요', suppliesHeading: '준비물:', - supplyUno: 'Arduino Uno R3', + supplyUno: '지원되는 버튼 보드', supplyUsb: 'USB 케이블', supplyButton: '연결된 아케이드 버튼', startAction: '시작하기', - connect: 'Arduino Uno를 USB로 연결해 주세요', - found: 'Arduino Uno를 찾았습니다', - chooseBoard: '연결할 Arduino Uno를 선택해 주세요', + connect: '지원되는 버튼 보드를 USB로 연결해 주세요', + found: '지원되는 버튼 보드를 찾았습니다', + chooseBoard: '연결할 버튼 보드를 선택해 주세요', continue: '다음', alreadyInstalled: '전용 펌웨어가 이미 설치되어 있습니다', confirmNeed: '버튼을 사용하려면 전용 펌웨어를 설치해야 합니다.', @@ -75,11 +75,11 @@ export const FIRMWARE_COPY = { later: '나중에 하기', retry: '다시 시도', installing: '설치가 진행 중', - unoOnly: '이 설치는 공식 Arduino Uno R3만 지원합니다.', + unoOnly: '등록된 버튼 보드의 전용 펌웨어를 설치합니다.', } as const export const FIRMWARE_ERROR_COPY: Record = { - notFound: 'Arduino를 찾지 못했습니다', + notFound: '지원되는 버튼 보드를 찾지 못했습니다', portUnavailable: '포트를 사용할 수 없습니다', downloadFailed: '펌웨어를 내려받지 못했습니다', uploadFailed: '펌웨어 전송에 실패했습니다', diff --git a/bun.lock b/bun.lock index a348aac..44f85a1 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - "lockfileVersion": 2, + "lockfileVersion": 1, "configVersion": 1, "workspaces": { "": { @@ -91,21 +91,21 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@devup-ui/bun-plugin": ["@devup-ui/bun-plugin@1.0.12", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.8", "@devup-ui/wasm": "^1.0.75" } }, "sha512-V5eJqpy3oFinm0TZfnD7hBQsMBh1vfSM9Kb/cOwmXQUrqV+YKv8ThA/nUKS7zKGMJhaafAbKJeLtALEnR2ZE1Q=="], + "@devup-ui/bun-plugin": ["@devup-ui/bun-plugin@1.0.15", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.13", "@devup-ui/wasm": "^1.0.78" } }, "sha512-RE2cWF6OUK8s5ZDfHpVJakC1vc35ThVF0K678e0PTysJ2tFkJvTlSNCX5Y8L4QJcIMW1L9hzUprtqCLTwJ3EqQ=="], - "@devup-ui/eslint-plugin": ["@devup-ui/eslint-plugin@1.0.16", "", { "dependencies": { "@typescript-eslint/utils": "^8.60", "typescript-eslint": "^8.60" } }, "sha512-gXhEVO9c4qGfR6HcCXsnRZHZlepDBZ1BnA0M2pB1/9asXSqWoJmt75xE0beXtt7wgrBHO/Z5gh+iX8Xu3e2ewQ=="], + "@devup-ui/eslint-plugin": ["@devup-ui/eslint-plugin@1.0.18", "", { "dependencies": { "@typescript-eslint/utils": "^8.67", "typescript-eslint": "^8.67" } }, "sha512-NnJcj9ecAnIpz0/CLSsNkGgKhctN+nTUSMda8oDMLMiSEoTLuOngpZweyqsVwkUsFAXKZIlB8vTnioV7jtmrSg=="], - "@devup-ui/next-plugin": ["@devup-ui/next-plugin@1.0.80", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.8", "@devup-ui/wasm": "^1.0.75", "@devup-ui/webpack-plugin": "^1.0.61" }, "peerDependencies": { "next": "*" } }, "sha512-NmMPIVQylVH1l5YZqZsjdPWQZhGc2HFCDhLDTGPOfaB4qm4q45iZGsuGk4j7fyf6cMg14BFAQWIr74UmJyZLYw=="], + "@devup-ui/next-plugin": ["@devup-ui/next-plugin@1.0.83", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.13", "@devup-ui/wasm": "^1.0.78", "@devup-ui/webpack-plugin": "^1.0.66" }, "peerDependencies": { "next": "*" } }, "sha512-H3s5aRbhuz91G5UgXUlcdDCgKrbdsNowKQ47dSNGHH45fAvCeXDj0W8PKx3sfc8nzwDKX79WOIHR1KnQvUZlcg=="], - "@devup-ui/plugin-utils": ["@devup-ui/plugin-utils@1.0.10", "", {}, "sha512-jA8RDae2BXAN0Xsm0JPG7NZvlwsf8iRP2q1Do9G5V9zKZ5mO+qGUd/MVvYc8Xk4A6Av+OsPHDnctdyXC7VBXQw=="], + "@devup-ui/plugin-utils": ["@devup-ui/plugin-utils@1.0.13", "", {}, "sha512-fV0KoD4wgKDTFcXcaBSSBznylg81KJEayAVrirKYWpTeDKaeiAsVo58MU5+N+K4PL1T8bay2yHf7WKnlCgmPTg=="], - "@devup-ui/react": ["@devup-ui/react@1.0.37", "", { "dependencies": { "csstype-extra": "latest", "react": "^19.2" } }, "sha512-zeHO2ke7X5vnM8w9vl4knDmXameG0X8OCb5E+qZPS2G4tsFJ98B3LKhioHTtnTs8YxxFaErRjUoeXylG4AiMpg=="], + "@devup-ui/react": ["@devup-ui/react@1.0.40", "", { "dependencies": { "csstype-extra": "latest", "react": "^19.2" } }, "sha512-Ffvl2hIbuLMQbWznAgZ/98NMxiDIh3xjh6XA7F6MPvVS8/PVw6nc8I9/1fdQe6zgx2Av6zYJM5axsOT+cQYgOA=="], - "@devup-ui/reset-css": ["@devup-ui/reset-css@1.0.24", "", { "dependencies": { "@devup-ui/react": "^1.0.36" } }, "sha512-yz2Pkbh5KyhqvHExajmXkwVUTBhh64XN4TyE6jgs7gogYE7ab8glPHtsBPEARTIPhK0MjLorDJswNVdMrbDw7w=="], + "@devup-ui/reset-css": ["@devup-ui/reset-css@1.0.27", "", { "dependencies": { "@devup-ui/react": "^1.0.40" } }, "sha512-Q9f9crLgRNTcJSLIud7K7d9A6FW8kYMTt4O/PyaQujacdbw0vom3WyH3u5jStEh+MVjGnxOkOurPQghv22RH+w=="], - "@devup-ui/wasm": ["@devup-ui/wasm@1.0.75", "", {}, "sha512-MqANK1YxKqrYYxpFN8jb89nVzbLOGrlgh/oshfCwm9VaMFdx6qbWxAETlkHkXmkOp5UAhFOlgJbFLVm0MT1t2g=="], + "@devup-ui/wasm": ["@devup-ui/wasm@1.0.78", "", {}, "sha512-Ue6Gbw2o102kpQ5usa6eAHvNudUuxICtGtTUljs9MtOI2Fyvhvmm/OuNpCqBZWFudq5dzK/8ckfetcbDVtiq1g=="], - "@devup-ui/webpack-plugin": ["@devup-ui/webpack-plugin@1.0.63", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.8", "@devup-ui/wasm": "^1.0.75" } }, "sha512-7UHELv3afXhTiqNotZcNWD0wfKN9nC4HHGlT0RmKdpoRkNhcMnaffOCWQuD+61Q6dakhwyoF7V4+/pEB8fkFlw=="], + "@devup-ui/webpack-plugin": ["@devup-ui/webpack-plugin@1.0.66", "", { "dependencies": { "@devup-ui/plugin-utils": "^1.0.13", "@devup-ui/wasm": "^1.0.78" } }, "sha512-eru6WOgsf55/sb6RSfQiphSi+2SDYzGx/nqc27jTkP2TT2A3bRAOycgAXFceRclf/vPKthe4l+cs9j7PehSemQ=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -293,7 +293,7 @@ "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], - "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.102.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.6.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-PQ3mXSpRWFC22j3yCOJEX6pCFiPjVuHTUVpVKq0pG2rwOQOuiKvkggEo6mYE/IooN/cstso1abM1LSfitL60WQ=="], + "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.102.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.6.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-0uRcRXvrZN3JLtGDMXU8Qp/or/fp1VhDZEVcD8WrUc7CF0uWbJbloJ88dfFDhiocYP31wXX4Buar/WC31fS5Ug=="], "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], @@ -387,7 +387,7 @@ "@typescript/native": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@typescript/old": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "@typescript/old": ["@typescript/typescript6@6.0.2", "", { "dependencies": { "@typescript/old": "npm:typescript@^6" }, "bin": { "tsc6": "bin/tsc6" } }, "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], @@ -501,7 +501,7 @@ "csstype-extra": ["csstype-extra@0.1.35", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-pEJ+ZP1nE0TT5E5XXRuNBiU44O2CCwgm1zd0ogXOX/We8GxX++T7KhThrQaSO2V2Yvdmh2OsqBWLH0+OQTUXKA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -519,7 +519,7 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.412", "", {}, "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.413", "", {}, "sha512-F1XPKvt7HVfly5WND90ec16nFsdr4g5x/cVUP3EqjeyXynupabGDqpMa84wwvuYGDnldXLBz6DLXyZXWO9TPvw=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -857,7 +857,7 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -891,7 +891,7 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "supports-color": ["supports-color@9.4.0", "", {}, "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw=="], @@ -997,8 +997,6 @@ "@npmcli/promise-spawn/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - "@testing-library/jest-dom/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], From 1b8831710ffdc0010ae0b71f33bb47014c400096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Thu, 27 Aug 2026 01:12:24 +0900 Subject: [PATCH 2/3] fix(arduino): retain rebased XIAO installer lint cleanliness --- apps/desktop/src-tauri/src/firmware.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/firmware.rs b/apps/desktop/src-tauri/src/firmware.rs index 6084530..5de7ad4 100644 --- a/apps/desktop/src-tauri/src/firmware.rs +++ b/apps/desktop/src-tauri/src/firmware.rs @@ -24,7 +24,6 @@ pub const EVENT_FIRMWARE: &str = "arduino://firmware"; /// ATmega328P의 SPM 페이지 크기(optiboot가 한 번에 쓰는 단위). const FLASH_PAGE_BYTES: usize = 128; /// Uno R3 bootloader 영역(512 bytes)을 제외한 애플리케이션 플래시 한계. -const UNO_APPLICATION_BYTES: u32 = 32_256; /// optiboot 부팅 창을 몇 번까지 기다려 볼지. DTR 리셋 후 부트로더가 잠깐만 /// 열리므로 arduino-cli의 재시도 동작을 그대로 옮긴다. const SYNC_ATTEMPTS: usize = 12; @@ -632,8 +631,8 @@ fn flash_with_retry( } let flash_result = match family { BootloaderFamily::Stk500v1 => { - let port = open_flash_port(&active_candidate.port) - .map_err(InstallFailure::Upload)?; + let port = + open_flash_port(&active_candidate.port).map_err(InstallFailure::Upload)?; let mut flash_port = Box::new(port) as Box; let mut written_bytes = 0usize; let result = flasher::synchronize(flash_port.as_mut(), SYNC_ATTEMPTS, cancelled) From cb0305446a224526c7ceeaab915dc66d200c1677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9E=AC=ED=98=84?= Date: Thu, 27 Aug 2026 11:46:07 +0900 Subject: [PATCH 3/3] fix(build): use TypeScript CLI with Next --- apps/admin/next.config.ts | 6 +++--- apps/admin/package.json | 2 +- apps/desktop/next.config.ts | 6 +++--- apps/desktop/package.json | 2 +- bun.lock | 10 ++++------ package.json | 2 +- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/apps/admin/next.config.ts b/apps/admin/next.config.ts index 8b6b3b5..4119564 100644 --- a/apps/admin/next.config.ts +++ b/apps/admin/next.config.ts @@ -4,9 +4,9 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { optimizePackageImports: ['@devup-ui/reset-css'], - // TypeScript 7 has no compiler API yet. Next and typescript-eslint use the - // official TypeScript 6 compatibility package until the 7.1 API lands. - useTypeScriptCli: false, + // TypeScript 7 has no compiler API yet. Use Next's CLI bridge through the + // TypeScript 6 compatibility package until the 7.1 API lands. + useTypeScriptCli: true, }, reactCompiler: true, } diff --git a/apps/admin/package.json b/apps/admin/package.json index 65cef76..156694d 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -21,6 +21,6 @@ "@types/react": "^19", "@typescript/native": "npm:typescript@^7.0.2", "babel-plugin-react-compiler": "^1.0.0", - "typescript": "npm:@typescript/typescript6@^6.0.2" + "typescript": "6.0.3" } } diff --git a/apps/desktop/next.config.ts b/apps/desktop/next.config.ts index dd2285a..1d900ab 100644 --- a/apps/desktop/next.config.ts +++ b/apps/desktop/next.config.ts @@ -11,9 +11,9 @@ const nextConfig: NextConfig = { }, experimental: { optimizePackageImports: ['@devup-ui/reset-css'], - // TypeScript 7 has no compiler API yet. Next and typescript-eslint use the - // official TypeScript 6 compatibility package until the 7.1 API lands. - useTypeScriptCli: false, + // TypeScript 7 has no compiler API yet. Use Next's CLI bridge through the + // TypeScript 6 compatibility package until the 7.1 API lands. + useTypeScriptCli: true, }, reactCompiler: true, } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e898f23..8e86760 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,6 +23,6 @@ "@types/react": "^19", "@typescript/native": "npm:typescript@^7.0.2", "babel-plugin-react-compiler": "^1.0.0", - "typescript": "npm:@typescript/typescript6@^6.0.2" + "typescript": "6.0.3" } } diff --git a/bun.lock b/bun.lock index 44f85a1..d38a796 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "eslint-plugin-devup": "^2.1.0", "husky": "^9.1", "oxlint": "1.79", - "typescript": "npm:@typescript/typescript6@^6.0.2", + "typescript": "6.0.3", }, }, "apps/admin": { @@ -30,7 +30,7 @@ "@types/react": "^19", "@typescript/native": "npm:typescript@^7.0.2", "babel-plugin-react-compiler": "^1.0.0", - "typescript": "npm:@typescript/typescript6@^6.0.2", + "typescript": "6.0.3", }, }, "apps/desktop": { @@ -50,7 +50,7 @@ "@types/react": "^19", "@typescript/native": "npm:typescript@^7.0.2", "babel-plugin-react-compiler": "^1.0.0", - "typescript": "npm:@typescript/typescript6@^6.0.2", + "typescript": "6.0.3", }, }, }, @@ -387,8 +387,6 @@ "@typescript/native": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@typescript/old": ["@typescript/typescript6@6.0.2", "", { "dependencies": { "@typescript/old": "npm:typescript@^6" }, "bin": { "tsc6": "bin/tsc6" } }, "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w=="], - "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -913,7 +911,7 @@ "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], - "typescript": ["@typescript/typescript6@6.0.2", "", { "dependencies": { "@typescript/old": "npm:typescript@^6" }, "bin": { "tsc6": "bin/tsc6" } }, "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "typescript-eslint": ["typescript-eslint@8.67.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.67.0", "@typescript-eslint/parser": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0", "@typescript-eslint/utils": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg=="], diff --git a/package.json b/package.json index c720779..95b002f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@devup-ui/bun-plugin": "^1.0.12", "@types/bun": "^1.4", "@typescript/native": "npm:typescript@^7.0.2", - "typescript": "npm:@typescript/typescript6@^6.0.2" + "typescript": "6.0.3" }, "workspaces": [ "apps/*"