---
title: "Reverse Engineering My AULA F75 Keyboard on Linux (Ongoing)"
description: "Reverse engineering a budget mechanical keyboard to get full RGB and macro support on Linux."
date: 2026-03-30
tags:
  - linux
  - reverse-engineering
---

## Why I'm Doing This

I run Fedora 43 with Hyprland. My keyboard is an AULA F75. The software to configure it, RGB effects, speed, macros, is Windows-only. There is no Linux support, no open protocol spec, nothing.

The keyboard has a "Ripples_shining" effect I like. It lights up from a keypress and spreads outward like a wave. The problem is the speed slider only goes from 0 to 4, and even at maximum it feels sluggish. I want faster. I want to push values the UI doesn't allow.

This post is not a success story. I haven't cracked it yet. But I've learned a lot about how this keyboard talks to the OS, and I'm documenting everything here so I don't lose the thread, and in case someone else is doing the same thing.

---

## The Setup

- Fedora 43, Hyprland, kernel 6.x
- AULA F75 (VendorID: `258a`, ProductID: `010c`)
- Wine 11.0 Staging
- Connected via USB-C (wired mode only, wireless is a separate problem)

---

## Step 1: Getting the Software Running Under Wine

The AULA software needs raw HID access via `/dev/hidraw`. By default, the `hid-generic` kernel driver claims the device and blocks this. The fix is a udev rule.

Find your keyboard's IDs:

```bash
lsusb
# Bus 001 Device 003: ID 258a:010c BY Tech Gaming Keyboard
```

Create the rule:

```bash
sudo nano /etc/udev/rules.d/99-aula-f75.rules
```

```
SUBSYSTEM=="usb", ATTRS{idVendor}=="258a", ATTRS{idProduct}=="010c", MODE="0666"
KERNEL=="hidraw*", ATTRS{idVendor}=="258a", ATTRS{idProduct}=="010c", MODE="0666"
```

Reload:

```bash
sudo udevadm control --reload-rules && sudo udevadm trigger
```

Verify:

```bash
ls -la /dev/hidraw*
# should show crw-rw-rw-
```

The F75 exposes two hidraw nodes. Confirm which belong to the keyboard:

```bash
for f in /dev/hidraw*; do
  echo -n "$f: "
  udevadm info --attribute-walk --name=$f | grep -E "idVendor|idProduct" | head -2
done
```

Mine: `/dev/hidraw1` and `/dev/hidraw2` both show `258a:010c`.

Install Wine, reset the prefix if needed, and run:

```bash
rm -rf ~/.wine  # if you get kernel32.dll errors
WINEARCH=win64 wineboot --init
wine ~/Downloads/AULA_F75_Setup.exe
wine ~/.wine/drive_c/Program\ Files\ \(x86\)/AULA/F75/OemDrv.exe
```

It opens. The keyboard is detected. Effects apply. Wine's hidraw integration works fine for this, this part is solved.

---

## Step 2: Capturing the Packets

Plan: use `usbmon` and `tshark` to capture USB traffic while moving the speed slider.

```bash
sudo modprobe usbmon
sudo tshark -i usbmon1 -w /tmp/aula_capture.pcapng
# move slider in AULA software, Ctrl+C
```

Extract HID payloads:

```bash
tshark -r /tmp/aula_capture.pcapng \
  -Y "usb.device_address == 3 && usb.transfer_type == 0x02 && usb.dst == \"1.3.0\"" \
  -V 2>/dev/null | grep "Data Fragment"
```

This worked once. I got real `SET_REPORT` control transfers:

```
bRequest: SET_REPORT (0x09)
wValue: 0x0306  (Report ID 6, Feature type)
wLength: 520
```

Three packets per Apply. 520 bytes each, zero-padded. Diffing the packets between min and max speed:

```
# Only one byte differs:
# index 79 (0x4F): min speed = 0x40, max speed = 0x00
```

Speed is a single byte. `0x40` at slider position 0 (slowest), `0x00` at position 4 (fastest). Linear steps of `0x10`. Lower value = faster. The UI is already sending the minimum possible value (`0x00`) at its maximum setting, so there's nothing to unlock by going below, unless the firmware wraps or interprets `0x00` as "default" rather than true zero.

