diff --git a/.bazelrc b/.bazelrc index abdeacb42..715d3fce9 100644 --- a/.bazelrc +++ b/.bazelrc @@ -10,6 +10,14 @@ common --tool_java_runtime_version=remotejdk_21 # compilation (and Turbine can't run Lombok), so disable header compilation. build --java_header_compilation=false +# Stamping: workspace status provides STABLE_VERSION as YYYY.MMDD.HHMM +# (per-minute UTC, each component <65535 for MSI). Invoked as `bash` so it +# works on Linux/macOS and on Windows via Git bash (C:\Program Files\Git\bin\bash.exe). +# Use --config=stamp or --stamp together with this command. The stamp config +# bundles both. +build:stamp --stamp +build:stamp --workspace_status_command="bash scripts/workspace_status.sh" + # Dev machine toolchain workarounds: the system gcc is wrapped by ccache and # the system linker (lld) lives in /opt/bin, outside Bazel's default action PATH. build --repo_env=CC=/usr/bin/gcc diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index 049718ecf..39cdac4a7 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -23,7 +23,8 @@ jobs: - name: Build with Bazel run: | cd fluxengine - bazel test //... //:fluxengine_deb //:fluxengine_rpm //:fluxengine_app_image --test_output=errors + bazel test //... --test_output=errors + bazel build //:fluxengine_deb //:fluxengine_rpm //:fluxengine_app_image --config=stamp --test_output=errors - name: Upload build artifacts uses: actions/upload-artifact@v4 with: @@ -52,7 +53,8 @@ jobs: - name: Build with Bazel run: | cd fluxengine - bazel test //... //:fluxengine_dmg --test_output=errors + bazel test //... --test_output=errors + bazel build //:fluxengine_dmg --config=stamp --test_output=errors - name: Upload build artifacts uses: actions/upload-artifact@v4 @@ -92,7 +94,12 @@ jobs: $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true cd fluxengine - bazel test //... //:fluxengine_msi --test_output=errors ` + bazel test //... --test_output=errors ` + --action_env=PATH ` + --action_env=INCLUDE ` + --action_env=LIB ` + --action_env=LIBPATH + bazel build //:fluxengine_msi --config=stamp --test_output=errors ` --action_env=PATH ` --action_env=INCLUDE ` --action_env=LIB ` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa22567cb..1e0ebae51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,10 +33,10 @@ jobs: - name: Build with Bazel run: | cd fluxengine - bazel build //:fluxengine_deb //:fluxengine_rpm //:fluxengine_app_image --test_output=errors - mv bazel-bin/java/com/cowlark/fluxengine/fluxengine_1.0.0.deb fluxengine-1.0.0.${{ runner.arch }}.deb - mv bazel-bin/java/com/cowlark/fluxengine/fluxengine_1.0.0.rpm fluxengine-1.0.0.${{ runner.arch }}.rpm - mv bazel-bin/java/com/cowlark/fluxengine/fluxengine_1.0.0.tar.xz fluxengine-1.0.0.${{ runner.arch }}.tar.xz + bazel build //:fluxengine_deb //:fluxengine_rpm //:fluxengine_app_image --config=stamp --test_output=errors + mv bazel-bin/java/com/cowlark/fluxengine/*.deb fluxengine-${{ runner.arch }}.deb + mv bazel-bin/java/com/cowlark/fluxengine/*.rpm fluxengine-${{ runner.arch }}.rpm + mv bazel-bin/java/com/cowlark/fluxengine/*.tar.xz fluxengine-${{ runner.arch }}.tar.xz - name: date @@ -58,9 +58,9 @@ jobs: token: ${{ github.token }} tag: dev assets: | - fluxengine-1.0.0.${{ runner.arch }}.deb - fluxengine-1.0.0.${{ runner.arch }}.rpm - fluxengine-1.0.0.${{ runner.arch }}.tar.xz + fluxengine-${{ runner.arch }}.deb + fluxengine-${{ runner.arch }}.rpm + fluxengine-${{ runner.arch }}.tar.xz fail-if-no-assets: false - name: release @@ -68,9 +68,9 @@ jobs: with: name: Development build ${{ env.RELEASE_DATE }} files: | - fluxengine/fluxengine-1.0.0.${{ runner.arch }}.deb - fluxengine/fluxengine-1.0.0.${{ runner.arch }}.rpm - fluxengine/fluxengine-1.0.0.${{ runner.arch }}.tar.xz + fluxengine/fluxengine-${{ runner.arch }}.deb + fluxengine/fluxengine-${{ runner.arch }}.rpm + fluxengine/fluxengine-${{ runner.arch }}.tar.xz tag_name: dev env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -97,8 +97,8 @@ jobs: - name: Build with Bazel run: | cd fluxengine - bazel build //:fluxengine_dmg --test_output=errors - mv bazel-bin/java/com/cowlark/fluxengine/fluxengine_1.0.0.dmg fluxengine_1.0.0.${{ runner.arch }}.dmg + bazel build //:fluxengine_dmg --config=stamp --test_output=errors + mv bazel-bin/java/com/cowlark/fluxengine/*.dmg fluxengine-${{ runner.arch }}.dmg - name: date run: | @@ -119,7 +119,7 @@ jobs: token: ${{ github.token }} tag: dev assets: | - fluxengine_1.0.0.${{ runner.arch }}.dmg + fluxengine-${{ runner.arch }}.dmg fail-if-no-assets: false - name: release @@ -127,7 +127,7 @@ jobs: with: name: Development build ${{ env.RELEASE_DATE }} files: | - fluxengine/fluxengine_1.0.0.${{ runner.arch }}.dmg + fluxengine/fluxengine-${{ runner.arch }}.dmg tag_name: dev env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -167,12 +167,12 @@ jobs: $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true cd fluxengine - bazel build //:fluxengine_msi --test_output=errors ` + bazel build //:fluxengine_msi --config=stamp --test_output=errors ` --action_env=PATH ` --action_env=INCLUDE ` --action_env=LIB ` --action_env=LIBPATH - move bazel-bin\java\com\cowlark\fluxengine\fluxengine_1.0.0.msi fluxengine-1.0.0.${{ runner.arch }}.msi + move bazel-bin\java\com\cowlark\fluxengine\*.msi fluxengine-${{ runner.arch }}.msi - name: date run: | @@ -193,7 +193,7 @@ jobs: token: ${{ github.token }} tag: dev assets: | - fluxengine-1.0.0.${{ runner.arch }}.msi + fluxengine-${{ runner.arch }}.msi fail-if-no-assets: false - name: release @@ -201,7 +201,7 @@ jobs: with: name: Development build ${{ env.RELEASE_DATE }} files: | - fluxengine/fluxengine-1.0.0.${{ runner.arch }}.msi + fluxengine/fluxengine-${{ runner.arch }}.msi tag_name: dev env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 2f01dacdf..df28edb72 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ opencode.json fluxengine.iml /nbproject/private/ images/ +.bazelbsp/ +.bazelproject diff --git a/AGENTS.md b/AGENTS.md index 271f49504..463099799 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,12 +33,6 @@ Useful commands: - Because there is no WORKSPACE, Java rules are **not autoloaded**. Every BUILD file must explicitly load what it uses, e.g. `load("@rules_java//java:defs.bzl", "java_library", "java_binary", "java_plugin", "java_test")`. -- `javax.usb.properties` must sit at the **classpath root** (the usb4java `Services` - constructor requires it via `UsbHostManager.getProperties()`). It lives at - `java/javax.usb.properties`, is exported from `java/BUILD.bazel`, and is pulled in as a - resource (`resources = ["//java:javax.usb.properties"]`) by the usb library. Bazel's - resource jarring strips the leading `java/`, so it lands at the jar root. Do not move it - into the package directory. - The `.deb` and `.rpm` installers are built with jpackage via the `jpackage` rule in `jpackage.bzl` (which uses the configured Java toolchain's `jpackage`). Because `rpmbuild` writes to `/var/tmp` and read-only sandbox paths by default, the rule stages everything @@ -73,11 +67,17 @@ Useful commands: ## USB - `UsbFinder` (`java/com/cowlark/fluxengine/usb/`) is the Java port of - `lib/usb/usbfinder.{cc,h}`. It uses usb4java-javax (javax.usb API). `UsbFinder` is - Dagger-injectable (`@Inject` constructor, instance methods) and `findUsbDevices()` - returns an `ImmutableList`. + `lib/usb/usbfinder.{cc,h}`. It uses `org.usb4java` directly (`Device`/`DeviceDescriptor`/ + `DeviceHandle`/`LibUsb`); enumeration goes through a singleton `UsbContext` (explicit + `Context` via `LibUsb.init`/`exit` with shutdown hook) and `LibUsb.getDeviceList`/ + `freeDeviceList` with `refDevice`/`unrefDevice` so `CandidateDevice.device` is a retained + `Device` (not an open handle). `HackyUsbSerialNumberResolver` still provides the Windows + SetupAPI fallback. `FluxEngineUsbDevice` opens the retained `Device` to a `DeviceHandle`, + claims interface 0, and uses `LibUsb.interruptTransfer` for `CMD_*` and + `LibUsb.bulkTransfer` for `DATA_*`. - `DeviceType` is an enum carrying its display name as a property (`getDeviceName()`). -- jSerialComm is available for serial-port access (not yet used). +- jSerialComm is available for serial-port access (used by Greaseweazle/Applesauce via + `Serial` and `findSerialPort`). ## Code style diff --git a/MODULE.bazel b/MODULE.bazel index c24b6f1ee..5331a7ab1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ bazel_dep(name = "platforms", version = "1.0.0") -bazel_dep(name = "rules_java", version = "9.1.0") -bazel_dep(name = "rules_jvm_external", version = "6.7") +bazel_dep(name = "rules_java", version = "9.3.0") +bazel_dep(name = "rules_jvm_external", version = "7.1") bazel_dep(name = "rules_proto", version = "7.1.0") bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf") @@ -17,10 +17,8 @@ maven.install( "com.google.guava:guava:32.0.1-jre", "com.google.truth:truth:1.4.5", "com.jayway.jsonpath:json-path:3.0.0", - "javax.usb:usb-api:1.0.2", "junit:junit:4.13.2", "org.usb4java:usb4java:1.3.0", - "org.usb4java:usb4java-javax:1.3.0", "com.formdev:flatlaf:3.7.2", "com.formdev:flatlaf-swingx:3.7.2", "org.exbin.bined:bined-core:0.2.2", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 6c14c92b8..d0b19d173 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,25 +10,31 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", + "https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5", "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a", + "https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b", "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", + "https://bcr.bazel.build/modules/bazel_features/1.25.0/MODULE.bazel": "e2e60a10a6da64bbf533f15ca652bf61a033e41c2ed734d79a9a08ba87f68c1a", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", @@ -37,7 +43,10 @@ "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", + "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -52,8 +61,22 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/MODULE.bazel": "396c1ef53835aafe3d42ce6619080531ee770648303731f16cfaa33fa056bf0c", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/source.json": "abaf8ac9d2ab2f47bda9af4c0c080ff7907378888e1f4bc62a0539dd13ba61e8", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel": "e76479eae70bd4e8f5f4c2dfc5d03ab971cfb18750246c7b3f3454c5c2ee6629", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/source.json": "9395c4679444bc47bf7e51a710366a4480aa371c6f6bed01868e2fabcf11acec", "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", + "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", @@ -67,6 +90,11 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", + "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", @@ -78,11 +106,21 @@ "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", + "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://bcr.bazel.build/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", + "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/29.3/MODULE.bazel": "77480eea5fb5541903e49683f24dc3e09f4a79e0eea247414887bb9fc0066e94", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e", "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", @@ -94,7 +132,9 @@ "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4", "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", - "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_android/0.6.4/MODULE.bazel": "b4cde12d506dd65d82b2be39761f49f5797303343a3d5b4ee191c0cdf9ef387c", + "https://bcr.bazel.build/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363", + "https://bcr.bazel.build/modules/rules_android/0.7.1/source.json": "151440aed3f0f73a00d4ed5cec5d31f63a6fef9b95d8fab1eb1810150fa525f2", "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", @@ -113,31 +153,53 @@ "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", + "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", + "https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", + "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", + "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", + "https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", + "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71", "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", + "https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed", + "https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a", "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d", - "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6", + "https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2", + "https://bcr.bazel.build/modules/rules_java/9.3.0/source.json": "59ae7e662c3c7042b88bbb42ad12483523e234c65ebe4c51611baa43e85cb248", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/6.2/MODULE.bazel": "36a6e52487a855f33cb960724eb56547fa87e2c98a0474c3acad94339d7f8e99", "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55", "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", - "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274", + "https://bcr.bazel.build/modules/rules_jvm_external/7.1/MODULE.bazel": "e151b1beaa45bfc9e5de4df637d980e27039eb2b1092a95972bf2adbf99ac1a0", + "https://bcr.bazel.build/modules/rules_jvm_external/7.1/source.json": "8cb571a9631c39bf29c1842958542f14bba1c704fe2bcde4fa54679b2a6b3d6f", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/MODULE.bazel": "00d39c5e0fa78cd86193946265bb849e7878c24e44260f9525108428852b315c", + "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/source.json": "7a32c2259c79ae0c9a036121f120de825e3ba5f0f3a209ffbbdccf4dc62489b9", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", @@ -148,7 +210,9 @@ "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", + "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", @@ -157,14 +221,21 @@ "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", + "https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500", + "https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", + "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", + "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea", + "https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", @@ -174,14 +245,19 @@ "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/toml.bzl/0.3.0/MODULE.bazel": "5016e5dd1ad2200e119a4b28b2b3935e276c4b480f2fe3e952bea7eeba88f578", + "https://bcr.bazel.build/modules/toml.bzl/0.3.0/source.json": "0cf7c878c419b37ddb55f3dd93dd7c0c409bd7c4efacb3da504e0748780b2fa9", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" @@ -210,63 +286,55 @@ } } }, - "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { "general": { - "bzlTransitiveDigest": "+Kp6j204mBZ3mxlIDDR0gBoP45BZ4jYRhRAcB8sU0qc=", - "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "bzlTransitiveDigest": "qEcqhUuyYlPmvAtyYRqS+4Y7cRHA2Vho3JIBJZJJX+s=", + "usagesDigest": "zr/niBQ/s2fHozWAsg4vI70wAxcuFjG+QtM15qGkq9o=", "recordedInputs": [ - "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools" + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" ], "generatedRepoSpecs": { - "com_github_jetbrains_kotlin_git": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", - "attributes": { - "urls": [ - "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" - ], - "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" - } - }, - "com_github_jetbrains_kotlin": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", - "attributes": { - "git_repository_name": "com_github_jetbrains_kotlin_git", - "compiler_version": "1.9.23" - } - }, - "com_github_google_ksp": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", + "apksig": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "urls": [ - "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" - ], - "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", - "strip_version": "1.9.23-1.0.20" + "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD" } - }, - "com_github_pinterest_ktlint": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", - "attributes": { - "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", - "urls": [ - "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" - ], - "executable": true - } - }, - "rules_android": { + } + } + } + }, + "@@rules_android+//bzlmod_extensions:com_android_dex.bzl%com_android_dex_extension": { + "general": { + "bzlTransitiveDigest": "gTuOqknGgqPw9YQSCY+qXxgxVwiLRGdBCsCe4i4q/xw=", + "usagesDigest": "c1Y/KGGjUYCyd8zNIVTUh1bynVXRFz6xGKaSCBpQANM=", + "recordedInputs": [ + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "com_android_dex": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", - "strip_prefix": "rules_android-0.1.1", - "urls": [ - "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" - ] + "url": "https://android.googlesource.com/platform/dalvik/+archive/5a81c499a569731e2395f7c8d13c0e0d4e17a2b6.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:com_android_dex.BUILD" } } } } }, + "@@rules_android+//rules/android_sdk_repository:rule.bzl%android_sdk_repository_extension": { + "general": { + "bzlTransitiveDigest": "+rMrzIrv7sImYmkbXJYv+gFpTJQ79X3MpwwMLI2A+oA=", + "usagesDigest": "iEGI2aNDMkHt9LXCdViLNUUOslpiVj2DrevWWXZEFnU=", + "recordedInputs": [], + "generatedRepoSpecs": { + "androidsdk": { + "repoRuleId": "@@rules_android+//rules/android_sdk_repository:rule.bzl%_android_sdk_repository", + "attributes": {} + } + } + } + }, "@@rules_python+//python/extensions:config.bzl%config": { "general": { "bzlTransitiveDigest": "dzD8Q2YmrP3fz8saWLHPmlwPLO91ImtTmP/c9JKTStM=", @@ -466,6 +534,333 @@ } } }, - "facts": {}, + "facts": { + "@@rules_go+//go:extensions.bzl%go_sdk": { + "1.22.4": { + "aix_ppc64": [ + "go1.22.4.aix-ppc64.tar.gz", + "b9647fa9fc83a0cc5d4f092a19eaeaecf45f063a5aa7d4962fde65aeb7ae6ce1" + ], + "darwin_amd64": [ + "go1.22.4.darwin-amd64.tar.gz", + "c95967f50aa4ace34af0c236cbdb49a9a3e80ee2ad09d85775cb4462a5c19ed3" + ], + "darwin_arm64": [ + "go1.22.4.darwin-arm64.tar.gz", + "242b78dc4c8f3d5435d28a0d2cec9b4c1aa999b601fb8aa59fb4e5a1364bf827" + ], + "dragonfly_amd64": [ + "go1.22.4.dragonfly-amd64.tar.gz", + "f2fbb51af4719d3616efb482d6ed2b96579b474156f85a7ddc6f126764feec4b" + ], + "freebsd_386": [ + "go1.22.4.freebsd-386.tar.gz", + "7c54884bb9f274884651d41e61d1bc12738863ad1497e97ea19ad0e9aa6bf7b5" + ], + "freebsd_amd64": [ + "go1.22.4.freebsd-amd64.tar.gz", + "88d44500e1701dd35797619774d6dd51bf60f45a8338b0a82ddc018e4e63fb78" + ], + "freebsd_arm64": [ + "go1.22.4.freebsd-arm64.tar.gz", + "726dc093cf020277be45debf03c3b02b43c2efb3e2a5d4fba8f52579d65327dc" + ], + "freebsd_armv6l": [ + "go1.22.4.freebsd-arm.tar.gz", + "3d9efe47db142a22679aba46b1772e3900b0d87ae13bd2b3bc80dbf2ac0b2cd6" + ], + "freebsd_riscv64": [ + "go1.22.4.freebsd-riscv64.tar.gz", + "5f6b67e5e32f1d6ccb2d4dcb44934a5e2e870a877ba7443d86ec43cfc28afa71" + ], + "illumos_amd64": [ + "go1.22.4.illumos-amd64.tar.gz", + "d56ecc2f85b6418a21ef83879594d0c42ab4f65391a676bb12254870e6690d63" + ], + "linux_386": [ + "go1.22.4.linux-386.tar.gz", + "47a2a8d249a91eb8605c33bceec63aedda0441a43eac47b4721e3975ff916cec" + ], + "linux_amd64": [ + "go1.22.4.linux-amd64.tar.gz", + "ba79d4526102575196273416239cca418a651e049c2b099f3159db85e7bade7d" + ], + "linux_arm64": [ + "go1.22.4.linux-arm64.tar.gz", + "a8e177c354d2e4a1b61020aca3562e27ea3e8f8247eca3170e3fa1e0c2f9e771" + ], + "linux_armv6l": [ + "go1.22.4.linux-armv6l.tar.gz", + "e2b143fbacbc9cbd448e9ef41ac3981f0488ce849af1cf37e2341d09670661de" + ], + "linux_loong64": [ + "go1.22.4.linux-loong64.tar.gz", + "e2ff9436e4b34bf6926b06d97916e26d67a909a2effec17967245900f0816f1d" + ], + "linux_mips": [ + "go1.22.4.linux-mips.tar.gz", + "73f0dcc60458c4770593b05a7bc01cc0d31fc98f948c0c2334812c7a1f2fc3f1" + ], + "linux_mips64": [ + "go1.22.4.linux-mips64.tar.gz", + "417af97fc2630a647052375768be4c38adcc5af946352ea5b28613ea81ca5d45" + ], + "linux_mips64le": [ + "go1.22.4.linux-mips64le.tar.gz", + "7486e2d7dd8c98eb44df815ace35a7fe7f30b7c02326e3741bd934077508139b" + ], + "linux_mipsle": [ + "go1.22.4.linux-mipsle.tar.gz", + "69479c8aad301e459a8365b40cad1074a0dbba5defb9291669f94809c4c4be6e" + ], + "linux_ppc64": [ + "go1.22.4.linux-ppc64.tar.gz", + "dd238847e65bc3e2745caca475a5db6522a2fcf85cf6c38fc36a06642b19efd7" + ], + "linux_ppc64le": [ + "go1.22.4.linux-ppc64le.tar.gz", + "a3e5834657ef92523f570f798fed42f1f87bc18222a16815ec76b84169649ec4" + ], + "linux_riscv64": [ + "go1.22.4.linux-riscv64.tar.gz", + "56a827ff7dc6245bcd7a1e9288dffaa1d8b0fd7468562264c1523daf3b4f1b4a" + ], + "linux_s390x": [ + "go1.22.4.linux-s390x.tar.gz", + "7590c3e278e2dc6040aae0a39da3ca1eb2e3921673a7304cc34d588c45889eec" + ], + "netbsd_386": [ + "go1.22.4.netbsd-386.tar.gz", + "ddd2eebe34471a2502de6c5dad04ab27c9fc80cbde7a9ad5b3c66ecec4504e1d" + ], + "netbsd_amd64": [ + "go1.22.4.netbsd-amd64.tar.gz", + "33af79f6f935f6fbacc5d23876450b3567b79348fc065beef8e64081127dd234" + ], + "netbsd_arm64": [ + "go1.22.4.netbsd-arm64.tar.gz", + "c9a2971dec9f6d320c6f2b049b2353c6d0a2d35e87b8a4b2d78a2f0d62545f8e" + ], + "netbsd_armv6l": [ + "go1.22.4.netbsd-arm.tar.gz", + "fa3550ebd5375a70b3bcd342b5a71f4bd271dcbbfaf4eabefa2144ab5d8924b6" + ], + "openbsd_386": [ + "go1.22.4.openbsd-386.tar.gz", + "d21af022331bfdc2b5b161d616c3a1a4573d33cf7a30416ee509a8f3641deb47" + ], + "openbsd_amd64": [ + "go1.22.4.openbsd-amd64.tar.gz", + "72c0094c43f7e5722ec49c2a3e9dfa7a1123ac43a5f3a63eecf3e3795d3ff0ae" + ], + "openbsd_arm64": [ + "go1.22.4.openbsd-arm64.tar.gz", + "a7ab8d4e0b02bf06ed144ba42c61c0e93ee00f2b433415dfd4ad4b6e79f31650" + ], + "openbsd_armv6l": [ + "go1.22.4.openbsd-arm.tar.gz", + "1096831ea3c5ea3ca57d14251d9eda3786889531eb40d7d6775dcaa324d4b065" + ], + "openbsd_ppc64": [ + "go1.22.4.openbsd-ppc64.tar.gz", + "9716327c8a628358798898dc5148c49dbbeb5196bf2cbf088e550721a6e4f60b" + ], + "plan9_386": [ + "go1.22.4.plan9-386.tar.gz", + "a8dd4503c95c32a502a616ab78870a19889c9325fe9bd31eb16dd69346e4bfa8" + ], + "plan9_amd64": [ + "go1.22.4.plan9-amd64.tar.gz", + "5423a25808d76fe5aca8607a2e5ac5673abf45446b168cb5e9d8519ee9fe39a1" + ], + "plan9_armv6l": [ + "go1.22.4.plan9-arm.tar.gz", + "6af939ad583f5c85c09c53728ab7d38c3cc2b39167562d6c18a07c5c6608b370" + ], + "solaris_amd64": [ + "go1.22.4.solaris-amd64.tar.gz", + "e8cabe69c03085725afdb32a6f9998191a3e55a747b270d835fd05000d56abba" + ], + "windows_386": [ + "go1.22.4.windows-386.zip", + "aca4e2c37278a10f1c70dd0df142f7d66b50334fcee48978d409202d308d6d25" + ], + "windows_amd64": [ + "go1.22.4.windows-amd64.zip", + "26321c4d945a0035d8a5bc4a1965b0df401ff8ceac66ce2daadabf9030419a98" + ], + "windows_arm64": [ + "go1.22.4.windows-arm64.zip", + "8a2daa9ea28cbdafddc6171aefed384f4e5b6e714fb52116fe9ed25a132f37ed" + ], + "windows_armv6l": [ + "go1.22.4.windows-arm.zip", + "5fcd0671a49cecf39b41021621ee1b6e7aa1370f37122b72e80d4fd4185833b6" + ] + }, + "1.25.0": { + "aix_ppc64": [ + "go1.25.0.aix-ppc64.tar.gz", + "e5234a7dac67bc86c528fe9752fc9d63557918627707a733ab4cac1a6faed2d4" + ], + "darwin_amd64": [ + "go1.25.0.darwin-amd64.tar.gz", + "5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef" + ], + "darwin_arm64": [ + "go1.25.0.darwin-arm64.tar.gz", + "544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c" + ], + "dragonfly_amd64": [ + "go1.25.0.dragonfly-amd64.tar.gz", + "5ed3cf9a810a1483822538674f1336c06b51aa1b94d6d545a1a0319a48177120" + ], + "freebsd_386": [ + "go1.25.0.freebsd-386.tar.gz", + "abea5d5c6697e6b5c224731f2158fe87c602996a2a233ac0c4730cd57bf8374e" + ], + "freebsd_amd64": [ + "go1.25.0.freebsd-amd64.tar.gz", + "86e6fe0a29698d7601c4442052dac48bd58d532c51cccb8f1917df648138730b" + ], + "freebsd_arm": [ + "go1.25.0.freebsd-arm.tar.gz", + "d90b78e41921f72f30e8bbc81d9dec2cff7ff384a33d8d8debb24053e4336bfe" + ], + "freebsd_arm64": [ + "go1.25.0.freebsd-arm64.tar.gz", + "451d0da1affd886bfb291b7c63a6018527b269505db21ce6e14724f22ab0662e" + ], + "freebsd_riscv64": [ + "go1.25.0.freebsd-riscv64.tar.gz", + "7b565f76bd8bda46549eeaaefe0e53b251e644c230577290c0f66b1ecdb3cdbe" + ], + "illumos_amd64": [ + "go1.25.0.illumos-amd64.tar.gz", + "b1e1fdaab1ad25aa1c08d7a36c97d45d74b98b89c3f78c6d2145f77face54a2c" + ], + "linux_386": [ + "go1.25.0.linux-386.tar.gz", + "8c602dd9d99bc9453b3995d20ce4baf382cc50855900a0ece5de9929df4a993a" + ], + "linux_amd64": [ + "go1.25.0.linux-amd64.tar.gz", + "2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613" + ], + "linux_arm64": [ + "go1.25.0.linux-arm64.tar.gz", + "05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae" + ], + "linux_armv6l": [ + "go1.25.0.linux-armv6l.tar.gz", + "a5a8f8198fcf00e1e485b8ecef9ee020778bf32a408a4e8873371bfce458cd09" + ], + "linux_loong64": [ + "go1.25.0.linux-loong64.tar.gz", + "cab86b1cf761b1cb3bac86a8877cfc92e7b036fc0d3084123d77013d61432afc" + ], + "linux_mips": [ + "go1.25.0.linux-mips.tar.gz", + "d66b6fb74c3d91b9829dc95ec10ca1f047ef5e89332152f92e136cf0e2da5be1" + ], + "linux_mips64": [ + "go1.25.0.linux-mips64.tar.gz", + "4082e4381a8661bc2a839ff94ba3daf4f6cde20f8fb771b5b3d4762dc84198a2" + ], + "linux_mips64le": [ + "go1.25.0.linux-mips64le.tar.gz", + "70002c299ec7f7175ac2ef673b1b347eecfa54ae11f34416a6053c17f855afcc" + ], + "linux_mipsle": [ + "go1.25.0.linux-mipsle.tar.gz", + "b00a3a39eff099f6df9f1c7355bf28e4589d0586f42d7d4a394efb763d145a73" + ], + "linux_ppc64": [ + "go1.25.0.linux-ppc64.tar.gz", + "df166f33bd98160662560a72ff0b4ba731f969a80f088922bddcf566a88c1ec1" + ], + "linux_ppc64le": [ + "go1.25.0.linux-ppc64le.tar.gz", + "0f18a89e7576cf2c5fa0b487a1635d9bcbf843df5f110e9982c64df52a983ad0" + ], + "linux_riscv64": [ + "go1.25.0.linux-riscv64.tar.gz", + "c018ff74a2c48d55c8ca9b07c8e24163558ffec8bea08b326d6336905d956b67" + ], + "linux_s390x": [ + "go1.25.0.linux-s390x.tar.gz", + "34e5a2e19f2292fbaf8783e3a241e6e49689276aef6510a8060ea5ef54eee408" + ], + "netbsd_386": [ + "go1.25.0.netbsd-386.tar.gz", + "f8586cdb7aa855657609a5c5f6dbf523efa00c2bbd7c76d3936bec80aa6c0aba" + ], + "netbsd_amd64": [ + "go1.25.0.netbsd-amd64.tar.gz", + "ae8dc1469385b86a157a423bb56304ba45730de8a897615874f57dd096db2c2a" + ], + "netbsd_arm": [ + "go1.25.0.netbsd-arm.tar.gz", + "1ff7e4cc764425fc9dd6825eaee79d02b3c7cafffbb3691687c8d672ade76cb7" + ], + "netbsd_arm64": [ + "go1.25.0.netbsd-arm64.tar.gz", + "e1b310739f26724216aa6d7d7208c4031f9ff54c9b5b9a796ddc8bebcb4a5f16" + ], + "openbsd_386": [ + "go1.25.0.openbsd-386.tar.gz", + "4802a9b20e533da91adb84aab42e94aa56cfe3e5475d0550bed3385b182e69d8" + ], + "openbsd_amd64": [ + "go1.25.0.openbsd-amd64.tar.gz", + "c016cd984bebe317b19a4f297c4f50def120dc9788490540c89f28e42f1dabe1" + ], + "openbsd_arm": [ + "go1.25.0.openbsd-arm.tar.gz", + "a1e31d0bf22172ddde42edf5ec811ef81be43433df0948ece52fecb247ccfd8d" + ], + "openbsd_arm64": [ + "go1.25.0.openbsd-arm64.tar.gz", + "343ea8edd8c218196e15a859c6072d0dd3246fbbb168481ab665eb4c4140458d" + ], + "openbsd_ppc64": [ + "go1.25.0.openbsd-ppc64.tar.gz", + "694c14da1bcaeb5e3332d49bdc2b6d155067648f8fe1540c5de8f3cf8e157154" + ], + "openbsd_riscv64": [ + "go1.25.0.openbsd-riscv64.tar.gz", + "aa510ad25cf54c06cd9c70b6d80ded69cb20188ac6e1735655eef29ff7e7885f" + ], + "plan9_386": [ + "go1.25.0.plan9-386.tar.gz", + "46f8cef02086cf04bf186c5912776b56535178d4cb319cd19c9fdbdd29231986" + ], + "plan9_amd64": [ + "go1.25.0.plan9-amd64.tar.gz", + "29b34391d84095e44608a228f63f2f88113a37b74a79781353ec043dfbcb427b" + ], + "plan9_arm": [ + "go1.25.0.plan9-arm.tar.gz", + "0a047107d13ebe7943aaa6d54b1d7bbd2e45e68ce449b52915a818da715799c2" + ], + "solaris_amd64": [ + "go1.25.0.solaris-amd64.tar.gz", + "9977f9e4351984364a3b2b78f8b88bfd1d339812356d5237678514594b7d3611" + ], + "windows_386": [ + "go1.25.0.windows-386.zip", + "df9f39db82a803af0db639e3613a36681ab7a42866b1384b3f3a1045663961a7" + ], + "windows_amd64": [ + "go1.25.0.windows-amd64.zip", + "89efb4f9b30812eee083cc1770fdd2913c14d301064f6454851428f9707d190b" + ], + "windows_arm64": [ + "go1.25.0.windows-arm64.zip", + "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c" + ] + } + } + }, "factsVersions": {} } diff --git a/bazel.bat b/bazel.bat new file mode 100644 index 000000000..93208c31c --- /dev/null +++ b/bazel.bat @@ -0,0 +1,3 @@ +@echo off +set BAZEL_SH=c:\msys64\usr\bin\bash.exe +bazelisk --output_user_root=C:/tmp/bazelcache %* diff --git a/java/BUILD.bazel b/java/BUILD.bazel index 6200e1a77..3819242a0 100644 --- a/java/BUILD.bazel +++ b/java/BUILD.bazel @@ -1,6 +1,4 @@ package(default_visibility = ["//visibility:public"]) -exports_files(["javax.usb.properties"]) - # Top-level package BUILD within java/ kept minimal: per-package BUILD files live under com/. # This file intentionally contains no targets that compile sources; see per-package BUILD files. diff --git a/java/com/cowlark/fluxengine/BUILD.bazel b/java/com/cowlark/fluxengine/BUILD.bazel index 56317c79b..27c681c91 100644 --- a/java/com/cowlark/fluxengine/BUILD.bazel +++ b/java/com/cowlark/fluxengine/BUILD.bazel @@ -17,6 +17,7 @@ jpackage( name = "fluxengine_deb", package_name = "fluxengine", app_version = "1.0.0", + stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", @@ -29,6 +30,7 @@ jpackage( name = "fluxengine_rpm", package_name = "fluxengine", app_version = "1.0.0", + stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", @@ -45,6 +47,7 @@ jpackage( name = "fluxengine_msi", package_name = "fluxengine", app_version = "1.0.0", + stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = select({ @@ -63,6 +66,7 @@ jpackage( name = "fluxengine_dmg", package_name = "fluxengine", app_version = "1.0.0", + stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = select({ @@ -81,6 +85,7 @@ jpackage_app_image( name = "fluxengine_app_image", package_name = "fluxengine", app_version = "1.0.0", + stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", diff --git a/java/com/cowlark/fluxengine/cli/Command.java b/java/com/cowlark/fluxengine/cli/Command.java index a4afe19ab..95f27e64e 100644 --- a/java/com/cowlark/fluxengine/cli/Command.java +++ b/java/com/cowlark/fluxengine/cli/Command.java @@ -68,6 +68,9 @@ static boolean dispatch( Map> commands, ImmutableList args) throws Exception { + if (args.isEmpty()) + return false; + Supplier supplier = commands.get(args.getFirst()); if (supplier != null) { diff --git a/java/com/cowlark/fluxengine/cli/CommandGroup.java b/java/com/cowlark/fluxengine/cli/CommandGroup.java index 72e1a286e..60041c156 100644 --- a/java/com/cowlark/fluxengine/cli/CommandGroup.java +++ b/java/com/cowlark/fluxengine/cli/CommandGroup.java @@ -29,7 +29,7 @@ public String getHelp() @Override public void run(ImmutableList args) throws Exception { - if (args.getFirst().startsWith("-")) + if (!args.isEmpty() && args.getFirst().startsWith("-")) throw new FluxEngineException("CLI verbs must come before all options, sorry!"); if (!Command.dispatch(subcommands, args)) diff --git a/java/com/cowlark/fluxengine/cli/DevicesCommand.java b/java/com/cowlark/fluxengine/cli/DevicesCommand.java index c90f10084..eafce44b6 100644 --- a/java/com/cowlark/fluxengine/cli/DevicesCommand.java +++ b/java/com/cowlark/fluxengine/cli/DevicesCommand.java @@ -22,31 +22,37 @@ public String getHelp() public void run(ImmutableList args) { List candidates = UsbFinder.findUsbDevices(); - switch (candidates.size()) + try { - case 0: - System.out.println("Detected no devices."); - break; + switch (candidates.size()) + { + case 0: + System.out.println("Detected no devices."); + break; - case 1: - System.out.println("Detected one device:"); - break; + case 1: + System.out.println("Detected one device:"); + break; - default: - System.out.printf("Detected %d devices:\n", candidates.size()); - } + default: + System.out.printf("Detected %d devices:\n", candidates.size()); + } - if (!candidates.isEmpty()) - { - System.out.printf("%-15s %-30s %s\n", "Type", "Serial number", "Port (if any)"); - for (CandidateDevice candidate : candidates) + if (!candidates.isEmpty()) { - System.out.printf( - "%-15s %-30s %s\n", - candidate.type.getDeviceName(), - candidate.serial, - nullToEmpty(candidate.serialPort)); + System.out.printf("%-15s %-30s %s\n", "Type", "Serial number", "Port (if any)"); + for (CandidateDevice candidate : candidates) + { + System.out.printf( + "%-15s %-30s %s\n", + candidate.type.getDeviceName(), + candidate.serial, + nullToEmpty(candidate.serialPort)); + } } + } finally + { + UsbFinder.freeDevices(candidates); } } } diff --git a/java/com/cowlark/fluxengine/cli/Main.java b/java/com/cowlark/fluxengine/cli/Main.java index a4d5c2d76..2a70dedd9 100644 --- a/java/com/cowlark/fluxengine/cli/Main.java +++ b/java/com/cowlark/fluxengine/cli/Main.java @@ -2,6 +2,7 @@ import com.cowlark.fluxengine.core.LogRenderer; import com.cowlark.fluxengine.core.Logger; +import com.cowlark.fluxengine.core.Version; import com.google.common.collect.ImmutableList; import java.util.Map; import java.util.function.Supplier; @@ -22,6 +23,15 @@ public static void main(String[] args) throws Exception { Logger.setLogger(LogRenderer.create(System.out)::add); + if (args.length != 0 + && (args[0].equals("--version") + || args[0].equals("-V") + || args[0].equals("version"))) + { + System.out.println(Version.get()); + return; + } + if (args.length == 0 || args[0].equals("--help")) { help(Command.COMMANDS, " [...]"); diff --git a/java/com/cowlark/fluxengine/cli/TestVoltagesCommand.java b/java/com/cowlark/fluxengine/cli/TestVoltagesCommand.java index 284219e4a..ef1b15d05 100644 --- a/java/com/cowlark/fluxengine/cli/TestVoltagesCommand.java +++ b/java/com/cowlark/fluxengine/cli/TestVoltagesCommand.java @@ -5,6 +5,7 @@ import com.cowlark.fluxengine.usb.UsbFactory; import com.cowlark.fluxengine.usb.VoltageMeasurements; import com.cowlark.fluxengine.usb.Voltages; +import com.cowlark.fluxengine.wiring.FluxEngine; import com.google.common.collect.ImmutableList; /** diff --git a/java/com/cowlark/fluxengine/config/BUILD.bazel b/java/com/cowlark/fluxengine/config/BUILD.bazel index 39aa8e5d0..4fdd4142f 100644 --- a/java/com/cowlark/fluxengine/config/BUILD.bazel +++ b/java/com/cowlark/fluxengine/config/BUILD.bazel @@ -93,8 +93,7 @@ java_library( "@maven//:com_fazecast_jSerialComm", "@maven//:com_google_guava_guava", "@maven//:com_jayway_jsonpath_json_path", - "@maven//:javax_usb_usb_api", "@maven//:org_projectlombok_lombok", - "@maven//:org_usb4java_usb4java_javax", + "@maven//:org_usb4java_usb4java", ], ) diff --git a/java/com/cowlark/fluxengine/core/BUILD.bazel b/java/com/cowlark/fluxengine/core/BUILD.bazel index 61cbc2c8e..107358761 100644 --- a/java/com/cowlark/fluxengine/core/BUILD.bazel +++ b/java/com/cowlark/fluxengine/core/BUILD.bazel @@ -1,12 +1,19 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//scripts:version.bzl", "version_properties") package(default_visibility = ["//visibility:public"]) +version_properties( + name = "version_properties", + stamp = True, +) + java_library( name = "core", srcs = glob( ["*.java"], ), + resources = [":version_properties"], deps = [ "@maven//:com_google_guava_guava", ], diff --git a/java/com/cowlark/fluxengine/core/Bytes.java b/java/com/cowlark/fluxengine/core/Bytes.java index 9c3b097c9..242b3b958 100644 --- a/java/com/cowlark/fluxengine/core/Bytes.java +++ b/java/com/cowlark/fluxengine/core/Bytes.java @@ -638,7 +638,10 @@ public String toString() public ByteBuffer toByteBuffer() { - return ByteBuffer.wrap(toByteArray()); + ByteBuffer buffer = ByteBuffer.allocateDirect(size()); + buffer.put(toByteArray()); + buffer.flip(); + return buffer; } diff --git a/java/com/cowlark/fluxengine/core/Version.java b/java/com/cowlark/fluxengine/core/Version.java new file mode 100644 index 000000000..1bdee2e0b --- /dev/null +++ b/java/com/cowlark/fluxengine/core/Version.java @@ -0,0 +1,51 @@ +package com.cowlark.fluxengine.core; + +import java.io.InputStream; +import java.util.Properties; + +/* Provides the application version, supplied at build time via Bazel stamping. + * + * Bazel generates version.properties via scripts/version.bzl from + * STABLE_VERSION (scripts/workspace_status.sh as YYYY.MMDD.HHMM per-minute UTC, + * each component <65535 so valid for Windows MSI ProductVersion). The value is + * also available as -Dfluxengine.version if set; otherwise the properties file + * is read. When built without --stamp, falls back to 1.0.0-dev. + */ +public final class Version +{ + private Version() + { + } + + public static String get() + { + /* System property takes precedence (useful for testing / java_binary + * override), but falls back to stamped resource. */ + String v = System.getProperty("fluxengine.version", ""); + if (v != null && !v.isEmpty() && !v.startsWith("$(")) + return v; + + String[] paths = { + "/version.properties", + "/com/cowlark/fluxengine/core/version.properties" + }; + for (String path : paths) + { + try (InputStream is = Version.class.getResourceAsStream(path)) + { + if (is != null) + { + Properties p = new Properties(); + p.load(is); + String pv = p.getProperty("fluxengine.version", ""); + if (pv != null && !pv.isEmpty() && !pv.startsWith("$(")) + return pv; + } + } catch (Exception e) + { + /* Ignore and fall back. */ + } + } + return "1.0.0-dev"; + } +} diff --git a/java/com/cowlark/fluxengine/gui/AboutAction.java b/java/com/cowlark/fluxengine/gui/AboutAction.java index 3d819f315..88a35f880 100644 --- a/java/com/cowlark/fluxengine/gui/AboutAction.java +++ b/java/com/cowlark/fluxengine/gui/AboutAction.java @@ -1,5 +1,6 @@ package com.cowlark.fluxengine.gui; +import com.cowlark.fluxengine.core.Version; import javax.swing.AbstractAction; import javax.swing.JOptionPane; import java.awt.event.ActionEvent; @@ -11,7 +12,7 @@ public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog( null, - "FluxEngine\nA disk-flux reader/writer", + "FluxEngine\nA disk-flux reader/writer\nVersion: " + Version.get(), "About FluxEngine", JOptionPane.INFORMATION_MESSAGE); } diff --git a/java/com/cowlark/fluxengine/gui/ApplicationFrame.java b/java/com/cowlark/fluxengine/gui/ApplicationFrame.java index ef6665875..49c7c83ee 100644 --- a/java/com/cowlark/fluxengine/gui/ApplicationFrame.java +++ b/java/com/cowlark/fluxengine/gui/ApplicationFrame.java @@ -21,6 +21,7 @@ import sprouts.Var; import sprouts.Viewable; import swingtree.UI; +import swingtree.UI.Axis; import swingtree.UIForPanel; import swingtree.UIForSplitPane; import swingtree.UIForTabbedPane; @@ -29,6 +30,7 @@ import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; import java.awt.Color; import java.awt.Toolkit; import java.net.URL; @@ -65,20 +67,31 @@ public class ApplicationFrame extends JFrame setIconImage(Toolkit.getDefaultToolkit().getImage(iconUrl)); setTitle("FluxEngine"); - UIForTabbedPane leftPane = - tabbedPane().add(tab("Configuration").add(scrollPane().add(of(configurationPanel)))); + int leftPaneWidth = configurationPanel.getPreferredSize().width; + UIForTabbedPane leftPane = tabbedPane().add(tab("Configuration").add(scrollPane() + .withPrefWidth(leftPaneWidth) + .withMinSize(leftPaneWidth, 0) + .withMaxSize(leftPaneWidth, Integer.MAX_VALUE) + .withVerticalScrollBarPolicy(UI.Active.ALWAYS) + .add(of(configurationPanel)))); UIForSplitPane topPane = splitPane(HORIZONTAL) - .peek(pane -> pane.setResizeWeight(1.0)) + .peek(pane -> { + pane.setResizeWeight(1.0); + }) .add( - TOP, tabbedPane() - .add(tab("Image").add(of(imagePanel).withPrefSize(sizePts( - 500, - 300)))) + TOP, + tabbedPane() + .withMinSize(100, 0) + .add(tab("Image").add(of(imagePanel))) .add(tab("Log").add(of(logPanel))) .add(tab("Files").add(of(filesystemPanel)))) - .add(BOTTOM, tabbedPane().add(tab("Visualiser").add(of(visualiserPanel)))); + .add( + BOTTOM, + tabbedPane() + .withMinSize(100, 0) + .add(tab("Visualiser").add(of(visualiserPanel)))); UIForPanel controlPanel = panel("wrap 4, center, nogrid") @@ -105,22 +118,44 @@ TOP, tabbedPane() .onClick(model::onEmergencyStop)); UIForTabbedPane bottomPane = - tabbedPane().add(tab("Summary").add(panel("fillx, wrap 1, aligny center") - .add("growx, h 100pt!", of(summaryPanel)) + tabbedPane().add(tab("Summary").add(panel("fillx, wrap 1") + .add("growx, pushx, hmin 100pt", of(summaryPanel)) .add("growx", controlPanel))); + UIForSplitPane outerSplit = splitPane(HORIZONTAL) + .peek(pane -> { + pane.setResizeWeight(0.0); + pane.setDividerSize(5); + pane.setContinuousLayout(true); + /* setDividerLocation(int) before the component is + * realized has no effect (the split pane has zero size + * so the location is clamped). Defer until after pack/ + * setVisible so the ConfigurationPanel gets its intended + * width without needing a manual resize or sash touch. + * withPrefWidth scales via UI.scale, so the divider must + * be scaled the same way. */ + SwingUtilities.invokeLater(() -> pane.setDividerLocation( + UI.scale(leftPaneWidth))); + }) + .add(TOP, leftPane) + .add( + BOTTOM, + splitPane(Axis.VERTICAL) + .peek(pane -> pane.setResizeWeight(1.0)) + .add(TOP, topPane) + .add(BOTTOM, bottomPane)); + of(this) .withOnCloseOperation(UI.OnWindowClose.DISPOSE) - .onClose(it -> System.exit(0)) + .onClose(it -> { + model.onEmergencyStop(); + dispose(); + System.exit(0); + }) .peek(frame -> { frame.setJMenuBar(ApplicationMenu.createMenu()); }) - .add(panel("fill, wrap 2").add("growy", leftPane).add( - "grow, push", - splitPane(UI.Axis.VERTICAL) - .peek(pane -> pane.setResizeWeight(1.0)) - .add(TOP, topPane) - .add(BOTTOM, bottomPane))); + .add(outerSplit); } private static @NonNull ViewSupplier createControlPanelCard( diff --git a/java/com/cowlark/fluxengine/gui/ConfigurationPanel.java b/java/com/cowlark/fluxengine/gui/ConfigurationPanel.java index 883058a6f..189c2338e 100644 --- a/java/com/cowlark/fluxengine/gui/ConfigurationPanel.java +++ b/java/com/cowlark/fluxengine/gui/ConfigurationPanel.java @@ -41,10 +41,16 @@ import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JViewport; import javax.swing.ListCellRenderer; +import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; import java.io.File; @@ -55,13 +61,13 @@ import java.util.function.BiFunction; import java.util.function.Function; -public class ConfigurationPanel extends JPanel +public class ConfigurationPanel extends JPanel implements Scrollable { private static final ImmutableList YES_NO = ImmutableList.of(false, true); private static final ImmutableList DRIVES = ImmutableList.of(0, 1); private static final ConfigProto GLOBAL_CONFIG = Formats.get("_global_options"); private static final String LABEL_FORMAT = "wmax 100lp"; - private static final String SETTING_FORMAT = "wmax 200lp, growx, pushx"; + private static final String SETTING_FORMAT = "wmax 200lp, growx"; /* Black-and-white device icons, recoloured at render time to the text * foreground and background colours (see deviceIcon()). */ @@ -188,12 +194,76 @@ private static String commentRenderer(String comment) return String.format("%s:", StringUtils.capitalize(comment)); } + @Override + public Dimension getMaximumSize() + { + Dimension max = super.getMaximumSize(); + int twoColumnMax = 340 + getInsets().left + getInsets().right; + if (max.width > twoColumnMax) { + max.width = twoColumnMax; + } + return max; + } + + @Override + public Dimension getPreferredSize() + { + /* Preferred width is capped to two columns so the scroll pane + * sizes correctly; height remains the layout's preferred height + * so the panel contracts to its content and Scrollable can + * stretch it to the viewport when needed. */ + Dimension pref = super.getPreferredSize(); + int twoColumnMax = 340 + getInsets().left + getInsets().right; + if (pref.width > twoColumnMax) + pref.width = twoColumnMax; + return pref; + } + + @Override + public Dimension getPreferredScrollableViewportSize() + { + return getPreferredSize(); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) + { + return 10; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) + { + return 100; + } + + @Override + public boolean getScrollableTracksViewportWidth() + { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() + { + /* When the preferred height is smaller than the viewport we stretch + * to fill the viewport so the advanced-settings text area (with + * grow/push) expands to the bottom. When the preferred height is + * larger the outer scroll pane scrolls normally. */ + Container parent = getParent(); + if (parent instanceof JViewport) + { + return parent.getHeight() > getPreferredSize().height; + } + return false; + } + /* Removes the existing UI and recreates it. */ private void rebuildUi() { removeAll(); - UIForPanel panel = of(this).withLayout("wrap 2, insets 5"); + UIForPanel panel = of(this).withLayout("wrap 2, insets 5 5 5 0"); panel = buildFormatPane(panel); panel = buildDevicesPane(panel); @@ -334,7 +404,54 @@ private UIForPanel buildAdvancedPane(UIForPanel { + /* Keep the outer scroll pane's preferred size in + * sync with the document so the area contracts + * to the text height (and re-expands when the + * viewport is larger via Scrollable). */ + ta.getDocument().addDocumentListener( + new javax.swing.event.DocumentListener() + { + private void update() + { + SwingUtilities.invokeLater(() -> { + revalidate(); + repaint(); + Container viewport = getParent(); + if (viewport instanceof JViewport) + viewport.revalidate(); + Container scroll = viewport != null + ? viewport.getParent() + : null; + if (scroll instanceof JScrollPane) + scroll.revalidate(); + }); + } + + @Override + public void insertUpdate( + javax.swing.event.DocumentEvent e) + { + update(); + } + + @Override + public void removeUpdate( + javax.swing.event.DocumentEvent e) + { + update(); + } + + @Override + public void changedUpdate( + javax.swing.event.DocumentEvent e) + { + update(); + } + }); + })); } /* A renderer which shows each device entry with the placeholder icon diff --git a/java/com/cowlark/fluxengine/gui/ImageViewModel.java b/java/com/cowlark/fluxengine/gui/ImageViewModel.java index 4c2b0d323..1957f9250 100644 --- a/java/com/cowlark/fluxengine/gui/ImageViewModel.java +++ b/java/com/cowlark/fluxengine/gui/ImageViewModel.java @@ -168,7 +168,8 @@ Var> getOptionsForDevice() void refreshUsbDevices() { - usbDevices.set(Association + Association old = usbDevices.get(); + Association next = Association .between(String.class, UsbFinder.CandidateDevice.class) .put(DEVICE_FLUXFILE, new CandidateDevice()) .put(DEVICE_SERIALPORT, new CandidateDevice()) @@ -176,7 +177,17 @@ void refreshUsbDevices() .findUsbDevices() .stream() .map(candidate -> Pair.of(candidate.getSerial(), candidate)) - .collect(toList()))); + .collect(toList())); + usbDevices.set(next); + if (old != null) + { + for (Pair e : old) + { + CandidateDevice c = e.second(); + if (c != null) + c.release(); + } + } } void onReadDisk(ComponentDelegate delegate) @@ -367,19 +378,27 @@ public void dispose() }); } - void onEmergencyStop(ComponentDelegate delegate) + void onEmergencyStop() { - Disposable operation = getCurrentDisposable().get(); - if ((operation == null) || operation.isDisposed()) - return; + if (getCurrentDisposable().isPresent()) + { + Disposable operation = getCurrentDisposable().get(); + if ((operation == null) || operation.isDisposed()) + return; + + ReadWriteFluxOperation.requestEmergencyStop(); + operation.dispose(); - ReadWriteFluxOperation.requestEmergencyStop(); - operation.dispose(); + /* Only clear the property once the dispose has actually happened; + * dispose() blocks until the worker thread has exited. */ + getCurrentDisposable().set(From.VIEW_MODEL, null); + getDriveActivity().set(new DriveActivity(ActivityType.IDLE, 0, 0)); + } + } - /* Only clear the property once the dispose has actually happened; - * dispose() blocks until the worker thread has exited. */ - getCurrentDisposable().set(From.VIEW_MODEL, null); - getDriveActivity().set(new DriveActivity(ActivityType.IDLE, 0, 0)); + void onEmergencyStop(ComponentDelegate delegate) + { + onEmergencyStop(); } private void performOperation( diff --git a/java/com/cowlark/fluxengine/gui/SummaryPanel.java b/java/com/cowlark/fluxengine/gui/SummaryPanel.java index 30d4602f7..cae780127 100644 --- a/java/com/cowlark/fluxengine/gui/SummaryPanel.java +++ b/java/com/cowlark/fluxengine/gui/SummaryPanel.java @@ -26,6 +26,7 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; +import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -117,7 +118,7 @@ private void rebuildUi() currentLayout.maxPhysicalCylinder - currentLayout.minPhysicalCylinder + 1 + 2; UIForPanel ui = of(this).withLayout( "wrap " + columns + ",gap 1 1,fill", - "[sizegroup 1,grow,fill]", + String.join("", Collections.nCopies(columns, "[sizegroup 1,grow,fill]")), "grow,fill"); ui = ui.add( diff --git a/java/com/cowlark/fluxengine/usb/BUILD.bazel b/java/com/cowlark/fluxengine/usb/BUILD.bazel index 937b2ed46..34c40e3a4 100644 --- a/java/com/cowlark/fluxengine/usb/BUILD.bazel +++ b/java/com/cowlark/fluxengine/usb/BUILD.bazel @@ -20,7 +20,6 @@ java_library( name = "usb", srcs = glob(["*.java"]), plugins = ["//java/com/cowlark/fluxengine/wiring:lombok_plugin"], - resources = ["//java:javax.usb.properties"], deps = [ ":usb_java_proto", "//java/com/cowlark/fluxengine/config:config_java_proto", @@ -31,11 +30,12 @@ java_library( "//java/com/cowlark/fluxengine/wiring", "@maven//:com_fazecast_jSerialComm", "@maven//:com_google_guava_guava", - "@maven//:javax_usb_usb_api", "@maven//:net_java_dev_jna_jna", "@maven//:net_java_dev_jna_jna_platform", + "@maven//:org_indunet_fastproto", + "@maven//:org_indunet_fastproto_core", "@maven//:org_projectlombok_lombok", "@maven//:org_slf4j_slf4j_api", - "@maven//:org_usb4java_usb4java_javax", + "@maven//:org_usb4java_usb4java", ], ) diff --git a/java/com/cowlark/fluxengine/usb/FluxEngineUsbDevice.java b/java/com/cowlark/fluxengine/usb/FluxEngineUsbDevice.java index 376eae8b0..b884b2de9 100644 --- a/java/com/cowlark/fluxengine/usb/FluxEngineUsbDevice.java +++ b/java/com/cowlark/fluxengine/usb/FluxEngineUsbDevice.java @@ -7,130 +7,144 @@ import static com.cowlark.fluxengine.wiring.FluxEngine.FLUXENGINE_PROTOCOL_VERSION; import static com.cowlark.fluxengine.wiring.FluxEngine.FRAME_SIZE; import static com.cowlark.fluxengine.wiring.FluxEngine.F_ERROR_BAD_COMMAND; +import static com.cowlark.fluxengine.wiring.FluxEngine.F_ERROR_INTERNAL; +import static com.cowlark.fluxengine.wiring.FluxEngine.F_ERROR_INVALID_VALUE; import static com.cowlark.fluxengine.wiring.FluxEngine.F_ERROR_UNDERRUN; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_BULK_READ_TEST_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_BULK_READ_TEST_REPLY; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_BULK_WRITE_TEST_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_BULK_WRITE_TEST_REPLY; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_DEBUG; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_ERASE_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_ERASE_REPLY; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_ERROR; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_GET_VERSION_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_GET_VERSION_REPLY; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_MEASURE_SPEED_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_MEASURE_SPEED_REPLY; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_MEASURE_VOLTAGES_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_MEASURE_VOLTAGES_REPLY; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_READ_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_READ_REPLY; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_SEEK_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_SEEK_REPLY; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_SET_DRIVE_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_SET_DRIVE_REPLY; -import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_WRITE_CMD; import static com.cowlark.fluxengine.wiring.FluxEngine.F_FRAME_WRITE_REPLY; +import static com.cowlark.fluxengine.wiring.FluxEngine.ReadFrame; import com.cowlark.fluxengine.config.ConfigProto; import com.cowlark.fluxengine.core.ByteWriter; import com.cowlark.fluxengine.core.Bytes; import com.cowlark.fluxengine.core.FluxEngineException; -import javax.usb.UsbConfiguration; -import javax.usb.UsbEndpoint; -import javax.usb.UsbException; -import javax.usb.UsbInterface; -import javax.usb.UsbPipe; -import java.util.List; - -/** - * FluxEngine floppy drive device, ported from lib/usb/fluxengineusb.cc. - */ +import com.cowlark.fluxengine.wiring.FluxEngine; +import com.cowlark.fluxengine.wiring.FluxEngine.AnyFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.EraseFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.MeasureSpeedFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.MeasureSpeedReplyFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.SeekFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.SetDriveFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.VersionFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.VersionReplyFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.VoltagesReplyFrame; +import com.cowlark.fluxengine.wiring.FluxEngine.WriteFrame; +import org.indunet.fastproto.FastProto; +import org.slf4j.LoggerFactory; +import org.usb4java.DeviceHandle; +import org.usb4java.LibUsb; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + class FluxEngineUsbDevice extends UsbDevice { + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(FluxEngineUsbDevice.class); + private static final int MAX_TRANSFER = 32 * 1024; + private static final long TIMEOUT_MS = 5000; - private final javax.usb.UsbDevice device; private final ConfigProto config; - private final UsbInterface usbInterface; - private final UsbPipe cmdOut; - private final UsbPipe cmdIn; - private final UsbPipe dataOut; - private final UsbPipe dataIn; + private final DeviceHandle handle; private final byte[] buffer = new byte[FRAME_SIZE]; + private boolean closed = false; + private boolean deviceNeedsReset = false; - FluxEngineUsbDevice(javax.usb.UsbDevice device, ConfigProto config) + FluxEngineUsbDevice(UsbFinder.CandidateDevice candidate, ConfigProto config) { - this.device = device; this.config = config; + this.handle = new DeviceHandle(); + boolean handleOpened = false; + boolean interfaceClaimed = false; try { - UsbInterface iface = null; - try + UsbFinder.openDevice(candidate, handle); + handleOpened = true; + + /* Try to enable auto-detach of kernel driver where supported. */ + int autoDetachRc = LibUsb.setAutoDetachKernelDriver(handle, true); + if (autoDetachRc != LibUsb.SUCCESS && autoDetachRc != LibUsb.ERROR_NOT_SUPPORTED && + autoDetachRc != LibUsb.ERROR_NOT_FOUND) + { + logger + .atDebug() + .log( + "setAutoDetachKernelDriver failed: {}", + LibUsb.errorName(autoDetachRc)); + } + + /* Ensure configuration 1 is active (mirrors libusb_set_configuration + * in lib/usb/usb.cc). If getConfiguration fails we still try to set + * it; a missing configuration typically means the Zadig driver is not + * installed. */ + IntBuffer cfg = IntBuffer.allocate(1); + switch (LibUsb.getConfiguration(handle, cfg)) { - for (Object o : device.getUsbConfigurations()) + case LibUsb.SUCCESS -> { - UsbConfiguration usbConfig = (UsbConfiguration) o; - for (Object i : usbConfig.getUsbInterfaces()) + if (cfg.get(0) != 1) { - UsbInterface candidate = (UsbInterface) i; - if (candidate.getUsbEndpoints().size() >= 4) - iface = candidate; + check( + LibUsb.setConfiguration(handle, 1), + "FluxEngine: setConfiguration failed"); } } - if (iface == null) - throw new FluxEngineException("FluxEngine: no suitable USB interface found"); - - iface.claim(); - usbInterface = iface; - - List endpoints = iface.getUsbEndpoints(); - UsbPipe cOut = null; - UsbPipe cIn = null; - UsbPipe dOut = null; - UsbPipe dIn = null; - for (UsbEndpoint endpoint : endpoints) + + case LibUsb.ERROR_NOT_FOUND, LibUsb.ERROR_NO_DEVICE -> + throw new FluxEngineException("You need to install the Zadig driver"); + + default -> { - int address = endpoint.getUsbEndpointDescriptor().bEndpointAddress() & 0xff; - UsbPipe pipe = endpoint.getUsbPipe(); - pipe.open(); - switch (address) - { - case FLUXENGINE_CMD_OUT_EP: - cOut = pipe; - break; - case FLUXENGINE_CMD_IN_EP: - cIn = pipe; - break; - case FLUXENGINE_DATA_OUT_EP: - dOut = pipe; - break; - case FLUXENGINE_DATA_IN_EP: - dIn = pipe; - break; - } + /* Best-effort: try to set configuration 1 anyway. */ + int rc = LibUsb.setConfiguration(handle, 1); + if (rc != LibUsb.SUCCESS && rc != LibUsb.ERROR_BUSY) + logger + .atDebug() + .log("setConfiguration(1) returned {}", LibUsb.errorName(rc)); } - if (cOut == null || cIn == null || dOut == null || dIn == null) - throw new FluxEngineException("FluxEngine: could not open all USB pipes"); - cmdOut = cOut; - cmdIn = cIn; - dataOut = dOut; - dataIn = dIn; - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: USB error: " + e.getMessage()); } + logger.atDebug().log("claiming USB interface 0"); + check(LibUsb.claimInterface(handle, 0), "FluxEngine: claimInterface failed"); + interfaceClaimed = true; + int version = getVersion(); if (version != FLUXENGINE_PROTOCOL_VERSION) throw new FluxEngineException(String.format( "your FluxEngine firmware is at version %d but the client is for version " + - "%d; " + "please upgrade", version, FLUXENGINE_PROTOCOL_VERSION)); + "%d; please upgrade", version, FLUXENGINE_PROTOCOL_VERSION)); } catch (RuntimeException e) { try { - close(); + logger.atDebug().setCause(e).log("opening device failed, cleaning up"); + if (interfaceClaimed) + { + int rc = LibUsb.releaseInterface(handle, 0); + if (rc != LibUsb.SUCCESS) + logger + .atWarn() + .log( + "releaseInterface failed during cleanup: {}", + LibUsb.errorName(rc)); + } + if (handleOpened) + LibUsb.close(handle); + closed = true; } catch (RuntimeException suppressed) { e.addSuppressed(suppressed); @@ -139,39 +153,89 @@ class FluxEngineUsbDevice extends UsbDevice } } + /* Throws FluxEngineException with a formatted message if rc is not + * SUCCESS. The message is prefixed with context and suffixed with the + * libusb error name and string. */ + private void check(int rc, String context) + { + if (rc != LibUsb.SUCCESS) + { + deviceNeedsReset = true; + throw new FluxEngineException( + context + ": " + LibUsb.errorName(rc) + " " + LibUsb.strError(rc)); + } + } + + @Override + public void close() + { + if (closed) + return; + closed = true; + try + { + if (deviceNeedsReset) + { + logger.atDebug().log("resetting USB device"); + LibUsb.resetDevice(handle); + } + + int rc = LibUsb.releaseInterface(handle, 0); + if (rc != LibUsb.SUCCESS && rc != LibUsb.ERROR_NO_DEVICE && + rc != LibUsb.ERROR_NOT_FOUND) + { + logger.atDebug().log("releaseInterface failed: {}", LibUsb.errorName(rc)); + } + } finally + { + LibUsb.close(handle); + } + } + private static double getCurrentTime() { return System.nanoTime() / 1e9; } - private static Voltages readVoltages(byte[] r, int ptr) + private void usbCmdSend(byte[] data) { - int logic0 = (r[ptr] & 0xff) | ((r[ptr + 1] & 0xff) << 8); - int logic1 = (r[ptr + 2] & 0xff) | ((r[ptr + 3] & 0xff) << 8); - return new Voltages(logic0, logic1); + ByteBuffer buf = ByteBuffer.allocateDirect(data.length); + buf.put(data, 0, data.length); + buf.rewind(); + IntBuffer transferred = IntBuffer.allocate(1); + check( + LibUsb.interruptTransfer( + handle, + (byte) FLUXENGINE_CMD_OUT_EP, + buf, + transferred, + TIMEOUT_MS), + "FluxEngine: command send failed"); } - private void usbCmdSend(byte[] data) + private void usbCmdSend(Object object) { - try - { - cmdOut.syncSubmit(data); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: command send failed: " + e.getMessage()); - } + usbCmdSend(FastProto.encode(object)); } private byte[] usbCmdRecv(int len) { + ByteBuffer buf = ByteBuffer.allocateDirect(len); + IntBuffer transferred = IntBuffer.allocate(1); + check( + LibUsb.interruptTransfer( + handle, + (byte) FLUXENGINE_CMD_IN_EP, + buf, + transferred, + TIMEOUT_MS), + "FluxEngine: command recv failed"); + int n = transferred.get(0); byte[] data = new byte[len]; - try - { - cmdIn.syncSubmit(data); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: command recv failed: " + e.getMessage()); - } + buf.rewind(); + buf.get(data, 0, Math.min(n, len)); + /* libusb may return fewer bytes with an interrupt transfer; pad + * remaining bytes with zero to mimic javax.usb syncSubmit behaviour. */ return data; } @@ -181,17 +245,19 @@ private void usbDataSend(Bytes bytes) while (ptr < bytes.size()) { int len = Math.min(bytes.size() - ptr, MAX_TRANSFER); - byte[] data = new byte[len]; + ByteBuffer buf = ByteBuffer.allocateDirect(len); for (int i = 0; i < len; i++) - data[i] = (byte) bytes.getByte(ptr + i); - try - { - dataOut.syncSubmit(data); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: data send failed: " + e.getMessage()); - } - ptr += len; + buf.put((byte) bytes.getByte(ptr + i)); + buf.rewind(); + IntBuffer transferred = IntBuffer.allocate(1); + int rc = LibUsb.bulkTransfer( + handle, + (byte) FLUXENGINE_DATA_OUT_EP, + buf, + transferred, + TIMEOUT_MS); + check(rc, "FluxEngine: data send failed"); + ptr += transferred.get(0); } } @@ -203,19 +269,24 @@ private Bytes usbDataRecv(int maxLength) while (ptr < maxLength) { int len = Math.min(maxLength - ptr, MAX_TRANSFER); - byte[] data = new byte[len]; - int transferred; - try - { - transferred = dataIn.syncSubmit(data); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: data recv failed: " + e.getMessage()); - } - for (int i = 0; i < transferred; i++) - bw.write8(data[i] & 0xff); - ptr += transferred; - if (transferred < MAX_TRANSFER) + ByteBuffer buf = ByteBuffer.allocateDirect(len); + IntBuffer transferred = IntBuffer.allocate(1); + check( + LibUsb.bulkTransfer( + handle, + (byte) FLUXENGINE_DATA_IN_EP, + buf, + transferred, + TIMEOUT_MS), + "FluxEngine: data recv failed"); + int n = transferred.get(0); + buf.rewind(); + for (int i = 0; i < n; i++) + bw.write8(buf.get() & 0xff); + ptr += n; + if (n < len) + break; + if (n == 0) break; } return bytes; @@ -226,13 +297,19 @@ private void badReply() int type = buffer[0] & 0xff; if (type != F_FRAME_ERROR) throw new FluxEngineException(String.format("bad USB reply 0x%2x", type)); - switch (buffer[1] & 0xff) + switch (buffer[2] & 0xff) { case F_ERROR_BAD_COMMAND: throw new FluxEngineException("device did not understand command"); case F_ERROR_UNDERRUN: - throw new FluxEngineException("USB underrun (not enough bandwidth)"); + throw new UsbUnderrunException(); + + case F_ERROR_INVALID_VALUE: + throw new FluxEngineException("device received a bad parameter"); + + case F_ERROR_INTERNAL: + throw new FluxEngineException("device experienced an internal error"); default: throw new FluxEngineException("unknown device error " + (buffer[1] & 0xff)); @@ -252,7 +329,7 @@ private byte[] awaitReply(int desired) StringBuilder sb = new StringBuilder(); for (int i = 2; i < r.length && r[i] != 0; i++) sb.append((char) r[i]); - System.out.println("dev: " + sb); + logger.atDebug().log("dev: {}", sb); continue; } if (type != desired) @@ -261,46 +338,51 @@ private byte[] awaitReply(int desired) } } + private T awaitReply(int desired, Class dataClass) + { + byte[] bytes = awaitReply(desired); + return FastProto.decode(bytes, dataClass); + } + private int getVersion() { - byte[] f = {F_FRAME_GET_VERSION_CMD, 2}; - usbCmdSend(f); - byte[] r = awaitReply(F_FRAME_GET_VERSION_REPLY); - return r[2] & 0xff; + usbCmdSend(new VersionFrame()); + VersionReplyFrame reply = awaitReply(F_FRAME_GET_VERSION_REPLY, VersionReplyFrame.class); + return reply.getVersion(); } @Override public void seek(int cylinder) { - byte[] f = {F_FRAME_SET_DRIVE_CMD, - 5, - (byte) config.getDrive().getDrive(), - (byte) (config.getDrive().getHighDensity() ? 1 : 0), - (byte) 0}; - usbCmdSend(f); + usbCmdSend(SetDriveFrame + .builder() + .setDrive(config.getDrive().getDrive()) + .setHighDensity(config.getDrive().getHighDensity() ? 1 : 0) + .setIndexMode(0) + .build()); awaitReply(F_FRAME_SET_DRIVE_REPLY); - byte[] f2 = {F_FRAME_SEEK_CMD, 3, (byte) cylinder}; - usbCmdSend(f2); + usbCmdSend(SeekFrame.builder().setTrack(cylinder).build()); awaitReply(F_FRAME_SEEK_REPLY); } @Override public double getRotationalPeriod() { - byte[] f = {F_FRAME_MEASURE_SPEED_CMD, 3, (byte) config.getDrive().getHardSectorCount()}; - usbCmdSend(f); - - byte[] r = awaitReply(F_FRAME_MEASURE_SPEED_REPLY); - int periodMs = (r[2] & 0xff) | ((r[3] & 0xff) << 8); - return periodMs * 1000000.0; + usbCmdSend(MeasureSpeedFrame + .builder() + .setHardSectorCount(config.getDrive().getHardSectorCount()) + .build()); + + MeasureSpeedReplyFrame r = + awaitReply(F_FRAME_MEASURE_SPEED_REPLY, MeasureSpeedReplyFrame.class); + return r.getPeriodMs() * 1000000.0; } @Override public void testBulkWrite() { - byte[] f = {F_FRAME_BULK_WRITE_TEST_CMD, 2}; - usbCmdSend(f); + usbCmdSend(AnyFrame.builder().setType(F_FRAME_BULK_WRITE_TEST_CMD).setSize(2).build()); /* These must match the device. */ final int XSIZE = 64; @@ -324,7 +406,11 @@ public void testBulkWrite() int offset = x * XSIZE * YSIZE + y * ZSIZE + z; if ((bulkBuffer.getByte(offset) & 0xff) != (x + y + z) % 256) throw new FluxEngineException(String.format( - "data transfer corrupted at " + "0x%x %d.%d.%d", offset, x, y, z)); + "data transfer corrupted at " + "0x%x %d.%d.%d", + offset, + x, + y, + z)); } awaitReply(F_FRAME_BULK_WRITE_TEST_REPLY); @@ -333,8 +419,7 @@ public void testBulkWrite() @Override public void testBulkRead() { - byte[] f = {F_FRAME_BULK_READ_TEST_CMD, 2}; - usbCmdSend(f); + usbCmdSend(AnyFrame.builder().setType(F_FRAME_BULK_READ_TEST_CMD).setSize(2).build()); /* These must match the device. */ final int XSIZE = 64; @@ -366,50 +451,66 @@ public void testBulkRead() @Override public Bytes read(int cylinder, int head, double readTimeNs) { - seek(cylinder); - - Bytes f = new Bytes(0); - ByteWriter bw = f.writer(); - bw.write8(F_FRAME_READ_CMD); - bw.write8(6); - bw.write8(head); - bw.write8(config.getDrive().getSyncWithIndex() ? 1 : 0); - int milliseconds = (int) (readTimeNs / 1e6); - bw.write8(milliseconds & 0xff); - bw.write8((milliseconds >> 8) & 0xff); - bw.write8((int) ((config.getDrive().getHardSectorThresholdNs() + 5e5) / - 1e6)); /* round to nearest ms */ - usbCmdSend(f.toByteArray()); - - Bytes buffer = usbDataRecv(1024 * 1024); - - awaitReply(F_FRAME_READ_REPLY); - return buffer; + for (int i = 0; i < config.getUsb().getFluxengine().getUnderrunRetries(); i++) + { + try + { + seek(cylinder); + + usbCmdSend(ReadFrame + .builder() + .setSide(head) + .setSynced(config.getDrive().getSyncWithIndex() ? 1 : 0) + .setMilliseconds((int) (readTimeNs / 1e6)) + .setHardsecThresholdMs((int) ( + (config.getDrive().getHardSectorThresholdNs() + 5e5) / 1e6) + /* round to nearest ms */) + .build()); + + Bytes buffer = usbDataRecv(1024 * 1024); + + awaitReply(F_FRAME_READ_REPLY); + return buffer; + } catch (UsbUnderrunException e) + { + logger.atInfo().log("USB underrun, retrying"); + } + } + throw new FluxEngineException( + "Consistent USB underruns --- you don't have enough " + "bandwidth"); } @Override public void write(int cylinder, int head, Bytes bytes) { - seek(cylinder); - - int safelen = bytes.size() & ~(FRAME_SIZE - 1); - Bytes safeBytes = bytes.slice(0, safelen); - - Bytes f = new Bytes(0); - ByteWriter bw = f.writer(); - bw.write8(F_FRAME_WRITE_CMD); - bw.write8(7); - bw.write8(head); - bw.write8(safelen & 0xff); - bw.write8((safelen >> 8) & 0xff); - bw.write8((safelen >> 16) & 0xff); - bw.write8((safelen >> 24) & 0xff); - bw.write8((int) ((config.getDrive().getHardSectorThresholdNs() + 5e5) / - 1e6)); /* round to nearest ms */ - usbCmdSend(f.toByteArray()); - usbDataSend(safeBytes); - - awaitReply(F_FRAME_WRITE_REPLY); + for (int i = 0; i < config.getUsb().getFluxengine().getUnderrunRetries(); i++) + { + try + { + seek(cylinder); + + int safelen = bytes.size() & ~(FRAME_SIZE - 1); + Bytes safeBytes = bytes.slice(0, safelen); + + usbCmdSend(WriteFrame + .builder() + .setSide(head) + .setBytesToWrite(safelen) + .setHardsecThresholdMs((int) ( + (config.getDrive().getHardSectorThresholdNs() + 5e5) / 1e6) + /* round to nearest ms */) + .build()); + usbDataSend(safeBytes); + + awaitReply(F_FRAME_WRITE_REPLY); + return; + } catch (UsbUnderrunException e) + { + logger.atInfo().log("USB underrun, retrying"); + } + } + throw new FluxEngineException( + "Consistent USB underruns --- you don't have enough " + "bandwidth"); } @Override @@ -417,76 +518,45 @@ public void erase(int cylinder, int head) { seek(cylinder); - Bytes f = new Bytes(0); - ByteWriter bw = f.writer(); - bw.write8(F_FRAME_ERASE_CMD); - bw.write8(3); - bw.write8(head); - bw.write8((int) ((config.getDrive().getHardSectorThresholdNs() + 5e5) / - 1e6)); /* round to nearest ms */ - usbCmdSend(f.toByteArray()); + usbCmdSend(EraseFrame + .builder() + .setSide(head) + .setHardsecThresholdMs((int) ((config.getDrive().getHardSectorThresholdNs() + 5e5) / + 1e6)) + .build()); awaitReply(F_FRAME_ERASE_REPLY); } - @Override - public VoltageMeasurements measureVoltages() + private static Voltages toVoltages( + FluxEngine.Voltages raw) { - byte[] f = {F_FRAME_MEASURE_VOLTAGES_CMD, 2}; - usbCmdSend(f); - - byte[] r = awaitReply(F_FRAME_MEASURE_VOLTAGES_REPLY); - - VoltageMeasurements measurements = new VoltageMeasurements(); - int ptr = 2; - measurements.outputBothOff = readVoltages(r, ptr); - ptr += 4; - measurements.outputDrive0Selected = readVoltages(r, ptr); - ptr += 4; - measurements.outputDrive1Selected = readVoltages(r, ptr); - ptr += 4; - measurements.outputDrive0Running = readVoltages(r, ptr); - ptr += 4; - measurements.outputDrive1Running = readVoltages(r, ptr); - ptr += 4; - measurements.inputBothOff = readVoltages(r, ptr); - ptr += 4; - measurements.inputDrive0Selected = readVoltages(r, ptr); - ptr += 4; - measurements.inputDrive1Selected = readVoltages(r, ptr); - ptr += 4; - measurements.inputDrive0Running = readVoltages(r, ptr); - ptr += 4; - measurements.inputDrive1Running = readVoltages(r, ptr); - return measurements; + return Voltages + .builder() + .setLogic0Mv(raw.getLogic0Mv()) + .setLogic1Mv(raw.getLogic1Mv()) + .build(); } @Override - public void close() + public VoltageMeasurements measureVoltages() { - try - { - if (cmdOut != null) - cmdOut.close(); - if (cmdIn != null) - cmdIn.close(); - if (dataOut != null) - dataOut.close(); - if (dataIn != null) - dataIn.close(); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: USB error closing pipe: " + e.getMessage()); - } finally - { - try - { - if ((usbInterface != null) && usbInterface.isClaimed()) - usbInterface.release(); - } catch (UsbException e) - { - throw new FluxEngineException("FluxEngine: USB error: " + e.getMessage()); - } - } + usbCmdSend(AnyFrame.builder().setType(F_FRAME_MEASURE_VOLTAGES_CMD).setSize(2).build()); + + VoltagesReplyFrame res = + awaitReply(F_FRAME_MEASURE_VOLTAGES_REPLY, VoltagesReplyFrame.class); + return VoltageMeasurements + .builder() + .setInputBothOff(toVoltages(res.voltages.get(0))) + .setInputDrive0Selected(toVoltages(res.voltages.get(1))) + .setInputDrive1Selected(toVoltages(res.voltages.get(2))) + .setInputDrive0Running(toVoltages(res.voltages.get(3))) + .setInputDrive1Running(toVoltages(res.voltages.get(4))) + .setOutputBothOff(toVoltages(res.voltages.get(5))) + .setOutputDrive0Selected(toVoltages(res.voltages.get(6))) + .setOutputDrive1Selected(toVoltages(res.voltages.get(7))) + .setOutputDrive0Running(toVoltages(res.voltages.get(8))) + .setOutputDrive1Running(toVoltages(res.voltages.get(9))) + .build(); } } diff --git a/java/com/cowlark/fluxengine/usb/HackyUsbSerialNumberResolver.java b/java/com/cowlark/fluxengine/usb/HackyUsbSerialNumberResolver.java index 69133e837..9883917a3 100644 --- a/java/com/cowlark/fluxengine/usb/HackyUsbSerialNumberResolver.java +++ b/java/com/cowlark/fluxengine/usb/HackyUsbSerialNumberResolver.java @@ -6,31 +6,30 @@ import com.sun.jna.platform.win32.Guid.GUID; import com.sun.jna.win32.StdCallLibrary; import com.sun.jna.win32.W32APIOptions; - -import javax.usb.UsbDevice; -import javax.usb.UsbDeviceDescriptor; -import javax.usb.UsbException; -import java.io.UnsupportedEncodingException; +import org.usb4java.Device; +import org.usb4java.DeviceDescriptor; +import org.usb4java.DeviceHandle; +import org.usb4java.LibUsb; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -/// Resolves a USB device's serial number across Windows, macOS, and Linux. -/// -/// Strategy: try javax.usb's UsbDevice.getSerialNumberString() first — this -/// only requires a control transfer on endpoint 0, which macOS and Linux's -/// libusb backends allow even while a class driver (CDC/ACM, FTDI, etc.) -/// still owns the device's interfaces. Windows is the exception: libusb -/// there can't open a device at all unless it's bound to WinUSB/libusbK/ -/// libusb-win32, so the direct call throws for devices left on their normal -/// driver (e.g. a standard serial port device). In that case, fall back to -/// a native SetupAPI lookup keyed on the VID/PID you already have from -/// javax.usb — no driver changes required for this path. -/// -/// Requires net.java.dev.jna:jna and jna-platform (only exercised on -/// Windows; harmless to have as a dependency on other platforms). +/* Resolves a USB device's serial number across Windows, macOS, and Linux. + * + * Strategy: try libusb's GetStringDescriptor (via usb4java) first — this + * only requires a control transfer on endpoint 0, which macOS and Linux's + * libusb backends allow even while a class driver (CDC/ACM, FTDI, etc.) + * still owns the device's interfaces. Windows is the exception: libusb + * there can't open a device at all unless it's bound to WinUSB/libusbK/ + * libusb-win32, so the direct call throws for devices left on their normal + * driver (e.g. a standard serial port device). In that case, fall back to + * a native SetupAPI lookup keyed on the VID/PID you already have from the + * device descriptor — no driver changes required for this path. + * + * Requires net.java.dev.jna:jna and jna-platform (only exercised on + * Windows; harmless to have as a dependency on other platforms). */ public final class HackyUsbSerialNumberResolver { @@ -38,50 +37,46 @@ private HackyUsbSerialNumberResolver() { } - /** - * @return the device's serial number, or null if it genuinely has none - * (iSerialNumber == 0 in its device descriptor). - */ - public static String resolve(UsbDevice device) throws UsbException, UnsupportedEncodingException + /* @return the device's serial number, or null if it genuinely has none + * (iSerialNumber == 0 in its device descriptor) or it could not be read. */ + public static String resolve(Device device, DeviceDescriptor descriptor) { - try - { - String serial = device.getSerialNumberString(); - if (serial != null) - { - return serial; - } - } catch (UsbException e) - { - if (!isWindows()) - { - throw e; - } - } - if (isWindows()) { - UsbDeviceDescriptor descriptor = device.getUsbDeviceDescriptor(); int vendorId = descriptor.idVendor() & 0xFFFF; int productId = descriptor.idProduct() & 0xFFFF; return Windows.getSerialNumberForUsbId(vendorId, productId); } - return null; + if (descriptor.iSerialNumber() == 0) + return null; + + DeviceHandle handle = new DeviceHandle(); + int rc = LibUsb.open(device, handle); + if (rc != LibUsb.SUCCESS) + return null; + try + { + return LibUsb.getStringDescriptor(handle, descriptor.iSerialNumber()); + } finally + { + LibUsb.close(handle); + } } - private static boolean isWindows() + /* Convenience overload which fetches the descriptor itself. */ + public static String resolve(Device device) { - return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win"); + DeviceDescriptor descriptor = new DeviceDescriptor(); + int rc = LibUsb.getDeviceDescriptor(device, descriptor); + if (rc != LibUsb.SUCCESS) + return null; + return resolve(device, descriptor); } - /** - * True if the value looks like a genuine device-reported serial rather - * than a Windows-generated placeholder (e.g. "6&2c3d1a4&0&0002"). - */ - public static boolean looksLikeGenuineSerial(String serial) + private static boolean isWindows() { - return serial != null && !serial.contains("&"); + return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win"); } /* Windows fallback using internal APIs. */ @@ -97,8 +92,8 @@ static String getSerialNumberForUsbId(int vendorId, int productId) String.format("^USB\\\\VID_%04X&PID_%04X\\\\([^\\\\]+)$", vendorId, productId), Pattern.CASE_INSENSITIVE); - // classGuid = null + enumerator "USB" + DIGCF_ALLCLASSES enumerates - // every USB device node present, regardless of device class. + /* classGuid = null + enumerator "USB" + DIGCF_ALLCLASSES enumerates + * every USB device node present, regardless of device class. */ Pointer deviceInfoSet = SetupApi.INSTANCE.SetupDiGetClassDevsW( null, "USB", diff --git a/java/com/cowlark/fluxengine/usb/UsbContext.java b/java/com/cowlark/fluxengine/usb/UsbContext.java new file mode 100644 index 000000000..16a8d0e3b --- /dev/null +++ b/java/com/cowlark/fluxengine/usb/UsbContext.java @@ -0,0 +1,41 @@ +package com.cowlark.fluxengine.usb; + +import com.cowlark.fluxengine.core.FluxEngineException; +import org.slf4j.LoggerFactory; +import org.usb4java.Context; +import org.usb4java.LibUsb; + +/* Singleton holder for the explicit libusb Context, ported from libusb_init + * usage in lib/usb/usb.cc. The Context is initialised once via + * LibUsb.init and lives until JVM shutdown. */ +final class UsbContext +{ + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbContext.class); + + private static final Context CONTEXT; + + static + { + Context ctx = new Context(); + logger.atDebug().log("initialising libusb Context"); + int rc = LibUsb.init(ctx); + if (rc != LibUsb.SUCCESS) + throw new FluxEngineException("Failed to initialize libusb"); + + CONTEXT = ctx; + Runtime.getRuntime().addShutdownHook(new Thread(() -> + { + logger.atDebug().log("exiting libusb Context"); + LibUsb.exit(CONTEXT); + })); + } + + private UsbContext() + { + } + + static Context get() + { + return CONTEXT; + } +} diff --git a/java/com/cowlark/fluxengine/usb/UsbDevice.java b/java/com/cowlark/fluxengine/usb/UsbDevice.java index ea091deee..b974e478b 100644 --- a/java/com/cowlark/fluxengine/usb/UsbDevice.java +++ b/java/com/cowlark/fluxengine/usb/UsbDevice.java @@ -35,10 +35,12 @@ public abstract class UsbDevice implements AutoCloseable /* Closes the device, releasing any underlying resources. */ @Override - public abstract void close(); + public void close() + { + } protected String usbError(int error) { - return String.format("USB error %d", error); + return String.format("USB error %d (%s)", error, org.usb4java.LibUsb.errorName(error)); } } diff --git a/java/com/cowlark/fluxengine/usb/UsbFactory.java b/java/com/cowlark/fluxengine/usb/UsbFactory.java index 579f750e5..8caf0d38f 100644 --- a/java/com/cowlark/fluxengine/usb/UsbFactory.java +++ b/java/com/cowlark/fluxengine/usb/UsbFactory.java @@ -66,7 +66,7 @@ public UsbDevice createConnection() candidateDevice.serialPort, config, config.getUsb().getApplesauce()); - case FLUXENGINE -> new FluxEngineUsbDevice(candidateDevice.device, config); + case FLUXENGINE -> new FluxEngineUsbDevice(candidateDevice, config); }; device.seek(config.getDrive().getDrive()); diff --git a/java/com/cowlark/fluxengine/usb/UsbFinder.java b/java/com/cowlark/fluxengine/usb/UsbFinder.java index 2f9e14a60..42f8a5e87 100644 --- a/java/com/cowlark/fluxengine/usb/UsbFinder.java +++ b/java/com/cowlark/fluxengine/usb/UsbFinder.java @@ -8,13 +8,13 @@ import com.google.common.collect.Iterables; import lombok.Data; import lombok.experimental.Accessors; -import org.usb4java.javax.Services; -import javax.usb.UsbDevice; -import javax.usb.UsbDeviceDescriptor; -import javax.usb.UsbException; -import javax.usb.UsbHub; -import javax.usb.UsbServices; -import java.io.UnsupportedEncodingException; +import org.usb4java.Context; +import org.usb4java.Device; +import org.usb4java.DeviceDescriptor; +import org.usb4java.DeviceHandle; +import org.usb4java.DeviceList; +import org.usb4java.LibUsb; +import java.nio.ByteBuffer; import java.util.Set; public class UsbFinder @@ -25,28 +25,61 @@ public class UsbFinder private static final Set VALID_DEVICES = Set.of(GREASEWEAZLE_ID, FLUXENGINE_ID, APPLESAUCE_ID); - private static String getSerialNumber(UsbDevice device) + private static String getSerialNumber(Device device, DeviceDescriptor descriptor) { - try - { - return HackyUsbSerialNumberResolver.resolve(device); - } catch (UsbException | UnsupportedEncodingException e) - { + String s = HackyUsbSerialNumberResolver.resolve(device, descriptor); + if (s == null) return "n/a"; - } + return s; } public static synchronized ImmutableList findUsbDevices() { ImmutableList.Builder candidates = ImmutableList.builder(); + Context ctx = UsbContext.get(); + DeviceList list = new DeviceList(); + int rc = LibUsb.getDeviceList(ctx, list); + if (rc < 0) + { + System.err.println("USB error: " + LibUsb.errorName(rc)); + return candidates.build(); + } try { - UsbServices services = new Services(); - UsbHub rootHub = services.getRootUsbHub(); - walkHub(rootHub, candidates); - } catch (UsbException e) + for (Device device : list) + { + DeviceDescriptor descriptor = new DeviceDescriptor(); + int r = LibUsb.getDeviceDescriptor(device, descriptor); + if (r != LibUsb.SUCCESS) + continue; + + int id = ((descriptor.idVendor() & 0xffff) << 16) + | (descriptor.idProduct() & 0xffff); + if (!VALID_DEVICES.contains(id)) + continue; + + CandidateDevice candidate = new CandidateDevice(); + candidate.id = id; + candidate.serial = getSerialNumber(device, descriptor); + candidate.busNumber = LibUsb.getBusNumber(device); + candidate.deviceAddress = LibUsb.getDeviceAddress(device); + candidate.portPath = getPortPath(device); + + if (id == GREASEWEAZLE_ID) + candidate.type = DeviceType.GREASEWEAZLE; + else if (id == APPLESAUCE_ID) + candidate.type = DeviceType.APPLESAUCE; + else + candidate.type = DeviceType.FLUXENGINE; + + if (id == GREASEWEAZLE_ID || id == APPLESAUCE_ID) + candidate.serialPort = findSerialPort(id, candidate.serial); + + candidates.add(candidate); + } + } finally { - System.err.println("USB error: " + e.getMessage()); + LibUsb.freeDeviceList(list, true); } return candidates.build(); } @@ -57,60 +90,90 @@ public static CandidateDevice selectDevice(ConfigProtoOrBuilder config) { ImmutableList candidates = findUsbDevices(); if (candidates.isEmpty()) - throw new ConfigException("no devices found (is one plugged in? Do you have the " + - "appropriate permissions?"); + throw new ConfigException("no devices found (is one plugged in? Do you have the " + + "appropriate permissions?"); String wantedSerial = config.getUsb().getSerial(); if (!Strings.isNullOrEmpty(wantedSerial)) { + CandidateDevice found = null; for (CandidateDevice candidate : candidates) { if (candidate.serial.equals(wantedSerial)) - return candidate; + { + found = candidate; + break; + } } - throw new ConfigException("serial number not found"); + if (found == null) + throw new ConfigException("serial number not found"); + return found; } if (candidates.size() == 1) return Iterables.getOnlyElement(candidates); throw new ConfigException( - "more than one device detected; you'll need to explicitly specify the serial " + - "number of the device you want"); + "more than one device detected; you'll need to explicitly specify the serial " + + "number of the device you want"); } - private static void walkHub(UsbHub hub, ImmutableList.Builder candidates) + /* Opens the USB device indicated by the candidate's location. The caller + * receives an open DeviceHandle; the underlying Device is not retained. */ + static void openDevice(CandidateDevice candidate, DeviceHandle handle) { - for (Object o : hub.getAttachedUsbDevices()) + Context ctx = UsbContext.get(); + DeviceList list = new DeviceList(); + int rc = LibUsb.getDeviceList(ctx, list); + if (rc < 0) + throw new com.cowlark.fluxengine.core.FluxEngineException( + "USB error: " + LibUsb.errorName(rc)); + try + { + for (Device device : list) + { + if (LibUsb.getBusNumber(device) != candidate.busNumber) + continue; + if (LibUsb.getDeviceAddress(device) != candidate.deviceAddress) + continue; + /* If we have a port path, verify it as well for robustness + * against address reuse. */ + if (candidate.portPath != null && !candidate.portPath.isEmpty()) + { + ImmutableList actual = getPortPath(device); + if (!actual.equals(candidate.portPath)) + continue; + } + int openRc = LibUsb.open(device, handle); + if (openRc != LibUsb.SUCCESS) + throw new com.cowlark.fluxengine.core.FluxEngineException( + "FluxEngine: USB open failed: " + LibUsb.errorName(openRc) + " " + + LibUsb.strError(openRc)); + return; + } + throw new com.cowlark.fluxengine.core.FluxEngineException( + String.format( + "USB device at bus %d address %d not found (was it unplugged?)", + candidate.busNumber, + candidate.deviceAddress)); + } finally { - UsbDevice usbDevice = (UsbDevice) o; - if (usbDevice.isUsbHub()) - walkHub((UsbHub) usbDevice, candidates); - - UsbDeviceDescriptor descriptor = usbDevice.getUsbDeviceDescriptor(); - int id = ((descriptor.idVendor() & 0xffff) << 16) | (descriptor.idProduct() & 0xffff); - if (!VALID_DEVICES.contains(id)) - continue; - - CandidateDevice candidate = new CandidateDevice(); - candidate.device = usbDevice; - candidate.id = id; - candidate.serial = getSerialNumber(usbDevice); - - if (id == GREASEWEAZLE_ID) - candidate.type = DeviceType.GREASEWEAZLE; - else if (id == APPLESAUCE_ID) - candidate.type = DeviceType.APPLESAUCE; - else - candidate.type = DeviceType.FLUXENGINE; - - if (id == GREASEWEAZLE_ID || id == APPLESAUCE_ID) - candidate.serialPort = findSerialPort(id, candidate.serial); - - candidates.add(candidate); + LibUsb.freeDeviceList(list, true); } } + private static ImmutableList getPortPath(Device device) + { + ByteBuffer buffer = ByteBuffer.allocateDirect(8); + int len = LibUsb.getPortNumbers(device, buffer); + if (len <= 0) + return ImmutableList.of(); + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < len; i++) + builder.add((int) buffer.get(i) & 0xff); + return builder.build(); + } + private static String findSerialPort(int id, String serial) { int vendorId = id >>> 16; @@ -120,8 +183,8 @@ private static String findSerialPort(int id, String serial) if (port.getVendorID() == vendorId && port.getProductID() == productId) { String portSerial = port.getSerialNumber(); - if (serial == null || serial.isEmpty() || portSerial == null || - serial.equals(portSerial)) + if (serial == null || serial.isEmpty() || portSerial == null + || serial.equals(portSerial)) { return port.getSystemPortName(); } @@ -130,6 +193,12 @@ private static String findSerialPort(int id, String serial) return null; } + /* Retained for API compatibility; previously freed retained Device refs. + * Now a no-op as CandidateDevice no longer holds a Device reference. */ + public static void freeDevices(Iterable devices) + { + } + public enum DeviceType { FLUXENGINE("FluxEngine"), GREASEWEAZLE("Greaseweazle"), APPLESAUCE("Applesauce"); @@ -152,9 +221,18 @@ public String getDeviceName() public static final class CandidateDevice { public DeviceType type; - public UsbDevice device; public int id; public String serial; public String serialPort; + public int busNumber; + public int deviceAddress; + public ImmutableList portPath = ImmutableList.of(); + + /* Retained for API compatibility; previously released the retained + * Device ref. Now a no-op as CandidateDevice no longer holds a + * Device reference. */ + public void release() + { + } } } diff --git a/java/com/cowlark/fluxengine/usb/UsbUnderrunException.java b/java/com/cowlark/fluxengine/usb/UsbUnderrunException.java new file mode 100644 index 000000000..b149bc151 --- /dev/null +++ b/java/com/cowlark/fluxengine/usb/UsbUnderrunException.java @@ -0,0 +1,9 @@ +package com.cowlark.fluxengine.usb; + +public class UsbUnderrunException extends RetryableUsbException +{ + public UsbUnderrunException() + { + super("USB underrun (not enough bandwidth)"); + } +} diff --git a/java/com/cowlark/fluxengine/usb/VoltageMeasurements.java b/java/com/cowlark/fluxengine/usb/VoltageMeasurements.java index 2f40ee890..2d7251745 100644 --- a/java/com/cowlark/fluxengine/usb/VoltageMeasurements.java +++ b/java/com/cowlark/fluxengine/usb/VoltageMeasurements.java @@ -1,9 +1,12 @@ package com.cowlark.fluxengine.usb; +import lombok.Builder; + /** * A set of FDD bus voltage readings, ported from struct voltages_frame in * protocol.h. */ +@Builder(setterPrefix = "set") public class VoltageMeasurements { public Voltages inputBothOff; diff --git a/java/com/cowlark/fluxengine/usb/Voltages.java b/java/com/cowlark/fluxengine/usb/Voltages.java index 2fffb9d52..8c7b79bc3 100644 --- a/java/com/cowlark/fluxengine/usb/Voltages.java +++ b/java/com/cowlark/fluxengine/usb/Voltages.java @@ -1,8 +1,11 @@ package com.cowlark.fluxengine.usb; +import lombok.Builder; + /** * Voltage readings, ported from struct voltages in protocol.h. */ +@Builder(setterPrefix = "set") public record Voltages(int logic0Mv, int logic1Mv) { } diff --git a/java/com/cowlark/fluxengine/usb/usb.proto b/java/com/cowlark/fluxengine/usb/usb.proto index 81c1a3851..056b12903 100644 --- a/java/com/cowlark/fluxengine/usb/usb.proto +++ b/java/com/cowlark/fluxengine/usb/usb.proto @@ -26,10 +26,16 @@ message ApplesauceProto { [(help) = "Enable verbose protocol logging", default = false]; } +message FluxEngineProto { + optional int32 underrun_retries = 1 + [(help) = "Number of retries if a USB bandwidth underrun occurs", default = 10]; +} + message UsbProto { optional string serial = 1 [(help) = "serial number of FluxEngine or Greaseweazle device to use"]; optional GreaseweazleProto greaseweazle = 2 [(help) = "Greaseweazle-specific options"]; optional ApplesauceProto applesauce = 3 [(help) = "Applesauce-specific options"]; + optional FluxEngineProto fluxengine = 4 [(help) = "FluxEngine-specific options"]; } diff --git a/java/com/cowlark/fluxengine/wiring/BUILD.bazel b/java/com/cowlark/fluxengine/wiring/BUILD.bazel index 2b0545e28..c4a5283e1 100644 --- a/java/com/cowlark/fluxengine/wiring/BUILD.bazel +++ b/java/com/cowlark/fluxengine/wiring/BUILD.bazel @@ -12,4 +12,11 @@ java_plugin( java_library( name = "wiring", srcs = glob(["*.java"]), + plugins = ["//java/com/cowlark/fluxengine/wiring:lombok_plugin"], + deps = [ + "@maven//:com_google_guava_guava", + "@maven//:org_indunet_fastproto", + "@maven//:org_indunet_fastproto_core", + "@maven//:org_projectlombok_lombok", + ], ) diff --git a/java/com/cowlark/fluxengine/wiring/FluxEngine.java b/java/com/cowlark/fluxengine/wiring/FluxEngine.java index da8e83922..bf371e270 100644 --- a/java/com/cowlark/fluxengine/wiring/FluxEngine.java +++ b/java/com/cowlark/fluxengine/wiring/FluxEngine.java @@ -1,7 +1,22 @@ package com.cowlark.fluxengine.wiring; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.indunet.fastproto.annotation.BinaryType; +import org.indunet.fastproto.annotation.Expect; +import org.indunet.fastproto.annotation.StructArrayType; +import org.indunet.fastproto.annotation.UInt16Type; +import org.indunet.fastproto.annotation.UInt32Type; +import org.indunet.fastproto.annotation.UInt8Type; +import java.util.List; + /** - * Wire protocol definitions for the FluxEngine hardware. + * Wire protocol definitions for the FluxEngine hardware, ported from protocol.h. + * + *

