Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf7f748
feat(transport): add native BLE and USB configuration library
lgcyaxi Sep 14, 2026
ce3d3b5
feat(speech): add offline SenseVoice capture and recognition
lgcyaxi Sep 14, 2026
4e18380
feat(cloud): add Doubao streaming recognition and credential storage
lgcyaxi Sep 14, 2026
75856cc
feat(profiles): add four-key bindings and persisted input preferences
lgcyaxi Sep 14, 2026
0a67e3c
feat(voice): add hold-to-talk sessions and monitor-aware captions
lgcyaxi Sep 14, 2026
3509906
feat(hooks): add local coding-event listener for keyboard lighting
lgcyaxi Sep 14, 2026
f4394df
feat(devices): add transport recovery and per-slot host management
lgcyaxi Sep 14, 2026
33d2d2e
feat(display): add artwork previews and extensible quota cards
lgcyaxi Sep 14, 2026
f35372a
feat(ui): assemble React settings and live device panels
lgcyaxi Sep 14, 2026
1648b2a
feat(runtime): wire desktop commands and system tray lifecycle
lgcyaxi Sep 14, 2026
5e6ccb1
build(client): add packaging checks, Windows CI and usage guides
lgcyaxi Sep 14, 2026
f694786
fix(ble): confirm config writes with firmware ACK and status readback
lgcyaxi Sep 17, 2026
3434f92
fix(cloud): scope doubao credential namespace by application identity
lgcyaxi Sep 17, 2026
99ae4af
fix(voice): release key latch when sessions end without a key end-edge
lgcyaxi Sep 17, 2026
22bb463
style(ble): apply rustfmt to ACK confirmation code
lgcyaxi Sep 17, 2026
3b09c2a
fix(voice): preserve key latch during recording startup
ZephyrKeXiner Sep 20, 2026
06904de
fix(hooks): migrate the known Java dispatcher with a backup
ZephyrKeXiner Sep 20, 2026
3755455
fix(ble): gate lighting writes and verify config readback
ZephyrKeXiner Sep 20, 2026
1c10f96
chore(client): document compatibility fixes and pass macOS lint
ZephyrKeXiner Sep 20, 2026
c35fca1
refactor(client): isolate Tauri app under ahakey-studio-tauri
ZephyrKeXiner Sep 20, 2026
8c3a2ad
fix(hooks): normalize legacy fixture line endings on Windows
ZephyrKeXiner Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/ahakey-studio-tauri.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: AhaKey Studio Tauri

