Native macOS Swift library for controlling the Ambiglow LEDs on the Philips Evnia 49M2C8900.
This repository also documents the USB protocol that was reverse-engineered to make that work.
Confirmed working on macOS for:
- monitor:
PHL49M2C8900 - LED controller: ENE KB7730
- USB IDs: VID
0x0CF2, PID0xB201
Confirmed working features:
- take software ownership of the LEDs
- push a full 44-LED RGB frame
- release ownership back to the monitor OSD
The current Swift API is intentionally small:
let controller = try EvniaLEDController()
try controller.setCaptureEnabled(true)
try controller.setColors(frame) // LEDFrame owns exactly 44 slots
try controller.setCaptureEnabled(false)Sources/Evnia/EvniaLEDController.swift: high-level APISources/Evnia/LEDFrame.swift:RGBColorand fixed-sizeLEDFrameSources/Evnia/USBDeviceInterface.swift: low-level USB control-transfer wrapper usingIOKitandIOKit.usbSources/EvniaRainbowDemo/main.swift: demo executable
The working path uses vendor-specific USB control transfers on endpoint 0.
Read:
bmRequestType = 0xC0bRequest = 0x81wValue = 0x0000wIndex = register address
Write:
bmRequestType = 0x40bRequest = 0x80wValue = 0x0000wIndex = register address
This is the only confirmed useful namespace for this monitor on macOS.
The current Swift library uses the write path directly. The read form is documented here because it was confirmed during reverse-engineering and is part of the same protocol family.
Example:
- write register
0xE100->0x40 / 0x80 / 0x0000 / 0xE100 - read register
0xE020->0xC0 / 0x81 / 0x0000 / 0xE020
The practical working LED path is in the app-layer 0xE*** register family.
To take ownership from the monitor and allow USB-driven frames, write this 16-byte control block to both 0xE020 and 0xE030:
01 00 02 04 00 05 00 00 00 02 FF 00 00 00 00 01
Interpretation of the important bytes:
- byte
0= enabled / active app control mode - byte
1= software mode0(Normal) - byte
2= speed2(Low) - byte
3= brightness4(Bright) - byte
5= device selection5(AllZone)
In practice, the capture sequence is:
- write the block to
0xE020..0xE02F - write the same block to
0xE030..0xE03F
The LED RGB frame is a flat per-slot table at 0xE100.
For this monitor:
- LED count =
44 - bytes per LED =
3(R G B) - frame size =
132bytes - frame range =
0xE100..0xE183
Frame layout:
slot 0 -> E100 E101 E102
slot 1 -> E103 E104 E105
slot 2 -> E106 E107 E108
...
slot 43 -> E181 E182 E183
Each slot is written as raw R G B bytes.
The confirmed release path is not a one-byte toggle. The working release sequence is to restore the baseline 64-byte control region at 0xE020..0xE05F in one sequential write:
00 01 02 00 00 05 00 00 00 02 FF 00 00 00 00 00
00 00 02 00 00 05 00 00 00 02 FF 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 FF 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
That is the sequence behind setCaptureEnabled(false).
The E100 frame is ordered and directly rendered. The current physical mapping for the 49M2C8900 is, as seen from the front:
0..3: right-side vertical LEDs, with0at the bottom and3at the top-right corner3..11: top row on the right side of the center break12..20: top row on the left side of the center break20..23: left-side vertical LEDs, including the top-left corner overlap24..34: upper diffused center column above the monitor mount35..43: lower diffused center column below the mount
To make the entire monitor solid red:
- enable capture by patching
E020andE030 - write 132 bytes of repeating
FF 00 00to0xE100
Conceptually:
E020 <- capture block
E030 <- capture block
E100 <- [FF 00 00] * 44
If you are working on another Philips / Evnia display with an ENE controller, the best starting points are:
- Look for a vendor-class USB device, not just HID.
- Test EP0 control transfers with:
- read:
0xC0 / 0x81 / 0x0000 / addr - write:
0x40 / 0x80 / 0x0000 / addr
- read:
- Search for an app-layer register family in the
0xE***range. - Look for:
- small control blocks around
E020/E030 - a contiguous RGB table like
E100 + index * 3 - a baseline control-region restore path for release back to OSD
- small control blocks around
- Do not assume the HID path is the real lighting path just because it is easy to enumerate.
Also note:
- the monitor OSD does not appear to update this USB-visible register space directly
- OSD likely talks to the controller over a separate internal bus
- release back to OSD may require restoring a full control-state region, not just clearing a frame buffer
import Evnia
let controller = try EvniaLEDController()
var frame = LEDFrame()
for index in 0..<LEDFrame.count {
frame[index] = .red
}
try controller.setCaptureEnabled(true)
try controller.setColors(frame)
// later
try controller.setCaptureEnabled(false)