Each inner class corresponds to a struct in protocol.h and is annotated for + * use with FastProto. See RolandFilesystem.java for an example of FastProto usage. */ public final class FluxEngine { @@ -84,103 +99,165 @@ private FluxEngine() { } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class FrameHeader { - public int type; - public int size; + @UInt8Type(offset = 0) public int type; + @UInt8Type(offset = 1) public int size; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class AnyFrame { - public FrameHeader f; + @UInt8Type(offset = 0) public int type; + @UInt8Type(offset = 1) public int size; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class ErrorFrame { - public FrameHeader f; - public int error; + @Expect(offset = 0, bytes = {(byte) F_FRAME_ERROR, 3}) transient int _expects; + @UInt8Type(offset = 2) public int error; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class DebugFrame { - public FrameHeader f; - public byte[] payload = new byte[60]; + @Expect(offset = 0, bytes = {(byte) F_FRAME_DEBUG, 62}) transient int _expects; + @BinaryType(offset = 2, length = 60) public byte[] payload; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class VersionFrame { - public FrameHeader f; - public int version; + @Expect(offset = 0, bytes = {(byte) F_FRAME_GET_VERSION_CMD, 2}) transient int _expects; + } + + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor + public static class VersionReplyFrame + { + @Expect(offset = 0, bytes = {(byte) F_FRAME_GET_VERSION_REPLY, 3}) transient int _expects; + @UInt8Type(offset = 2) public int version; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class SeekFrame { - public FrameHeader f; - public int track; + @Expect(offset = 0, bytes = {(byte) F_FRAME_SEEK_CMD, 3}) transient int _expects; + @UInt8Type(offset = 2) public int track; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class MeasureSpeedFrame { - public FrameHeader f; - public int hardSectorCount; + @Expect(offset = 0, bytes = {(byte) F_FRAME_MEASURE_SPEED_CMD, 3}) transient int _expects; + @UInt8Type(offset = 2) public int hardSectorCount; } - public static class SpeedFrame + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor + public static class MeasureSpeedReplyFrame { - public FrameHeader f; - public int periodMs; + @Expect(offset = 0, bytes = {(byte) F_FRAME_MEASURE_SPEED_REPLY, 4}) transient int _expects; + @UInt16Type(offset = 2) public int periodMs; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class ReadFrame { - public FrameHeader f; - public int side; - public int synced; - public int milliseconds; - public int hardsecThresholdMs; + @Expect(offset = 0, bytes = {(byte) F_FRAME_READ_CMD, 7}) transient int _expects; + @UInt8Type(offset = 2) public int side; + @UInt8Type(offset = 3) public int synced; + @UInt16Type(offset = 4) public int milliseconds; + @UInt8Type(offset = 6) public int hardsecThresholdMs; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class WriteFrame { - public FrameHeader f; - public int side; - public long bytesToWrite; - public int hardsecThresholdMs; + @Expect(offset = 0, bytes = {(byte) F_FRAME_WRITE_CMD, 12}) transient int _expects; + @UInt8Type(offset = 2) public int side; + @UInt32Type(offset = 4) public long bytesToWrite; + @UInt8Type(offset = 8) public int hardsecThresholdMs; + @UInt8Type(offset = 9) public int _pad1; + @UInt8Type(offset = 10) public int _pad2; + @UInt8Type(offset = 11) public int _pad3; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class EraseFrame { - public FrameHeader f; - public int side; - public int hardsecThresholdMs; + @Expect(offset = 0, bytes = {(byte) F_FRAME_ERASE_CMD, 4}) transient int _expects; + @UInt8Type(offset = 2) public int side; + @UInt8Type(offset = 3) public int hardsecThresholdMs; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class SetDriveFrame { - public FrameHeader f; - public int drive; - public int highDensity; - public int indexMode; + @Expect(offset = 0, bytes = {(byte) F_FRAME_SET_DRIVE_CMD, 5}) transient int _expects; + @UInt8Type(offset = 2) public int drive; + @UInt8Type(offset = 3) public int highDensity; + @UInt8Type(offset = 4) public int indexMode; } + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor public static class Voltages { - public int logic0Mv; - public int logic1Mv; + @UInt16Type(offset = 0) public int logic0Mv; + @UInt16Type(offset = 2) public int logic1Mv; } - public static class VoltagesFrame + @Data + @Builder(setterPrefix = "set") + @NoArgsConstructor + @AllArgsConstructor + public static class VoltagesReplyFrame { - public FrameHeader f; - public Voltages outputBothOff = new Voltages(); - public Voltages outputDrive0Selected = new Voltages(); - public Voltages outputDrive1Selected = new Voltages(); - public Voltages outputDrive0Running = new Voltages(); - public Voltages outputDrive1Running = new Voltages(); - public Voltages inputBothOff = new Voltages(); - public Voltages inputDrive0Selected = new Voltages(); - public Voltages inputDrive1Selected = new Voltages(); - public Voltages inputDrive0Running = new Voltages(); - public Voltages inputDrive1Running = new Voltages(); + @Expect(offset = 0, bytes = {(byte) F_FRAME_MEASURE_VOLTAGES_REPLY, 42}) transient int + _expects; + @StructArrayType(offset = 2, length = 10, element = Voltages.class) + public List voltages; } } diff --git a/java/javax.usb.properties b/java/javax.usb.properties deleted file mode 100644 index fd8c2f41f..000000000 --- a/java/javax.usb.properties +++ /dev/null @@ -1 +0,0 @@ -javax.usb.services=org.usb4java.javax.Services diff --git a/scripts/jpackage.bzl b/scripts/jpackage.bzl index 03eb9c5c1..ccea96216 100644 --- a/scripts/jpackage.bzl +++ b/scripts/jpackage.bzl @@ -57,7 +57,14 @@ def _jpackage_impl(ctx): }[package_type] jar = ctx.file.jar - out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + "." + extension) + # When stamp is true, the output name cannot contain the stamped version + # (which is only known at execution time via the stable-status file). Use a + # generic name in that case; the package *inside* will still carry the + # stamped --app-version. CI uses wildcards (ccpp.yml) so this is fine. + if ctx.attr.stamp: + out = ctx.actions.declare_file(ctx.attr.package_name + "." + extension) + else: + out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + "." + extension) launchers = _launcher_properties(ctx) # jpackage writes a lot of scratch state (a jlink runtime image and an app @@ -75,10 +82,16 @@ def _jpackage_impl(ctx): # Windows). The GUI `fluxengine-gui` is an --add-launcher whose desktop # file's Name is defined in fluxengine-gui.properties ("FluxEngine") and # supplied via --resource-dir to avoid post-processing. + # + # Stamping: when stamp=True and --stamp is set, read STABLE_VERSION from + # the stable-status file (produced by scripts/workspace_status.sh via + # `bash scripts/workspace_status.sh`, which works on Linux/macOS and on + # Windows via Git bash). Fall back to app_version (1.0.0) otherwise. + stamp_inputs = [ctx.info_file] if ctx.attr.stamp else [] ctx.actions.run_shell( outputs = [out], inputs = [jar] + [f for (_, f) in launchers] +\ - ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []), + ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []) + stamp_inputs, tools = [java_runtime.files], use_default_shell_env = True, command = """ @@ -86,11 +99,11 @@ def _jpackage_impl(ctx): mkdir -p workdir/input workdir/tmp workdir/dest workdir/home workdir/rpmbuild workdir/resources cp -L "{jar}" workdir/input/ chmod u+w workdir/input/* - printf '[Desktop Entry]\nName=FluxEngine\nComment=FluxEngine\nExec=APPLICATION_LAUNCHER\nIcon=APPLICATION_ICON\nTerminal=false\nType=Application\nCategories=DEPLOY_BUNDLE_CATEGORY\n' > workdir/resources/fluxengine-gui.desktop + printf '[Desktop Entry]\\nName=FluxEngine\\nComment=FluxEngine\\nExec=APPLICATION_LAUNCHER\\nIcon=APPLICATION_ICON\\nTerminal=false\\nType=Application\\nCategories=DEPLOY_BUNDLE_CATEGORY\\n' > workdir/resources/fluxengine-gui.desktop if [ "{package_type}" = "rpm" ]; then WORKTMP="$(pwd)/workdir/tmp" RPMPREFIX="$(pwd)/workdir/rpmbuild" - printf '%%_tmppath %s\n%%_builddir %s/BUILD\n%%_buildrootdir %s/BUILDROOT\n%%_sourcedir %s/SOURCES\n%%_specdir %s/SPECS\n%%_srcrpmdir %s/SRPMS\n%%_rpmdir %s/RPMS\n' "$WORKTMP" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" > workdir/home/.rpmmacros + printf '%%_tmppath %s\\n%%_builddir %s/BUILD\\n%%_buildrootdir %s/BUILDROOT\\n%%_sourcedir %s/SOURCES\\n%%_specdir %s/SPECS\\n%%_srcrpmdir %s/SRPMS\\n%%_rpmdir %s/RPMS\\n' "$WORKTMP" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" "$RPMPREFIX" > workdir/home/.rpmmacros fi TMPDIR="$(pwd)/workdir/tmp" HOME="$(pwd)/workdir/home" @@ -99,10 +112,18 @@ def _jpackage_impl(ctx): if [ "{package_type}" = "msi" ]; then WIN_CONSOLE="--win-console"; fi LINUX_PACKAGE_NAME="" if [ "{package_type}" = "deb" ] || [ "{package_type}" = "rpm" ]; then LINUX_PACKAGE_NAME="--linux-package-name {package_name}"; fi + # Determine app version: STABLE_VERSION from Bazel stamp, else fallback. + APP_VERSION="{app_version}" + if [ "{stamp}" = "True" ] && [ -f "{info_file}" ]; then + STABLE_VERSION=$(grep STABLE_VERSION "{info_file}" | cut -d' ' -f2) + if [ -n "$STABLE_VERSION" ]; then + APP_VERSION="$STABLE_VERSION" + fi + fi "{jpackage}" -J-Djava.io.tmpdir="$(pwd)/workdir/tmp" --type {package_type} \ --name "{package_name}" \ $LINUX_PACKAGE_NAME \ - --app-version "{app_version}" \ + --app-version "$APP_VERSION" \ --input "$(pwd)/workdir/input" \ --main-jar "{main_jar}" \ --main-class "{main_class}" \ @@ -119,6 +140,8 @@ def _jpackage_impl(ctx): extension = extension, package_name = ctx.attr.package_name, app_version = ctx.attr.app_version, + stamp = str(ctx.attr.stamp), + info_file = ctx.info_file.path if ctx.attr.stamp else "", jar = jar.path, main_jar = jar.basename, main_class = ctx.attr.main_class, @@ -138,7 +161,10 @@ def _jpackage_app_image_impl(ctx): jpackage_path = java_runtime.java_home + "/bin/jpackage" jar = ctx.file.jar - out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + ".tar.xz") + if ctx.attr.stamp: + out = ctx.actions.declare_file(ctx.attr.package_name + ".tar.xz") + else: + out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + ".tar.xz") launchers = _launcher_properties(ctx) # jpackage --type app-image writes a directory (with a jlink runtime image @@ -150,10 +176,11 @@ def _jpackage_app_image_impl(ctx): # The main launcher is the CLI `fluxengine` (--win-console) and the GUI # `fluxengine-gui` is an --add-launcher. The GUI desktop file's Name is # supplied via --resource-dir (fluxengine-gui.desktop with Name=FluxEngine). + stamp_inputs = [ctx.info_file] if ctx.attr.stamp else [] ctx.actions.run_shell( outputs = [out], inputs = [jar] + [f for (_, f) in launchers] +\ - ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []), + ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []) + stamp_inputs, tools = [java_runtime.files], use_default_shell_env = True, command = """ @@ -165,9 +192,16 @@ def _jpackage_app_image_impl(ctx): TMPDIR="$(pwd)/workdir/tmp" HOME="$(pwd)/workdir/home" export TMPDIR HOME + APP_VERSION="{app_version}" + if [ "{stamp}" = "True" ] && [ -f "{info_file}" ]; then + STABLE_VERSION=$(grep STABLE_VERSION "{info_file}" | cut -d' ' -f2) + if [ -n "$STABLE_VERSION" ]; then + APP_VERSION="$STABLE_VERSION" + fi + fi "{jpackage}" -J-Djava.io.tmpdir="$(pwd)/workdir/tmp" --type app-image \ --name "{package_name}" \ - --app-version "{app_version}" \ + --app-version "$APP_VERSION" \ --input "$(pwd)/workdir/input" \ --main-jar "{main_jar}" \ --main-class "{main_class}" \ @@ -185,6 +219,8 @@ def _jpackage_app_image_impl(ctx): jpackage = jpackage_path, package_name = ctx.attr.package_name, app_version = ctx.attr.app_version, + stamp = str(ctx.attr.stamp), + info_file = ctx.info_file.path if ctx.attr.stamp else "", jar = jar.path, main_jar = jar.basename, main_class = ctx.attr.main_class, @@ -223,7 +259,11 @@ _jpackage_attrs = { ), "app_version": attr.string( mandatory = True, - doc = "Application version, e.g. '1.0.0'.", + doc = "Application version, e.g. '1.0.0'. Used as fallback when stamp is off.", + ), + "stamp": attr.bool( + default = False, + doc = "If true, use STABLE_VERSION from workspace status (scripts/workspace_status.sh) as app-version when --stamp is set. Falls back to app_version otherwise.", ), "package_type": attr.string( mandatory = True, diff --git a/scripts/version.bzl b/scripts/version.bzl new file mode 100644 index 000000000..a8d5e3659 --- /dev/null +++ b/scripts/version.bzl @@ -0,0 +1,38 @@ +"""Generates version.properties containing fluxengine.version from Bazel stamp.""" + +def _version_properties_impl(ctx): + out = ctx.actions.declare_file("version.properties") + inputs = [] + if ctx.attr.stamp: + inputs.append(ctx.info_file) + ctx.actions.run_shell( + outputs = [out], + inputs = inputs, + command = """ + VER="1.0.0-dev" + if [ "{stamp}" = "True" ] && [ -f "{info_file}" ]; then + STABLE=$(grep STABLE_VERSION "{info_file}" | cut -d' ' -f2 || true) + if [ -n "$STABLE" ]; then + VER="$STABLE" + fi + fi + echo "fluxengine.version=$VER" > "{out}" + """.format( + stamp = str(ctx.attr.stamp), + info_file = ctx.info_file.path if ctx.attr.stamp else "", + out = out.path, + ), + mnemonic = "VersionProperties", + progress_message = "Generating version.properties %{label}", + ) + return [DefaultInfo(files = depset([out]))] + +version_properties = rule( + implementation = _version_properties_impl, + attrs = { + "stamp": attr.bool( + default = True, + doc = "If true, read STABLE_VERSION from stable-status (requires --stamp).", + ), + }, +) diff --git a/scripts/workspace_status.sh b/scripts/workspace_status.sh new file mode 100755 index 000000000..195d8ab14 --- /dev/null +++ b/scripts/workspace_status.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Workspace status for Bazel stamping — invoked via +# bazel build --stamp --workspace_status_command="bash scripts/workspace_status.sh" +# Uses bash from Git on Windows (C:\Program Files\Git\bin\bash.exe) as well as +# Linux/macOS. Produces STABLE_VERSION for jpackage and -Dfluxengine.version. +# +# Format YYYY.MMDD.HHMM (per-minute, UTC) e.g. 2026.0919.1430 +# Each dotted component <65535 so valid for Windows MSI ProductVersion, +# Debian upstream_version and RPM Version. +echo "STABLE_VERSION $(date -u +%Y.%m%d.%H%M)" +echo "BUILD_TIMESTAMP $(date -u +%Y-%m-%dT%H:%M:%SZ)"