The problem: every subsequent capture returned empty output. Wine's HID backend is not consistent. Sometimes it routes through the USB stack (visible to usbmon), sometimes it bypasses it entirely and writes directly to hidraw. You can't rely on usbmon alone.

---

## Step 3: What the HID Descriptor Says

```bash
sudo usbhid-dump -a 1:3 2>/dev/null
```

Relevant section for Report ID 6:

```
85 06          → Report ID = 6
75 08          → field size = 8 bits
96 07 02       → report count = 519 bytes
B1 02          → Feature report
```

Device expects: Report ID 6, Feature type, 519 bytes payload = 520 bytes total. Matches the control transfer capture.

---

## Step 4: Trying to Send Packets Directly

The correct ioctl for HID Feature Reports on Linux:

```python
# _IOC(READ|WRITE, 'H', 0x06, 520)
HIDIOCSFEATURE = 0xC2084806
```

Script that sends all three captured packets in sequence:

```python
import fcntl, time

HIDIOCSFEATURE = 0xC2084806

PKT1 = bytes.fromhex("0684000001008000...")  # init/handshake
PKT2 = bytes.fromhex("0604000001008000...")  # effect config
PKT3 = bytes.fromhex("060a000001000002...")  # color data

def pad(pkt):
    return bytes(pkt) + b'\x00' * (520 - len(pkt))

def send(path, pkt):
    with open(path, 'rb+', buffering=0) as f:
        fcntl.ioctl(f, HIDIOCSFEATURE, bytearray(pad(pkt)))

for speed in [0x40, 0x20, 0x00, 0x01, 0x05, 0xff]:
    pkt2 = bytearray(PKT2)
    pkt2[79] = speed
    send('/dev/hidraw2', PKT1)
    send('/dev/hidraw2', bytes(pkt2))
    send('/dev/hidraw2', PKT3)
    time.sleep(4)
```

Result: ioctl succeeded, something happened on the keyboard, but it was the wrong effect. The ripple changed to something else entirely and speed was identical across all values.

The packets from capture session 1 were probably not from Ripples_shining, I don't know what effect was active during that capture. And even if they were, something about the packet content or sequence is wrong.

---

## Step 5: strace, Where Things Got Complicated

Since usbmon was unreliable, next approach: strace to intercept the actual syscalls Wine makes when writing to hidraw.

Find which Wine process holds the hidraw fds:

```bash
for pid in $(ps aux | grep -i wine | grep -v grep | awk '{print $2}'); do
  echo "=== PID $pid ==="
  ls -la /proc/$pid/fd 2>/dev/null | grep hidraw
done
```

Found it: the `winedevice` process holds:
- fd 51 → `/dev/hidraw1`
- fd 52 → `/dev/hidraw2`

Attaching strace on write to those fds returned nothing. Tried following threads with `-f`, tried all fd numbers, still nothing useful coming through.

The discovery (from continuing the investigation separately) is that Wine uses `writev()` with internal pipes, not direct `write()` to the hidraw fd. The real 520-byte HID payload appears inside a `writev` iov buffer:

```
writev(...)
  iov_len=520  ← this is the real device packet
```

Other sizes (64, 40, 12 bytes) in the strace output are Wine's internal message framing, not device data. The filter was wrong the whole time.

---

## Step 6: What the Real Packets Look Like

Using `writev` interception, two real 520-byte payloads were extracted, `pkt_0.bin` (low speed) and `pkt_1.bin` (high speed). Diffing them:

```
Only ONE byte changes:
offset 109: 0x47 → 0x07
```

No checksum changes. No header changes. Just that one byte inside what appears to be a repeated pattern block:

```
...09 47 09 47 09 47...
→
...09 07 09 47 09 47...
```

So the speed value isn't a standalone "speed field", it's embedded inside a table or pattern structure. Modifying only offset 109 and sending the packet had no visible effect on the keyboard. The firmware either ignores isolated single-byte changes, or requires multiple entries in the pattern to be updated, or needs a separate commit packet after the config.

---

## Where Things Stand

