Bloggity Blog

Whole-room notification LED

This whole project started several months ago, when I discovered the most amazing thing: 5m of RGB LEDs can be had for less than 5€ on AliExpress!

The idea was simple: my phone has a notification LED that blinks with different colors to signal that there are unread notifications, but since my phone is usually on my desk in a flip case (and probably under a stack of paper), I don’t usually see it. So how neat would it be, if my entire room mirrored that LED so I never missed a notification again?

If you don’t care about the LEDs and control circuits themselves and just want to know how I achieved notification LED mirroring, skip to this section.

The LEDs

The RGB(W) strip I chose is the RGBWW 5050 model. The WW part stands for Warm White, meaning the actual RGB LEDs alternate with warm white LEDs and the 5050 refers to the RGB LED model, which is the stronger of the two most common ones (the other being 3528).

I opted for RGBW instead of standard RBG as I wanted to be able to flash a notification color while still illuminating my room enough to work. The warm white (as opposed to a more standard, cooler white) is just a personal preference.

The driver circuit

The control server

I was initially planning on using my home NAS server to control the LEDs with just a long USB extension cable running to them, but looking at the pile of ESP8266s on my desk gave me a better idea: use esp-link to expose the driver board’s serial port over the network and control it directly.

As the ESP8266 requires a 3,3V supply, I had to step down some of the existing 5V even further. Luckily, I had some step-down boards handy and just jerry-rigged them onto the main board with some wires.

I loaded the ESP8266 with esp-link, which involved first updating its bootloader and then flashing the esp-link binary onto the chip. The configuration was also rather simple and involved connecting to an open Wi-Fi hotspot, created by the chip and entering the credentials to my home Wi-Fi, after which the ESP switched to station mode and acted just like any other device on my network.

I proceeded to connect the TX, RX and RST pins of my MCU to the ESP according to the official instructions, which gave me access to its serial port from any device on the network, as well as allowed me to remotely flash the MCU’s firmware at any time.

The firmware

The original firmware consisted of some 200 lines of C that were about as fast and fexible as an 80-year-old with osteoporosis. You can see it [here], but I don’t advise you to.

The firmware that I use now is actually not terrible and is available on GitLab [here]. You’re free to use it if you want to – it’s all GPLv2.
Because I can’t be bothered with proper documentation, here’s a short snippet that shows off the functionality (> is the prompt)

