Tag Planets

15 feb


Reparation af Nordlux IP S12 badeværelseslampe der ikke lyser længere


Denne badeværelseslampe er udgået af produktion, og pga. monteringen og at man ofte har mere end én er det noget træls at skulle udskifte – det giver ihvertfald en del skrot uden grund. Heldigvis er konstruktionen super simpel: det er udelukkende en LED driver (230V AC til 24V DC) og en LED.

Lad os starte med det nemme: LED-driveren er direkte tilgængelig bagfra, og med lidt forsigtighed kan spændingen udmåles. I dette tilfælde var der ca. 24V DC, og det er jo fint indenfor specifikationen.

Selve LED’en er lidt sværere at komme til: fronten af glasset skal drejes af via de to huller deri. Jeg brugte en låseringstang af ca. korrekt dimension, med lidt forsigtighed. Lidt ridser gør nok ikke det store når lyset skinner. LED’en kan nu loddes af.

En ny LED kan købes for ca. 10 kr, f.x. på AliExpress. Det rigtige søgterm er måske “Bridgelux 2020 COB LED”, jeg endte med en 7W i Warm White (3000 Kelvin).

Efter lidt fidlen og lodden er den nye LED monteret, og kan testes. Stor succes!

Gemt under: Extern, HAL9k

Tags: ,

28 okt


Fantus-button part 2: the physical button build and the network communication


First part of this series is here, covering the reverse engineering of the DRTV Chromecast App.

I wanted the physical appearance to be extremely minimalistic, with slight references to various cubes from videogames. Because it is a remote control, it of course has to be wireless and battery-powered.

The box is lasercut from 6 mm MDF, and with a giant red arcade button on top with a red LED inside.

The electronics inside is a battery-powered Wemos D1, along with 4 x 18650 Lithium battery cells. After some experimentation on the response time, which is primarily dominated by the time it takes to reconnect to the WiFi network, I initially only used “light sleep”. This resulted in a battery time of just over a week, which is okay, but not great.

In order to preserve battery deep sleep would be really nice. The problem is deep sleep on the Wemos can only be interrupted by a reset. The idea was to use a MOSFET (in this case an N-channel logic level mosfet, IRFZ44N) for the Wemos to be able to select whether a press of the button should reset it, or it should just register on a pin as normal.

This circuit allows RST to be pulled low by the button, as long as D0 is high. Luckily, D0 is high during deep sleep, so as long as the Arduino code keeps D0 low button presses will not reset — but can still be registered by reading pin D1.

This works out “responsively enough” because the initial start has some delay due to the Chromecast initializing the app and loading media. Any subsequent button presses within the 30 seconds the Arduino stays awake are instant though. With this setup the battery life is not a problem – I’ve only had to charge it once. As a bonus feature/bug whenever the battery gets low the Wemos will trigger a bit sporadically: this causes “Fantus-bombing” where Fantus will just randomly start; quite quickly thereafter the Fantus-button is being charged 😉