| Thing | Status |
|---|---|
| Wine runs the software on Fedora | ✅ |
| udev rule for hidraw access | ✅ |
| HID descriptor parsed | ✅ Report ID 6, 520 bytes |
| Speed byte identified (index 79 from usbmon, index 109 from writev) | ⚠️ conflicting |
| Reliable packet capture method | ❌ |
| Sending correct effect-specific packets | ❌ |
| Speed values beyond UI limits tested | ❌ |

The two different byte indices (79 from the usbmon capture, 109 from the writev capture) are suspicious. They might be from different packet types, or the usbmon capture was from a different effect entirely. I need a clean, confirmed capture of Ripples_shining specifically at all five speed positions.

---

## UPDATE (June 2026): Cracked the protocol, and learned why the speed cap can't be beaten

Coming back to this months later, I got a lot further. I'll lead with the punchline because it's not the one I wanted: **the speed cap is real and lives in the keyboard's firmware. You cannot beat it by sending a bigger number.** But I now have full native-Linux control of the keyboard with no Wine at all, and I understand the actual path to a faster ripple. Here's everything.

### The "conflicting offset" mystery was embarrassingly simple

Offsets 79 and 109 were never in conflict. The `pkt_*.bin` files I'd extracted from strace had a **30-byte text prefix** glued to the front — the literal strace filename `/tmp/aula_trace_final2.412637:`. `109 − 30 = 79`. Same byte the whole time. Two captures that agreed perfectly, and I'd spent days suspecting they came from different effects. Always check your extraction boundaries.

A few more things from the old post turned out to be mislabeled:

- The `0684...` packet I'd called the "init/handshake" is actually a **read request** (command byte `0x84`, bit 7 = read).
- The 520-byte packet "with full content" I saw in `writev` was the device's **response** to a read, flowing back through Wine's pipe — not something the app was sending.
- "Modifying one byte did nothing" was because my base packet hex was corrupted/truncated, so every write was replacing the whole config region with garbage. There were even stray bytes left in the live effect table from those bad March writes, still there in June.

### The protocol

It's HID **feature reports, Report ID 6, 520 bytes**, on the vendor interface (the hidraw node whose report descriptor contains `85 06 ... 96 07 02 ... b1 02`). Header:

```
06 CMD A0 A1 A2 A3 L0 L1  <data...>
   CMD: bit 7 = read. 0x04 = write config, 0x84 = read config,
        0x0a = write per-key color table, 0x8a = read it
   A*:  address (00 00 01 00 = main config region)
   L*:  length, little-endian (0x0080 config, 0x0200 color table)
```

The missing piece for reading was the sequence: **`SET_FEATURE` the read request, then `GET_FEATURE` report 6** returns the data. A bare GET returns nothing. Once I had that, I could dump and diff the keyboard's live state at will — no more guessing from captures.

### Decoding the effect table (on real hardware this time)

The config region has a per-effect table of `(a, b)` pairs. By changing one byte at a time and watching the keyboard, I pinned them down:

- `a` = **brightness** (`0x09` full, `0x01` visibly dim)
- `b` = `(speed << 4) | color` — high nibble speed, low nibble color preset

Ripple lives at offsets 78–79. And here's the wall: **the firmware clamps speed at 4.** Writing `0x57` (speed 5) or `0xF7` (speed 15) looks *identical* to `0x47` (speed 4). The UI stops at 4 because 4 is the hardware maximum. There is no magic value below or above the range that unlocks anything.

### Why the cap exists — and it's not going anywhere

I dumped the keyboard's memory with the read commands and hit the answer. Read commands `0x88`, `0x89`, `0x9d`, `0x9e`, `0x9f` return **8051 microcontroller machine code**. This is a SinoWealth-family chip, and the effects — including Ripples_shining — are rendered *inside that firmware*. The speed field is just an index into a fixed animation table baked into the chip. You'd have to reflash custom firmware to change the animation engine itself. The number-tweaking approach was doomed from the start.

### The real path to a faster ripple: render it on the host

This is how the flashier keyboards actually do it. They don't send a bigger speed value — they put the board into a **static / custom per-key mode** where it just displays whatever color each key is told to be, then stream color frames from the PC 30–60 times a second, computing the animation in software. At that point "speed" is just a variable in your code.

