Installing/Configuring ZSH

Default for most computers I use is still Bash. On some I installed Zsh manually. Here’s what I do:

❯ apt install zsh
❯ zsh
# menu system, select (2) (default .zshrc)

❯ curl -sfL git.io/antibody | sh -s - -b ~/bin
❯ echo "romkatv/powerlevel10k" > ~/.zsh_plugins.txt
❯ ~/bin/antibody bundle < ~/.zsh_plugins.txt > ~/.zsh_plugins.sh
❯ cat >>.zshrc <<_EOF_
typeset -U path
for i in ~/bin ~/dart-sdk/bin ; do
  if [[ -d "$i" ]] ; then
    path+="$i"
  fi
done

source ~/.zsh_plugins.sh
_EOF_

source ~/.zshrc
# not needed, but if the p10k configurator does not start,
# then run p10k manually
# In either case: configure p10k to your liking
# or copy ~/.p10k.sh from another machine

# Change default shell to zsh
chsh -s /bin/zsh

I’d love to say that I automated this with an Ansible role, but since I change/configure zsh less than once a year, it really makes no sense to maintain an Ansible script just for this.

A Dart Package called “l2ethernet”

Since part of making the Colorlight card work with Dart was to be able to send Layer 2 Ethernet packets, I created a small Dart package for that: l2ethernet.

There’s plenty improvements I could do, from being able to listen to packets to making sure the shared library is being found, but for now it works for the purpose of sending out packets at over 50fps.

Winter project: Colorlight 5A 75B Protocol

