Electron + React + BLE app for sit-stand desk control. Supports IKEA Idasen, LINAK DPG, Jiecang/Uplift, and any custom desk via user-configured BLE profiles.
- Node.js 18+
- Windows 10+ (BLE via WinRT — no extra deps)
- Linux: BlueZ + D-Bus (
sudo apt install bluez) - macOS: CoreBluetooth (built-in, no deps)
npm install # installs deps + runs electron-rebuild for native modules
npm run dev # start in dev mode with hot reloadnpm run dist # produces installer in /distnpm test # runs Vitest unit tests (db, BLE encoding, voice parser)The app auto-detects supported desks by matching the advertised BLE device name:
| Desk family | Match names | Height encoding |
|---|---|---|
| IKEA Idasen | idasen |
LINAK |
| LINAK DPG | linak, dpg, desk |
LINAK |
| Jiecang / Uplift | uplift, jiecang, autonomous, progressa, standing |
Jiecang |
If auto-detection fails, open Settings → Custom Desk Profile and enter your desk's BLE service UUID, characteristic UUIDs, height range, and encoding type. The app will prefer your custom profile over all built-in ones.
If BLE is unavailable or noble isn't compiled, the app falls back to a mock desk automatically — all UI features work without real hardware.
Voice control is off by default. Enable it in Settings → Voice Commands.
You can give the app a custom wake word (default: desk). Speak the wake word followed by a command:
| Say | Action |
|---|---|
desk stand / desk stand up |
Move to stand preset |
desk sit / desk sit down |
Move to sit preset |
desk position 1 |
Move to preset #1 |
desk preset 2 |
Move to preset #2 |
desk up / desk raise |
Move up continuously |
desk down / desk lower |
Move down continuously |
desk stop / desk halt |
Stop movement |
Only the commands listed above are recognised. The voice engine uses word-boundary matching — partial word matches (e.g. "outstanding" containing "stand") are ignored.
src/
shared/
deskTypes.ts CustomDeskProfile type + DEFAULT_CUSTOM_PROFILE (shared main ↔ renderer)
main/
index.ts Electron main process, window creation
tray.ts System tray icon + context menu
driver.ts DeskDriver interface — contract for any transport (BLE, ESP32, …)
ble.ts BLE implementation of DeskDriver; built-in + custom profiles
db.ts JSON file persistence — height history, presets, sessions
store.ts electron-store — settings (theme, reminders, voice, custom desk profile)
ipc.ts IPC handlers (main ↔ renderer); swap driver here to change transport
__tests__/
db.test.ts Unit tests — height logging, presets, session tracking
ble.test.ts Unit tests — BLE profile encoding (LINAK, Jiecang, linear)
preload/
index.ts contextBridge — exposes window.deskAPI
renderer/src/
App.tsx Full UI (dashboard, history chart, settings, voice UI)
voiceParser.ts Pure voice command parser — no DOM dependencies
stores/
deskStore.ts Zustand store — all renderer state + IPC calls
__tests__/
voiceParser.test.ts Unit tests — all voice command patterns
The BLE driver and any future transport both implement the DeskDriver interface in src/main/driver.ts. To switch:
-
Create a new class (e.g.
RestDriver) implementingDeskDriver. -
In
src/main/ipc.ts, change the one line:const driver: DeskDriver = new RestDriver('http://esp32.local')
No renderer or IPC code changes are needed.