> asdf
E Invalid command!
> P ("print" the current state of the LEDs)
> SB150W0 ("set" specified channels to new values)
> FR0G0B0W0 ("fade" specified channels to new values

The firmware allows you to define (at compile time) any number of channels, each with a channel name (a single character) and its corresponding PWM output pin. You can see some examples in the README.

The notification mirroring

Ok, now for the interesting part. The never-before-seen part. The only part that hasn’t been posted all over Instructables 100s of times. How can I monitor the state of my phone’s notification LED and send that information to the light server?

Not all that surprisingly, there isn’t a simple function in the Android SDK that would allow me to just do that. So, I had to get creative and dig deeper into the system itself.

Looking through the sources of the particular ROM I was using (based on LineageOS, although the same applies for stock Android, too), [………..]

Once I know what function I am interested in, I need a way to tap into it and mess with its execution. Fortunately, that is exactly what the XPosed Framework was designed to do. After taking care of all the boilerplate required for XPosed to load my module into the right binary (which can be found here), I was able to tap the […] function like so:

public class Main implements IXposedHookLoadPackage {
    private static final String NMS_CLASS = "";
    @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        if (lpparam.packageName.equals("android") && lpparam.processName.equals("android")) {
            XposedBridge.log("Hooked android");
            XposedHelpers.findAndHookMethod(NMS_CLASS, lpparam.classLoader, "updateLightsLocked", methodHook); 

This allows me to execute my code every time that function is called, just before it starts executing, giving me full control over the parameters it receives. Using a possibly one of the ugliest parts of Java – reflection, I was finally, after hours of staring at Android source code and dumping strange variables to logcat (because setting up an OS-level debugger is simply too much hassle), able to understand what all the variables meant and how they related to the LED’s action and came up with this beautiful mess of spaghetti code:

static XC_MethodHook methodHook = new XC_MethodHook() {
    protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
        // type: NotificationManagerService
        Object nms = param.thisObject;

        Field mNotificationLightField = NotificationManagerService.getDeclaredField("mNotificationLight");

        // type: Light
        Object mNotificationLight = mNotificationLightField.get(nms);

        Class Light = mNotificationLight.getClass()

        Field mColorField = Light.getDeclaredField("mColor");
        int mColor = mColorField.getInt(mNotificationLight);

        Field mFlashingField = Light.getDeclaredField("mFlashing");
        boolean mFlashing = mFlashingField.getBoolean(mNotificationLight);

        XposedBridge.log("Light color: " + mColor);
        XposedBridge.log("Light flashing: " + mFlashing);

Tying it all together

With all the pieces working, it was time to connect everything together. For every light event, after the above code decoded it, the details of the event (ON/OFF, color, blinking speed) would be send to the light server in the form of a special command. The server would forward it to the MCU, where the firmware would read the details and save them into its memory.

With the LED state information now synchronized to the controller, it was simply a matter of writing a fade function to mimic the one used by my phone and the project was done.

The ultimate presentation setup

We’ve all been there. You’re all set for your big presentation: beautiful template, just the right amount of text, fancy graphics and that one perfectly timed star wipe for comedic effect. You’re just about to leave when you remember, that the world isn’t an Apple commercial. You pack the HDMI-VGA adapter and a second one for backup, your brand new absurdly expensive slide clicker, you make sure your presentations looks good on a 4:3 screen too and walk into the hall with the confidence of a Python developer, only to be greeted by a hard-wired Windows XP computer with Office 2007…

I won’t bother you with any more stories. Here’s what I want: I want to be able to walk around with a tablet in my hand, the presenter notes on it and generally run whatever I want on it without having to worry about compatibility ever again. Why? Mostly because it looks futuristic and cool.

Possible easy solutions

Let’s get some of the obvious solutions out of the way first:

  • Office365 PowerPoint broadcast: low quality, requires a good Internet connection on both sides, works only in PowerPoint
  • Windows 10 “Stream to this PC”: only works on Windows 10, requires a (compatible) Wi-Fi card in the target PC
  • Miracast/Chromecast/WhateverCast: rarely available, even if I had my own dongle it would still require messing with the projector


If you want to use this setup, here’s what you’ll need:

  • A rooted Android smartphone (that supports USB tethering)
  • A Windows (7 or above) laptop (a touch 2-in-1 is even better)
  • A USB cable for your phone

The virtual display

The trickiest part of this setup is the virtual display. I don’t want to stream my laptop’s display, because I want to have my presenter view on it. I want Windows to think there’s an actual extra physical monitor connected to it. This is something that is very hard to achieve, but luckily, some Windows driver magicians over at datronicsoft have already done that and packaged into a rather neat application called spacedesk.

After installing the spacedesk driver on the laptop, I can connect to it from any device on the network using one of several available viewers: Windows, Android, iOS and HTML5. I’m interested in the HTML5 one because it is as cross-platform as anything can possibly be.

The viewer

Spacedesk’s HTML5 viewer is normally available on their website, but for several reasons, I want to serve the files to the target computer myself. A quick CTRL+S gets me the page and all of its JS into a folder that I can serve with a simple web server on my phone.

Out-of-the-box, the viewer doesn’t support Mozilla Firefox for “performance reasons”, but in my testing, it works just fine and I still prefer a slow connection to no connection at all, so a simple search-and-replace on the source code gets rid of the browser checks:

sed -i 's/ === "Firefox"/\0 \&\& false/' spacedesk.min.js

Some further modification was done to pre-populate the address field with the (static) address of the phone’s tethering interface.

A simple httpd from Busybox will suffice to serve the files from the phone and present them to the target computer.

The network

The proxy

YouTube-MPRIS2 bridge

Basically every computer platform these days has some form of universal media controls (like MPRIS2 for Linux*). They’re all extremely useful, but what do you do when you’re a cheap bastard from a country without Spotify and you listen to all of your depressing music on YouTube? You bodge together a browser extension and a script to connect it to the system bus, of course! And then you write about it on your blog. That nobody reads. Because you have no friends. Because you spend all your time listening to sad music. Because you’re sad that nobody reads your blog. GOTO 1.

1. That was the actual intro from the draft…

2. About a month later KDE Browser Integration was released, making all of this obsolete

3. This is fine!

Me, in 2019, discovering this unfinished post

Xiaomi Yi camera

General info

  • OS: Buildroot
  • CPU: ARMv6-compatible processor rev 5 (v6l)
  • BogoMIPS: 524.28
  • Memory: 37MB

The root filesystem is rootfs, which is stored in memory and therefore wiped on reboot. The Micro SD card is mounted at /tmp/fuse_d/. Something is also mounted at /tmp/fuse_a and /tmp/fuse_z.


  • SSID: YDXJ_[last 7 digits of SN]
  • Default pass: 1234567890
  • Securtiy: WPA2-Personal
  • Camera IP:

Nmap scan:

Opened ports on 
Discovered open port 80/tcp on
Discovered open port 554/tcp on
Discovered open port 53/tcp on
Discovered open port 8787/tcp on
Discovered open port 7878/tcp on

Shell access

To enable shell access you must create an empty file on the SD card named enable_info_display.script. Then you can connect to the camera using telnet:

telnet 23

You will be presented a login promt:

buildroot login: 

Type root and hit enter. You are now connected to the camera’s root Linux shell

Telnet commands

Technically just a raw TCP socket connection, but it’s easier to just call it telnet.

telnet 7878


Error messages sent by the camera:

  • {"rval": -7} – Input is not a valid JSON object
  • {"rval":-4,"msg_id":0} – Input object is empty
  • {"rval":-9,"msg_id":0} – Input object is not a valid command


All requests require a token you have to request when connecting to the camera:


{"msg_id":257, "token":0}


{ "rval": 0, "msg_id": 257, "param": 1 }

param is your token. All requests in this article have the token set to 1. This will be different for you.



{"msg_id":3, "token":1} 


NOTE: The config is a list of objects of settings, not just an object of settings. You have to use a loop!

	"rval": 0,
	"msg_id": 3,
	"param": [
		{"camera_clock": "2015-04-07 02:32:29"},
		{"video_standard": "NTSC"},
		{"app_status": "idle"},
		{"video_resolution": "1920x1080 60P 16:9"},
		{"video_stamp": "off"},
		{"video_quality": "S.Fine"},
		{"timelapse_video": "off"},
		{"capture_mode": "precise quality"},
		{"photo_size": "16M (4608x3456 4:3)"},
		{"photo_stamp": "off"},
		{"photo_quality": "S.Fine"},
		{"timelapse_photo": "60"},
		{"preview_status": "on"},
		{"buzzer_volume": "mute"},
		{"buzzer_ring": "off"},
		{"capture_default_mode": "precise quality"},
		{"precise_cont_time": "60.0 sec"},
		{"burst_capture_number": "7 p / s"},
		{"restore_factory_settings": "on"},
		{"led_mode": "all enable"},
		{"dev_reboot": "on"},
		{"meter_mode": "center"},
		{"sd_card_status": "insert"},
		{"video_output_dev_type": "tv"},
		{"sw_version": "YDXJv22_1.0.7_build-20150330113749_b690_i446_s699"},
		{"hw_version": "YDXJ_v22"},
		{"dual_stream_status": "on"},
		{"streaming_status": "off"},
		{"precise_cont_capturing": "off"},
		{"piv_enable": "off"},
		{"auto_low_light": "on"},
		{"loop_record": "off"},
		{"warp_enable": "off"},
		{"support_auto_low_light": "on"},
		{"precise_selftime": "5s"},
		{"precise_self_running": "off"},
		{"auto_power_off": "5 minutes"},
		{"serial_number": "xxxxx"},
		{"system_mode": "capture"},
		{"system_default_mode": "capture"},
		{"start_wifi_while_booted": "off"},
		{"quick_record_time": "0"},
		{"precise_self_remain_time": "0"},
		{"sdcard_need_format": "no-need"},
		{"video_rotate": "off"}

Photo capture




{ "msg_id": 7, "type": "start_photo_capture", "param":"precise quality;off"}
{ "msg_id": 7, "type": "photo_taken", "param":"/tmp/fuse_d/DCIM/100MEDIA/YDXJ0047.jpg"}
  • param is the full path of the image

NOTE: To get the download URL of the image, replace /tmp/fuse_d/ with

Telnet events

All messages with msg_id: 7 are events and are sent automatically by the camera.

{ "msg_id": 7, "type": "<string: event name>"}

Some events also provide additional data in the param attribute:

{ "msg_id": 7, "type": "<string: event name>", "param":"<int: event data>"}

Below is a list of known events, their parameters and an example for each


Fired when the battery level changes

{ "msg_id": 7, "type": "battery", "param":"20"}
  • type is battery when discharging and adapter when charging
  • param is the battery charge percentage (int 1-100, obviously)

Charger status

Fired when the USB charging cable is connected/disconnected

{ "msg_id": 7, "type": "adapter_status" ,"param":"1"}
  • param:0 – cable disconnected
  • param:1 – cable connected

Camera mode switched

Fired when the recording mode is changed (big button on the front)

{ "msg_id": 7, "type": "switch_to_rec_mode" }
  • type:"switch_to_rec_mode" – switched to video mode
  • type:"switch_to_cap_mode" – switched to photo mode

Photo taken

{ "msg_id": 7, "type": "start_photo_capture" ,"param":"precise quality;off"}
{ "msg_id": 7, "type": "precise_capture_data_ready" }
{ "msg_id": 7, "type": "photo_taken" ,"param":"/tmp/fuse_d/DCIM/100MEDIA/YDXJ0513.jpg"}

SD Card status

Fired when the SD card is inserted/removed

{ "msg_id": 7, "type": "sd_card_status" ,"param":"insert"}
  • type:"insert" – SD card inserted
  • type:"remove" – SD card removed When SD card is removed, the following event also fires:
{ "msg_id": 7, "type": "CARD_REMOVED" }

Live view

After you’ve successfully authenticated through telnet, you can connect to rtsp:// with VLC (or similar) to see the live view of the camera.

This doesn’t always work. The only 100% way I know is to first connect with the official app.

Unknown events

Photo taken vf_start/stop

Fired after start_photo_capture Best guess: stop live view

{ "msg_id": 7, "type": "vf_stop" }

Fired after photo_taken Best guess: start live view

{ "msg_id": 7, "type": "vf_start" }

vf == “viewfinder” ?

Battery status

This message has been observed when taking out the battery with the cam on USB power, but I haven’t been able to reproduce it so far.

{ "msg_id": 7, "type": "battery_status" ,"param":"0"}
  • param – observed values0 and -1

Some of my favourite free, modern-looking, easy to use, web-based email clients that anyone can install and use with their mail server (in no particular order).


  • Built-in calendar, contacts and task list
  • VERY advanced filters
  • PGP support
  • Multiple identities/e-mail accounts
  • Many – too many – skins (free and paid)
  • A few responsive skins (default isn’t)


  • Single-pane or horizontal/vertical split
  • Advanced filtering (using Sieve)
  • Two-factor authentication
  • OpenPGP support (client-side JS)
  • No mobile support


  • Tag-based sorting
  • OpenPGP support
  • Multiple identities/e-mail accounts
  • No mobile support
  • Not (yet) multi-user

AfterLogin WebMail Lite

  • Contact manager with groups
  • Multiple identities/e-mail accounts
  • No mobile support

Neutron (ProtonMail WebClient)

  • A server for the ProtonMail WebClient
  • OpenPGP support
  • Fully responsive

Why a blog?

See, I really enjoy making things. I love figuring out how to turn a crazy idea I had on a bus into a working thing that I can impress my only 2 friends with enjoy and use. But I have a problem: I never seem to finish anything!

Some of the projects that I stared working on but are now just boxes of parts collecting dust in my closet include: a motorized pan&tilt camera head for gigapanos, a fully modular 3D printer / laser cutter / CNC engraver, a (motorized) camera slider, a HAM radio answering machine, a universal high-speed camera trigger, a log-distance data connection over laser beams
There’s also the Renault 5 that was supposed to be all computerized and open-source, but that isn’t in my closet. It’s in a junkyard. Because I was too slow.

But I think I might’ve found my problem:

*hours and hours of measurement and calculation*

“Ok, let’s do this!”

*a single resistor blows*

“Well, I guess I’m just incompetent”

me, during every single project ever

So, how do I fix this? My theory is that if I force myself to write about it, I will think about it more and thus screw up less. And writing “articles” makes me feel all “professional” or whatever, which also helps…

Copyright © 2020 Bloggity Blog

Theme by Anders NorenUp ↑