on:
pull_request:
paths:
- "ahakey-studio-tauri/**"
- ".github/workflows/ahakey-studio-tauri.yml"
push:
paths:
- "ahakey-studio-tauri/**"
- ".github/workflows/ahakey-studio-tauri.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: rust-client-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
windows:
name: Windows Rust client · tests and build
runs-on: windows-latest
defaults:
run:
working-directory: ahakey-studio-tauri
shell: pwsh
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.12.3
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
cache-dependency-path: ahakey-studio-tauri/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Prepare pinned native speech runtime (no model download)
run: |
$runtime = & ./crates/speech/scripts/prepare-runtime.ps1
"SHERPA_ONNX_LIB_DIR=$runtime" | Out-File $env:GITHUB_ENV -Append -Encoding utf8
$runtime | Out-File $env:GITHUB_PATH -Append -Encoding utf8
- name: Frontend tests and production build
run: |
pnpm install --frozen-lockfile
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
pnpm test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
pnpm build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Rust formatting, tests and lint
run: |
cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cargo test --locked --manifest-path src-tauri/Cargo.toml -p ahakey-desktop -p ahakey-ble
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cargo clippy --locked --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Compile Windows client
run: cargo build --locked --release --manifest-path src-tauri/Cargo.toml --features custom-protocol
14 changes: 14 additions & 0 deletions ahakey-studio-tauri/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
node_modules/
dist/
src-tauri/target/
src-tauri/gen/
.cache/
*.log
*.local
.playwright-cli/
output/
src-tauri/icons/*
!src-tauri/icons/icon.svg
!src-tauri/icons/icon.ico
!src-tauri/icons/icon.png
!src-tauri/icons/icon.icns
74 changes: 74 additions & 0 deletions ahakey-studio-tauri/FIRMWARE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Community firmware for AhaKey X1

## Identity and compatibility

- Hardware: **AhaKey X1, CH582M**. Do not flash a generic WCH development board,
a different AhaKey model or an unidentified hardware revision.
- Firmware: **0.1.17**, unofficial, community, experimental.
- Companion client: AhaKey Studio **1.1.5**. The two version numbers are independent.
- Download: [fork Release](https://github.com/lgcyaxi/AhakeyAI/releases/tag/ahakey-studio-1.1.5).
- File: `AhaKey-X1-DualBLE-0.1.17.hex`.
- SHA-256: `771efe5a1d16bd8530c3cd0dd64931c400a757f6bc18ff1175418f9530dd67e8`.

The HEX contains application code, not chip configuration or DataFlash records.
The client neither downloads nor flashes it automatically. Building the Rust
client does not need any firmware source or binary.

## Changes and limits

USB and two BLE hosts may remain connected; physical input goes only to the
selected destination. In host-switch mode, attaching USB replaces the upper
lever's BLE destination; the lower BLE destination stays fixed. Removing USB
restores the upper BLE slot. Approval-lever mode remains optional.

Only an eligible selected, unpaired BLE slot is discoverable for new pairing.
Bonded peers have a separate reconnect path. USB attachment is not a command to
disconnect an existing BLE link. The client shows separate A/B status, labels
and recent faults. Client pairing resets require USB; a physical long press
resets the selected BLE slot, while selecting USB does not clear either bond.

Known-peer encryption-handshake timeouts enter bounded recovery; invalid
identity, key-size and explicit security failures remain blocked. Recovery is
not a guarantee that an OS will reconnect without user intervention.

On battery, about 60 seconds of suitable inactivity turns off the screen and
lights while BLE and input services continue. A key or lever wakes the display
without discarding the first input. USB power keeps presentation awake.
MCU deep sleep is disabled; battery current and total runtime are not measured.

The maintainer reports stable current dual-host use and satisfactory standby.
This is user-reported testing, not universal interoperability certification.
Image/card preview exists in the client, but uploading custom images or quota
cards to the device is not implemented in this release.

## Flashing and recovery

Experimental flashing can make input unavailable, invalidate pairing, or require
manual recovery. Keep another keyboard available and follow the vendor's
[official firmware instructions](https://github.com/AhakeyAI/firmware).
Use the exact X1 target and verified HEX, not a WCH demo image.

The tested update procedure preserves the existing chip configuration and
DataFlash: do not enable RST-as-reset, serial keyless download, or clear DataFlash
as part of this update. Do not improvise pin shorts or change protection bits.
Use the vendor's procedure to enter ISP, download, verify, and restart.

Keep the [official X1 v1.1.0 recovery Release](https://github.com/AhakeyAI/firmware/releases/tag/AhaKey-X1-v1.1.0)
available before flashing. Its `HID_Keyboard_582m_vibe_coding.hex` SHA-256 is
`09f4b60751c0bfcb374e5c62f6be5a2b4fa180f6d7dc623b8e8ca46d1d34205a`.
Rollback may require pairing again; it does not promise to restore every setting.

## Permission and attribution

Hardware and board implementation: [AhaKey](https://github.com/AhakeyAI).
[Written permission, 2026-09-13](https://github.com/AhakeyAI/desktop/issues/63#issuecomment-5653215712)
allows this project's modified compiled unofficial HEX in the maintainer's fork
Releases for personal, noncommercial research. It does **not** authorize publishing
controlled source, schematics, or modifications derived from controlled source.
Commercial use or disclosure outside that scope needs additional permission.
This notice does not relicense third-party components.

The Release includes `AhaKey-X1-0.1.17-NOTICES.txt` and
`AhaKey-X1-0.1.17-Apache-2.0.txt` with WCH, MultiButton and LwRB attribution.
Preserve these notices with the binary. This is not an official AhaKey firmware,
and no warranty of fitness or recoverability is provided.
144 changes: 144 additions & 0 deletions ahakey-studio-tauri/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# AhaKey Studio: Rust client

Rust + Tauri 2 + React client, version 1.1.5. The client lives in the independent
`ahakey-studio-tauri/` subdirectory at the repository root; it does not replace
the Java, Swift or bridge source trees.
Windows x64 is the tested distribution target. macOS/Linux adapters exist but
their native packaging, permissions and hardware behavior still need validation.
This is a community client for hardware by [AhaKey](https://github.com/AhakeyAI),
not an official replacement for the original desktop suite.

## Companion firmware

The [1.1.5 community Release](https://github.com/lgcyaxi/AhakeyAI/releases/tag/ahakey-studio-1.1.5)
also offers **unofficial experimental firmware 0.1.17 for AhaKey X1 (CH582M)**.
It adds simultaneous USB/two-BLE links, single-destination lever routing,
bonded reconnect recovery and battery-only screen/light standby. The maintainer
reports stable dual-host use; interoperability with every host is not guaranteed.
Firmware and client versions are independent. No firmware is flashed by this app.

Flashing may lose input or pairing, or require recovery. Read the
[firmware guide](https://github.com/lgcyaxi/AhakeyAI/blob/main/ahakey-desktop/FIRMWARE.md)
and keep the official recovery HEX before proceeding. Binary distribution in
the fork Release is permitted for personal, noncommercial research under
[AhaKey's written permission](https://github.com/AhakeyAI/desktop/issues/63#issuecomment-5653215712).
Controlled firmware source and schematics are not part of this project.

## Features

- Four editable profiles for Claude Code, Claude Desktop, Codex CLI and ChatGPT
App, with four physical key assignments and firmware-dependent lighting.
- Windows USB auto-detection and read-only battery/profile/brightness information,
independent of Bluetooth connection management. Information comes from one
confirmed transport at a time; disconnect clears stale USB values.
- Native Bluetooth using WinRT/CoreBluetooth/BlueZ. Connect requires a valid
device response; Windows commands request an encrypted link. Complete system
pairing before using protected firmware services.
- Capability-gated USB/BLE routing configuration for compatible firmware:
fixed upper/lower BLE slots, with USB replacing the upper target while attached,
or preserve the approval lever. Slot labels and connection diagnostics are
separate from pairing; pairing resets in the client require USB.
Unsupported firmware does not receive fabricated successful-save feedback.
Concurrent links require compatible device firmware; they are not implemented
by the client alone.
- Voice-key listening defaults on without recording. Explicitly disabling it
is remembered; corrupt settings do not trigger automatic listener startup.
Press/hold and toggle modes are supported.
- WeChat and Windows dictation use their external shortcuts. They do not expose
reliable recording-state readback; end an out-of-sync input-method popup
manually before resuming. The client does not invent timing-based state.
- Local SenseVoice Small INT8 via sherpa-onnx. Runtime is bundled on Windows;
weights download/import is opt-in. Local audio is not uploaded.
- Optional Doubao streaming recognition with user-provided API credentials.
Windows local/cloud captions follow the target window's monitor/work area.
- Image crop/fit/RGB565 preview and export, plus extensible quota cards for
MiniMax, GLM, Kimi, Codex and custom HTTPS data. Provider credentials remain on
the host. On-device image/card upload is not implemented in this release.
- Tray provider/profile/caption actions; an optional loopback Hook-event
receiver. Hooks never auto-approve requests or edit external harness settings.

Full profile and lighting writes first read the connected device's brightness
capability. Legacy firmware that leaves this field at zero is rejected before
configuration writes, even if it would acknowledge unsupported commands as
successful. Key-only writes remain available. Mode and brightness changes also
require matching status readback on the same connection.

When enabling Hooks after using the Java client, an exact match of its generated
`~/.ahakey/hooks/ahakey-hook.ps1` is backed up as `ahakey-hook.legacy.ps1` and
replaced with the dispatcher that reads `active-endpoint.json`. Modified or
unrecognized scripts and conflicting backups are preserved; enabling Hooks
reports an error with the path to resolve instead of claiming success.

USB input needs no BLE pairing or companion app, but the keyboard must select
USB as its input target. Voice recognition still needs the receiving computer's
listener/input method. Full profile/light writes currently use BLE; USB supports
information and routing controls. Firmware status 1.0 may be a compatibility
field, not its actual release version.

## Build and test on Windows

Install Node.js 22+, pnpm 10, stable Rust, MSVC Build Tools, Windows SDK and
WebView2. From this subproject:

```powershell
pnpm install --frozen-lockfile
$env:SHERPA_ONNX_LIB_DIR = & .\crates\speech\scripts\prepare-runtime.ps1
pnpm test
pnpm build
cargo test --locked --manifest-path src-tauri/Cargo.toml
cargo test --locked --manifest-path crates/ble/Cargo.toml
cargo clippy --locked --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
.\scripts\build-windows.ps1
```

Run the executable inside the complete output directory; all four native ASR
DLLs and license notices must remain beside it. The build script never installs,
signs, publishes, or stops another app. No firmware source or HEX is needed to
build this client. Firmware-specific features remain capability-gated.

Windows distribution may name the launcher AhaKeyStudio.exe. Do not run it
alongside the Java client or another preview's voice listener.
The production application identity is ai.ahakey.studio. On first launch only,
validated settings from the known previous preview identities are copied if no
production settings exist. Preview files and explicit listener-off choices are
preserved. Credentials are not copied between identities; reconfigure those
explicitly. Import existing models explicitly rather than overwriting preview data.

## Verification boundary

### macOS app and menu-bar icons

Use an application bundle for the Dock/Finder icon, not the bare Cargo executable.
On your Mac, first prepare the matching native speech libraries as described in
the [speech guide](crates/speech/README.md), setting `SHERPA_ONNX_LIB_DIR`.
Then run from this directory:

```sh
pnpm install --frozen-lockfile
pnpm desktop:bundle
open 'src-tauri/target/release/bundle/macos/AhaKey Studio.app'
```

The macOS config embeds the existing ICNS app icon and usage descriptions.
The menu bar has a separate monochrome template icon, with contrast supplied
by macOS in light/dark mode. Failure to create the tray now reports a startup
error instead of silently skipping it. `desktop:build` remains an explicitly
unbundled developer build, not a macOS installer.

This local-build path is not a claim of portable distribution: signed native
dylib embedding, notarization and clean-machine macOS acceptance remain pending.
Do not publish or copy the app to another machine assuming those steps are done.

### Acceptance limits

Automated checks cover protocol frames, USB and BLE source selection, settings,
caption placement, provider parsers and key-edge state. A separate known-answer
ASR test accepts explicit local fixtures and never opens a microphone or
downloads weights. USB information has been exercised on hardware on Windows;
three-host concurrency, all host sleep/wake cases and non-Windows acceptance
are not implied by unit tests or a successful build.

See the [BLE](crates/ble/README.md), [speech](crates/speech/README.md) and
[cloud](crates/cloud/README.md) module guides. Mutating IPC is restricted to the
main window; the renderer allows local assets and IPC only. Sensitive credentials
do not belong in configuration exports, source control or device payloads.
2 changes: 2 additions & 0 deletions ahakey-studio-tauri/crates/ble/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target/
/Cargo.lock
18 changes: 18 additions & 0 deletions ahakey-studio-tauri/crates/ble/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "ahakey-ble"
version = "0.1.0"
edition = "2021"
rust-version = "1.85"

[dependencies]
btleplug = "0.13.0"
futures = "0.3"
serde = { version = "1", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "macros"] }
tokio-util = "0.7"
uuid = "1"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Devices_Enumeration", "Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_HumanInterfaceDevice", "Foundation", "Foundation_Collections", "Storage", "Storage_Streams"] }
windows-future = "0.3.2"
42 changes: 42 additions & 0 deletions ahakey-studio-tauri/crates/ble/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Native BLE transport

`ahakey-ble` uses btleplug's native WinRT, CoreBluetooth and BlueZ implementations.
It requires no separate bridge, TCP listener or driver application. The Windows
crate has been compiled and unit-tested; macOS/Linux and physical-device acceptance
must be tested separately. macOS applications require a Bluetooth usage description
and permission; Linux requires a running BlueZ service and D-Bus access.

Create one `BleClient::new().await` handle per application, subscribe to its
broadcast events, then expose explicit scan, connect, disconnect and configuration
actions in the UI. `scan(Duration)` only discovers advertisements. Save the chosen
`DeviceInfo.id` in the application's local settings and use `reconnect(id)` on the
next launch; IDs are OS-local and cannot be transferred between platforms. If no
adapter is available, show the initialization error and offer retry.

`Ready` requires all three firmware characteristics (7341 data, 7343 commands,
7344 notifications), a successful subscription and a valid 13-byte status response.
The service checks connectivity every three seconds and requests fresh status about
every fifteen seconds; no valid response for 45 seconds clears stale telemetry and
reports an error. A dropped link never becomes a fake zero-percent battery.
Reconnect is bounded to three attempts. No adapter reset, pairing removal or
unbounded background reconnection is performed.

All writes use acknowledged GATT requests. Config batches and periodic status
queries share a write gate. `save_profiles` validates all four profiles before
writing the 39-frame batch; each has four raw-HID key mappings and nine AI-state
light effects, indexed by firmware state 0..8. Typical voice usages are F17=0x6C
and F18=0x6D, Enter=0x28, Escape=0x29 and Backspace=0x2A. Modifier usages E0..E7
precede the base key usage. Descriptions are printable ASCII, capped at 20 bytes.
Save completion proves acknowledged writes, not persistence after power cycling.
Brightness, mode, light-effect and IDE-state methods are nonpersistent until a
save command is included in a profile batch.

Await `disconnect()` during application shutdown. It cancels pending connection
and write operations, joins the notification worker, unsubscribes and detaches with
timeouts. A monotonic generation blocks notifications and retries from obsolete
sessions. Dropping the handle cancels its worker as a fallback, but does not replace
the explicit asynchronous shutdown path.

Run `cargo test` and `cargo clippy --all-targets -- -D warnings` from this crate.
Tests exercise byte-for-byte protocol vectors, strict status parsing, generation
isolation and cancellation without scanning or writing a real device.
Loading
Loading