I wish the protocol would be officially documented for the Colorlight 5A 75 as it’s an awesome piece of affordable technology to drive those LED panels (see here as an example). I rather like using Ethernet instead of some odd adapter card which is tied to a specific hardware (RPi in above case). Ethernet on the other hand is perfect: it’s fast (Gigabit can transfer a 256×256 24 bit/pixel frame in 1.6µs or over 600fps, cables can be 100m long, you can use (L2) switches to distribute the traffic and you can create traffic with any software which can send Ethernet frames.

To configure the Colorlight 5A 75B, you need software named LEDVISION. It’s needed to configure the panels, their wiring and some other items. Once done, it’s no longer required.

A limitation I did not expect is that LEDVISION does require a Ethernet port to work. It might be possible to send actual frames via WiFi as long as the receiver card is connected to a bridged Gigabit Ethernet port, but that remains to be seen1.

Some frame decoding is visible in the source code of FPP which took the work from the original reverse engineering here. So here the frames I found (Colorlight 5A 75B, version 5A 10.16).

Image Data Frame

20 fps (default from LEDVISION software)

  • Display Frame: Show the stored image
    • eth.type==0x0107
    • src.mac: 22:22:33:44:55:66 (take this literal)
    • dest.mac: 11:22:33:44:55:66 (this too)
    • Data length: 98
      • Data[11]: Brightness (linear 0…255)
        • 0x00: 0% brightness
        • 0x03: 1%, 0x05: 2%, 0x08: 3%, 0x0a: 4%, 0x0d: 5%, 0x0f: 6%, 0x1a: 10%
        • 0x40: 25% brightness
        • 0x80: 50% brightness
        • 0xbf: 75% brightness
        • 0xff: 100% brightness
      • Data[12]: 5 (no idea what this is, but it’s always 5)
      • Data[14, 15, 16]: Brightness (linear) for R, G and B (to adjust color temperature):
        • 2000K at 10% brightness: 0x1a, 0x0c, 0x01
        • 6500K at 10% brightness: 0x1a, 0x1a, 0x1a
        • 2000K at 100% brightness: 0xff, 0x76, 0x06
        • 4500K at 100% brightness: 0xff, 0xdc, 0x8f
        • 6500K at 100% brightness: 0xff, 0xff, 0xff
        • 8000K at 100% brightness: 0xce, 0xd8, 0xff
    • Full frame:
0000 11 22 33 44 55 66 22 22  33 44 55 66 01 07 00 00
0010 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0020 00 00 00 bf 05 00 bf bf  bf 00 00 00 00 00 00 00
0030 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0040 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0050 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0060 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
  • Set Brightness Frame: Set frame brightness and color temperature
    • eth.type==0x0aXX (XX=00…ff)
    • Data length: 63 byte
    • Data: XXXXff00…00
    • XX is the brightness from 0x00 (black) to 0xff (100% brightness)
    • 1%=0x28, 2%=0x35, 5%=0x4d, 25%=0x92 , 50%=0xc1, 75%=0xe3, 100%=0xff (based on LEDVISION software)
  • Pixel Data Frame: Send a row of pixels
    • eth.type==0x5500 or 0x5501 (decode from FPP source)
    • Data length: 391 byte (row length * 3 + 7), Pixel format: BGR (for my panel)
    • Ether Type: 0x5500 + MSB of Row Number
      • 0x5500 for rows 0-255
      • 0x5501 for rows 256-511
    • Data[0]: Row Number LSB
    • Data[1]: MSB of pixel offset for this packet
    • Data[2]: LSB of pixel offset for this packet
    • Data[3]: MSB of pixel count in packet
    • Data[4]: LSB of pixel count in packet
    • Data[5]: 0x08 – ?? unsure what this is
    • Data[6]: 0x80 – ?? unsure what this is (I got 0x88)
    • Data[7-end]: RGB order pixel data (resp. BGR for me)

Receiver Card Detection

This is always 3 frames: one to the receiver card, one response from the card (broadcasted), and an ack to the card with Data[2]=1.

  • Detect Receiver Card Frame: eth.type==0x0700
    • Data length: 270 byte
    • Data: 00000000…00
  • Detect Receiver Response Frame: eth.type==0x0805
    • src.mac: 11:22:33:44:55:66
    • dest.mac: ff:ff:ff:ff:ff:ff
    • Data length: 1056 byte
      • Data[0]: Receiver card version (5A)
      • Data[1]: Receiver card version major (10)
      • Data[2]: Receiver card version minor (16)
      • Data[20]: Pixel columns HSB
      • Data[21]: Pixel columns LSB
      • Data[22]: Pixel rows HSB
      • Data[23]: Pixel rows LSB
      • Data[39,40]: 16 bit (at least) counter: frames received / panels (or divide by 4)
      • Data[45, 46, 47, 48]: Run Time in ms: 32 bit (at least) counter
0000   ff ff ff ff ff ff 11 22  33 44 55 66 08 05 04 0a
0010   10 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0020   00 00 00 80 00 40 00 00  00 00 00 00 01 02 03 ff
0030   ff ff ff 00 00 0c a8 00  00 00 00 00 08 05 a0 00
0040   00 00 00 00 00 00 00 00  00 00 00 00 ba 00 00 00
0050   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0060   00 00 00 00 00 00 00 00  00 00 00 00 0f 04 08 00
0070   00 00 00 00 00 00 00 00  00 00 00 00 00 00 01 00
0080   01 00 01 00 00 28 00 ff  ff ff 00 00 00 00 00 00
0090   00 00 0c 03 00 00 00 00  00 00 00 00 00 00 00 00
00a0   00 00 00 00 00 00 00 00  01 00 00 00 00 00 04 00
00b0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
[...]
0410   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
0420   00 00 00 00 00 00 00 00  00 00 00 00 00 00

Lacking official documentation this is all only a guess. If I had more samples with more versions of the card, it would make those guesses better, but they’d be still guesses.

  • Detect Receiver Response Ack: eth.type==0x700
    • Data length: 270 byte
    • Data: 00000100…00

Why all this trouble?

Now I can find the receiver card (response on 0x0700 packet) and I get the configured pixel size. One thing less the user needs to provide or which has to be hard-coded.

Here the result (source code is here):

Dart sending frames to the Colorlight 5A

Updates

1 When sending frames via WiFi, the result is having…problems. Think of analog TV having a bad reception: you can see what’s going on, but frames are missing, colors are off for a frame, and the first line of the graphics is not always on the first line.

More Baking: Cupcakes

As a reminder: this is not a baking blog. Now that this is out of the way, I was looking for non-cookies for a change, and cupcakes would fit the bill. I checked several recipes and settle to use this. Here the result:

Very tasty cupcakes

I recommend to read the well made web page above, but the summary of the recipe:

  • 150 g plain / all purpose flour
  • 1 1/4 tsp baking powder
  • 1/8 tsp salt
  • 2 large eggs at room temp
  • 150 g caster / superfine sugar
  • 60g  unsalted butter
  • 125 ml milk
  • 2 tsp vanilla extract
  • 1 1/2 tsp vegetable or canola oil

The only change I did was to replace the superfine sugar with the fine brown sugar.

Time was spot on 22 minutes. The result was 11 small cupcakes which probably should have been 9 slightly larger ones.

More Baking: Coconut Cookies

Coconut cookies – Simple and very tasty

Since I had dried coconut left over (see here), I was searching for a recipe for cookies with coconuts. There’s plenty, so I picked one and it worked really well. The biggest problem was converting cups into something I can use, so here the metric translation of that recipe:

  • 150g flour (= 1 1/4 c)
  • 1/2 tsp baking soda
  • 1/4 tsp salt
  • 110g butter (= 1/2 c)
  • 200g sugar (brown only as I had no white sugar) (= 1 c in total)
  • 1 egg
  • 1/2 tsp vanilla extract
  • 120g coconut shredding (= 1 1/3 c)

Don’t let me rant about the use of tsp. That is pure insanity. Luckily those don’t need to be exact anyway.

  1. Preheat oven to 175°C
  2. In one bowl mix flour, baking soda and salt
  3. In a larger bowl add butter and sugar and mix well
  4. Add egg and vanilla
  5. Add flour little by little (sieving it in)
  6. Mix in the coconut shreddings
  7. Form small balls (about 2-3cm diameter) and put them on baking paper
  8. Bake about 12-14min. Move to wire rack to cool.

The result was as good as I expected. Crispy outside, a bit chewy inside. Perfect on the first try!

Steel-Cut Oats for Breakfast

My whole life I have mostly cereals for breakfast: cornflakes and Müsli. There are so many variations of the latter, that it never gets boring: with dried fruits, with chocolate or nuts, crunchy or not, just add with milk or like Bircher keep it soaking for some hours…

But this is all rolled oats and until recently I didn’t even know there’s non-rolled oats, namely “steel-cut oats”. I followed this recipe with 60g (2oz for the metrically challenged) of oats and it reminded me a lot of Griessbrei which I had when I was a kid. Ah, good old memories! It was surprisingly nice. Not a big fan of the 25min cooking time though, but it’s a nice variation which is especially nice in cold winter days.

Ubuntu 21.10, PulseAudio 1.15, Bluetooth and its Codecs

When it comes to in-ear headphones, the neckband format is by far my personal favorite. While I have several Bluetooth headphones, my most liked in-ear one is the Fiio FH3: comfortable, great sound. But it’s cabled, and I do not like cables. So I got a Shanling MW200 for my FH3. And the FH3 still sound great!

But the MW200 blinks in blue which means it’s using the SBC audio codec when connecting to my Linux PC! Unacceptable!

It turns out that on an older Ubuntu version (20.04), the Bluetooth Codecs only support SBC. Not a big problem: PulseAudio 1.15 supports better codecs like aptX, AAC and LDAC. And it’s part of Ubuntu 21.10.

Time to upgrade from 20.04 to 21.10 then!

After doing the usual

❯ apt update
❯ apt upgrade
❯ do-release-update

I still only see SBC used: the MW200 blinks blue when it’s using it. If it was using another codec, it would blink in another color (e.g. green=LDAC or purple=aptX).

The fix is simple:

❯ add-apt-repository ppa:berglh/pulseaudio-a2dp
❯ apt update
❯ apt install pulseaudio-modules-bt libldac

Once done, you can see the codecs:

❯ pactl list
[...]
Card #4
        Name: bluez_card.4C_00_00_00_00_24
        Driver: module-bluez5-device.c
        Owner Module: 29
        Properties:
                device.description = "Shanling MW200"
                device.string = "4C:00:00:00:00:24"
                device.api = "bluez"
                device.class = "sound"
                device.bus = "bluetooth"
                device.form_factor = "headphone"
                bluez.path = "/org/bluez/hci0/dev_4C_00_00_00_00_24"
                bluez.class = "0x240418"
                bluez.alias = "Shanling MW200"
                device.icon_name = "audio-headphones-bluetooth"
        Profiles:
                headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 30, available: yes)
                a2dp_sink_sbc: High Fidelity Playback (A2DP Sink: SBC) (sinks: 1, sources: 0, priority: 40, available: yes)
                a2dp_sink_aac: High Fidelity Playback (A2DP Sink: AAC) (sinks: 1, sources: 0, priority: 40, available: yes)
                a2dp_sink_aptx: High Fidelity Playback (A2DP Sink: aptX) (sinks: 1, sources: 0, priority: 40, available: yes)
                a2dp_sink_aptx_hd: High Fidelity Playback (A2DP Sink: aptX HD) (sinks: 1, sources: 0, priority: 40, available: yes)
                a2dp_sink_ldac: High Fidelity Playback (A2DP Sink: LDAC) (sinks: 1, sources: 0, priority: 40, available: yes)
                off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
        Active Profile: a2dp_sink_ldac
        Ports:
                headphone-output: Headphone (type: Unknown, priority: 0, latency offset: 0 usec, available)
                        Part of profile(s): headset_head_unit, a2dp_sink_sbc, a2dp_sink_aac, a2dp_sink_aptx, a2dp_sink_aptx_hd, a2dp_sink_ldac
                headphone-input: Bluetooth Input (type: Unknown, priority: 0, latency offset: 0 usec, availability unknown)
                        Part of profile(s): headset_head_unit

Even better is that it’s very easy to change the codec via the volume control and clicking on the hamburger menu next to the Bluetooth headset:

Audio Volume Control for MW200

And the MW200 now blinks in green (means it’s using not SBC, but LDAC)!

If anyone wonders: I cannot differentiate between aptX, aptX HD and LDAC. SBC sounds a bit duller though.