This is the story of how I turned a box of motorised roller shutters — the kind that came with the house, controlled by a proprietary 433 MHz remote nobody else supports — into proper first-class citizens in a Matter smart home. It took signal analysis with a logic analyser, a failed chip experiment, a successful chip swap, a RAM crisis that forced a board upgrade, and a protocol migration from HomeKit to Matter. Here is how it went.
The Problem: A-OK, But Not Actually OK
The house came fitted with A-OK AM25 tubular motors driving half a dozen interior roller shutters. The remote is an AC123-06 — a six-channel 433 MHz RF clicker. Works perfectly. The problem is that “433 MHz” tells you nothing useful about the protocol on the wire. A-OK uses a proprietary OOK (On-Off Keying) encoding that no off-the-shelf smart home bridge understands. There is no Zigbee module, no Z-Wave variant, no Tuya cloud version. The only integration path is the original remote.
Off-the-shelf RF bridges — Broadlink, Sonoff RF Bridge, and friends — are designed for simple fixed-code devices. They record a blast of RF and replay it. That works for some remotes but the A-OK system uses per-channel identifiers and the motor’s timing tolerance is tight enough that poorly-timed replays get silently ignored. I tried. It was unreliable enough to be useless.
The decision to reverse-engineer the protocol and build my own transmitter was less about stubbornness and more about the maths: a custom hub costs about $12 in parts, takes a weekend to get working, and lasts as long as the hardware does. No subscription. No cloud dependency. No waiting for someone else to add support. That was the why.
Phase 1 — Reading the Signal
The first step was getting a ground-truth capture of what the remote actually transmits. I connected a 433 MHz OOK receiver module to a Saleae logic analyser and pressed every button, many times. The captures showed a clean digital OOK signal and with enough repetitions the pattern became obvious: pulse-width modulation. Each bit is represented by a HIGH pulse, and the width tells you whether it is a 0 or a 1.
A short pulse — roughly 300 µs — means bit 0. A longer pulse — roughly 580 µs — means bit 1. Each frame is preceded by a synchronisation pulse of around 5 ms. A complete button press sends 8 to 10 identical frame repeats in rapid succession, which is how the motor knows it was not a random noise hit.

Each frame is 8 bytes long, transmitted MSB-first. The layout, once decoded, is elegant in its simplicity:
- Bytes 0–3: Remote ID — a 32-bit value unique to each physical remote. Mine is
0xA3758110. - Bytes 4–5: Channel — a bit-shifted word. CH1 =
0x2000, CH2 =0x1000, CH3 =0x0800, and so on down. - Byte 6: Command —
0x0B= UP,0x43= DOWN,0x23= STOP,0x24= RELEASE. - Byte 7: Checksum —
sum(bytes 1 through 6) & 0xFF.

Once the structure was clear I wrote an AC123Decoder library: a stateless, ISR-friendly decoder that captures edge timings, identifies sync pulses, scores each candidate frame on timing quality (0–100), and selects the winner by highest repeat count with checksum validation. This library became the foundation of every version of the hub that followed.

Phase 2 — The Si4463 Detour (and Why I Abandoned It)
For the transmitter I initially reached for an NICERF RF4463PRO module — a Si4463-based 433 MHz front-end with impressive specs on paper: −126 dBm receive sensitivity, +20 dBm TX power, highly configurable modulation. Silicon Labs provides a Wireless Development Suite (WDS) GUI tool that exports a register configuration blob.
In practice the experience was genuinely miserable. The WDS-generated config headers are opaque — hundreds of radio register writes that are poorly documented in the context of raw OOK timing. Getting the chip into true asynchronous OOK mode (no packet framing, no CRC, raw bit-bang data on a GPIO) requires fighting undocumented register interactions. I got the chip transmitting something, but the timing was never stable enough to trigger the motors reliably. After about a week of register archaeology I shelved it.

The lesson here was clear in hindsight: for raw OOK bit-banging at sub-millisecond timing, you want a chip that exposes simple “put this GPIO state out and the carrier follows” hardware. The Si4463 is designed for packet-oriented FSK/GFSK applications and fights you every step of the way if that is not what you need.
Phase 3 — CC1101: The Right Tool for the Job
The CC1101 from Texas Instruments has an asynchronous serial mode (PKTCTRL0.PKT_FORMAT = 3) that literally exposes raw OOK data on a dedicated GPIO pin — GDO0 for TX, GDO2 for RX. No packet framing. No CRC engine in the path. No buffer to manage. Put a 1 on GDO0 and the carrier switches on; put a 0 and it goes off. That is exactly the abstraction I needed.
The wiring is straightforward — SPI for configuration, two data GPIOs for the async serial path:

RX capture works via an edge-triggered ISR on GDO2: each rising and falling edge is timestamped in microseconds, and the AC123Decoder processes the resulting pulse buffer after the burst ends. TX works by bit-banging GDO0 with precise esp_rom_delay_us() calls — 5 ms HIGH for the sync pulse, then alternating 300 µs or 580 µs HIGHs with LOW separators between each bit, repeated 8 times.
The SPI Bus Problem Nobody Documents
During the Arduino-framework build phase, the global SPI bus (FSPI / SPI2) was left in a partially-initialised state at startup by arduino-esp32 3.x. Attaching the CC1101 there caused spi_device_polling_transmit() to spin with interrupts disabled long enough to fire the watchdog (~908 ms). That discovery drove the move to SPI3 for all CC1101 communication. In the final ESP-IDF build this is a non-issue — SPI is initialised directly with spi_bus_initialize() and spi_device_add_to_bus() on SPI3 from the start, giving clean, predictable behaviour every time.
Phase 4 — The RAM Wall: ESP32 to ESP32-S3
Version 3 of the firmware used HomeSpan for HomeKit integration and ran fine on a standard ESP32-WROOM. Then I decided to migrate to Matter — arduino-esp32 3.x bundles the full Matter stack natively, and Matter means Apple Home, Google Home, and Alexa all at once. The build succeeded. The flash succeeded. The device refused to boot, overflowing DRAM by about 4 KB.
The standard ESP32-WROOM has 320 KB of internal SRAM. The Matter stack alone consumes roughly 200 KB, leaving very little headroom for the AC123 decoder, the web server, NVS persistence, and the FreeRTOS task stacks. The solution was straightforward but annoying: swap to the ESP32-S3-DevKitC-1 N8R8 variant (8 MB flash, 8 MB Octal PSRAM). With the S3 the build uses about 39% of internal SRAM and runs without complaint. The PSRAM is left uninitialised — the decoder works fine without it.
There was one additional S3-specific trap: GPIO19 on the DevKit is USB D-minus. Assigning it to SPI MISO caused hangs during chip initialisation with no useful error message. Moving MISO to GPIO13 fixed it. That cost an afternoon.
Phase 5 — Matter, Dual-Core, and the Final Build
The final build targets ESP-IDF directly using the esp32s3_source PlatformIO environment. The pioarduino community fork provides the platform toolchain that makes ESP-IDF 5.4.x available inside PlatformIO — the framework itself is pure ESP-IDF, not Arduino. The architecture looks like this:

The dual-core split is the key design decision. The Matter stack runs on Core 0 in its own FreeRTOS task. The RF work — ISR handling, pulse decode, TX bit-banging — runs entirely on Core 1 in a dedicated FreeRTOS task (idf_rf_manager). There is zero contention between timing-sensitive RF operations and the WiFi/Matter stack. When a shutter command arrives via Matter, a FreeRTOS queue passes it from Core 0 to Core 1, which handles the actual transmission.
The web dashboard is a single-page application embedded as a compressed PROGMEM blob — no filesystem, no SPIFFS partition, nothing to corrupt on power loss. It weighs about 51 KB compressed. The full REST API handles shutter management, remote learning, and OTA firmware updates. All routes require a session token, and the first login forces a password change from the default.
One feature worth calling out: quick-learn. Press a single button on any AC123 remote and the hub captures the Remote ID and Channel from that one packet. It then mathematically infers all three codes — UP, DOWN, and STOP — using the known command byte values and the checksum formula. One button press, all three commands learned, ready to use immediately.
Matter commissioning uses the on-network (WiFi) path rather than BLE. BLE commissioning was disabled entirely (CONFIG_BT_ENABLED=n) — early ESP32-S3 silicon has a BT controller crash in the prebuilt BLEManagerImpl that is painful to work around, and BLE is unnecessary if the hub is already on WiFi. The Home app pairs over TCP via mDNS discovery, which is simpler and more stable anyway.
The Full Journey in One Diagram

What I Would Do Differently
Skip the Si4463. For anyone doing raw OOK work at sub-millisecond timing, the CC1101’s async serial mode is the right abstraction. The Si4463 is an excellent chip for packet-oriented FSK/GFSK work, but its OOK async mode is underdocumented and fighting it cost me a week I will not get back.
Start on the S3. If you know you are eventually running Matter on ESP32, buy the S3 from the beginning. The RAM headroom costs almost nothing extra and saves the pain of a mid-project board swap with its GPIO remapping exercise.
Use SPI3 for external SPI devices. The FSPI partial-init issue that appeared during the Arduino-framework build phase is not CC1101-specific — any peripheral on that bus can hit it. More broadly: in ESP-IDF, initialise the CC1101 SPI bus explicitly with spi_bus_initialize(SPI3_HOST, ...) and you get a clean, deterministic startup with no hidden state from the framework.
Trust your behavioural observations over captures. Several times during this project, signal captures from a receiver board looked subtly wrong compared to what I was observing behaviourally — remotes triggering motors, hub not triggering them. Receiver hardware introduces its own timing distortion. The motor responding is always ground truth. Do not let ambiguous captures talk you out of timing values that are demonstrably working.
The Result
Six shutters, all controllable from Apple Home, Google Home, and Alexa. A web dashboard for direct control and configuration without a smart home ecosystem. OTA firmware updates over WiFi. The whole thing fits on a $10 ESP32-S3 DevKit and a $2 CC1101 module, mounted in a 3D-printed enclosure tucked in a corner. No subscription. No cloud. No dependencies on anyone else staying in business.
More than the outcome though, this project was a reminder of why reverse engineering is satisfying work. You start with a black box — radio pulses in the air that only one remote in the world officially understands — and you end up with a complete specification you wrote yourself from first principles. The moment the first self-constructed packet triggered a motor was genuinely excellent.
The full source — AC123Decoder library, ESP-IDF main, and web SPA — will be open-sourced once a few rough edges are tidied. If you are working on A-OK or similar OOK-based shutter systems and want to compare notes, feel free to reach out.
Hardware: ESP32-S3-DevKitC-1 N8R8, CC1101 SPI module, A-OK AC123-06 6-channel remote, A-OK AM25 tubular motor. Tools: PlatformIO (pioarduino community fork), Saleae logic analyser, ESP-IDF 5.4.x.

Leave a Reply