diff --git a/.gitignore b/.gitignore index 915bf3e7..61bc5b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,11 @@ Package.StoreAssociation.xml **/fastlane/report.xml **/fastlane/screenshots **/fastlane/test_output + +# Visual Studio project upgrade artifacts +UpgradeLog*.htm +_UpgradeReport_Files/ +Backup*/ + +# Local-only debug captures (DebugView dumps, browser console logs, etc.) +ref/ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 43748a8b..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,497 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [2.1.0-develop.10](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.9...v2.1.0-develop.10) (2023-09-25) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.10 ([911bbba](https:/home/circleci/project/semantic-release-remote/commit/911bbbab8a4ff484a450e688b5953a3fd3a4e7d3)) -* **deps:** update dependency scratch-vm to v1.6.11 ([5544897](https:/home/circleci/project/semantic-release-remote/commit/5544897487c66def49af50c1a07e65c75af86335)) -* **deps:** update dependency scratch-vm to v1.6.12 ([5948b02](https:/home/circleci/project/semantic-release-remote/commit/5948b02473773f33f581a9731a28f86a26612881)) -* **deps:** update dependency scratch-vm to v1.6.13 ([31bc061](https:/home/circleci/project/semantic-release-remote/commit/31bc061932429a387a607256d6a37355ea6bf6a2)) -* **deps:** update dependency scratch-vm to v1.6.14 ([1323ac0](https:/home/circleci/project/semantic-release-remote/commit/1323ac0d39ea4f68bcd0a39621cae87ce1e42507)) -* **deps:** update dependency scratch-vm to v1.6.15 ([9d9a801](https:/home/circleci/project/semantic-release-remote/commit/9d9a8014727aa23601e42292d3d6f423eb164f53)) -* **deps:** update dependency scratch-vm to v1.6.16 ([b2d429e](https:/home/circleci/project/semantic-release-remote/commit/b2d429e426416ebef6e32183df68fbae8dbb2c3c)) -* **deps:** update dependency scratch-vm to v1.6.17 ([239e35a](https:/home/circleci/project/semantic-release-remote/commit/239e35a28f27ce7686a36831cf8f098954e1a1be)) -* **deps:** update dependency scratch-vm to v1.6.18 ([9bb27fa](https:/home/circleci/project/semantic-release-remote/commit/9bb27fa0c5d5d74cc8d9e1486cb130fff27b7a74)) -* **deps:** update dependency scratch-vm to v1.6.19 ([8a72117](https:/home/circleci/project/semantic-release-remote/commit/8a7211749f93ad2b1b9f563dc19d8a109ab33090)) - -# [2.1.0-develop.9](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.8...v2.1.0-develop.9) (2023-09-09) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.9 ([d6db186](https:/home/circleci/project/semantic-release-remote/commit/d6db1861a274be97474a4a0a3f89ec4a51b2dde6)) - -# [2.1.0-develop.8](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.7...v2.1.0-develop.8) (2023-09-02) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.8 ([e0f852d](https:/home/circleci/project/semantic-release-remote/commit/e0f852d8eb165e93cb181cc36c84bd0c21ccb27a)) - -# [2.1.0-develop.7](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.6...v2.1.0-develop.7) (2023-08-30) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.7 ([21a5e3d](https:/home/circleci/project/semantic-release-remote/commit/21a5e3d69882a6f1f683d5b84ca4cde08b48cb10)) - -# [2.1.0-develop.6](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.5...v2.1.0-develop.6) (2023-08-28) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.6 ([9365bad](https:/home/circleci/project/semantic-release-remote/commit/9365bad80e094d09a55e12f0fc1cd299714745be)) - -# [2.1.0-develop.5](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.4...v2.1.0-develop.5) (2023-08-27) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.5 ([97159bb](https:/home/circleci/project/semantic-release-remote/commit/97159bbabae791f1848795765444b414221dfc9d)) - -# [2.1.0-develop.4](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.3...v2.1.0-develop.4) (2023-08-23) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.4 ([78f868b](https:/home/circleci/project/semantic-release-remote/commit/78f868b724a0e3bcc28e80f7d4e82965f461c95d)) - -# [2.1.0-develop.3](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.2...v2.1.0-develop.3) (2023-08-21) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.2 ([9cb3de7](https:/home/circleci/project/semantic-release-remote/commit/9cb3de7d5589a7e4b68e984655b5173dadabed24)) -* **deps:** update dependency scratch-vm to v1.6.3 ([c325210](https:/home/circleci/project/semantic-release-remote/commit/c325210a8c8cdf4f94896d39998bbddf2fc1b9f9)) - -# [2.1.0-develop.2](https:/home/circleci/project/semantic-release-remote/compare/v2.1.0-develop.1...v2.1.0-develop.2) (2023-08-09) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.6.1 ([eeba9f3](https:/home/circleci/project/semantic-release-remote/commit/eeba9f36fef825e1cbd02322db488cc75f467049)) - -# [2.1.0-develop.1](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.8...v2.1.0-develop.1) (2023-08-08) - - -### Features - -* **deps:** update dependency scratch-vm to v1.6.0 ([f85e72c](https:/home/circleci/project/semantic-release-remote/commit/f85e72c48afbe1f4ae95b83f8166b6b0afd06751)) - -## [2.0.3-develop.8](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.7...v2.0.3-develop.8) (2023-08-08) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.98 ([42d8912](https:/home/circleci/project/semantic-release-remote/commit/42d8912b824ed9c18f9a5585050df77d77057d8f)) - -## [2.0.3-develop.7](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.6...v2.0.3-develop.7) (2023-08-06) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.97 ([490155c](https:/home/circleci/project/semantic-release-remote/commit/490155c249ea5495081d16681540feec5471be88)) - -## [2.0.3-develop.6](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.5...v2.0.3-develop.6) (2023-08-05) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.96 ([9a46cbd](https:/home/circleci/project/semantic-release-remote/commit/9a46cbdd45d6f0955d070e5b1c2c75e343deef6a)) - -## [2.0.3-develop.5](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.4...v2.0.3-develop.5) (2023-08-04) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.95 ([71a0b81](https:/home/circleci/project/semantic-release-remote/commit/71a0b81b456b5b60a84011a76c209a1074776697)) - -## [2.0.3-develop.4](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.3...v2.0.3-develop.4) (2023-08-03) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.94 ([414055a](https:/home/circleci/project/semantic-release-remote/commit/414055a3cd3824379dde09c5a34a41658b372bc9)) - -## [2.0.3-develop.3](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.2...v2.0.3-develop.3) (2023-08-02) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.93 ([cd024c6](https:/home/circleci/project/semantic-release-remote/commit/cd024c6698914ef7ff8854f10753ff2ff13f2e8c)) - -## [2.0.3-develop.2](https:/home/circleci/project/semantic-release-remote/compare/v2.0.3-develop.1...v2.0.3-develop.2) (2023-08-01) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.92 ([26f1c6a](https:/home/circleci/project/semantic-release-remote/commit/26f1c6ae250ed240a5159a20a634264bfd98480b)) - -## [2.0.3-develop.1](https:/home/circleci/project/semantic-release-remote/compare/v2.0.2...v2.0.3-develop.1) (2023-07-27) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.43 ([b75b289](https:/home/circleci/project/semantic-release-remote/commit/b75b2896a580e76282344e8b5c6ee404de85adfa)) -* **deps:** update dependency scratch-vm to v1.5.91 ([524712f](https:/home/circleci/project/semantic-release-remote/commit/524712fa1716ac952f984511c18ab8d53f2155ae)) -* **win:** apply the calculated version number to packaging output ([57a6ebc](https:/home/circleci/project/semantic-release-remote/commit/57a6ebc1df4e7bf32e599e4b6a93b149f3f34053)) -* **WinBLE:** disconnect event handlers during session dispose ([3b15ac5](https:/home/circleci/project/semantic-release-remote/commit/3b15ac5e3988b6596eb1c3d6417dca89e68a38f8)) -* **Win:** fix sizing problem in context menu ([c35c934](https:/home/circleci/project/semantic-release-remote/commit/c35c93496a58349c0fd9b0341a23a94cb4107e36)) - -## [2.0.2-develop.4](https:/home/circleci/project/semantic-release-remote/compare/v2.0.2-develop.3...v2.0.2-develop.4) (2023-07-26) - - -### Bug Fixes - -* **win:** apply the calculated version number to packaging output ([57a6ebc](https:/home/circleci/project/semantic-release-remote/commit/57a6ebc1df4e7bf32e599e4b6a93b149f3f34053)) - -## [2.0.2-develop.3](https:/home/circleci/project/semantic-release-remote/compare/v2.0.2-develop.2...v2.0.2-develop.3) (2023-07-05) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.43 ([b75b289](https:/home/circleci/project/semantic-release-remote/commit/b75b2896a580e76282344e8b5c6ee404de85adfa)) - -## [2.0.2-develop.2](https:/home/circleci/project/semantic-release-remote/compare/v2.0.2-develop.1...v2.0.2-develop.2) (2023-06-09) - - -### Bug Fixes - -* **Win:** fix sizing problem in context menu ([c35c934](https:/home/circleci/project/semantic-release-remote/commit/c35c93496a58349c0fd9b0341a23a94cb4107e36)) - -## [2.0.2-develop.1](https:/home/circleci/project/semantic-release-remote/compare/v2.0.1...v2.0.2-develop.1) (2023-06-08) - - -### Bug Fixes - -* **WinBLE:** disconnect event handlers during session dispose ([3b15ac5](https:/home/circleci/project/semantic-release-remote/commit/3b15ac5e3988b6596eb1c3d6417dca89e68a38f8)) - -## [2.0.1](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0...v2.0.1) (2023-05-25) - - -### Bug Fixes - -* **mac:** even more versioning fixes ([07c035d](https:/home/circleci/project/semantic-release-remote/commit/07c035dea84a16ce02368cc1bb5f7f32ff1b3885)) - -# [2.0.0](https:/home/circleci/project/semantic-release-remote/compare/v1.4.0...v2.0.0) (2023-05-25) - - -### Bug Fixes - -* always call context.completeRequest, even when not returning a value ([9cabb03](https:/home/circleci/project/semantic-release-remote/commit/9cabb03495b089cdc23fb257a0c3fea7e603c225)) -* **build:** fix missing CFBundleVersion in Safari extension ([7a67ea1](https:/home/circleci/project/semantic-release-remote/commit/7a67ea18025396c26a359f435a2c1cb1ed7ab8c3)) -* calculate build number from label ([2eb8015](https:/home/circleci/project/semantic-release-remote/commit/2eb8015d457263111219f2fc4a5e7d5505c6efb4)) -* **ci:** speculative fix for Homebrew failing on CI ([4b12ce4](https:/home/circleci/project/semantic-release-remote/commit/4b12ce4d1501eaea92ee76c1e11fc808ccc7ad11)) -* **ci:** update VS Mac installer script for 17.4 ([9221e1e](https:/home/circleci/project/semantic-release-remote/commit/9221e1e68a0c5cb8e35777c914fd9e17e954a5d7)) -* **common:** make session immediately so we don't miss the first message ([d53d5c8](https:/home/circleci/project/semantic-release-remote/commit/d53d5c8a9dd02563c5e75208cfc7125386d5f85a)) -* **common:** remove `EventAwaiter(EventHandler, ...` ([9032a01](https:/home/circleci/project/semantic-release-remote/commit/9032a013c8b5dc967f5a53a50546499b55af6b55)) -* **deps:** update dependency scratch-vm to v1.5.28 ([441b7fd](https:/home/circleci/project/semantic-release-remote/commit/441b7fdf3572b093d3b4c0c1022e5472dbcdaff9)) -* **deps:** update dependency scratch-vm to v1.5.31 ([2c60027](https:/home/circleci/project/semantic-release-remote/commit/2c60027674e6cb3f0942d2c380b01b765bef12f8)) -* **deps:** update dependency scratch-vm to v1.5.32 ([964a53f](https:/home/circleci/project/semantic-release-remote/commit/964a53f0ed1e594d1cf3e983c9830402ce743f05)) -* **deps:** update dependency scratch-vm to v1.5.33 ([1c3a4cf](https:/home/circleci/project/semantic-release-remote/commit/1c3a4cfd0b13ca938cd53b44e26491415ea80e43)) -* **deps:** update dependency scratch-vm to v1.5.34 ([b19fe2a](https:/home/circleci/project/semantic-release-remote/commit/b19fe2a64639e4ff17cd965d82965bfea6ce0603)) -* **deps:** update dependency scratch-vm to v1.5.35 ([7543466](https:/home/circleci/project/semantic-release-remote/commit/7543466407de1f1f297afd148f07036bd977109b)) -* **deps:** update dependency scratch-vm to v1.5.36 ([cbc0e7c](https:/home/circleci/project/semantic-release-remote/commit/cbc0e7ca8f472199acc52a2e408a878e462d0240)) -* **deps:** update dependency scratch-vm to v1.5.37 ([79af6ab](https:/home/circleci/project/semantic-release-remote/commit/79af6ab191eb881c14e0403a5524c4da42e865d6)) -* **deps:** update dependency scratch-vm to v1.5.38 ([f200619](https:/home/circleci/project/semantic-release-remote/commit/f2006198489bc938ce4c46ec879fe4d182ec8c5f)) -* **deps:** update dependency scratch-vm to v1.5.40 ([f2b6787](https:/home/circleci/project/semantic-release-remote/commit/f2b67876984b75fca7286902be800d584959f58a)) -* **deps:** update dependency scratch-vm to v1.5.41 ([5e25dba](https:/home/circleci/project/semantic-release-remote/commit/5e25dba6cceb03df542aa1e4d920326f3f0b534e)) -* **deps:** update dependency scratch-vm to v1.5.42 ([7d8d1b2](https:/home/circleci/project/semantic-release-remote/commit/7d8d1b25a873866aef4cf9fe12a664ab94ada90d)) -* disable BLE restore to fix 'Bluetooth unavailable' issue ([8fdc3d1](https:/home/circleci/project/semantic-release-remote/commit/8fdc3d166edb6fb49b25ed2f467a0f77227dc630)) -* dispose of cbManager on session shutdown ([5423e78](https:/home/circleci/project/semantic-release-remote/commit/5423e7800ba21bdb50874d23a88d0cee64c2c54d)) -* don't embed IOBluetooth.framework ([563070d](https:/home/circleci/project/semantic-release-remote/commit/563070d67e5a88cc96a196759f2d2b59b0f4706b)) -* **extension:** inject project marketing version into web extension manifest ([6aa609d](https:/home/circleci/project/semantic-release-remote/commit/6aa609d100961b7f74f4345c28137393988a2835)) -* fix DisposedException by removing cancellation token ([eed937f](https:/home/circleci/project/semantic-release-remote/commit/eed937fd185f58295733e63dc8879a32e5a5ee10)) -* fix minor MAS compliance issues ([149076c](https:/home/circleci/project/semantic-release-remote/commit/149076c07aa6c6e725e09130ee23a397b3e6e9eb)) -* generate icons directly from SVGs for better quality ([8d3b8ce](https:/home/circleci/project/semantic-release-remote/commit/8d3b8ce38a1000552d92bdce7da1cf98fbd9b134)) -* implement a BT connection dance that works on macOS 10 and 12 ([159ca00](https:/home/circleci/project/semantic-release-remote/commit/159ca006789956de12e4282b2d088217eb5bb17a)) -* **Mac:** add real Bluetooth permissions request messages ([39cdf3c](https:/home/circleci/project/semantic-release-remote/commit/39cdf3cd509a1c475dbc80b08d919607a6ac1f22)) -* **Mac:** add real icons for Safari extension ([f081c71](https:/home/circleci/project/semantic-release-remote/commit/f081c7130d97a86f55259bd76eef4fdd51bd1856)) -* **MacBLE:** allow more time for the Bluetooth state to settle ([d2c1cf9](https:/home/circleci/project/semantic-release-remote/commit/d2c1cf97845060e88a00d69a66c13580abb7c74e)) -* **macBLE:** fix 'API MISUSE' log message ([b46f435](https:/home/circleci/project/semantic-release-remote/commit/b46f4359f6ed9feb8734cfbc66d9936af6303201)) -* **macBLE:** handle UpdatedState even if it fires during CBCentralManager ctor ([d2df409](https:/home/circleci/project/semantic-release-remote/commit/d2df40995861311b02875c03c2f2151038e3c8e5)) -* **macBT:** add 'Options' / PIN instructions to pairing dialog ([d58f5d2](https:/home/circleci/project/semantic-release-remote/commit/d58f5d243aeafb7756c987350b439b698c7eaa7d)) -* **MacBT:** dispose of inquiry & channel properly ([b3c48ef](https:/home/circleci/project/semantic-release-remote/commit/b3c48ef1662a93776e68181a5e745a4b88b9670d)) -* **MacBT:** make BT disconnect/reconnect more reliable, especially after pairing ([53bbe3b](https:/home/circleci/project/semantic-release-remote/commit/53bbe3b6e39fc9b27bf11119c888c4b36a39771c)) -* **macBT:** poll to reliably detect RFCOMM channel open ([d42cfdb](https:/home/circleci/project/semantic-release-remote/commit/d42cfdb63751ce511f2053ff4130e2a41b99a751)) -* **Mac:** correct target macOS version ([71e7a13](https:/home/circleci/project/semantic-release-remote/commit/71e7a1303397c7138604131c89bbdcf5793adc9a)) -* **Mac:** embed Safari helper extension into the Scratch Link app bundle ([9c6bb30](https:/home/circleci/project/semantic-release-remote/commit/9c6bb30273b4597e1e3ddd451167cffe6231a854)) -* **mac:** fix CI artifact renaming ([7a05fdd](https:/home/circleci/project/semantic-release-remote/commit/7a05fdda50fc7a498bbdc6d4068cf305177669b7)) -* **Mac:** fix Safari, especially Link->Client notifications ([5bae1ea](https:/home/circleci/project/semantic-release-remote/commit/5bae1ea319dd96eed6a92074a1ba59ecdaca89ca)) -* **mac:** fix tccd error message about kTCCServiceAppleEvents ([bdfc8c0](https:/home/circleci/project/semantic-release-remote/commit/bdfc8c08a6caae205e599b9cca28aedc627d1589)) -* **Mac:** hide Safari extensions for non-MAS builds ([58138c5](https:/home/circleci/project/semantic-release-remote/commit/58138c5c89d17ff6d4dfd40d1bfa3ad95c88f27b)) -* **Mac:** make sure GetSettledBluetoothState() doesn't miss an event ([124b6a0](https:/home/circleci/project/semantic-release-remote/commit/124b6a0cef58bd027249656ac4d183f76454d8f5)) -* **Mac:** properly Dispose() of the status bar item ([4cb46b5](https:/home/circleci/project/semantic-release-remote/commit/4cb46b56588d74cd8cf54e79f36a7a6fafe53f59)) -* **Mac:** remove browser_action popup ([9717935](https:/home/circleci/project/semantic-release-remote/commit/971793558fdf949622c79e28db93dd43083c8938)) -* **Mac:** Safari extension improvements ([14f9f99](https:/home/circleci/project/semantic-release-remote/commit/14f9f99b8cb25e7704e53f31f6589f7205b4c66a)) -* **Mac:** show Safari extension menu only if supported ([d019142](https:/home/circleci/project/semantic-release-remote/commit/d01914241789fc639def818f8553799b2915c198)) -* make CI robust against VS updates ([950d3de](https:/home/circleci/project/semantic-release-remote/commit/950d3deb307226403b537874cadb1f64d2886ac6)) -* make didDiscoverPeripheral a notification ([e51fa01](https:/home/circleci/project/semantic-release-remote/commit/e51fa01b799fcc2c9030a66c4bfe448f4aabbc08)) -* **menu:** 'Manage Safari Extensions' => 'Manage Safari Extensions...' ([dc5c481](https:/home/circleci/project/semantic-release-remote/commit/dc5c48127842be5e3f756f077a0d1e284d1002e8)) -* more BT connection tweaks ([7a1e0d0](https:/home/circleci/project/semantic-release-remote/commit/7a1e0d014a05f3af968d998c2caf888987501618)) -* resolve crash on session close while connecting ([32f8981](https:/home/circleci/project/semantic-release-remote/commit/32f89814873eb19045cffcfe40a3c96f70bce54b)) -* **Safari:** add timeout for initial connection ([e1c9de0](https:/home/circleci/project/semantic-release-remote/commit/e1c9de00f1dbf55c1da8bd2bd935f23015b34450)) -* **Safari:** close session if Scratch Link goes away ([83f85f0](https:/home/circleci/project/semantic-release-remote/commit/83f85f028996d12e2a7d6f2b6c4f93608d60bef8)) -* **safari:** don't cause Safari to steal focus for every Scratch Link -> page message ([f17184f](https:/home/circleci/project/semantic-release-remote/commit/f17184f5a1e163232a0ee76133cd2953bb382a0d)) -* use semantic-release version for build ([17709dd](https:/home/circleci/project/semantic-release-remote/commit/17709dd709a59a1b4d5fa10b4a4ed50834ffd893)) -* **version:** embed GitVersion info correctly and document version scheme ([6501e49](https:/home/circleci/project/semantic-release-remote/commit/6501e49073ac852e71ccd048973fb7b5a383c506)) -* **webextension:** close session on client unload ([caac99e](https:/home/circleci/project/semantic-release-remote/commit/caac99e9c0fa15a940642dc5c9063dba45a40b5f)) -* **webextension:** keep Safari sessions alive for longer than 5 seconds ([4981508](https:/home/circleci/project/semantic-release-remote/commit/498150869982c3d21f5463cf646e337fd789b970)) -* **webextension:** limit number of outstanding poll requests ([c5137bb](https:/home/circleci/project/semantic-release-remote/commit/c5137bb7a06c1701592669196508ae9b26ee97be)) -* **win:** build framework-dependent AnyCPU for further install size reduction ([b1f776c](https:/home/circleci/project/semantic-release-remote/commit/b1f776c19f07652ea09c3152325a35578f9fdcf1)) -* **win:** discover both paired and unpaired BT devices ([23ff634](https:/home/circleci/project/semantic-release-remote/commit/23ff634560930041ebb66ae6476839825bb713ba)) -* **win:** don't crash if BT connect fails ([522f65f](https:/home/circleci/project/semantic-release-remote/commit/522f65f199741e2e704f716952a2db8c7508640f)) -* **windows:** fix *.msixupload generation ([3a1c172](https:/home/circleci/project/semantic-release-remote/commit/3a1c1727bcfbe46aa549a4c15b3b0f7e750b0527)) -* **windows:** fix incorrect root namespace ([e25a604](https:/home/circleci/project/semantic-release-remote/commit/e25a604be0238ef3501447df411c7816aea31f26)) -* **windows:** implement WinBLESession.Dispose ([9a0e1f7](https:/home/circleci/project/semantic-release-remote/commit/9a0e1f7ec1202ae24abc5ca988c4fa54c822bffd)) -* **Win:** fix larger icon sizes being ignored sometimes ([e79252f](https:/home/circleci/project/semantic-release-remote/commit/e79252f2ddf2e15987aab8e2205a95aceaa80cb1)) -* **Win:** set assembly attributes including version info ([8379c15](https:/home/circleci/project/semantic-release-remote/commit/8379c153d9b4273bf0e2814a3ebf6be3f2d3e260)) -* **win:** set WindowsPackageType=None to fix debugging ([4b151e1](https:/home/circleci/project/semantic-release-remote/commit/4b151e1884915a39f059d696a968557f04e4ff7b)) -* work around macOS 12 OpenRfcommChannelSync timeout ([68e7efc](https:/home/circleci/project/semantic-release-remote/commit/68e7efc069e8188dd7ee4d0b0e5deff43d7bdd14)) - - -### chore - -* clean slate for Scratch Link 2.0 ([f30cff3](https:/home/circleci/project/semantic-release-remote/commit/f30cff3e5b0fbd2fda423e8609cbd6576c45131a)) - - -### Features - -* add Windows tray icon ([29b961b](https:/home/circleci/project/semantic-release-remote/commit/29b961b8bb86070fb67012def05f195b75438086)) -* **MacBT:** display pairing help when connecting to unpaired peripheral ([feb100e](https:/home/circleci/project/semantic-release-remote/commit/feb100e3c0e40ce34759246ca27b247ecbb201fc)) -* **Safari:** inject client script into page if script ID is present ([9bc1ef4](https:/home/circleci/project/semantic-release-remote/commit/9bc1ef433ced60b1dc40dc68d0ffe833ce137199)) -* **Win:** add proper Windows icon for app and tray ([e0e96c2](https:/home/circleci/project/semantic-release-remote/commit/e0e96c23e791eef77e136f4188a0fa621c1f0cb3)) -* **win:** convert BT session for Scratch Link 2.0 ([b2bc874](https:/home/circleci/project/semantic-release-remote/commit/b2bc874b7dea108b10fe2eaa4cd8cdd42a1b4f76)) -* **windows:** BLE session first draft ([224e694](https:/home/circleci/project/semantic-release-remote/commit/224e6948749997395102f2c2de2e12163627c37a)) -* **windows:** build and run ScratchApp, receive WS connections ([05d2866](https:/home/circleci/project/semantic-release-remote/commit/05d2866f2bca7f3bee8af67e0769458b7c4399e9)) -* **windows:** generate image assets for MSIX ([d77a006](https:/home/circleci/project/semantic-release-remote/commit/d77a0064a0cd25bac8b8b2b7e3c7d0b146ead69a)) - - -### Performance Improvements - -* **Win:** shrink tray icon, speed up svg-convert.sh ([adeaf1d](https:/home/circleci/project/semantic-release-remote/commit/adeaf1da6b1f48ce993391aa764a0acf53898f74)) - - -### BREAKING CHANGES - -* Scratch Link 2.0 will drop support for some older -versions of macOS. - -# [2.0.0-develop.18](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.17...v2.0.0-develop.18) (2023-05-24) - - -### Bug Fixes - -* **build:** fix missing CFBundleVersion in Safari extension ([7a67ea1](https:/home/circleci/project/semantic-release-remote/commit/7a67ea18025396c26a359f435a2c1cb1ed7ab8c3)) - -# [2.0.0-develop.17](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.16...v2.0.0-develop.17) (2023-04-29) - - -### Bug Fixes - -* **Win:** fix larger icon sizes being ignored sometimes ([e79252f](https:/home/circleci/project/semantic-release-remote/commit/e79252f2ddf2e15987aab8e2205a95aceaa80cb1)) -* **Win:** set assembly attributes including version info ([8379c15](https:/home/circleci/project/semantic-release-remote/commit/8379c153d9b4273bf0e2814a3ebf6be3f2d3e260)) - - -### Features - -* add Windows tray icon ([29b961b](https:/home/circleci/project/semantic-release-remote/commit/29b961b8bb86070fb67012def05f195b75438086)) -* **Win:** add proper Windows icon for app and tray ([e0e96c2](https:/home/circleci/project/semantic-release-remote/commit/e0e96c23e791eef77e136f4188a0fa621c1f0cb3)) - - -### Performance Improvements - -* **Win:** shrink tray icon, speed up svg-convert.sh ([adeaf1d](https:/home/circleci/project/semantic-release-remote/commit/adeaf1da6b1f48ce993391aa764a0acf53898f74)) - -# [2.0.0-develop.16](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.15...v2.0.0-develop.16) (2023-04-24) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.42 ([7d8d1b2](https:/home/circleci/project/semantic-release-remote/commit/7d8d1b25a873866aef4cf9fe12a664ab94ada90d)) - -# [2.0.0-develop.15](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.14...v2.0.0-develop.15) (2023-04-22) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.41 ([5e25dba](https:/home/circleci/project/semantic-release-remote/commit/5e25dba6cceb03df542aa1e4d920326f3f0b534e)) - -# [2.0.0-develop.14](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.13...v2.0.0-develop.14) (2023-04-22) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.40 ([f2b6787](https:/home/circleci/project/semantic-release-remote/commit/f2b67876984b75fca7286902be800d584959f58a)) - -# [2.0.0-develop.13](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.12...v2.0.0-develop.13) (2023-04-21) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.38 ([f200619](https:/home/circleci/project/semantic-release-remote/commit/f2006198489bc938ce4c46ec879fe4d182ec8c5f)) - -# [2.0.0-develop.12](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.11...v2.0.0-develop.12) (2023-04-21) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.37 ([79af6ab](https:/home/circleci/project/semantic-release-remote/commit/79af6ab191eb881c14e0403a5524c4da42e865d6)) - -# [2.0.0-develop.11](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.10...v2.0.0-develop.11) (2023-04-20) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.36 ([cbc0e7c](https:/home/circleci/project/semantic-release-remote/commit/cbc0e7ca8f472199acc52a2e408a878e462d0240)) - -# [2.0.0-develop.10](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.9...v2.0.0-develop.10) (2023-04-19) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.35 ([7543466](https:/home/circleci/project/semantic-release-remote/commit/7543466407de1f1f297afd148f07036bd977109b)) - -# [2.0.0-develop.9](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.8...v2.0.0-develop.9) (2023-04-19) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.34 ([b19fe2a](https:/home/circleci/project/semantic-release-remote/commit/b19fe2a64639e4ff17cd965d82965bfea6ce0603)) - -# [2.0.0-develop.8](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.7...v2.0.0-develop.8) (2023-04-17) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.33 ([1c3a4cf](https:/home/circleci/project/semantic-release-remote/commit/1c3a4cfd0b13ca938cd53b44e26491415ea80e43)) - -# [2.0.0-develop.7](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.6...v2.0.0-develop.7) (2023-04-15) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.32 ([964a53f](https:/home/circleci/project/semantic-release-remote/commit/964a53f0ed1e594d1cf3e983c9830402ce743f05)) - -# [2.0.0-develop.6](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.5...v2.0.0-develop.6) (2023-04-14) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.31 ([2c60027](https:/home/circleci/project/semantic-release-remote/commit/2c60027674e6cb3f0942d2c380b01b765bef12f8)) - -# [2.0.0-develop.5](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.4...v2.0.0-develop.5) (2023-04-06) - - -### Bug Fixes - -* generate icons directly from SVGs for better quality ([8d3b8ce](https:/home/circleci/project/semantic-release-remote/commit/8d3b8ce38a1000552d92bdce7da1cf98fbd9b134)) -* **mac:** fix CI artifact renaming ([7a05fdd](https:/home/circleci/project/semantic-release-remote/commit/7a05fdda50fc7a498bbdc6d4068cf305177669b7)) -* **win:** build framework-dependent AnyCPU for further install size reduction ([b1f776c](https:/home/circleci/project/semantic-release-remote/commit/b1f776c19f07652ea09c3152325a35578f9fdcf1)) -* **win:** discover both paired and unpaired BT devices ([23ff634](https:/home/circleci/project/semantic-release-remote/commit/23ff634560930041ebb66ae6476839825bb713ba)) -* **win:** don't crash if BT connect fails ([522f65f](https:/home/circleci/project/semantic-release-remote/commit/522f65f199741e2e704f716952a2db8c7508640f)) -* **windows:** fix *.msixupload generation ([3a1c172](https:/home/circleci/project/semantic-release-remote/commit/3a1c1727bcfbe46aa549a4c15b3b0f7e750b0527)) -* **windows:** fix incorrect root namespace ([e25a604](https:/home/circleci/project/semantic-release-remote/commit/e25a604be0238ef3501447df411c7816aea31f26)) -* **windows:** implement WinBLESession.Dispose ([9a0e1f7](https:/home/circleci/project/semantic-release-remote/commit/9a0e1f7ec1202ae24abc5ca988c4fa54c822bffd)) -* **win:** set WindowsPackageType=None to fix debugging ([4b151e1](https:/home/circleci/project/semantic-release-remote/commit/4b151e1884915a39f059d696a968557f04e4ff7b)) - - -### Features - -* **win:** convert BT session for Scratch Link 2.0 ([b2bc874](https:/home/circleci/project/semantic-release-remote/commit/b2bc874b7dea108b10fe2eaa4cd8cdd42a1b4f76)) -* **windows:** BLE session first draft ([224e694](https:/home/circleci/project/semantic-release-remote/commit/224e6948749997395102f2c2de2e12163627c37a)) -* **windows:** build and run ScratchApp, receive WS connections ([05d2866](https:/home/circleci/project/semantic-release-remote/commit/05d2866f2bca7f3bee8af67e0769458b7c4399e9)) -* **windows:** generate image assets for MSIX ([d77a006](https:/home/circleci/project/semantic-release-remote/commit/d77a0064a0cd25bac8b8b2b7e3c7d0b146ead69a)) - -# [2.0.0-develop.4](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.3...v2.0.0-develop.4) (2023-04-06) - - -### Bug Fixes - -* calculate build number from label ([2eb8015](https:/home/circleci/project/semantic-release-remote/commit/2eb8015d457263111219f2fc4a5e7d5505c6efb4)) - -# [2.0.0-develop.3](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.2...v2.0.0-develop.3) (2023-04-06) - - -### Bug Fixes - -* **deps:** update dependency scratch-vm to v1.5.28 ([441b7fd](https:/home/circleci/project/semantic-release-remote/commit/441b7fdf3572b093d3b4c0c1022e5472dbcdaff9)) - -# [2.0.0-develop.2](https:/home/circleci/project/semantic-release-remote/compare/v2.0.0-develop.1...v2.0.0-develop.2) (2023-04-06) - - -### Bug Fixes - -* use semantic-release version for build ([17709dd](https:/home/circleci/project/semantic-release-remote/commit/17709dd709a59a1b4d5fa10b4a4ed50834ffd893)) - -# [2.0.0-develop.1](https:/home/circleci/project/semantic-release-remote/compare/v1.4.0...v2.0.0-develop.1) (2023-04-06) - - -### Bug Fixes - -* always call context.completeRequest, even when not returning a value ([9cabb03](https:/home/circleci/project/semantic-release-remote/commit/9cabb03495b089cdc23fb257a0c3fea7e603c225)) -* **ci:** speculative fix for Homebrew failing on CI ([4b12ce4](https:/home/circleci/project/semantic-release-remote/commit/4b12ce4d1501eaea92ee76c1e11fc808ccc7ad11)) -* **ci:** update VS Mac installer script for 17.4 ([9221e1e](https:/home/circleci/project/semantic-release-remote/commit/9221e1e68a0c5cb8e35777c914fd9e17e954a5d7)) -* **common:** make session immediately so we don't miss the first message ([d53d5c8](https:/home/circleci/project/semantic-release-remote/commit/d53d5c8a9dd02563c5e75208cfc7125386d5f85a)) -* **common:** remove `EventAwaiter(EventHandler, ...` ([9032a01](https:/home/circleci/project/semantic-release-remote/commit/9032a013c8b5dc967f5a53a50546499b55af6b55)) -* disable BLE restore to fix 'Bluetooth unavailable' issue ([8fdc3d1](https:/home/circleci/project/semantic-release-remote/commit/8fdc3d166edb6fb49b25ed2f467a0f77227dc630)) -* dispose of cbManager on session shutdown ([5423e78](https:/home/circleci/project/semantic-release-remote/commit/5423e7800ba21bdb50874d23a88d0cee64c2c54d)) -* don't embed IOBluetooth.framework ([563070d](https:/home/circleci/project/semantic-release-remote/commit/563070d67e5a88cc96a196759f2d2b59b0f4706b)) -* **extension:** inject project marketing version into web extension manifest ([6aa609d](https:/home/circleci/project/semantic-release-remote/commit/6aa609d100961b7f74f4345c28137393988a2835)) -* fix DisposedException by removing cancellation token ([eed937f](https:/home/circleci/project/semantic-release-remote/commit/eed937fd185f58295733e63dc8879a32e5a5ee10)) -* fix minor MAS compliance issues ([149076c](https:/home/circleci/project/semantic-release-remote/commit/149076c07aa6c6e725e09130ee23a397b3e6e9eb)) -* implement a BT connection dance that works on macOS 10 and 12 ([159ca00](https:/home/circleci/project/semantic-release-remote/commit/159ca006789956de12e4282b2d088217eb5bb17a)) -* **Mac:** add real Bluetooth permissions request messages ([39cdf3c](https:/home/circleci/project/semantic-release-remote/commit/39cdf3cd509a1c475dbc80b08d919607a6ac1f22)) -* **Mac:** add real icons for Safari extension ([f081c71](https:/home/circleci/project/semantic-release-remote/commit/f081c7130d97a86f55259bd76eef4fdd51bd1856)) -* **MacBLE:** allow more time for the Bluetooth state to settle ([d2c1cf9](https:/home/circleci/project/semantic-release-remote/commit/d2c1cf97845060e88a00d69a66c13580abb7c74e)) -* **macBLE:** fix 'API MISUSE' log message ([b46f435](https:/home/circleci/project/semantic-release-remote/commit/b46f4359f6ed9feb8734cfbc66d9936af6303201)) -* **macBLE:** handle UpdatedState even if it fires during CBCentralManager ctor ([d2df409](https:/home/circleci/project/semantic-release-remote/commit/d2df40995861311b02875c03c2f2151038e3c8e5)) -* **macBT:** add 'Options' / PIN instructions to pairing dialog ([d58f5d2](https:/home/circleci/project/semantic-release-remote/commit/d58f5d243aeafb7756c987350b439b698c7eaa7d)) -* **MacBT:** dispose of inquiry & channel properly ([b3c48ef](https:/home/circleci/project/semantic-release-remote/commit/b3c48ef1662a93776e68181a5e745a4b88b9670d)) -* **MacBT:** make BT disconnect/reconnect more reliable, especially after pairing ([53bbe3b](https:/home/circleci/project/semantic-release-remote/commit/53bbe3b6e39fc9b27bf11119c888c4b36a39771c)) -* **macBT:** poll to reliably detect RFCOMM channel open ([d42cfdb](https:/home/circleci/project/semantic-release-remote/commit/d42cfdb63751ce511f2053ff4130e2a41b99a751)) -* **Mac:** correct target macOS version ([71e7a13](https:/home/circleci/project/semantic-release-remote/commit/71e7a1303397c7138604131c89bbdcf5793adc9a)) -* **Mac:** embed Safari helper extension into the Scratch Link app bundle ([9c6bb30](https:/home/circleci/project/semantic-release-remote/commit/9c6bb30273b4597e1e3ddd451167cffe6231a854)) -* **Mac:** fix Safari, especially Link->Client notifications ([5bae1ea](https:/home/circleci/project/semantic-release-remote/commit/5bae1ea319dd96eed6a92074a1ba59ecdaca89ca)) -* **mac:** fix tccd error message about kTCCServiceAppleEvents ([bdfc8c0](https:/home/circleci/project/semantic-release-remote/commit/bdfc8c08a6caae205e599b9cca28aedc627d1589)) -* **Mac:** hide Safari extensions for non-MAS builds ([58138c5](https:/home/circleci/project/semantic-release-remote/commit/58138c5c89d17ff6d4dfd40d1bfa3ad95c88f27b)) -* **Mac:** make sure GetSettledBluetoothState() doesn't miss an event ([124b6a0](https:/home/circleci/project/semantic-release-remote/commit/124b6a0cef58bd027249656ac4d183f76454d8f5)) -* **Mac:** properly Dispose() of the status bar item ([4cb46b5](https:/home/circleci/project/semantic-release-remote/commit/4cb46b56588d74cd8cf54e79f36a7a6fafe53f59)) -* **Mac:** remove browser_action popup ([9717935](https:/home/circleci/project/semantic-release-remote/commit/971793558fdf949622c79e28db93dd43083c8938)) -* **Mac:** Safari extension improvements ([14f9f99](https:/home/circleci/project/semantic-release-remote/commit/14f9f99b8cb25e7704e53f31f6589f7205b4c66a)) -* **Mac:** show Safari extension menu only if supported ([d019142](https:/home/circleci/project/semantic-release-remote/commit/d01914241789fc639def818f8553799b2915c198)) -* make CI robust against VS updates ([950d3de](https:/home/circleci/project/semantic-release-remote/commit/950d3deb307226403b537874cadb1f64d2886ac6)) -* make didDiscoverPeripheral a notification ([e51fa01](https:/home/circleci/project/semantic-release-remote/commit/e51fa01b799fcc2c9030a66c4bfe448f4aabbc08)) -* **menu:** 'Manage Safari Extensions' => 'Manage Safari Extensions...' ([dc5c481](https:/home/circleci/project/semantic-release-remote/commit/dc5c48127842be5e3f756f077a0d1e284d1002e8)) -* more BT connection tweaks ([7a1e0d0](https:/home/circleci/project/semantic-release-remote/commit/7a1e0d014a05f3af968d998c2caf888987501618)) -* resolve crash on session close while connecting ([32f8981](https:/home/circleci/project/semantic-release-remote/commit/32f89814873eb19045cffcfe40a3c96f70bce54b)) -* **Safari:** add timeout for initial connection ([e1c9de0](https:/home/circleci/project/semantic-release-remote/commit/e1c9de00f1dbf55c1da8bd2bd935f23015b34450)) -* **Safari:** close session if Scratch Link goes away ([83f85f0](https:/home/circleci/project/semantic-release-remote/commit/83f85f028996d12e2a7d6f2b6c4f93608d60bef8)) -* **safari:** don't cause Safari to steal focus for every Scratch Link -> page message ([f17184f](https:/home/circleci/project/semantic-release-remote/commit/f17184f5a1e163232a0ee76133cd2953bb382a0d)) -* **version:** embed GitVersion info correctly and document version scheme ([6501e49](https:/home/circleci/project/semantic-release-remote/commit/6501e49073ac852e71ccd048973fb7b5a383c506)) -* **webextension:** close session on client unload ([caac99e](https:/home/circleci/project/semantic-release-remote/commit/caac99e9c0fa15a940642dc5c9063dba45a40b5f)) -* **webextension:** keep Safari sessions alive for longer than 5 seconds ([4981508](https:/home/circleci/project/semantic-release-remote/commit/498150869982c3d21f5463cf646e337fd789b970)) -* **webextension:** limit number of outstanding poll requests ([c5137bb](https:/home/circleci/project/semantic-release-remote/commit/c5137bb7a06c1701592669196508ae9b26ee97be)) -* work around macOS 12 OpenRfcommChannelSync timeout ([68e7efc](https:/home/circleci/project/semantic-release-remote/commit/68e7efc069e8188dd7ee4d0b0e5deff43d7bdd14)) - - -### chore - -* clean slate for Scratch Link 2.0 ([f30cff3](https:/home/circleci/project/semantic-release-remote/commit/f30cff3e5b0fbd2fda423e8609cbd6576c45131a)) - - -### Features - -* **MacBT:** display pairing help when connecting to unpaired peripheral ([feb100e](https:/home/circleci/project/semantic-release-remote/commit/feb100e3c0e40ce34759246ca27b247ecbb201fc)) -* **Safari:** inject client script into page if script ID is present ([9bc1ef4](https:/home/circleci/project/semantic-release-remote/commit/9bc1ef433ced60b1dc40dc68d0ffe833ce137199)) - - -### BREAKING CHANGES - -* Scratch Link 2.0 will drop support for some older -versions of macOS. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7c4fa5cb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# AluxCoding Scratch Link — Claude 행동 규칙 + +Windows-only desktop app. Scratch 3.0 ↔ 하드웨어(BLE, Bluetooth Classic, USB Serial) 중계. +C# / .NET 8 + WinUI 3, Visual Studio Solution. + +--- + +## 1. 최우선 규칙 (위반 시 즉시 수정) + +- **`Console.*` 사용 금지** → `Trace.WriteLine()` (`System.Diagnostics`) 사용 (§4 참조) +- **`scratch-link-common`이 `scratch-link-win`을 참조 금지** → 의존 방향은 `scratch-link-win → scratch-link-common` 단방향 (§8 참조) +- **`SharedProps/*.props` / `*.targets` 직접 편집 시 주의** → 전체 빌드에 영향. 개별 csproj에서 중복 선언 금지 +- **기존 아키텍처·패턴을 우회하는 수정 금지** (§8 참조) → 새 코드·수정 코드에 적용. 기존 위반은 명시 요청 없이 건드리지 않는다. 단, 수정 범위 안에서 우회 코드를 발견하면 사용자에게 알리고 처리 여부를 묻는다. + +## 2. Using 지시문 규칙 + +- `using` 선언은 반드시 **namespace 내부**에 배치 (`.editorconfig` 강제) +- 멤버 접근 시 `this.` 한정자 필수: 필드(`this.field`), 메서드(`this.Method()`), 프로퍼티(`this.Property`), 이벤트(`this.Event`) +- 불필요한 `using` 추가 금지 — 미사용 using은 컴파일 경고 원인 + +## 3. 코드 스타일 + +- 인덴트: 4 스페이스 (탭 금지) +- 줄 끝: LF (XML / CSPROJ / WAPPROJ 제외 — CRLF) +- 인코딩: UTF-8 +- StyleCop Analyzers 준수 필수 +- `interface` 명명: `I` 접두사 (예: `ISession`, `IPeripheralSession`) +- 미사용 변수·using 금지 (`CS8600`, `IDE0005` 등 경고 발생) + +## 4. 금지 패턴 + +- **`Console.*` 사용 금지** → `Trace.WriteLine()` 사용 + - **예외 — 임시 진단용**: `System.Diagnostics.Debug.WriteLine("[DEBUG-XXX] ...")` 형태로 한정 허용. `[DEBUG-XXX]` 식별 prefix 필수 (grep 가능). **커밋 전 반드시 전부 제거**. +- **`dynamic` 타입 남용 금지** → 구체적 타입 또는 제네릭 사용 +- **`#pragma warning disable` 무분별 사용 금지** → 근본 원인 해결 우선 +- **`types.cs` / `type.cs` 파일 생성 금지** → 타입·인터페이스는 사용하는 클래스 파일 안에 함께 정의 (코로케이션) +- **일반 주석(`//`)은 한 줄 max, "WHY"만** → 기본은 주석 없음. WHY가 비자명할 때만 한 줄 추가. WHAT 설명 / 현재 작업·callers 참조 금지. 설계 의도는 commit message / PR description으로. +- **XML doc(`///`)은 public API에 한해 단일 `` 허용** → 단, caller 참조(`"Used by X"`) 및 코드에서 자명한 WHAT 설명 금지. 비자명한 동작·제약·사이드이펙트만 기술한다. +- **공용 코드 (`scratch-link-common`) 의 주석·식별자에서 특정 프로토콜 구현 세부 언급 금지** → 일반화된 패턴 설명으로 표현하고, 구체적 예시는 해당 세션 클래스 안에서만 든다. + +## 5. 작업 전 확인사항 + +코드 작성·수정 전에 반드시 확인: + +1. 수정 대상 파일을 먼저 읽고 기존 패턴을 파악한다 +2. **`scratch-link-common` vs `scratch-link-win` 중 어느 쪽에 위치해야 하는지 판단한다** — 플랫폼 API(Windows.Devices.*, WinUI) 없이 동작 가능하면 common, 그렇지 않으면 win +3. 관련 타입·인터페이스가 이미 있는지 확인한다 +4. `SharedProps/`에 이미 선언된 속성·패키지 참조인지 확인한다 + +## 6. 작업 후 검증 체크리스트 + +- [ ] `Console.*`를 사용하지 않았는가? (임시 `[DEBUG-*]` 로그 전부 제거 확인) +- [ ] StyleCop 오류가 없는가? +- [ ] `using` 선언이 namespace 내부에 있는가? +- [ ] 멤버 접근에 `this.` 한정자를 사용했는가? +- [ ] 미사용 변수·using이 없는가? +- [ ] 의존 방향 규칙을 지켰는가? (`scratch-link-common`이 `scratch-link-win`을 참조하지 않음) +- [ ] `SharedProps/`에 이미 선언된 속성을 개별 csproj에 중복 선언하지 않았는가? +- [ ] 일반 주석(`//`)이 WHY만 담고 있는가? (WHAT / caller 참조 없음) +- [ ] XML doc(`///`)이 있다면 public API이고, caller 참조 및 자명한 WHAT 설명이 없는가? +- [ ] 기존 코드 패턴과 일관성이 있는가? + +## 7. 커밋 규칙 + +``` +feat(serial): USB Serial 트랜스포트 세션 추가 +fix(ble): BLE 연결 재시도 로직 오류 수정 +refactor(common): JSON-RPC 핸들러 구조 개선 +docs(architecture): Serial 프로토콜 설계 문서 업데이트 +``` + +- 접두사: `feat`, `fix`, `refactor`, `docs`, `chore`, `style`, `test`, `ci` +- scope 예시: `ble`, `bt`, `serial`, `common`, `win`, `jsonrpc`, `msix` +- 설명·본문·꼬리말은 한국어 + +## 8. 프로젝트 구조 핵심 + +- `scratch-link-common/` — 플랫폼 공통 C# Shared Project (BLE/BT/Serial 프로토콜 추상화, JSON-RPC 2.0, WebSocket 핸들링). **재사용 가능한 단위로 분리**, Windows API에 종속되지 않는다. +- `scratch-link-win/` — WinUI 3 플랫폼 구현 (Windows.Devices.* API 연동, 트레이 아이콘, 앱 진입점). **비즈니스 로직은 최소화**, 가능한 한 `scratch-link-common`으로 위임. +- `scratch-link-win-msix/` — MSIX 패키징 프로젝트. 직접 코드 편집 대상이 아니다. +- `SharedProps/` — MSBuild 공유 속성 (SDK 버전, NuGet 패키지 참조, 버전 자동화). 개별 csproj에서 중복 선언 금지. +- `Documentation/` — 아키텍처·프로토콜 설계 문서. 관련 코드 변경 시 함께 업데이트한다. +- **의존 방향: `scratch-link-win` → `scratch-link-common` 단방향** (§1 절대 규칙). 이유: common이 win을 참조하면 플랫폼 독립성이 깨지고 순환 의존이 발생한다. + +## 9. 개발 명령어 + +```bash +# 권장: Visual Studio 2022+에서 scratch-link.sln 열기 +# 빌드 구성: Debug_Win / Release_Win + +dotnet build -c Debug_Win # 디버그 빌드 +dotnet build -c Release_Win # 릴리즈 빌드 +dotnet run --project scratch-link-win -c Debug_Win # 실행 + +# 아이콘 생성 (cairosvg, ImageMagick, optipng 필요) +make icons +``` diff --git a/Documentation/Alux/SerialApiReference.md b/Documentation/Alux/SerialApiReference.md new file mode 100644 index 00000000..9a3c1789 --- /dev/null +++ b/Documentation/Alux/SerialApiReference.md @@ -0,0 +1,482 @@ +# Serial Transport API Reference + +## JSON-RPC Methods + +### discover + +Discovers available serial ports. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "discover", + "params": { + "filters": [ + { + "usbVendorId": 6790, + "usbProductId": 29987, + "pathHint": "COM" + } + ] + } +} +``` + +**Parameters:** +- `filters` (array, optional) — Filter discovered ports. If omitted or empty, every enumerated USB serial port is reported. A port is reported when it matches **any** filter (filters are OR'd); within one filter, all specified fields must match (fields are AND'd). + +**Filter fields:** +- `usbVendorId` (int, optional) — USB Vendor ID as a decimal integer. Exact match. Omit to skip this check. +- `usbProductId` (int, optional) — USB Product ID as a decimal integer. Exact match. Omit to skip this check. +- `pathHint` (string, optional) — **Case-insensitive substring** match against the OS-level port path (`info.Path`, e.g. `"COM7"` on Windows). Not a prefix, not exact, not a regex. Example: `"COM"` matches `COM3`, `COM12`, etc. Omit to skip this check. + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": {} +} +``` + +**Notifications:** +Discovered ports arrive as `didDiscoverPeripheral` notifications: +```json +{ + "jsonrpc": "2.0", + "method": "didDiscoverPeripheral", + "params": { + "peripheralId": "port-0", + "name": "COM7 (CH340)", + "path": "COM7", + "vendorId": "0x1a86", + "productId": "0x7523", + "rssi": 0 + } +} +``` + +--- + +### connect + +Opens a serial port connection. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "connect", + "params": { + "peripheralId": "port-0", + "baudRate": 115200, + "dataBits": 8, + "parity": "none", + "stopBits": "one", + "flowControl": "none", + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33, + "wireTrace": false + } +} +``` + +**Parameters:** +- `peripheralId` (string, required) — Port identifier from discovery +- `baudRate` (int, required) — Baud rate (e.g., 9600, 115200) +- `dataBits` (int, optional) — Data bits (default: 8) +- `parity` (string, optional) — "none" | "even" | "odd" | "mark" | "space" (default: "none") +- `stopBits` (string, optional) — "one" | "onePointFive" | "two" (default: "one") +- `flowControl` (string, optional) — "none" | "rtsCts" | "xonXoff" (default: "none") +- `peripheralType` (string, optional) — Device type identifier ("codetinker", "connect", "technic", etc.) +- `keepAliveIntervalMs` (int, optional) — Keep-alive interval in ms. Omit or null to disable. **Recommended: 33ms for Codetinker** +- `wireTrace` (bool, optional) — Diagnostic. When `true`, Link emits per-write/per-read hex dumps via `Trace.WriteLine` (visible in DebugView or attached debugger). Off by default. Use only for transport-level debugging; the dumps include payload bytes and can be verbose. + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": {} +} +``` + +**Errors:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "error": { + "code": -32603, + "message": "could not open serial port COM7: Port already in use" + } +} +``` + +--- + +### write + +Sends data to the serial port. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "write", + "params": { + "message": "AQIDBA==", + "encoding": "base64" + } +} +``` + +**Parameters:** +- `message` (string, required) — Data to send (base64-encoded) +- `encoding` (string, required) — Always "base64" + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "sentBytes": 4 + } +} +``` + +**Side Effects:** +- Resets the keep-alive timer +- Last sent packet is cached for keep-alive resend + +--- + +### startReading + +Enables data reception (usually implicit after connect). + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 4, + "method": "startReading", + "params": {} +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 4, + "result": {} +} +``` + +--- + +### stopReading + +Disables data reception (keep-alive timer continues running). + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 5, + "method": "stopReading", + "params": {} +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 5, + "result": {} +} +``` + +--- + +### setKeepAlive + +Toggle or reconfigure keep-alive at runtime, without disconnecting. Use to disable keep-alive before a firmware update and re-enable it afterwards, or to change the interval mid-session. + +**Request — disable:** +```json +{ + "jsonrpc": "2.0", + "id": 6, + "method": "setKeepAlive", + "params": { "intervalMs": null } +} +``` + +**Request — enable / change interval:** +```json +{ + "jsonrpc": "2.0", + "id": 7, + "method": "setKeepAlive", + "params": { "intervalMs": 33 } +} +``` + +**Parameters:** +- `intervalMs` (int or null, required) — Interval in milliseconds. `null`, `0`, or negative values **disable** keep-alive. Positive values (re)start it with the given interval. + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 7, + "result": { "intervalMs": 33 } +} +``` + +The `result.intervalMs` echoes the **applied** interval (`null` when disabled). Use this to confirm the operation took effect. + +**Side Effects:** +- If keep-alive was already running, the existing timer is stopped (blocking on any in-flight tick) before the new one starts. The call is fully idempotent. +- The cached last-TX packet is **preserved** across the toggle, so re-enabling keep-alive immediately resumes resending the same packet. + +**Typical DFU sequence (client-side):** +```javascript +// 1. Disable keep-alive before bootloader entry +await link.send("setKeepAlive", { intervalMs: null }); + +// 2. Run firmware update (writes/reads as usual) +await runDfu(...); + +// 3. Re-enable keep-alive for normal operation +await link.send("setKeepAlive", { intervalMs: 33 }); +``` + +--- + +### disconnect + +Closes the serial port connection. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 6, + "method": "disconnect", + "params": {} +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 6, + "result": {} +} +``` + +**Side Effects:** +- Stops the keep-alive timer +- Closes the port +- Does NOT fire `serialDidDisconnect` notification (client-initiated close) + +--- + +## Notifications + +### didDiscoverPeripheral + +Sent for each discovered serial port during discovery. + +```json +{ + "jsonrpc": "2.0", + "method": "didDiscoverPeripheral", + "params": { + "peripheralId": "port-0", + "name": "COM7 (CH340)", + "path": "COM7", + "vendorId": "0x1a86", + "productId": "0x7523", + "rssi": 0 + } +} +``` + +--- + +### serialDidReceiveData + +Sent when data is received on the serial port. + +```json +{ + "jsonrpc": "2.0", + "method": "serialDidReceiveData", + "params": { + "message": "SG93IGFyZSB5b3U/", + "encoding": "base64" + } +} +``` + +--- + +### serialDidDisconnect + +Sent when the connection is lost (external cause, not client-initiated). + +```json +{ + "jsonrpc": "2.0", + "method": "serialDidDisconnect", + "params": { + "reason": "device", + "message": "Port was removed" + } +} +``` + +**Disconnect Reasons:** +- `"device"` — Device physically disconnected or a read-side `IOException` occurred (cable unplug, USB stack hiccup, driver error, transient USB noise that the kernel surfaced as an I/O error) +- `"error"` — Unexpected non-I/O exception in the read loop +- `"user"` — User action (rare) +- `"shutdown"` — Application shutting down + +**Recovery policy (current):** + +Scratch Link does **not** retry on I/O errors. The moment the kernel surfaces a read-side `IOException`, Link: +1. Fires `serialDidDisconnect` with `reason: "device"`. +2. Closes the port. +3. Stops the keep-alive timer and the RX loop. + +The client (aluxlabs) is responsible for any reconnect logic — including any debounce or retry policy for transient USB noise. + +This is a deliberate design choice for v1: keep Link's transport thin and predictable, let the client decide policy. If transient-disconnect reports start to accumulate from the field, we can re-negotiate a Link-side retry (e.g. one 200 ms re-open before surfacing the disconnect) and add a `retryOnIoError` connect parameter to opt in. Until then, **assume immediate disconnect on any I/O error.** + +--- + +## Complete Example: Codetinker Connection + +```javascript +// 1. Discover ports +{ + "jsonrpc": "2.0", + "id": 1, + "method": "discover", + "params": {} +} +// → didDiscoverPeripheral: { peripheralId: "port-0", name: "COM7 (CH340)", ... } + +// 2. Connect with keep-alive +{ + "jsonrpc": "2.0", + "id": 2, + "method": "connect", + "params": { + "peripheralId": "port-0", + "baudRate": 115200, + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33 + } +} +// → result: {} +// → Keep-alive timer starts, will resend last TX packet every 33ms if idle + +// 3. Send command +{ + "jsonrpc": "2.0", + "id": 3, + "method": "write", + "params": { + "message": "AQIDBA==", + "encoding": "base64" + } +} +// → result: { sentBytes: 4 } +// → Keep-alive timer resets (cached packet = AQIDBA==) + +// 4. Receive response +// ← serialDidReceiveData: { message: "BwgJCg==", encoding: "base64" } + +// 5. Disconnect +{ + "jsonrpc": "2.0", + "id": 4, + "method": "disconnect", + "params": {} +} +// → result: {} +// → Keep-alive timer stops +``` + +--- + +## Error Codes + +| Code | Message | Description | +|------|---------|-------------| +| -32600 | Invalid Request | Malformed JSON-RPC | +| -32601 | Method not found | Unknown method | +| -32602 | Invalid params | Missing required parameter | +| -32603 | Internal error | Port error, invalid state, etc. | + +--- + +## Recommendations + +### For Codetinker +```json +{ + "baudRate": 115200, + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33 +} +``` + +### For Generic Serial Devices (no keep-alive) +```json +{ + "baudRate": 9600 +} +``` + +### For Firmware Updates + +Two layers of protection: + +1. **Automatic (no client change needed).** Each `write` resets the keep-alive interval, so a burst of writes (DFU chunks) suppresses the resend until the line goes idle again. +2. **Explicit (recommended for wireless DFU).** Before bootloader entry, call `setKeepAlive` with `intervalMs: null` to disable keep-alive entirely. Re-enable after DFU completes. This eliminates any chance of a resend racing with a bootloader handshake on a slow wireless link. + +```javascript +await link.send("setKeepAlive", { intervalMs: null }); +// ... run DFU ... +await link.send("setKeepAlive", { intervalMs: 33 }); +``` + +### For Transport-Level Debugging + +Enable `wireTrace: true` on `connect` to get per-write/per-read hex dumps via `Trace.WriteLine`. Output is visible in [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) (run as admin, "Capture Win32") or any attached debugger. Format: + +``` +wire-trace TX 12B 4c 4f 41 44 ... +wire-trace RX 31B 3c 1e af 00 ... +wire-trace TX(keep-alive) 4B aa bb cc dd +``` + +Buffers longer than 256 bytes are truncated with `…(+NB)` suffix. Compare these against the client's own per-message log to localize any drops or corruption. + +--- + +**API Version**: 1.1 +**Last Updated**: 2026-05-25 diff --git a/Documentation/Alux/SerialKeepAliveGuide.md b/Documentation/Alux/SerialKeepAliveGuide.md new file mode 100644 index 00000000..0050ff3b --- /dev/null +++ b/Documentation/Alux/SerialKeepAliveGuide.md @@ -0,0 +1,196 @@ +# Serial Keep-Alive Implementation Guide + +## Overview + +The Scratch Link serial transport now supports **keep-alive** functionality to prevent device timeout. This is particularly useful for devices like Codetinker that disconnect if no response is received within 1 second. + +## Problem Statement + +Some hardware devices (e.g., Codetinker with CH340 USB-to-serial) require continuous communication: +- If no packet is received for > 1 second, the device considers the connection lost +- This triggers device-side notifications (e.g., buzzer sound) +- Regular idle periods during normal operation cause unnecessary timeouts + +## Solution + +The keep-alive mechanism automatically resends the **last transmitted (TX) packet** at a configurable interval. This keeps the device "alive" without interfering with actual communication. + +### Key Features + +✅ **Automatic resend** — Last sent packet is cached and resent periodically +✅ **No interference with active communication** — Timer resets on every write, so frequent communication automatically pauses keep-alive +✅ **Firmware update safe** — During firmware updates (DFU), frequent writes prevent keep-alive from firing +✅ **Optional and configurable** — Can be enabled/disabled per connection + +## Usage + +### Serial Connection Request + +When connecting to a serial device, include the `keepAliveIntervalMs` parameter: + +```json +{ + "baudRate": 115200, + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33 +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `baudRate` | int | Yes | Baud rate (e.g., 115200) | +| `peripheralType` | string | No | Device type identifier (e.g., "codetinker", "connect", "technic") | +| `keepAliveIntervalMs` | int \| null | No | Keep-alive interval in milliseconds. `null` or omitted = disabled. Set to 33ms for Codetinker. | + +### Examples + +#### Codetinker (with keep-alive) +```json +{ + "baudRate": 115200, + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33 +} +``` + +#### Generic device (no keep-alive) +```json +{ + "baudRate": 9600 +} +``` + +#### Other device with custom interval +```json +{ + "baudRate": 57600, + "peripheralType": "custom", + "keepAliveIntervalMs": 100 +} +``` + +## How It Works + +### 1. Keep-Alive Enabled +``` +Time: 0ms → Client sends packet A +Time: 33ms → (no activity) Keep-alive resends packet A +Time: 66ms → (no activity) Keep-alive resends packet A +Time: 150ms → Client sends packet B → Timer resets +Time: 183ms → (no activity) Keep-alive resends packet B +... +``` + +### 2. During Firmware Update (DFU) +``` +Time: 0ms → DFU write #1 → Timer resets +Time: 10ms → DFU write #2 → Timer resets +Time: 20ms → DFU write #3 → Timer resets +... +Result: Keep-alive never fires because writes are too frequent +``` + +## Technical Details + +### Timer Reset Behavior + +The keep-alive timer **resets** whenever: +- A write request is issued (`write` JSON-RPC method) +- A packet is successfully sent + +This means: +- **Idle state** — Keep-alive kicks in after `keepAliveIntervalMs` milliseconds of inactivity +- **Active state** — During firmware updates or frequent communication, keep-alive is effectively paused +- **No explicit disable needed** — The mechanism self-manages based on activity + +### Architecture + +``` +SerialSession (abstract) +├── StartKeepAlive(interval) → Start timer with interval +├── StopKeepAlive() → Stop and dispose timer +├── ResetKeepAliveTimer() → Restart timer (called on write) +└── ResendLastData() → Resend cached packet (called by timer) + +Platform-specific implementation (WinSerialSession, etc.) +├── StartKeepAlive() in DoConnect() +└── StopKeepAlive() in DoDisconnect() +``` + +## Firmware Update Safety + +**Q: Won't keep-alive packets interfere with firmware updates?** + +A: There are two layers of protection. + +**Layer 1 — Automatic (no client change needed).** During an active DFU burst: +1. Each `write` request resets the keep-alive timer. +2. DFU chunks arrive faster than the keep-alive interval, so the timer never expires. +3. The first write after the burst is the only one that re-arms keep-alive. + +**Layer 2 — Explicit toggle (recommended for wireless DFU).** For setups where the bootloader handshake travels over a slow wireless link (e.g. USB dongle → wireless → CPU), there is a brief idle window between "wake bootloader" and the first DFU command where keep-alive *could* fire. Eliminate it by calling `setKeepAlive` to disable keep-alive before bootloader entry and re-enable it after DFU completes: + +```javascript +await link.send("setKeepAlive", { intervalMs: null }); +// ... run DFU ... +await link.send("setKeepAlive", { intervalMs: 33 }); +``` + +See [SerialApiReference.md](SerialApiReference.md#setkeepalive) for the full method spec. + +## Diagnosing Transport Issues + +If you suspect bytes are being dropped, corrupted, or stalled in the Link layer, enable `wireTrace: true` on `connect`: + +```json +{ + "baudRate": 115200, + "peripheralType": "codetinker", + "keepAliveIntervalMs": 33, + "wireTrace": true +} +``` + +Link will emit hex dumps via `Trace.WriteLine` for every TX/RX (including keep-alive resends). View them in [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or an attached debugger. Compare to the client's own message log to pinpoint where data diverges. + +## Troubleshooting + +### Device still times out +- Verify `keepAliveIntervalMs` is set in the connect request +- Check if the device actually requires keep-alive (some devices don't) +- Try a shorter interval (e.g., 25ms instead of 33ms) + +### Excessive resends visible in logs +- This is expected behavior +- Keep-alive packets only resend when there's no other activity +- During normal operation, keep-alive should rarely fire + +### Firmware update hangs or fails +- Ensure the device supports the DFU protocol +- Verify connection parameters (baud rate, etc.) +- Check device logs for error messages + +## Related Features + +### Phase B: Firmware Transport Abstraction +- `FirmwareTransportPort` interface (planned) +- WebSerial adapter (planned) +- WebSocketLink adapter (planned) +- firmware-updater.ts transport-agnostic implementation (planned) + +The keep-alive mechanism works transparently with future firmware update features. + +## Support + +For issues or questions about keep-alive functionality: +1. Check device documentation for timeout requirements +2. Enable debug logging to see keep-alive packets +3. Contact ALUX Labs support + +--- + +**Version**: 1.0 +**Last Updated**: 2026-05-24 +**Affected Devices**: Codetinker, and any device with sub-second timeout requirements diff --git a/Documentation/Alux/WindowsDevSetup-VS2026.md b/Documentation/Alux/WindowsDevSetup-VS2026.md new file mode 100644 index 00000000..150da42b --- /dev/null +++ b/Documentation/Alux/WindowsDevSetup-VS2026.md @@ -0,0 +1,97 @@ +# Windows 개발 환경 세팅 (Visual Studio 2026) + +Visual Studio 2026으로 Alux Scratch Link를 빌드/디버깅하기 위한 환경 세팅 절차. + +## 0. 사전 정보 + +- 솔루션 파일 `scratch-link.sln`은 VS 2026에서 그대로 열 수 있다. **버전 변환 프롬프트가 떠도 변환하지 말 것** (sln 포맷이 바뀌어 PR이 지저분해진다). +- 윈도우 관련 프로젝트: + - `scratch-link-win` — WinUI 3 기반 본체 EXE (net8.0-windows) + - `scratch-link-win-msix` — `.wapproj` (Desktop Bridge) 형식의 MSIX 패키징 프로젝트 + - `scratch-link-common` — 공유 C# 코드 (`.shproj`, 공유 아이템 프로젝트) +- `scratch-link-mac`은 솔루션을 열면 "Unsupported"로 표시되는데 **정상이다**. Windows VS에서는 어차피 빌드하지 않으므로 무시한다 (솔루션에서 제거하지 말 것). + +## 1. Visual Studio Installer 워크로드 + +VS Installer를 열고 **수정(Modify)**으로 다음 워크로드를 체크한다. + +### 워크로드 (Workloads 탭) + +- ☑ **.NET 데스크톱 개발** (.NET desktop development) +- ☑ **C++를 사용한 데스크톱 개발** (Desktop development with C++) +- ☑ **WinUI 애플리케이션 개발** (WinUI application development) + +### 각 워크로드의 선택 사항 + +**`.NET 데스크톱 개발`의 선택 사항:** + +- ☑ **MSIX Packaging Tools** — `.wapproj` 빌드에 필수. + +**`WinUI 애플리케이션 개발`의 선택 사항:** + +- ☑ **Windows 11 SDK (10.0.22621.0)** — `scratch-link-win.csproj`의 `TargetFramework=net8.0-windows10.0.22621.0`이 요구하는 SDK. + +> `.NET 8 SDK`는 VS 2026에 포함되어 있으므로 별도 설치가 필요 없다. + +## 2. Windows App Runtime 확인 + +이 프로젝트는 `Microsoft.WindowsAppSDK 1.8`을 framework-dependent 모드로 참조한다. F5 실행 시 **Windows App Runtime 1.8이 시스템에 설치되어 있어야** 한다. + +**Windows 11 최신 업데이트 적용 환경이라면 이미 설치되어 있을 가능성이 높다.** + +확인 방법: + +```powershell +Get-AppxPackage -Name "Microsoft.WindowsAppRuntime.1.8*" +``` + +`Microsoft.WindowsAppRuntime.1.8_*` 패키지가 보이면 OK. + +없으면 [Windows App SDK 다운로드 페이지](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads)에서 1.8 런타임 설치 파일을 받아 실행한다. + +## 3. 솔루션 열기 + +1. `scratch-link.sln` 더블클릭으로 VS 2026에서 열기 +2. "Migration Report"가 뜨면 **OK**로 닫는다. `scratch-link-mac`이 Unsupported로 나오는 것은 정상. +3. **솔루션 탐색기**에서 `scratch-link-win`을 우클릭 → **Set as Startup Project**. 프로젝트 이름이 굵게(bold) 변하면 적용된 것. + +## 4. 빌드/실행 설정 + +VS 상단 툴바에서: + +| 항목 | 값 | +|---|---| +| Solution Configurations | **`Debug_Win`** | +| Solution Platforms | **`x64`** (또는 본인 PC에 맞는 플랫폼) | +| Startup Project | **`scratch-link-win`** | + +> **시작 프로젝트는 반드시 `scratch-link-win`이어야 한다.** `scratch-link-win-msix`로 F5를 누르면 패키지 ID 충돌로 `MddBootstrapInitialize 0x80070032` 에러가 난다. + +이후 **F5**로 빌드 및 실행. 트레이 아이콘이 나타나면 정상 동작. + +## 5. 워크플로우 + +| 목적 | Startup Project | Configuration | 결과물 | +|---|---|---|---| +| **일상 개발/디버깅 (F5)** | `scratch-link-win` | `Debug_Win` / `x64` | 언패키지 EXE 직접 실행 | +| **MSIX 패키지 빌드** | `scratch-link-win-msix` | `Release_Win` / `x64` | publish profile (`win-x64.pubxml`)로 MSIX 생성 | +| **배포용 msixbundle** | `scratch-link-win-msix` | `Release_Win`, 전 플랫폼 | x86/x64/ARM64 `.msixbundle` 생성 | + +## 6. 알려진 이슈 + +| 증상 | 원인 / 해결 | +|---|---| +| `Microsoft.DesktopBridge.props was not found` | MSIX Packaging Tools 누락. §1의 ".NET 데스크톱 개발" 선택 사항 확인. | +| `Windows 10 SDK version 10.0.22621.0 was not found` | Windows 11 SDK 22621 미설치. §1의 "WinUI 애플리케이션 개발" 선택 사항 확인. | +| F5 시 `MddBootstrapInitialize ... 0x80070032` | 시작 프로젝트가 wapproj로 설정됨. §4 참고. | +| F5 시 "This application requires the Windows App Runtime 1.8" | 런타임 미설치. §2 참고. | +| `scratch-link-win-msix`가 회색으로 비활성화 | Configuration이 `*_Win`이 아닌 다른 것으로 되어 있음. `Debug_Win` 또는 `Release_Win`으로 전환. | +| CS 빌드 에러 (System.Management, Fleck 등 누락) | NuGet 캐시 불일치. CLI에서 `dotnet restore --force scratch-link-win/scratch-link-win.csproj` 실행 후 VS 재시작. | +| 빌드 시 `프로젝트에 'GitVersion' 대상이 없습니다` | VS 2026 MSBuild의 NuGetPackageRoot 경로 차이로 GitInfo.targets가 로드 안 되는 경우. `SharedProps/ScratchVersion.targets`에 fallback 타겟이 있어 정상 동작하므로 무시해도 된다. | + +## 7. 참고 + +- [`scratch-link-win/scratch-link-win.csproj`](../scratch-link-win/scratch-link-win.csproj) — 본체 프로젝트 설정 +- [`scratch-link-win-msix/scratch-link-win-msix.wapproj`](../scratch-link-win-msix/scratch-link-win-msix.wapproj) — MSIX 패키징 설정 +- [`SharedProps/WindowsSDK.props`](../SharedProps/WindowsSDK.props) — Windows App SDK 버전 핀 +- [`SharedProps/ScratchVersion.targets`](../SharedProps/ScratchVersion.targets) — 버전 자동 생성 로직 diff --git a/README.md b/README.md index d4355c91..276c9e32 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,100 @@ -# Scratch Link 2.0 +# Alux Scratch Link -Scratch Link is a helper application which allows Scratch 3.0 to communicate with hardware peripherals. Scratch Link -replaces the Scratch Device Manager and Scratch Device Plug-in. +Scratch 3.0과 PC에 연결된 하드웨어 주변기기를 중계하는 도우미 앱. +[scratchfoundation/scratch-link](https://github.com/scratchfoundation/scratch-link)의 **Windows 전용 포크**이며, 원본의 AGPL-3.0-only 라이선스를 그대로 따릅니다. -System Requirements: +## 원본과의 차이 -| | Minimum -| --- | --- -| macOS | 10.15 "Catalina" -| Windows | Windows 10 build 17763 +- **Windows 전용** — macOS 빌드와 Safari 확장은 제외. +- **Serial 전송 추가** — BLE / Bluetooth Classic에 더해 USB 시리얼(CDC/CH340 등) 장치를 `/scratch/serial` JSON-RPC 엔드포인트로 지원. 구현은 `scratch-link-common/Serial/`과 `scratch-link-win/Serial/` 참고. +- **포트 20211 사용** — 원본 Scratch Link(20110/20111)와 한 PC에서 공존 가능. +- **.NET 8 / WindowsAppSDK 1.8** — 원본의 .NET 6 / WindowsAppSDK 1.3에서 업그레이드. -The Windows version requires the Windows App Runtime version 1.2, and will install it automatically if possible. +## 시스템 요구사항 -Manual installation is available here (choose your platform): +| | 최소 사양 | +|---|---| +| Windows | Windows 10 build 17763 (1809) 이상 | +| Windows App Runtime | 1.8 (Windows 11 최신 업데이트 시 자동 설치됨) | -* https://aka.ms/windowsappsdk/1.2/latest/windowsappruntimeinstall-x64.exe -* https://aka.ms/windowsappsdk/1.2/latest/windowsappruntimeinstall-x86.exe -* https://aka.ms/windowsappsdk/1.2/latest/windowsappruntimeinstall-ARM64.exe +Windows App Runtime 1.8이 없는 경우 앱 실행 시 설치 안내가 표시됩니다. 수동 설치: -## Using Scratch Link with Scratch 3.0 +- [Windows App SDK 다운로드 페이지](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads)에서 1.8 런타임 설치 파일 다운로드 -To use Scratch Link with Scratch 3.0: +## AluxLabs와 함께 쓰기 -1. Install and run Scratch Link -2. Open [Scratch 3.0](https://scratch.mit.edu) -3. Select the "Add Extension" button (looks like Scratch blocks with a `+` at the bottom of the block categories list) -4. Select a compatible extension such as the micro:bit or LEGO EV3 extension. -5. Follow the prompts to connect your peripheral. -6. Build a project with the new extension blocks. Scratch Link will help Scratch communicate with your peripheral. +AluxLabs는 Alux 전용 Scratch 3.0입니다. -## Development: Getting started +1. Alux Scratch Link 실행 +2. AluxLabs 열기 +3. 블록 카테고리 아래 "확장 기능 추가" 선택 +4. CodeTinker, Connect, CodingDrone 등 지원 확장 선택 +5. 안내에 따라 주변기기 연결 -### Documentation +## 저장소 구조 -The general network protocol and all supported hardware protocols are documented in Markdown files in the -`Documentation` subdirectory. Please note that network protocol stability and compatibility are high priorities for -this project. Changes to the protocol are unlikely to be accepted without very strong justification combined with -thorough documentation. +``` +scratch-link/ +├── scratch-link-win/ # WinUI 3 앱 본체 (EXE) +│ ├── BLE/ # Bluetooth Low Energy (Windows) +│ ├── BT/ # Bluetooth Classic (Windows) +│ ├── Serial/ # USB 시리얼 (Windows) +│ └── Properties/PublishProfiles/ # win-x64/x86/arm64 publish 프로필 +├── scratch-link-win-msix/ # MSIX 패키징 프로젝트 (.wapproj) +├── scratch-link-common/ # 플랫폼 공유 C# 코드 (.shproj) +│ ├── BLE/ # BLE 세션 공통 로직 +│ ├── BT/ # BT 세션 공통 로직 +│ ├── Serial/ # 시리얼 세션 공통 로직 +│ ├── JsonRpc/ # JSON-RPC 2.0 구현 +│ └── Extensions/ # 유틸리티 확장 메서드 +├── SharedProps/ # 공유 MSBuild 프로퍼티 +│ ├── WindowsSDK.props # WindowsAppSDK 버전 핀 +│ ├── ScratchVersion.targets # Git 기반 버전 자동 생성 +│ ├── CommonPackageRefs.props # 공유 NuGet 패키지 +│ └── StyleCop.props # 코드 스타일 분석 +├── Documentation/ # upstream 원본 프로토콜 문서 +│ └── Alux/ # 이 포크 전용 문서 (upstream 동기화 시 제외) +└── brand/ # 아이콘 소스 SVG 및 빌드 스크립트 +``` -Please use [markdownlint](https://www.npmjs.com/package/markdownlint) to check documentation changes before submitting -a pull request. +## 개발 환경 구성 -### Version numbers +[Documentation/WindowsDevSetup-VS2026.md](Documentation/WindowsDevSetup-VS2026.md) 참고. -Scratch Link 2.0 uses [semantic-release](https://semantic-release.gitbook.io/semantic-release/) to control its version -number. The `develop` branch is treated as a pre-release branch, and `main` is treated as a release branch. Each time -a change is merged to either of those branches, `semantic-release` will calculate a new version number. +## 빌드 구성 -Apple requires that `CFBundleShortVersionString` is unique for published releases. The App Store will also reject an -upload unless the `CFBundleVersion` tuple is greater than that of previously uploaded builds. To make this easy, we -set `CFBundleShortVersionString` to the version calculated by `semantic-release`, and `CFBundleVersion` is calculated -from the date and time of the build commit. +| Configuration | 용도 | +|---|---| +| `Debug_Win` | 일상 개발/디버깅 (F5) | +| `Release_Win` | 배포용 빌드 및 MSIX 패키징 | -Extended version information is available within the application. This extended information is similar to `git -describe`. +Startup Project를 `scratch-link-win-msix`로 설정하면 MSIX 패키지 빌드가 실행됩니다. 일반 디버깅은 반드시 `scratch-link-win`으로 설정할 것. -### Secure WebSockets +## 버전 번호 -Some previous versions of Scratch Link used Secure WebSockets (`wss://`) to communicate with Scratch. This is no -longer the case: new versions of Scratch Link use regular WebSockets (`ws://`). It is no longer necessary to prepare -an SSL certificate for Scratch Link. +`SharedProps/ScratchVersion.targets`에서 git 메타데이터를 기반으로 자동 생성됩니다. -This change caused an incompatibility with some browsers, including Safari. The macOS version of Scratch Link 2.0 -includes a Safari extension to resolve this incompatibility. +- git semver 태그가 없으면 `1.0.0.<커밋수>` 형태 +- 정식 릴리즈 시 `git tag v1.1.0`처럼 태그를 찍으면 해당 버전을 따라감 -### Windows platforms and installer size +상세 버전 문자열은 트레이 메뉴의 버전 항목을 클릭해 클립보드로 복사할 수 있습니다. -The `PublishReadyToRun` (R2R) setting enables ahead-of-time (AOT) compilation, as opposed to just-in-time (JIT) -compilation. This can improve performance, especially at startup. The drawback is [R2R binaries are larger because -they contain both intermediate language (IL) code, which is still needed for some scenarios, and the native version -of the same code.](https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run) +## 브랜드 자산 -Recent versions of .NET (5.0 and above) can build a "Framework-Dependent Application" or a "Self-Contained -Application" depending on settings. +모든 아이콘은 [brand/labs-l.svg](brand/labs-l.svg)에서 파생됩니다. SVG 변경 시: -* A self-contained application includes the .NET runtime framework. This includes a platform-specific (x86, x64, or - ARM64) version of `dotnet.exe` to host the application. - * Cannot be built for "AnyCPU" because it must include the native portion of the runtime. - * The app can be "trimmed" to include only the portions of the framework needed by the application, but it'll - still be larger than a framework-dependent application. -* A framework-dependent application does not include the framework at all; it must be installed separately. - * The generated MSIX will trigger automatic framework installation if necessary (requires Internet connection). - * Can be built for "AnyCPU" since it doesn't include the native portion (or any other portion) of the runtime. - * Can be built for a specific CPU if desired. - * Debugging this requires setting `None` in the project file. +``` +pip install Pillow # 최초 1회 +python brand/build_icons.py +``` -When packaging an application: +생성물(ICO/PNG)은 커밋되어 있으므로 일반 빌드 시에는 실행 불필요. -* An MSIX file (`*.msix`) can contain exactly one platform (x86, x64, ARM64). -* An MSIX Bundle (`*.msixbundle`) can contain more than one MSIX -- one for each platform, for example. +## 패키징 및 배포 -Ideally, it would be possible to package a single "AnyCPU" build of the app with stub MSIX files to install each -platform-specific copy of the framework, resulting in a Bundle that's only a little larger than a single copy of the -app. More investigation needed. +현재 배포 방식은 **framework-dependent**입니다. Windows App Runtime 1.8이 없는 PC에서는 설치 안내가 표시됩니다. -However, it is possible to build a platform-specific MSIX containing an AnyCPU build of the app. That's much smaller -than a platform-specific build of the app, so even with 3 full copies of the AnyCPU app -- one each packaged for x86, -x64, and ARM64 -- the resulting bundle is significantly smaller. +- **MSIX 파일(`*.msix`)**: 단일 플랫폼(x86/x64/ARM64) +- **MSIX 번들(`*.msixbundle`)**: 여러 플랫폼을 하나로 묶어 배포 -Disabling R2R and bundling AnyCPU builds of the app generated a bundle roughly 12% of the size of a bundle of -self-contained apps for the same set of platforms. +self-contained 배포(런타임 번들링)로 전환하면 설치 안내가 완전히 사라지지만, 바이너리 크기가 증가합니다. AnyCPU 빌드에서는 self-contained를 지원하지 않으므로 플랫폼별 빌드(x64/x86/ARM64)가 필요합니다. diff --git a/SharedProps/ScratchVersion.targets b/SharedProps/ScratchVersion.targets index 469911a4..211fa17c 100644 --- a/SharedProps/ScratchVersion.targets +++ b/SharedProps/ScratchVersion.targets @@ -3,6 +3,24 @@ This file sets up version properties in our own Scratch way. --> + + + + 0 + 0 + 0 + + 0000000 + 0 + $([System.DateTime]::UtcNow.ToString("o")) + + + - $(GitSemVerMajor).$(GitSemVerMinor).$(GitSemVerPatch) + + 1.0.0 + $(GitSemVerMajor).$(GitSemVerMinor).$(GitSemVerPatch) $(ScratchVersionTriplet)$(GitSemVerDashLabel) $(GitCommit) $(ScratchVersionFull)+$(ScratchVersionHash) diff --git a/SharedProps/WindowsSDK.props b/SharedProps/WindowsSDK.props index 695044af..f681e936 100644 --- a/SharedProps/WindowsSDK.props +++ b/SharedProps/WindowsSDK.props @@ -2,11 +2,11 @@ - + build - + build diff --git a/brand/build_icons.py b/brand/build_icons.py new file mode 100644 index 00000000..59081a85 --- /dev/null +++ b/brand/build_icons.py @@ -0,0 +1,107 @@ +"""Rebuild Windows .ico + MSIX .png assets from the Alux brand SVG. + +Source of truth: + brand/labs-l.svg + +Outputs (overwrites in place; all paths relative to repo root): + scratch-link-win/scratch-link.ico app icon, 16..256 sizes + scratch-link-win/scratch-link-tray.ico tray icon, 16/24/32 + scratch-link-win-msix/Images/*.png MSIX tile/splash/store/lock assets + +How to run (from repo root): + pip install Pillow # one-time + python brand/build_icons.py + +When to re-run: + Whenever brand/labs-l.svg changes, or when a new MSIX asset slot needs + to be filled. Generated files are committed alongside the source so + that builds work without running this script. + +How it works: + labs-l.svg is a thin SVG wrapper around a single base64-encoded PNG + (non-square). We extract that PNG once, then for each target + slot fit it preserving aspect ratio onto a transparent canvas of the + required size. No external SVG renderer (cairosvg, rsvg, etc.) is + needed - Pillow is the only dependency. + +Editing the targets: + To add or change output sizes, edit ICO_TARGETS / PNG_TARGETS below. + ICO sizes must match the slots the existing scratch-link*.ico files + advertise; MSIX PNG dimensions are dictated by Windows + (Square44x44Logo.scale-200 must be 88x88, etc.). +""" + +from __future__ import annotations + +import base64 +import re +from io import BytesIO +from pathlib import Path + +from PIL import Image + +REPO = Path(__file__).resolve().parent.parent +SVG = REPO / "brand" / "labs-l.svg" + +WIN = REPO / "scratch-link-win" +MSIX = REPO / "scratch-link-win-msix" / "Images" + +ICO_TARGETS = { + WIN / "scratch-link.ico": [16, 24, 32, 48, 64, 96, 128, 256], + WIN / "scratch-link-tray.ico": [16, 24, 32], +} + +PNG_TARGETS = { + MSIX / "LockScreenLogo.scale-200.png": (48, 48), + MSIX / "SplashScreen.scale-200.png": (1240, 600), + MSIX / "Square150x150Logo.scale-200.png": (300, 300), + MSIX / "Square44x44Logo.scale-200.png": (88, 88), + MSIX / "Square44x44Logo.targetsize-24_altform-unplated.png": (24, 24), + MSIX / "StoreLogo.png": (50, 50), + MSIX / "Wide310x150Logo.scale-200.png": (620, 300), +} + + +def extract_source() -> Image.Image: + svg_text = SVG.read_text(encoding="utf-8") + match = re.search(r'xlink:href="data:image/png;base64,([^"]+)"', svg_text) + if not match: + raise SystemExit("Could not find embedded base64 PNG in SVG.") + png_bytes = base64.b64decode(match.group(1)) + img = Image.open(BytesIO(png_bytes)).convert("RGBA") + print(f"source image: {img.size}, mode={img.mode}") + return img + + +def fit_padded(src: Image.Image, target: tuple[int, int]) -> Image.Image: + """Scale src to fit inside `target` preserving aspect, center on transparent.""" + tw, th = target + sw, sh = src.size + scale = min(tw / sw, th / sh) + nw, nh = max(1, round(sw * scale)), max(1, round(sh * scale)) + resized = src.resize((nw, nh), Image.LANCZOS) + canvas = Image.new("RGBA", target, (0, 0, 0, 0)) + canvas.paste(resized, ((tw - nw) // 2, (th - nh) // 2), resized) + return canvas + + +def main() -> None: + src = extract_source() + + for path, sizes in ICO_TARGETS.items(): + biggest = max(sizes) + base = fit_padded(src, (biggest, biggest)) + # Pillow's ICO writer accepts a list of (w,h) sizes; it down-samples + # `base` for each entry. Providing pre-rendered frames isn't supported + # directly, but Lanczos downscaling from the 256x256 master is fine. + base.save(path, format="ICO", sizes=[(s, s) for s in sizes]) + print(f"wrote {path.relative_to(REPO)} with sizes {sorted(sizes)}") + + for path, target in PNG_TARGETS.items(): + out = fit_padded(src, target) + out.save(path, format="PNG", optimize=True) + print(f"wrote {path.relative_to(REPO)} {target}") + + +if __name__ == "__main__": + main() diff --git a/brand/labs-l.svg b/brand/labs-l.svg new file mode 100644 index 00000000..0cf24797 --- /dev/null +++ b/brand/labs-l.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/package.json b/package.json index 9ba2d76b..e258a44e 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "scratch-link", - "version": "2.1.0-develop.10", - "description": "Scratch Link is not published as an NPM package. This file only exists to facilitate installation of devDependencies.", - "author": "Scratch Foundation", + "name": "alux-scratch-link", + "version": "1.0.0", + "description": "Alux Scratch Link is not published as an NPM package. This file only exists to facilitate installation of devDependencies.", + "author": "ALUX, Inc.", "license": "AGPL-3.0-only", "scripts": { "prepare": "husky install" diff --git a/scratch-link-common/ScratchLinkApp.cs b/scratch-link-common/ScratchLinkApp.cs index 52626e00..fa7077a1 100644 --- a/scratch-link-common/ScratchLinkApp.cs +++ b/scratch-link-common/ScratchLinkApp.cs @@ -14,7 +14,7 @@ namespace ScratchLink; /// public class ScratchLinkApp { - private const int WebSocketPort = 20111; + private const int WebSocketPort = 20211; private readonly SessionManager sessionManager; private readonly WebSocketListener webSocketListener; diff --git a/scratch-link-common/Serial/SerialDiscoveryFilter.cs b/scratch-link-common/Serial/SerialDiscoveryFilter.cs new file mode 100644 index 00000000..1e2efd37 --- /dev/null +++ b/scratch-link-common/Serial/SerialDiscoveryFilter.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Serial; + +/// +/// A single filter entry passed by the client in a serial "discover" request. +/// A port is reported when it matches any filter in the list. +/// +internal class SerialDiscoveryFilter +{ + /// + /// Gets or sets the USB vendor ID to match, as a decimal integer. + /// + public int? UsbVendorId { get; set; } + + /// + /// Gets or sets the USB product ID to match, as a decimal integer. + /// + public int? UsbProductId { get; set; } + + /// + /// Gets or sets an optional port path substring to match (e.g. "COM7"). + /// + public string PathHint { get; set; } +} diff --git a/scratch-link-common/Serial/SerialOpenParams.cs b/scratch-link-common/Serial/SerialOpenParams.cs new file mode 100644 index 00000000..53316356 --- /dev/null +++ b/scratch-link-common/Serial/SerialOpenParams.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Serial; + +/// +/// Parameters extracted from a serial "connect" request. +/// +internal class SerialOpenParams +{ + /// + /// Gets or sets the baud rate. Required. + /// + public int BaudRate { get; set; } + + /// + /// Gets or sets the data bits. Defaults to 8. + /// + public int DataBits { get; set; } + + /// + /// Gets or sets the parity setting: "none", "even", or "odd". Defaults to "none". + /// + public string Parity { get; set; } + + /// + /// Gets or sets the stop bits: "one", "onePointFive", or "two". Defaults to "one". + /// + public string StopBits { get; set; } + + /// + /// Gets or sets the flow control: "none", "rtsCts", or "xonXoff". Defaults to "none". + /// + public string FlowControl { get; set; } + + /// + /// Gets or sets the client-supplied peripheral type identifier. Optional; used for diagnostic logging only. + /// + public string PeripheralType { get; set; } + + /// + /// Gets or sets the keep-alive interval in milliseconds. If positive, the most recently + /// sent TX packet is automatically re-sent at this interval while the line is idle. + /// Null or non-positive disables the timer. + /// + public int? KeepAliveIntervalMs { get; set; } + + /// + /// Gets or sets a value indicating whether wire-level TX/RX hex dumps are emitted via + /// . Diagnostic only; off by default. + /// + public bool WireTrace { get; set; } +} diff --git a/scratch-link-common/Serial/SerialSession.cs b/scratch-link-common/Serial/SerialSession.cs new file mode 100644 index 00000000..48dce96a --- /dev/null +++ b/scratch-link-common/Serial/SerialSession.cs @@ -0,0 +1,621 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Serial; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Fleck; +using ScratchLink.Extensions; +using ScratchLink.JsonRpc; + +/// +/// Cross-platform base for a USB Serial transport session. Uses Serial-specific +/// notification names (serialDidReceiveData, serialDidDisconnect) +/// so callers cannot confuse Serial events with BLE characteristic events or +/// BT message events. +/// +/// Platform-specific port handle, passed back to . +internal abstract class SerialSession : PeripheralSession + where TPort : class +{ + // Serializes DoWrite calls so two writes never overlap and corrupt the stream. + private readonly SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1); + + // Guards keep-alive lifecycle fields shared with the timer callback. + private readonly object stateLock = new object(); + + private byte[] lastSentData; + private Timer keepAliveTimer; + private int keepAliveIntervalMs; + private bool keepAliveActive; + + // volatile so threads outside stateLock see the latest value on the hot path. + private volatile bool wireTrace; + + /// + /// Initializes a new instance of the class. + /// + /// + public SerialSession(IWebSocketConnection webSocket) + : base(webSocket) + { + this.Handlers["discover"] = this.HandleDiscover; + this.Handlers["write"] = this.HandleWrite; + this.Handlers["disconnect"] = this.HandleDisconnect; + this.Handlers["startReading"] = this.HandleStartReading; + this.Handlers["stopReading"] = this.HandleStopReading; + this.Handlers["setKeepAlive"] = this.HandleSetKeepAlive; + } + + /// + /// Implement the JSON-RPC "discover" request. Parses the filter list and + /// kicks off platform-specific enumeration. Discovered ports are streamed + /// back via . + /// + /// The name of the method being called ("discover"). + /// A JSON object optionally containing a filters array. + /// A resolving to an empty result; discoveries are streamed via notifications. + protected Task HandleDiscover(string methodName, JsonElement? args) + { + var filters = ParseFilters(args); + Trace.WriteLine($"received serial discover request with {filters.Count} filter(s)"); + + this.ClearDiscoveredPeripherals(); + return this.DoDiscover(filters); + } + + /// + /// Platform-specific implementation for port discovery. Implementations + /// should return promptly and stream results via . + /// + /// The filter list from the client. Empty means "match all". + /// A representing the asynchronous operation. + protected abstract Task DoDiscover(IReadOnlyList filters); + + /// + protected override Task DoConnect(TPort port, JsonElement? args) + { + var openParams = ParseOpenParams(args); + this.wireTrace = openParams.WireTrace; + if (this.wireTrace) + { + Trace.WriteLine("wire-trace: enabled for this session"); + } + + return this.DoConnect(port, openParams); + } + + /// + /// Platform-specific implementation for opening the given port. On success, + /// RX should be active and incoming bytes should be reported via + /// . + /// + /// The port handle previously registered via . + /// Open parameters extracted from the connect request. + /// A representing the asynchronous operation. + protected abstract Task DoConnect(TPort port, SerialOpenParams openParams); + + /// + /// JSON-RPC write handler. Caches the payload as the most recent TX packet + /// and resets the keep-alive timer so resends are suppressed during active bursts. + /// + /// Dispatched method name. + /// Decoded request params. + /// sentBytes wrapper. + protected async Task HandleWrite(string methodName, JsonElement? args) + { + if (args == null) + { + throw JsonRpc2Error.InvalidParams("write requires a message buffer").ToException(); + } + + var buffer = EncodingHelpers.DecodeBuffer(args.Value); + + await this.writeSemaphore.WaitAsync().ConfigureAwait(false); + int sentBytes; + try + { + lock (this.stateLock) + { + this.lastSentData = buffer; + } + + this.ResetKeepAliveTimer(); + + if (this.wireTrace) + { + Trace.WriteLine($"wire-trace TX {buffer.Length}B {FormatHex(buffer)}"); + } + + sentBytes = await this.DoWrite(buffer).ConfigureAwait(false); + } + finally + { + this.writeSemaphore.Release(); + } + + return new Dictionary { ["sentBytes"] = sentBytes }; + } + + /// + /// Platform-specific implementation for sending bytes to the port. + /// + /// The bytes to send. + /// The number of bytes actually written. + protected abstract Task DoWrite(byte[] data); + + /// + /// Implement the JSON-RPC "disconnect" request. Closes the port without + /// firing a serialDidDisconnect notification (that is reserved for + /// external-cause disconnects). + /// + /// The name of the method being called ("disconnect"). + /// Unused. + /// An empty result. + protected async Task HandleDisconnect(string methodName, JsonElement? args) + { + await this.DoDisconnect(); + return new Dictionary(); + } + + /// + /// Platform-specific implementation for closing the port. + /// + /// A representing the asynchronous operation. + protected abstract Task DoDisconnect(); + + /// + /// Implement the JSON-RPC "startReading" request. RX is enabled automatically + /// on connect, so the default implementation is a no-op. Subclasses may + /// override to re-enable RX after a stopReading. + /// + /// The name of the method being called ("startReading"). + /// Unused. + /// An empty result. + protected virtual Task HandleStartReading(string methodName, JsonElement? args) + { + return Task.FromResult(new Dictionary()); + } + + /// + /// Implement the JSON-RPC "stopReading" request. Default is a no-op. + /// + /// The name of the method being called ("stopReading"). + /// Unused. + /// An empty result. + protected virtual Task HandleStopReading(string methodName, JsonElement? args) + { + return Task.FromResult(new Dictionary()); + } + + /// + /// JSON-RPC setKeepAlive handler. intervalMs: positive (re)starts, null/0/negative disables. + /// Idempotent: stop-then-start so repeated calls leave only one timer alive. + /// Response echoes the applied interval (null when disabled). + /// + /// Dispatched method name. + /// Decoded request params. + /// Echo of the applied interval. + protected Task HandleSetKeepAlive(string methodName, JsonElement? args) + { + int? requested = null; + if (args != null) + { + var prop = args.Value.TryGetProperty("intervalMs"); + if (prop.HasValue && prop.Value.ValueKind != JsonValueKind.Null) + { + requested = prop.Value.GetInt32(); + } + } + + // Stop-then-start makes the call idempotent regardless of current state. + this.StopKeepAlive(); + + int? applied = null; + if (requested.HasValue && requested.Value > 0) + { + this.StartKeepAlive(requested.Value); + applied = requested.Value; + } + + var appliedText = applied?.ToString() ?? "null"; + Trace.WriteLine($"keep-alive: setKeepAlive applied intervalMs={appliedText}"); + return Task.FromResult(new Dictionary { ["intervalMs"] = applied }); + } + + /// + /// Report received bytes to the client as a serialDidReceiveData + /// notification. The payload is base64-encoded. + /// + /// The bytes received. + /// A representing the asynchronous operation. + protected async Task DidReceiveData(byte[] data) + { + if (this.wireTrace) + { + Trace.WriteLine($"wire-trace RX {data.Length}B {FormatHex(data)}"); + } + + var encoded = EncodingHelpers.EncodeBuffer(data, "base64"); + await this.SendNotification("serialDidReceiveData", new SerialDataReceived + { + Encoding = "base64", + Message = encoded, + }); + } + + /// + /// Report an external-cause disconnect to the client as a + /// serialDidDisconnect notification. Does not fire for the + /// client-initiated disconnect request. + /// + /// One of "user", "device", "error", "shutdown". + /// Optional human-readable detail. + /// A representing the asynchronous operation. + protected async Task DidDisconnect(string reason, string message = null) + { + await this.SendNotification("serialDidDisconnect", new SerialDisconnectMessage + { + Reason = reason, + Message = message, + }); + } + + /// + /// Track a discovered port and report it to the client. Uses + /// to + /// obtain a session-scoped peripheral ID. + /// + /// Platform-specific port handle. + /// OS-level port path used as the address (e.g. "COM7"). + /// User-visible name, may include the path. + /// Vendor ID as a hex string (e.g. "0x1A86"), or null. + /// Product ID as a hex string (e.g. "0x7523"), or null. + /// A representing the asynchronous operation. + protected async Task OnPortDiscovered(TPort port, string path, string displayName, string vendorIdHex, string productIdHex) + { + var peripheralId = this.RegisterPeripheral(port, path); + + await this.SendNotification("didDiscoverPeripheral", new SerialPortDiscovered + { + PeripheralId = peripheralId, + Name = displayName, + Path = path, + VendorId = vendorIdHex, + ProductId = productIdHex, + RSSI = 0, + }); + } + + /// + /// Start the keep-alive timer at . Null or non-positive disables. + /// No-op if already running. + /// + /// Interval in milliseconds; null/non-positive disables. + protected void StartKeepAlive(int? keepAliveIntervalMs) + { + if (keepAliveIntervalMs == null || keepAliveIntervalMs.Value <= 0) + { + return; + } + + var interval = keepAliveIntervalMs.Value; + + lock (this.stateLock) + { + if (this.keepAliveActive) + { + Trace.WriteLine("keep-alive: StartKeepAlive called while already active; ignoring"); + return; + } + + this.keepAliveIntervalMs = interval; + this.keepAliveActive = true; + this.keepAliveTimer = new Timer(this.OnKeepAliveTick, null, interval, interval); + } + + Trace.WriteLine($"keep-alive: started ({interval}ms)"); + } + + /// + /// Stop the keep-alive timer and block until any in-flight tick finishes. + /// Safe to call repeatedly. + /// + protected void StopKeepAlive() + { + Timer toDispose; + lock (this.stateLock) + { + if (!this.keepAliveActive) + { + return; + } + + this.keepAliveActive = false; + toDispose = this.keepAliveTimer; + this.keepAliveTimer = null; + } + + if (toDispose != null) + { + // Block so no resend races a subsequent disconnect or port disposal. + using var waitHandle = new ManualResetEvent(false); + if (toDispose.Dispose(waitHandle)) + { + waitHandle.WaitOne(); + } + } + + Trace.WriteLine("keep-alive: stopped"); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing && !this.DisposedValue) + { + // Order matters: StopKeepAlive waits for in-flight ticks before we dispose the semaphore they use. + this.StopKeepAlive(); + this.writeSemaphore.Dispose(); + } + + base.Dispose(disposing); + } + + private static IReadOnlyList ParseFilters(JsonElement? args) + { + var result = new List(); + + var filtersElement = args?.TryGetProperty("filters"); + if (filtersElement == null) + { + return result; + } + + if (filtersElement.Value.ValueKind != JsonValueKind.Array) + { + throw JsonRpc2Error.InvalidParams("'filters' must be an array").ToException(); + } + + foreach (var item in filtersElement.Value.EnumerateArray()) + { + result.Add(new SerialDiscoveryFilter + { + UsbVendorId = item.TryGetProperty("usbVendorId")?.GetInt32(), + UsbProductId = item.TryGetProperty("usbProductId")?.GetInt32(), + PathHint = item.TryGetProperty("pathHint")?.GetString(), + }); + } + + return result; + } + + private static SerialOpenParams ParseOpenParams(JsonElement? args) + { + var baudRate = args?.TryGetProperty("baudRate")?.GetInt32(); + if (baudRate == null) + { + throw JsonRpc2Error.InvalidParams("connect requires baudRate").ToException(); + } + + return new SerialOpenParams + { + BaudRate = baudRate.Value, + DataBits = args?.TryGetProperty("dataBits")?.GetInt32() ?? 8, + Parity = args?.TryGetProperty("parity")?.GetString() ?? "none", + StopBits = args?.TryGetProperty("stopBits")?.GetString() ?? "one", + FlowControl = args?.TryGetProperty("flowControl")?.GetString() ?? "none", + PeripheralType = args?.TryGetProperty("peripheralType")?.GetString(), + KeepAliveIntervalMs = args?.TryGetProperty("keepAliveIntervalMs")?.GetInt32(), + WireTrace = args?.TryGetProperty("wireTrace")?.GetBoolean() ?? false, + }; + } + + /// + /// Hex preview for diagnostic logs, capped at with a tail marker. + /// + private static string FormatHex(byte[] data, int maxBytes = 256) + { + if (data == null || data.Length == 0) + { + return string.Empty; + } + + var take = data.Length <= maxBytes ? data.Length : maxBytes; + var sb = new System.Text.StringBuilder(take * 3); + for (var i = 0; i < take; i++) + { + if (i > 0) + { + sb.Append(' '); + } + + sb.Append(data[i].ToString("x2", System.Globalization.CultureInfo.InvariantCulture)); + } + + if (data.Length > take) + { + sb.Append($" …(+{data.Length - take}B)"); + } + + return sb.ToString(); + } + + /// + /// Push the next tick one full interval forward so that ongoing write bursts suppress the resend. + /// + private void ResetKeepAliveTimer() + { + Timer timer; + int interval; + lock (this.stateLock) + { + if (!this.keepAliveActive || this.keepAliveTimer == null) + { + return; + } + + timer = this.keepAliveTimer; + interval = this.keepAliveIntervalMs; + } + + try + { + timer.Change(interval, interval); + } + catch (ObjectDisposedException) + { + // Raced with StopKeepAlive. + } + } + + private async void OnKeepAliveTick(object state) + { + byte[] data; + lock (this.stateLock) + { + if (!this.keepAliveActive) + { + return; + } + + data = this.lastSentData; + } + + if (data == null || data.Length == 0 || !this.IsConnected) + { + return; + } + + // WaitAsync(0) makes the tick idle-only: during a write burst the semaphore is busy and we no-op. + if (!await this.writeSemaphore.WaitAsync(0).ConfigureAwait(false)) + { + return; + } + + try + { + if (!this.keepAliveActive || !this.IsConnected) + { + return; + } + + if (this.wireTrace) + { + Trace.WriteLine($"wire-trace TX(keep-alive) {data.Length}B {FormatHex(data)}"); + } + + await this.DoWrite(data).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // Session disposed mid-tick. + } + catch (Exception e) + { + // async-void Timer callback: an escaped exception terminates the process, so log and move on. + Trace.WriteLine($"keep-alive: resend failed: {e.GetType().Name}: {e.Message}"); + } + finally + { + try + { + this.writeSemaphore.Release(); + } + catch (ObjectDisposedException) + { + // Semaphore disposed during shutdown. + } + } + } + + /// + /// Payload of a serialDidReceiveData notification. + /// + protected class SerialDataReceived + { + /// + /// Gets or sets the encoding identifier; always "base64" for serial RX. + /// + [JsonPropertyName("encoding")] + public string Encoding { get; set; } + + /// + /// Gets or sets the encoded payload. + /// + [JsonPropertyName("message")] + public string Message { get; set; } + } + + /// + /// Payload of a serialDidDisconnect notification. + /// + protected class SerialDisconnectMessage + { + /// + /// Gets or sets the disconnect reason: "user", "device", "error", or "shutdown". + /// + [JsonPropertyName("reason")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Reason { get; set; } + + /// + /// Gets or sets an optional human-readable detail message. + /// + [JsonPropertyName("message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Message { get; set; } + } + + /// + /// Payload of a didDiscoverPeripheral notification on the serial transport. + /// + protected class SerialPortDiscovered + { + /// + /// Gets or sets the session-scoped peripheral ID used by the client to connect. + /// + [JsonPropertyName("peripheralId")] + public string PeripheralId { get; set; } + + /// + /// Gets or sets the user-visible name of the port. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the OS-level port path (e.g. "COM7"). + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// Gets or sets the USB vendor ID as a hex string (e.g. "0x1A86"), if known. + /// + [JsonPropertyName("vendorId")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string VendorId { get; set; } + + /// + /// Gets or sets the USB product ID as a hex string (e.g. "0x7523"), if known. + /// + [JsonPropertyName("productId")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string ProductId { get; set; } + + /// + /// Gets or sets a placeholder RSSI value for cross-transport message compatibility. + /// + [JsonPropertyName("rssi")] + public int RSSI { get; set; } + } +} diff --git a/scratch-link-common/scratch-link-common.projitems b/scratch-link-common/scratch-link-common.projitems index 9ce3b456..569953fd 100644 --- a/scratch-link-common/scratch-link-common.projitems +++ b/scratch-link-common/scratch-link-common.projitems @@ -13,6 +13,9 @@ + + + diff --git a/scratch-link-win-msix/Images/LockScreenLogo.scale-200.png b/scratch-link-win-msix/Images/LockScreenLogo.scale-200.png index ff37a993..de5f4da9 100644 Binary files a/scratch-link-win-msix/Images/LockScreenLogo.scale-200.png and b/scratch-link-win-msix/Images/LockScreenLogo.scale-200.png differ diff --git a/scratch-link-win-msix/Images/SplashScreen.scale-200.png b/scratch-link-win-msix/Images/SplashScreen.scale-200.png index afc0fdd2..38974c6e 100644 Binary files a/scratch-link-win-msix/Images/SplashScreen.scale-200.png and b/scratch-link-win-msix/Images/SplashScreen.scale-200.png differ diff --git a/scratch-link-win-msix/Images/Square150x150Logo.scale-200.png b/scratch-link-win-msix/Images/Square150x150Logo.scale-200.png index 5f8efebb..e8446163 100644 Binary files a/scratch-link-win-msix/Images/Square150x150Logo.scale-200.png and b/scratch-link-win-msix/Images/Square150x150Logo.scale-200.png differ diff --git a/scratch-link-win-msix/Images/Square44x44Logo.scale-200.png b/scratch-link-win-msix/Images/Square44x44Logo.scale-200.png index 777e7e6d..85715c35 100644 Binary files a/scratch-link-win-msix/Images/Square44x44Logo.scale-200.png and b/scratch-link-win-msix/Images/Square44x44Logo.scale-200.png differ diff --git a/scratch-link-win-msix/Images/Square44x44Logo.targetsize-24_altform-unplated.png b/scratch-link-win-msix/Images/Square44x44Logo.targetsize-24_altform-unplated.png index 93133511..d0ad3681 100644 Binary files a/scratch-link-win-msix/Images/Square44x44Logo.targetsize-24_altform-unplated.png and b/scratch-link-win-msix/Images/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/scratch-link-win-msix/Images/StoreLogo.png b/scratch-link-win-msix/Images/StoreLogo.png index d14b2627..dba26ba4 100644 Binary files a/scratch-link-win-msix/Images/StoreLogo.png and b/scratch-link-win-msix/Images/StoreLogo.png differ diff --git a/scratch-link-win-msix/Images/Wide310x150Logo.scale-200.png b/scratch-link-win-msix/Images/Wide310x150Logo.scale-200.png index 1530f6bd..d35fa8c1 100644 Binary files a/scratch-link-win-msix/Images/Wide310x150Logo.scale-200.png and b/scratch-link-win-msix/Images/Wide310x150Logo.scale-200.png differ diff --git a/scratch-link-win-msix/Package.appxmanifest b/scratch-link-win-msix/Package.appxmanifest index 9d911758..87fb2128 100644 --- a/scratch-link-win-msix/Package.appxmanifest +++ b/scratch-link-win-msix/Package.appxmanifest @@ -9,11 +9,11 @@ + Version="1.0.0.0" /> - Scratch Link - Scratch Foundation + Alux Scratch Link + ALUX, Inc. Images\StoreLogo.png @@ -31,8 +31,8 @@ Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$"> @@ -45,5 +45,10 @@ + + + + + diff --git a/scratch-link-win-msix/scratch-link-win-msix.wapproj b/scratch-link-win-msix/scratch-link-win-msix.wapproj index 3f5c7b37..1cea3e77 100644 --- a/scratch-link-win-msix/scratch-link-win-msix.wapproj +++ b/scratch-link-win-msix/scratch-link-win-msix.wapproj @@ -45,7 +45,7 @@ 265ca433-8639-4e8d-a7f3-09b1b3495d92 10.0.22621.0 10.0.17763.0 - net6.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) + net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) en-US false ..\scratch-link-win\scratch-link-win.csproj @@ -98,7 +98,7 @@ True - Properties\PublishProfiles\win10-$(Platform).pubxml + Properties\PublishProfiles\win-$(Platform).pubxml diff --git a/scratch-link-win/Properties/PublishProfiles/win10-arm64.pubxml b/scratch-link-win/Properties/PublishProfiles/win-arm64.pubxml similarity index 86% rename from scratch-link-win/Properties/PublishProfiles/win10-arm64.pubxml rename to scratch-link-win/Properties/PublishProfiles/win-arm64.pubxml index 9855be36..0f38a644 100644 --- a/scratch-link-win/Properties/PublishProfiles/win10-arm64.pubxml +++ b/scratch-link-win/Properties/PublishProfiles/win-arm64.pubxml @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem ARM64 - win10-arm64 + win-arm64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ false diff --git a/scratch-link-win/Properties/PublishProfiles/win10-x64.pubxml b/scratch-link-win/Properties/PublishProfiles/win-x64.pubxml similarity index 86% rename from scratch-link-win/Properties/PublishProfiles/win10-x64.pubxml rename to scratch-link-win/Properties/PublishProfiles/win-x64.pubxml index 6d97548c..ac14247a 100644 --- a/scratch-link-win/Properties/PublishProfiles/win10-x64.pubxml +++ b/scratch-link-win/Properties/PublishProfiles/win-x64.pubxml @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x64 - win10-x64 + win-x64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ false diff --git a/scratch-link-win/Properties/PublishProfiles/win10-x86.pubxml b/scratch-link-win/Properties/PublishProfiles/win-x86.pubxml similarity index 86% rename from scratch-link-win/Properties/PublishProfiles/win10-x86.pubxml rename to scratch-link-win/Properties/PublishProfiles/win-x86.pubxml index a6c966a4..3322027e 100644 --- a/scratch-link-win/Properties/PublishProfiles/win10-x86.pubxml +++ b/scratch-link-win/Properties/PublishProfiles/win-x86.pubxml @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem x86 - win10-x86 + win-x86 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ false diff --git a/scratch-link-win/Serial/WinSerialPortEnumerator.cs b/scratch-link-win/Serial/WinSerialPortEnumerator.cs new file mode 100644 index 00000000..4167521a --- /dev/null +++ b/scratch-link-win/Serial/WinSerialPortEnumerator.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Win.Serial; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Management; +using System.Text.RegularExpressions; +using ScratchLink.Serial; + +/// +/// Enumerates USB serial ports on Windows via WMI (Win32_PnPEntity), extracting +/// the COM port name plus USB VID/PID from the PNPDeviceID. Used by +/// for discovery. +/// +internal static class WinSerialPortEnumerator +{ + private const string WmiQuery = + "SELECT DeviceID, PNPDeviceID, Caption, Name FROM Win32_PnPEntity " + + "WHERE PNPClass = 'Ports' AND PNPDeviceID LIKE 'USB%'"; + + private static readonly Regex VidPidRegex = new ( + @"VID_([0-9A-F]{4})&PID_([0-9A-F]{4})", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex ComPortRegex = new ( + @"\((COM\d+)\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Synchronously query WMI for USB serial ports matching any of the given filters. + /// + /// Filter list. Empty means "return all matching USB serial ports". + /// List of matching ports. May be empty. + public static IReadOnlyList Query(IReadOnlyList filters) + { + var results = new List(); + + try + { + using var searcher = new ManagementObjectSearcher(WmiQuery); + using var collection = searcher.Get(); + + foreach (var item in collection) + { + using var mo = (ManagementObject)item; + var info = BuildPortInfo(mo); + if (info == null) + { + continue; + } + + if (!MatchesAnyFilter(info, filters)) + { + continue; + } + + results.Add(info); + } + } + catch (ManagementException e) + { + Trace.WriteLine($"WMI query failed during serial port enumeration: {e}"); + } + catch (Exception e) + { + Trace.WriteLine($"Unexpected error during serial port enumeration: {e}"); + } + + return results; + } + + private static WinSerialPortInfo BuildPortInfo(ManagementObject mo) + { + var pnpId = mo["PNPDeviceID"] as string ?? string.Empty; + var caption = mo["Caption"] as string ?? string.Empty; + var name = mo["Name"] as string ?? caption; + + var comMatch = ComPortRegex.Match(caption); + if (!comMatch.Success) + { + // No COM port number means this isn't a usable serial port from our point of view. + return null; + } + + int? vendorId = null; + int? productId = null; + var vidPidMatch = VidPidRegex.Match(pnpId); + if (vidPidMatch.Success) + { + vendorId = int.Parse(vidPidMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + productId = int.Parse(vidPidMatch.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return new WinSerialPortInfo + { + Path = comMatch.Groups[1].Value, + DisplayName = name, + VendorId = vendorId, + ProductId = productId, + PnpDeviceId = pnpId, + }; + } + + private static bool MatchesAnyFilter(WinSerialPortInfo info, IReadOnlyList filters) + { + if (filters == null || filters.Count == 0) + { + return true; + } + + foreach (var filter in filters) + { + if (filter.UsbVendorId.HasValue && filter.UsbVendorId.Value != info.VendorId) + { + continue; + } + + if (filter.UsbProductId.HasValue && filter.UsbProductId.Value != info.ProductId) + { + continue; + } + + if (!string.IsNullOrEmpty(filter.PathHint)) + { + if (info.Path == null || + info.Path.IndexOf(filter.PathHint, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + } + + return true; + } + + return false; + } +} diff --git a/scratch-link-win/Serial/WinSerialPortInfo.cs b/scratch-link-win/Serial/WinSerialPortInfo.cs new file mode 100644 index 00000000..b28f27d2 --- /dev/null +++ b/scratch-link-win/Serial/WinSerialPortInfo.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Win.Serial; + +/// +/// A single port returned by . +/// +internal class WinSerialPortInfo +{ + /// + /// Gets or sets the OS-level port path (e.g. "COM7"). + /// + public string Path { get; set; } + + /// + /// Gets or sets a user-visible name (typically includes the COM number). + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets the USB vendor ID, or null if not parseable. + /// + public int? VendorId { get; set; } + + /// + /// Gets or sets the USB product ID, or null if not parseable. + /// + public int? ProductId { get; set; } + + /// + /// Gets or sets the raw PNPDeviceID, useful for surprise-removal matching. + /// + public string PnpDeviceId { get; set; } +} diff --git a/scratch-link-win/Serial/WinSerialSession.cs b/scratch-link-win/Serial/WinSerialSession.cs new file mode 100644 index 00000000..6104731e --- /dev/null +++ b/scratch-link-win/Serial/WinSerialSession.cs @@ -0,0 +1,376 @@ +// +// Copyright (c) 2026 ALUX, Inc. All rights reserved. +// Based on scratch-link by Scratch Foundation, licensed under AGPL-3.0-only. +// + +namespace ScratchLink.Win.Serial; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Ports; +using System.Threading; +using System.Threading.Tasks; +using Fleck; +using ScratchLink.JsonRpc; +using ScratchLink.Serial; + +/// +/// Implements a USB Serial session on Windows using +/// for I/O and WMI for VID/PID-aware port discovery. +/// +internal class WinSerialSession : SerialSession +{ + // Serializes Read and Write on the same handle: concurrent calls trigger a TimeoutException burst on the read side with CH340/CP210x drivers. + private readonly object ioLock = new object(); + + private SerialPort port; + private CancellationTokenSource rxCts; + private Task rxLoop; + + /// + /// Initializes a new instance of the class. + /// + /// The WebSocket connection for this session. + public WinSerialSession(IWebSocketConnection webSocket) + : base(webSocket) + { + } + + /// + protected override bool IsConnected => this.port != null && this.port.IsOpen; + + /// + protected override async Task DoDiscover(IReadOnlyList filters) + { + var ports = await Task.Run(() => WinSerialPortEnumerator.Query(filters)); + + foreach (var portInfo in ports) + { + var vendorHex = portInfo.VendorId.HasValue + ? $"0x{portInfo.VendorId.Value:X4}" + : null; + var productHex = portInfo.ProductId.HasValue + ? $"0x{portInfo.ProductId.Value:X4}" + : null; + + await this.OnPortDiscovered(portInfo, portInfo.Path, portInfo.DisplayName, vendorHex, productHex); + } + + return new Dictionary(); + } + + /// + protected override Task DoConnect(WinSerialPortInfo info, SerialOpenParams openParams) + { + if (this.port != null) + { + throw JsonRpc2Error.InvalidRequest("already connected").ToException(); + } + + if (!string.IsNullOrEmpty(openParams.PeripheralType)) + { + Trace.WriteLine($"Connecting to {info.Path} with peripheral type: {openParams.PeripheralType}"); + } + + try + { + this.port = new SerialPort(info.Path) + { + BaudRate = openParams.BaudRate, + DataBits = openParams.DataBits, + Parity = MapParity(openParams.Parity), + StopBits = MapStopBits(openParams.StopBits), + Handshake = MapFlowControl(openParams.FlowControl), + ReadTimeout = 500, + WriteTimeout = SerialPort.InfiniteTimeout, + // CH340 + codetinker firmware treats DTR/RTS transitions as a reset signal; + // pin them low explicitly so SerialPort.Open does not toggle them. + DtrEnable = false, + RtsEnable = false, + }; + this.port.Open(); + } + catch (Exception e) + { + Trace.WriteLine($"Failed to open serial port {info.Path}: {e}"); + this.CloseConnectionSilently(); + throw JsonRpc2Error.ApplicationError($"could not open serial port {info.Path}: {e.Message}").ToException(); + } + + this.rxCts = new CancellationTokenSource(); + var token = this.rxCts.Token; + this.rxLoop = Task.Run(() => this.ReadLoop(token)); + + this.StartKeepAlive(openParams.KeepAliveIntervalMs); + + return Task.FromResult(new Dictionary()); + } + + /// + protected override async Task DoWrite(byte[] data) + { + var currentPort = this.port; + if (currentPort == null || !currentPort.IsOpen) + { + throw JsonRpc2Error.InvalidRequest("cannot write when not connected").ToException(); + } + + // Sync Write under ioLock; see ioLock declaration for why. Task.Run keeps the async signature off the dispatcher thread. + try + { + await Task.Run(() => + { + lock (this.ioLock) + { + if (!currentPort.IsOpen) + { + throw new InvalidOperationException("port closed"); + } + + currentPort.Write(data, 0, data.Length); + } + }).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + throw JsonRpc2Error.InternalError("write failed: port was disposed").ToException(); + } + catch (InvalidOperationException) + { + // Port was closed between the IsOpen check above and Write. + throw JsonRpc2Error.InvalidRequest("cannot write when not connected").ToException(); + } + catch (IOException e) + { + throw JsonRpc2Error.InternalError($"write failed: {e.Message}").ToException(); + } + + return data.Length; + } + + /// + protected override async Task DoDisconnect() + { + this.StopKeepAlive(); + var loop = this.rxLoop; + this.CloseConnectionSilently(); + + if (loop != null) + { + try + { + await loop; + } + catch + { + // swallow: the loop's own error path already reported anything client-visible + } + } + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + this.CloseConnectionSilently(); + } + + private static Parity MapParity(string parity) => + (parity ?? "none").ToLowerInvariant() switch + { + "none" => Parity.None, + "even" => Parity.Even, + "odd" => Parity.Odd, + "mark" => Parity.Mark, + "space" => Parity.Space, + _ => throw JsonRpc2Error.InvalidParams($"unsupported parity: {parity}").ToException(), + }; + + private static StopBits MapStopBits(string stopBits) => + (stopBits ?? "one").ToLowerInvariant() switch + { + "one" => StopBits.One, + "onepointfive" => StopBits.OnePointFive, + "two" => StopBits.Two, + _ => throw JsonRpc2Error.InvalidParams($"unsupported stopBits: {stopBits}").ToException(), + }; + + private static Handshake MapFlowControl(string flow) => + (flow ?? "none").ToLowerInvariant() switch + { + "none" => Handshake.None, + "rtscts" => Handshake.RequestToSend, + "xonxoff" => Handshake.XOnXOff, + _ => throw JsonRpc2Error.InvalidParams($"unsupported flowControl: {flow}").ToException(), + }; + + private void ReadLoop(CancellationToken ct) + { + var buf = new byte[4096]; + + while (!ct.IsCancellationRequested) + { + var currentPort = this.port; + if (currentPort == null || !currentPort.IsOpen) + { + break; + } + + // Poll BytesToRead so Read is only called when there's data to drain — keeps ioLock hold time minimal. + int available; + try + { + available = currentPort.BytesToRead; + } + catch (ObjectDisposedException) + { + // Derives from InvalidOperationException; catch first. + break; + } + catch (InvalidOperationException) + { + break; + } + catch (IOException) when (ct.IsCancellationRequested) + { + break; + } + catch (IOException e) + { + Trace.WriteLine($"Serial BytesToRead IOException on {currentPort.PortName}: {e.Message}"); + _ = this.DidDisconnect("device", e.Message); + this.CloseConnectionSilently(); + break; + } + + if (available <= 0) + { + // Wait on ct.WaitHandle so cancellation wakes the loop immediately; otherwise sleep 10ms. + try + { + if (ct.WaitHandle.WaitOne(10)) + { + break; + } + } + catch (ObjectDisposedException) + { + break; + } + + continue; + } + + int n; + try + { + lock (this.ioLock) + { + if (!currentPort.IsOpen) + { + break; + } + + n = currentPort.Read(buf, 0, Math.Min(available, buf.Length)); + } + } + catch (TimeoutException) + { + // Defensive: BytesToRead gate should prevent this. + continue; + } + catch (OperationCanceledException) + { + break; + } + catch (IOException) when (ct.IsCancellationRequested) + { + break; + } + catch (IOException e) + { + Trace.WriteLine($"Serial read IOException on {currentPort.PortName}: {e.Message}"); + _ = this.DidDisconnect("device", e.Message); + this.CloseConnectionSilently(); + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (InvalidOperationException) + { + break; + } + catch (Exception e) + { + Trace.WriteLine($"Unexpected serial read error: {e}"); + _ = this.DidDisconnect("error", e.Message); + this.CloseConnectionSilently(); + break; + } + + if (n <= 0) + { + continue; + } + + var data = new byte[n]; + Buffer.BlockCopy(buf, 0, data, 0, n); + _ = this.DidReceiveData(data); + } + } + + private void CloseConnectionSilently() + { + var localCts = this.rxCts; + this.rxCts = null; + + try + { + localCts?.Cancel(); + } + catch + { + // ignored + } + + var localPort = this.port; + this.port = null; + + if (localPort != null) + { + try + { + if (localPort.IsOpen) + { + localPort.Close(); + } + } + catch (Exception e) + { + Trace.WriteLine($"Error closing serial port: {e}"); + } + + try + { + localPort.Dispose(); + } + catch + { + // ignored + } + } + + try + { + localCts?.Dispose(); + } + catch + { + // ignored + } + } +} diff --git a/scratch-link-win/TrayIcon.xaml b/scratch-link-win/TrayIcon.xaml index ecce2afa..40fdf892 100644 --- a/scratch-link-win/TrayIcon.xaml +++ b/scratch-link-win/TrayIcon.xaml @@ -9,7 +9,7 @@ diff --git a/scratch-link-win/WinSessionManager.cs b/scratch-link-win/WinSessionManager.cs index 9d8e1313..36603a24 100644 --- a/scratch-link-win/WinSessionManager.cs +++ b/scratch-link-win/WinSessionManager.cs @@ -7,6 +7,7 @@ namespace ScratchLink.Win; using Fleck; using ScratchLink.Win.BLE; using ScratchLink.Win.BT; +using ScratchLink.Win.Serial; /// /// Implements the Windows-specific functionality of the SessionManager. @@ -21,6 +22,7 @@ protected override Session MakeNewSession(IWebSocketConnection webSocket) { "/scratch/ble" => new WinBLESession(webSocket), "/scratch/bt" => new WinBTSession(webSocket), + "/scratch/serial" => new WinSerialSession(webSocket), // for unrecognized paths, return a base Session for debugging _ => new Session(webSocket), diff --git a/scratch-link-win/app.manifest b/scratch-link-win/app.manifest index e0412302..dc82b519 100644 --- a/scratch-link-win/app.manifest +++ b/scratch-link-win/app.manifest @@ -1,6 +1,6 @@ - + @@ -8,6 +8,7 @@ For more info see https://docs.microsoft.com/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 It is also necessary to support features in unpackaged applications, for example the custom titlebar implementation.--> + diff --git a/scratch-link-win/scratch-link-tray.ico b/scratch-link-win/scratch-link-tray.ico index b53e3878..41fe21de 100644 Binary files a/scratch-link-win/scratch-link-tray.ico and b/scratch-link-win/scratch-link-tray.ico differ diff --git a/scratch-link-win/scratch-link-win.csproj b/scratch-link-win/scratch-link-win.csproj index df4ac45a..64f68fa0 100644 --- a/scratch-link-win/scratch-link-win.csproj +++ b/scratch-link-win/scratch-link-win.csproj @@ -1,17 +1,20 @@ WinExe - net6.0-windows10.0.22621.0 - win10-x86;win10-x64;win10-arm64 + net8.0-windows10.0.22621.0 + win-x86;win-x64;win-arm64 10.0.17763.0 ScratchLink.Win - Scratch Link - Scratch Foundation + Alux Scratch Link + ALUX, Inc. $(Company) disable true None - false + + + true + false enable app.manifest AnyCPU @@ -41,5 +44,7 @@ + + diff --git a/scratch-link-win/scratch-link.ico b/scratch-link-win/scratch-link.ico index 42da3ace..126b93e9 100644 Binary files a/scratch-link-win/scratch-link.ico and b/scratch-link-win/scratch-link.ico differ