A clean-room Bluetooth LE implementation of the AC Infinity UIS protocol — read sensors and write settings directly to the controller, with no vendor cloud account and no network dependency.
Zero runtime dependencies. 165 tests. Developed against a Controller 69 Pro (firmware version ≥ 6, multi-port) and a UIS power strip.
The official app talks to the controller over BLE and mirrors everything through AC Infinity's cloud. If you want local automation — Home Assistant, a Raspberry Pi, your own scheduler — you need the wire protocol, and it isn't published. This repository is that protocol, written down with its provenance, plus a tested implementation of it.
It was extracted from a running grow-tent telemetry system, where it has driven live hardware continuously since June 2026.
| Area | State |
|---|---|
| GATT service / characteristic UUIDs | Confirmed |
| Frame envelope + CRC16 | Confirmed bit-identical to the app across 20,000 random vectors and every captured reference frame |
| Sensor read (self-stream V6, A5 TLV queries) | Confirmed, per device type |
Port mode activation (0x10) |
Confirmed live, byte-exact |
AUTO thresholds (0x13) |
Encoder confirmed for firmware ≥ 6 (10-byte block); the write capture is still open |
VPD (0x51) |
Confirmed all versions |
Brightness (0x21) |
Confirmed |
Temperature unit (0x20) |
Confirmed — 1 = °C, 0 = °F |
Calibration (0x24) |
Partial. Bytes 1 and 2 map directly to the UI steppers; byte 0 is unresolved |
Known-unresolved items are listed as unresolved. See
docs/protocol/wire-protocol.md for each open question in
full, including one capture attempt that failed and why.
Worth knowing before you write anything. Controller settings are always sent as one bundled write, never per-field. A lone single-field write — changing only the temperature unit, say — sends zero bytes over BLE and reports no error. It is silently a no-op.
Any encoder for a settings field must read-modify-write the whole bundle. This is the class of bug a unit test against your own encoder will never catch, because your encoder is correct and the device simply ignores it.
git clone https://github.com/Nyx000/acinfinity-ble
cd acinfinity-ble
npm install # dev dependencies only — there are no runtime dependencies
npm testBLE transport goes through a small Python sidecar using bleak,
which is the only supported transport. src/acinfinity/ble-sidecar.py is the sidecar;
point BLE_SIDECAR_PYTHON and BLE_SIDECAR_SCRIPT at your interpreter and that file.
CONTROLLER_SOURCE=ble \
BLE_CONTROLLER_MAC=XX:XX:XX:XX:XX:XX \
BLE_POWERSTRIP_MAC=XX:XX:XX:XX:XX:XX \
BLE_SIDECAR_PYTHON=/usr/bin/python3 \
BLE_SIDECAR_SCRIPT=./src/acinfinity/ble-sidecar.py \
npm run ble:checkCONTROLLER_SOURCE defaults to mock, so nothing touches the radio unless you ask it to.
src/acinfinity/ protocol core — framing, CRC, encoders, decoders, transport
src/lib/ small pure helpers (hex, MAC, units, VPD, setpoints)
src/audit/ mode-ID → human name mapping
docs/protocol/ the wire protocol and how each claim was established
test/ 165 tests
.claude/skills/ the live-capture methodology, as a runnable procedure
Static analysis alone was not treated as sufficient, and that turned out to matter.
The protocol was first recovered by decompiling the vendor's Android app. It was then re-validated three independent ways: in-process hooks on the live app capturing real GATT writes, a second independent decompile of a freshly pulled APK, and a host-controller (HCI) snoop. That pass overturned two findings this project had already documented and believed — in both cases the shipping code was right and the written protocol was wrong.
Every claim in docs/protocol/ therefore carries its evidence, and disagreements between
sources are recorded rather than smoothed over. Where a question is open, it says so.
This is interoperability work. The protocol facts here — byte layouts, selectors, checksums, GATT UUIDs — were determined by analysing a program lawfully obtained, for the sole purpose of making independently written software talk to hardware the operator owns.
- No vendor code is reproduced or redistributed. No APK, no decompiler output, no
decompiled source. Citations of the form
jm1.java:1348are pointers that let a reader re-derive a finding themselves; they are not the vendor's code. - No vendor assets are included — no UI strings, no artwork, no branding.
- Protocol facts are not copyrightable subject matter.
- Not affiliated with, endorsed by, or supported by AC Infinity. "AC Infinity" and "UIS" are the marks of their owner, used here only to say what hardware this speaks to.
Writing to hardware carries real risk. See the warranty disclaimer in LICENSE, and
prefer verifying any new write against a read-back.
Captures from other UIS devices are the most useful thing anyone can add — particularly
firmware < 6 single-port controllers, which this implementation handles from the decompile
rather than from live evidence. The capture procedure is in
.claude/skills/ble-app-capture/SKILL.md.
If you are adding a protocol claim, cite how you established it. A finding with no provenance is the thing this repository is organised to avoid.
MIT — see LICENSE.