The Wemos itself is not powerful enough to do all the pyChromecast communication needed, so I setup a small Raspberry Pi to handle that part. Since I didn’t want to spend too much time and effort setting up the communication between them, I ended up using a trick from my youth: UDP broadcasting. Because UDP is datagram-oriented you can send a UDP packet to the broadcast address ( and then it will be received by all hosts on the local area network: no configuration needed. In Arduino code it looks like:

Udp.beginPacket("", 31337);

(Full Arduino code available here.)

At this point I had a UDP packet that I could receive on the Raspberry Pi, and it was just a matter of writing a small server program to listen, receive and process those UDP commands. However, at this point a thought entered my mind, that derailed the project for a while:

netcat | bash

Why write my own server to parse and execute commands, when Bash is already fully capable of doing exactly that with more flexibility than I could ever dream of? And netcat is perfectly capable of receiving UDP packets? This is a UNIX system, after all, and UNIX is all about combining simple commands in pipelines — each doing one thing well.

The diabolical simplicity of just executing commands directly from the network was a bit too insecure though. This is where Bash Restricted mode enters the project: I wouldn’t rely on it for high security (since it is trying to “enumerate badness“), but by locking down the PATH of commands that are allowed to execute it should be relatively safe from most of the common bypass techniques:

netcat -u -k -l 31337 | PATH=./handlers/ /bin/bash -r

The project was now fully working: press the button, Fantus starts. Press it while Fantus is playing: Fantus pauses. Press it while Fantus is paused: Fantus resumes. The little human was delighted about his new powers over the world, and pressed the button to his hearts content (and his parents slight annoyance at times).

(Full code for handler available here.)

But wouldn’t it be cool if the little human had a (limited) choice in what to view?…

Gemt under: Extern, HAL9k


18 jul


Fantus-button part 1: Reverse engineering the DRTV Chromecast App


I want to build a physical giant red button, that when pressed instantly starts a children’s TV-show, in my case Fantus on DRTV using a Chromecast.

The first part of the build is figuring out how to remotely start a specific video on a Chromecast. Initially I thought this would be pretty simple to do from an Arduino, because back in the day you could start a video just using a HTTP request. Very much not so anymore: the Chromecast protocol has evolved into some monster using JSON inside Protobuf over TLS/TCP, with multicast DNS for discovery. Chance of getting that working on a microcontroller is near-zero.

But remote control is possible using e.g. pychromecast which has support for not only the usual app of YouTube, but also a couple of custom ones like BBC. Let’s try and add support for DRTV to pychromecast, starting at the hints given on adding a new app.

Using the netlog-viewer to decode the captured net-export from Chrome, and looking at the unencrypted socket communication, the appId of the DRTV app is easily found.

However, one of the subsequent commands has a lot more customData than I expected, since it should more or less just be the contentId that is needed:

  "items": [
      "autoplay": true,
      "customData": {
        "accountToken": {
          "expirationDate": "2022-07-02T00:48:35.391Z",
          "geoLocation": "dk",
          "isCountryVerified": false,
          "isDeviceAbroad": false,
          "isFallbackToken": false,
          "isOptedOut": false,
          "profileId": "c4e0...f3e",
          "refreshable": true,
          "scope": "Catalog",
          "type": "UserAccount",
          "value": "eyJ0eX...Dh8kXg"
        "chainPlayCountdown": 10,
        "profileToken": {
          "expirationDate": "2022-07-02T00:48:35.389Z",
          "geoLocation": "dk",
          "isCountryVerified": false,
          "isDeviceAbroad": false,
          "isFallbackToken": false,
          "isOptedOut": false,
          "profileId": "c4e0a...f3e",
          "refreshable": true,
          "scope": "Catalog",
          "type": "UserProfile",
          "value": "eyJ0eXAi...IkWOU5TA"
        "senderAppVersion": "2.211.33",
        "senderDeviceType": "web_browser",
        "sessionId": "cd84eb44-bce0-495b-ab6a-41ef125b945d",
        "showDebugOverlay": false,
        "userId": ""
      "media": {
        "contentId": "278091",
        "contentType": "video/hls",
        "customData": {
          "accessService": "StandardVideo"
        "streamType": "BUFFERED"
      "preloadTime": 0,
      "startTime": 0
  "repeatMode": "REPEAT_OFF",
  "requestId": 202,
  "sessionId": "81bdf716-f28a-485b-8dc3-ac4881346f79",
  "startIndex": 0,
  "type": "QUEUE_LOAD"

Here I spent a long time trying without any customData, and just using the appId and contentId. Initially it seemed to work!

However, it turned out it only worked if the DRTV Chromecast app was already launched from another device. If launched directly from pychromecast the app would load, show a spinner, and then go back to idle. Here much frustration was spent; I guess the customData is actually needed. And indeed, putting that in works! But where do these tokens come from, and how do we get those tokens from Python?

Using Chrome’s developer tools (F12) on the DRTV page, and then searching globally (CTRL-SHIFT-f) for various terms (“expirationDate”, “customData”, “profileToken”, “accountToken” etc.) revealed some interesting code, that was as semi-readable as any pretty-printed minifyed Javascript. Eventually I found the tokens in local storage:

Using these tokens work really well, and allows starting playback!

Some further exploration proceeded: using the showDebugOverlay flag reveals that the DRTV player is just a rebranded Shaka Player. The autoplay functionality can be disabled by setting chainPlayCountdown to -1, which is honestly a real oversight that it cannot be disabled officially, to not have to rush to stop the playback of the item before the next autoplays.

With all the puzzle pieces ready, I prepared a pull request (still open) to add support for DRTV to pychromecast.

Fantus-button part 2 will follow, detailing the hardware build and network integration with the support from pychromecast.

Gemt under: Extern, HAL9k


20 feb


Floating Solid Wood Alcove Shelves


I have an alcove where I wanted to put in some floating shelves. I wanted to use some solid wood I had lying around, to match the rest of the interior; this ruled out most of the methods described online: (i) building up the shelf around a bracket, and (ii) using hidden mounting hardware would be hard to get precise and would not provide support on the sides.

So inspired by some of the options instead I tried to get by with just brackets on the three sides, in a solid wood shelf. I ended up with 12mm brackets of plywood in a 26mm solid wood shelf, and that was plenty sturdy.

Step 1 was to cut out the rough shelves, with plenty of extra width, and rough fitting the plywood bracket pieces. It makes sense to leave as much on the top of the slit as possible, as this will be the failure point if overloaded. The excellent wood workshop at Hal9k came in very handy!

Step 2 was to mount the plywood brackets in the alcove. Pretty easy to do using a laser level, biggest problem was getting the rawplugs in precise enough for the level to be kept.

Step 3 was fitting the shelves individually, accounting for the crookedness of the 3 walls. The scribing method used by Rag’n’Bone Brown was pretty useful, just doing it in multiple steps to make sure not to cut off too much.

Finally, all the shelves in final mounting. Getting them in took a bit of persuasion with a hammer, and minor adjustments with a knife to the plywood brackets, as it was a tight fit. The key again was small adjustments.

One concern with such a tight fit would be wood movement; however most of the wood movement is “across the grain” which in this application means “in and out” from the alcove, where the wood is basically free to move as the shelves are not fastened to the brackets in any way.

Another concern would be if the relatively small brackets (12x12mm) can handle the load of the relatively wide shelves (60cm wide, 35cm deep, and 2.6cm high). There are two failure scenarios: (i) the wood could split above the slit, (ii) or the bracket could deform or be pulled out. Neither seems likely as (i) applying a static (or even dynamic) load large enough to split the wood seems implausible, even at the weakest point in the middle of the front, and (ii) the tight fit counteracts the brackets ability to be pulled out since pulling out in one side would have the shelf hitting the wall on the opposite side.

All in all a very satisfying project to work on and complete!

Gemt under: Extern, HAL9k

Tags: ,

04 okt


Olimex A20-OLinuXino-LIME2 – 8 years in service, 2 PSUs and 1 SD-card down


4 years ago I posted a 4 year review of the Olimex LIME2. It seems that the lifetime of power supplies is approximately 4 years as now another power supply died, and this time also the SD-card was expiring. The LIME2 lives on however!

It was a bit hard to notice, because the battery pack of the LIME2 kept it running pretty well even with the poor power supply. So, better monitoring of the battery pack is also on the todo list.

Recovering the bad SD-card

Recovering the SD-card was relatively easy with minimal dataloss, when out of the LIME2:

$ sudo ddrescue /dev/mmcblk0 backup.img
# Put in a new SD-card
$ sudo dd if=backup.img of=/dev/mmcblk0 bs=16M

I have done this a couple of times with other SD-cards from Raspberry PIs, and though there is the potential for dataloss it is usually minimal. This time a few blocks were lost.

Upgrading Debian from Stretch to Bullseye

I took the opportunity to upgrade the Debian install while the system was offline anyway. Upgrading was generally painless, following the usual Debian method. I went through the Buster release just to be sure:

$ vim /etc/apt/sources.list
# replace all "stretch" with "buster" :%s/stretch/buster
$ apt update && apt upgrade && apt full-upgrade
$ reboot

$ vim /etc/apt/sources.list
# replace all "buster" with "bullseye" :%s/buster/bullseye
$ apt update && apt upgrade && apt full-upgrade
$ reboot

The only tricky part is booting the new kernel. Since that always fails for me on the first try, I always hookup the serial console. For future reference, this is how to hookup the serial console:

Pinout from left as labelled on the LIME2: TX, RX, GND

Now, of course the boot failed. I tried getting the flash-kernel package to work for my setup, but for historical reasons I have a separate boot partition. In the end I derived a simple bootscript from that package, that boots from p1 but loads the kernel, fdt and initrd from p2:

setenv bootargs  ${bootargs} console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait panic=10

#setenv fk_kvers '4.19.0-21-armmp-lpae'
setenv fk_kvers '5.10.0-18-armmp-lpae'
setenv fdtpath dtb-${fk_kvers}

load mmc 0:2 ${kernel_addr_r} /boot/vmlinuz-${fk_kvers}
load mmc 0:2 ${fdt_addr_r} /boot/${fdtpath}
load mmc 0:2 ${ramdisk_addr_r} /boot/initrd.img-${fk_kvers}
bootz ${kernel_addr_r} ${ramdisk_addr_r}:${filesize} ${fdt_addr_r}

The script can be manually input over the serial terminal, and thereby tested out.

The only downside is it needs to be manually updated after each kernel upgrade. To activate the uboot bootscript:

$ mount /dev/mmcblk0p1 /mnt/
$ cd /mnt
# ensure boot.cmd is as above
$ mkimage -C none -A arm -T script -d boot.cmd boot.scr

Monitoring the LIME2 battery pack

After upgrading to a recent 5.X mainline Linux kernel the battery pack is exposed in the sysfs filesystem:

$ cat /sys/class/power_supply/axp20x-battery/voltage_now 
4070000 # 4.07 V
$ cat /sys/class/power_supply/axp20x-ac/voltage_now 
4933000 # 4.93 V

I setup a couple of alerting rules for these in my home monitoring setup, so hopefully the next time the LIME2 defeats a power supply I’ll get notified.


I can still warmly recommend the LIME2. It is still available, and even a bit cheaper nowadays at 40 EUR + VAT, and still a little workhorse that just keeps on going.

Gemt under: Extern, HAL9k

Tags: ,