8 min read
Reverse Engineering My Studio Lights in an Afternoon
I have two Neewer studio lights for video calls. I don't keep them on all day: they're bright and I like working in the dark. But I need them on the moment a call starts, and off the moment it ends.
Turning them on and off is the whole problem.
The Neewer Control Center app is hot garbage. Half the time it doesn't detect my lights. The mobile app is was slightly less garbage, until recently when they added cloud sync: now I get a loading spinner every time I open it, for something that's completely local. The cloud sync doesn't even integrate with Google Home or Alexa. It's just there, making everything slower.
So lately I'd been standing up, reaching behind my desk, and flipping the physical switch. Then flipping it off after the call. Not a lot of work. But annoying.
What I wanted was simple: a system tray icon that toggles my lights with one click. No app window. No waiting. Just click → lights on. Click → lights off.
Neewer doesn't support this. There's no CLI, no API, no hotkey. The lights use a proprietary 2.4GHz protocol through a USB dongle, and Neewer keeps the whole thing locked inside their Windows application.
So I asked Claude to help me figure it out. I had Opus 4.5 loaded up in Cursor and was looking for a real project to try it on. This seemed like a good one: reverse engineering is tedious, requires constant context-switching between tools, and benefits from someone who can write the boilerplate while I focus on the actual problem.
A single afternoon later, I had a 50KB standalone tray app with zero dependencies on Neewer's software. Here's how.
Finding the DLL
My first message was vague on purpose:
Can you help me reverse engineer how an app on my PC works? It's called Neewer Control Center.exe and it controls my lights using a 2.4GHz dongle plugged into my PC via USB. There's a button in the UI that lets me turn on/off all my devices and I want to figure out how to do that programmatically. Maybe we use wireshark or a debugger? Can you attach to the running process?
Claude suggested we start by looking at the installation folder. Poking around, we found some interesting DLLs:
C:\Neewer Control Center\Neewer\
├── Set_Dongle_RF_API_x64.dll ← This looks promising
├── BLE_USB_LIB_API_x64.dll
└── ...
Running dumpbin /exports on the RF DLL revealed just two functions:
Send_RF_DATA
Get_USB_State
That's it. The entire app is basically a fancy wrapper around these two calls.
Watching the app with Frida
But what bytes do we actually send to Send_RF_DATA? We had no idea what the protocol looked like.
I'd used Frida before to reverse engineer an Android app, so I suggested we try it here. Frida lets you intercept function calls at runtime. The plan: attach to the running Neewer app, hook Send_RF_DATA, click buttons in the UI, watch what flows through.
# Hook Send_RF_DATA in the Neewer app
Interceptor.attach(sendRfData, {
onEnter: function(args) {
const ptr = args[0];
const len = args[1].toInt32();
const data = ptr.readByteArray(len);
console.log('Data (hex): ' + hexdump(data));
}
});
I opened Neewer Control Center, ran the hook, and clicked the on/off button. The packet fell out almost immediately: 77 58 01 85 01 56. Six bytes. That's the on/off toggle command.
Calling the DLL directly
Now that we knew the magic bytes, we could call the DLL ourselves from Python:
import ctypes
dll = ctypes.CDLL(r"C:\Neewer Control Center\Neewer\Set_Dongle_RF_API_x64.dll")
# Check if dongle is connected
state = dll.Get_USB_State() # Returns 1 if connected
# Send the on/off command we captured from Frida
ON_OFF_PACKET = bytes([0x77, 0x58, 0x01, 0x85, 0x01, 0x56]) + bytes(26)
buffer = (ctypes.c_ubyte * len(ON_OFF_PACKET))(*ON_OFF_PACKET)
dll.Send_RF_DATA(buffer, len(ON_OFF_PACKET))
It worked!
The lights toggled. We had a working solution. But it still depended on Neewer's DLL, which meant I still had to install their app.
Making it a tray app
I wanted one click. That meant a system tray icon:
Instead of python can you write this using C++, Zig, or Rust? Then add a taskbar icon I can click that'll toggle the lights on/off?
I pasted in my own C screenshot tool as a reference for the tray icon code. Claude picked Zig, a language I'd never touched, and produced a working tray app on the first try.
Then came the icon saga. The first icon had a black background instead of transparent. Then it was completely white. Then too low resolution. Then too fuzzy. Then the lines were too thick.
Nvm that's too thick. How did the screenshot app render its icon? Didn't it use a built in glyph?
Windows has a built-in icon font called Segoe MDL2 Assets. Claude switched to rendering a lightbulb glyph from that font directly, and suddenly:
That's genuinely perfect
Killing the DLL dependency
The app worked, but I still had Neewer's DLL sitting next to my binary. I wanted to uninstall Neewer Control Center entirely and have a single executable that talks directly to the dongle.
We'd used Frida to capture what the app sends to the DLL. Now we needed to see what the DLL sends to the hardware. Same trick, one layer deeper: hook the Windows API, trigger our working Python script, watch what bytes flow through.
This part took some back and forth. Claude kept trying to hook the Neewer app and wait for me to click buttons, even though we already had a Python script that could trigger the lights programmatically. I pushed back a few times before Claude set up a proper Frida project in TypeScript with Bun that hooked CreateFileW and WriteFile:
const createFileW = kernel32.findExportByName("CreateFileW");
Interceptor.attach(createFileW, {
onEnter(args) {
this.path = args[0].readUtf16String();
},
onLeave(retval) {
console.log(`[CreateFileW] ${this.path} -> handle ${retval}`);
},
});
const writeFile = kernel32.findExportByName("WriteFile");
Interceptor.attach(writeFile, {
onEnter(args) {
const size = args[2].toInt32();
const data = args[1].readByteArray(size);
console.log(hexdump(data));
},
});
We ran the Python script, and Frida caught everything:
[CreateFileW] \\?\hid#vid_0581&pid_011d#... -> handle 0x1234
[WriteFile] 64 bytes:
0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 ba 70 24 00 00 00 00 77 58 01 85 01 56 00 00 00
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
There it was: the actual bytes going to the dongle, and a device path that told us the USB identifiers (VID 0581, PID 011d).
The protocol
The packet decomposes cleanly:
| Offset | Content | What it is |
|---|---|---|
| 0-6 | ba 70 24 00 00 00 00 | Header (added by DLL) |
| 7-12 | 77 58 01 85 01 56 | The on/off toggle command |
| 13-63 | zeros | Padding to 64 bytes |
The 6-byte command was the exact packet we'd been handing to the DLL. The DLL was wrapping it in a 7-byte header and padding out to 64 bytes.
So: find the device by VID/PID, open it with CreateFileW, write the 64-byte packet with WriteFile. No DLL required.
The Zig version, dependency-free
Claude updated the Zig app to talk directly to the HID device:
const NEEWER_VID: u16 = 0x0581;
const NEEWER_PID: u16 = 0x011D;
const PACKET_HEADER = [_]u8{ 0xba, 0x70, 0x24, 0x00, 0x00, 0x00, 0x00 };
const ON_OFF_CMD = [_]u8{ 0x77, 0x58, 0x01, 0x85, 0x01, 0x56 };
fn toggleLights() bool {
const handle = findAndOpenDevice(NEEWER_VID, NEEWER_PID);
var packet: [64]u8 = std.mem.zeroes([64]u8);
@memcpy(packet[0..7], &PACKET_HEADER);
@memcpy(packet[7..13], &ON_OFF_CMD);
return WriteFile(handle, &packet, 64, ...) != 0;
}
Yes it worked!
~50KB. No DLL. No Neewer app. A tray icon that toggles my lights.
Timeline
2-3 hours, start to finish:
| Time | What happened |
|---|---|
| ~12:30 PM | Found the DLL, set up Frida to hook the Neewer app |
| ~1:00 PM | Captured the on/off packet, Python script working with DLL |
| ~1:30 PM | Zig tray app working (with DLL dependency) |
| ~2:00 PM | Hooked the DLL's Windows API calls with Frida |
| ~2:30 PM | Zig app working without DLL |
| ~3:00 PM | Cleaned up, committed, set up GitHub Actions |
For context on my background: I mostly work in TypeScript. I can read and write Python but I hate it as a language. I took Java in school and wrote some Minecraft mods and RuneScape private server stuff. Took C/C++ in college. I've used Frida once before to reverse engineer an Android app with Magisk. I've poked around in IDA Pro and Ghidra but never gotten very far with either. So I'm familiar with this stuff, not experienced. I'd definitely never written Zig before that afternoon.
The whole project was pair-programmed with Claude (Opus 4.5) in Cursor. I described what I wanted, Claude suggested approaches, I ran the code and reported what happened, Claude adjusted. When I got frustrated with inline Python or JavaScript, I pushed back and Claude adapted.
What made this work
The bottleneck wasn't writing code. It was understanding the problem well enough to ask the right questions. Claude handled the code. I handled the "what are we actually trying to do here" and "that didn't work, what's different about my setup."
A few things that helped:
- Frida for watching what the app actually does at the Windows API level
- Claude + Cursor for writing code in languages I don't know
- Zig for a tiny, dependency-free executable
- Stubbornness about not wanting to depend on Neewer's DLL
Proprietary software isn't magic. Data has to flow from A to B. If you can observe that flow, you can understand it, and reproduce it.
The code is at github.com/AugusDogus/neweer-tray. If you have Neewer lights with a 2.4GHz dongle, it might just work for you too.