And this keyboard supports it. There's a **per-key color table**: `0x0a` to write, `0x8a` to read, 512 bytes = 4 bytes per key (`RR GG BB 00`), 128 keys. I confirmed my writes land in it — read them straight back.

### Where I'm actually stuck now

One wall left, and it's a different wall than before: **writing the color table stores the colors but doesn't display them.** The LEDs only update when the keyboard is in custom/DIY mode *and* something triggers a refresh. The AULA app clearly sends a "commit/latch" command after writing colors that I haven't captured yet. (This is the old "missing commit packet" hypothesis from above — turns out it was right, just for the color path instead of the config path.)

Finding that commit command needs a clean strace of the app, and that fought me hard:

- `ptrace_scope` was `0`, but attaching still failed with **`Operation not permitted` even under sudo**.
- The cause: a **stale `strace` from an earlier debugging attempt** was still attached to the Wine process (`TracerPid` in `/proc/<pid>/status` pointed at it). A process can only have one tracer. Killing the orphan released the lock.

Lesson worth its own line: if `sudo strace` says "Operation not permitted," check `grep TracerPid /proc/<pid>/status` before assuming it's a security policy. It's often just a tracer you forgot to detach.

### What I built

A small native tool, `aula_f75.py`, that needs no Wine at all:

```bash
python3 aula_f75.py dump -o snapshot.bin   # read live config
python3 aula_f75.py speed 0x47             # set ripple speed (4 = max)
python3 aula_f75.py restore snapshot.bin   # roll back
```

I can read and write the full config, set effects, brightness, color, and write the per-key color table. The only thing between me and an arbitrarily-fast host-rendered ripple is that one commit command — which is the very next thing to capture now that strace works.

A note for anyone else: OpenRGB has an *open, unimplemented* support request for this exact device (258a:010c). Nobody's mapped the protocol there yet. If you're reading this and want it in OpenRGB, the notes above are most of the way.

---

## Current Hypotheses

Speed is probably not a simple single-byte field. Leading theories:

1. **Pattern table encoding** — the speed value is repeated across multiple entries in a lookup table. All entries need to change together.
2. **Derived encoding** — the byte value is computed from the speed setting rather than stored directly. Need to understand the formula.
3. **Missing commit packet** — there's a separate "apply" packet that triggers the firmware to use the new config. Without it, the keyboard ignores the data.
4. **Wrong interface** — the config might need to go to hidraw1, not hidraw2, or through a different report ID entirely.

---

## What's Next

The number-tweaking plan is dead (the firmware caps it), so the new plan is the host-rendering one:

1. Capture the AULA app's **commit/latch command** with strace now that ptrace works — write color table, click apply, diff the traffic for the short packet sent after the `0x0a` writes.
2. Reproduce that commit from `aula_f75.py` and confirm the LEDs update live.
3. Wire up an **evdev keypress listener** + a frame loop: on each keypress, expand a ring of lit keys outward, repaint the whole table at ~60fps. Speed becomes a variable — finally faster than the firmware allows.
4. Bonus: decode the remaining config fields and contribute the protocol to OpenRGB's open request for this device.

The udev rule, Wine setup, full config read/write, effect decoding, and per-key color writes are all done and reproducible. The single remaining unknown is the color-table commit command.

Will update this when I get further.

---

## Running the Software (TL;DR)

If you just want to use the AULA software on Fedora with Wine:

```bash
# udev rule (one-time setup)
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="258a", ATTRS{idProduct}=="010c", MODE="0666"
KERNEL=="hidraw*", ATTRS{idVendor}=="258a", ATTRS{idProduct}=="010c", MODE="0666"' \
  | sudo tee /etc/udev/rules.d/99-aula-f75.rules
sudo udevadm control --reload-rules && sudo udevadm trigger

# run the software
wine ~/.wine/drive_c/Program\ Files\ \(x86\)/AULA/F75/OemDrv.exe
```

Tested on Fedora 43, Wine 11.0 Staging, wired USB-C. Wireless mode needs a separate udev rule for the dongle's VID/PID.
