Calculate This

Illustration

As I moved to Linux, I slowly started moving all my apps along. But, as I played with electronics, I often had to boot up Windows just to get to a simple calculator. I made this calculator ages ago in order to calculate various values. But I made it for Windows Store which meant it was time to make it again, this time a bit more portable.

With the help of Avalonia and a bit of C# code it was a long overdue weekend project. Most of the time I just need LDO power and voltage divider calculations, but it seemed a shame not to reimplement the others.

Since, I wanted application to work on Linux (both KDE and Gnome), choice fell between two frameworks: Avalonia and ETO Forms. I was tempted to go the ETO Forms route because I actually like(d) Windows Forms. They’re easy, event drive, and without 50 shades of indirection. But, after playing with both for a while, Avalonia just seemed more suitable.

As previously, I created the following calculators:

  • E-Series
  • LDO Power
  • LED
  • LM117
  • LM317
  • Microchip PIC PWM
  • Microchip PIC TMR0
  • Ohm’s Law
  • Parallel and Series Resistors
  • Voltage Divider

I will implement more as I need them.

While development environment does contain unit tests, currently it’s a bit low on their count. I was too lazy to implement them all. Probably I’ll write them only as I fix bug since I’m lazy that way.

If this app seems interesting, You can download it here. It should work pretty much on any glibc-based Linux out there. I will eventually make Windows setup version too, but you can you Windows Store one in meantime.

A Key to Mute the Microphone

One thing I love about my work Lenovo is its Microphone mute button. While every application offers mute functionality, having it as a special button is really handy. You can even do scripting around that. So, I wanted the same for my Framework 16.

Since Framework 16 keyboard is QMK based (albeit older version), changing key assignment was mostly figuring out which key is muting microphone. Not to keep you in the suspense - that key is F20. Each press on F20 mutes and unmutes microphone - just how standard audio mute functionality does to outputs.

So, with the knowledge of the key, the only decision left was where to assign that key too. And for that, I found = key on numpad looking the best. My whole current Numpad setup looks like this (both with and without NumLock):

┌────┬────┬────┬────┐     ┌────┬────┬────┬────┐
│Esc │PScr│MicM│Mute│     │Esc │Calc│ =  │ <- │
├────┼────┼────┼────┤     ├────┼────┼────┼────┤
│ Num│Bck-│Bck+│Vol-│     │ Num│ /  │ *  │ -  │
├────┼────┼────┼────┤     ├────┼────┼────┼────┤
│Home│ ↑  │PgUp│    │     │ 7  │ 8  │ 9  │    │
├────┼────┼────┤    │     ├────┼────┼────┤    │
│ ←  │    │ →  │Vol+│     │ 4  │ 5  │ 6  │ +  │
├────┼────┼────┼────┤     ├────┼────┼────┼────┤
│End │ ↓  │PdDn│    │     │ 1  │ 2  │ 3  │    │
├────┴────┼────┤    │     ├────┴────┼────┤    │
│ Insert  │Del │Entr│     │ 0       │ .  │Entr│
└─────────┴────┴────┘     └─────────┴────┴────┘

Thus, that changed my definition of keyboard to:

[_FN] = LAYOUT(
    KC_ESC,  S(KC_PRINT_SCREEN), KC_F20,  KC_MUTE,
    KC_NUM,  KC_BRID, KC_BRIU, KC_VOLD,
    KC_P7,   KC_P8,   KC_P9,
    KC_P4,   KC_P5,   KC_P6,   KC_VOLU,
    KC_P1,   KC_P2,   KC_P3,
    KC_INS,  KC_DEL,  KC_ENT
),

Short recompile later and my numpad now has that extra key for much easier muting. As always, QMK code is freely available.

Capturing Govee Temperature in Docker

In my previous post I discussed reading Govee sensor temperature in a script. And that is perfectly fine. However, this is not ideal for my server environment. What I want is a Docker container.

Since I like Alpine images, the first step was to compile GoveeBTTempLogger. After installing prerequisites, I was greeted with bunch of errors:

/root/GoveeBTTempLogger/goveebttemplogger.cpp: In function 'bool ValidateDirectory(const std::filesystem::__cxx11::path&)':
/root/GoveeBTTempLogger/goveebttemplogger.cpp:924:23: error: aggregate 'ValidateDirectory(const std::filesystem::__cxx11::path&)::stat64 StatBuffer' has incomplete type and cannot be defined
  924 |         struct stat64 StatBuffer;
      |                       ^~~~~~~~~~
/root/GoveeBTTempLogger/goveebttemplogger.cpp:925:59: error: invalid use of incomplete type 'struct ValidateDirectory(const std::filesystem::__cxx11::path&)::stat64'
  925 |         if (0 == stat64(DirectoryName.c_str(), &StatBuffer))
      |                                                           ^
...

As lovers of Alpine know, due to its use of musl, this is not an uncommon occurrence. Fix was easy enough so I created a pull request myself. With this sorted out, it was time for Dockerfile.

Base prerequisites were obvious:

FROM alpine:latest
USER root

RUN apk add dbus bluez bluez-dev libstdc++
RUN rc-update add dbus bluetooth default
...

However, depending on services is not something Alpine does out-of-box. Openrc runlevel requires more direct access to machine. But, since I was not the first person needing it, solution already exists and it’s called softlevels. To enable them, three lines are enough:

RUN apk add openrc
RUN mkdir -p /run/openrc/exclusive
RUN touch /run/openrc/softlevel

This and a couple of wrapper scripts was all that was needed to get it running. But, I was still one step away from making it work in my environment. I needed compose.yaml and this is what I came up with (notice dbus volume):

services:
  govee2telegraf:
    container_name: govee2telegraf
    image: medo64/govee2telegraf:latest
    restart: unless-stopped
    privileged: true
    environment:
      TELEGRAF_HOST: <host>
      TELEGRAF_PORT: <port>
      TELEGRAF_BUCKET: <bucket>
      TELEGRAF_USERNAME: <username>
      TELEGRAF_PASSWORD: <password>
    volumes:
      - /var/run/dbus/:/var/run/dbus/:z

Image is available on DockerHub and everything else is on GitHub.

Capturing Temperature of Govee Bluetooth Sensors

I have an quite a few Govee temperature and humidity sensors. They’re reasonably priced, quite accurate, and they’re bluetooth LE. Yes, that allows them to sip power but at a cost that I cannot reach them when outside of home. Well, unless I get one of Govee hubs and connect them to cloud. But, is there a way to bypass the cloud and push all to my telegraf instance? Well, now there is!

First of all, why Telegraf? Obvious answer is because I have it already setup in my network and connected with my Grafana GUI. Longer answer is because I like the idea of telegraf. You have a centralized database and pushing to it is as easy as sending HTTP request. Everything is quite free-form and any mess you create is sorted out when data is displayed in Grafana.

Next question is, how? Well, I originally planned to roll my own script by abusing bluetoothctl scripting. However, during research I fount out that gentleman named William C Bonner already did pretty much the exact thing I wanted to. His GoveeBTTempLogger already both captures and decodes Govee temperature and humidity data.

And yes, there is no x64 package precompiled but, surprisingly, README.md instructions actually work. That said, I opted to build binaries a bit differently. This allowed me to install binary into /usr/local/bin/.

sudo apt install build-essential cmake git libbluetooth-dev libdbus-1-dev
git clone https://github.com/wcbonner/GoveeBTTempLogger.git
cd GoveeBTTempLogger
cmake -B ./build
sudo cmake --build ./build --target install

Once compiled, we can start application and, hopefully, see all the temperatures.

goveebttemplogger

And, if you just want to see the current values, that’s enough. If you check into README.md a bit more, you can also setup application to output web pages. Unfortunately, there is no telegraf output option. Or thankfully, since this gives me option to roll my own script around this nice tool.

What I ended up with is the following.

TG_HOST=<ip>
TG_PORT=<port>
TG_BUCKET=<bucket>
TG_USERNAME=<user>
TG_PASSWORD=<password>

while IFS= read -r LINE; do
  DATA=`echo "$LINE" | grep '(Temp)' | grep '(Humidity)' | grep '(Battery)'`
  if [ "$DATA" == "" ]; then continue; fi

  DEVICE=`echo $DATA | awk '{print $2}' | tr -d '[]'`
  TEMPERATURE=`echo $DATA | awk '{print $4}' | tr -dc '0-9.'`
  HUMIDITY=`echo $DATA | awk '{print $6}' | tr -dc '0-9.'`
  BATTERY=`echo $DATA | awk '{print $8}' | tr -dc '0-9.'`

  printf "%s %5s°C %4s%% %3s%%\n" $DEVICE $TEMPERATURE $HUMIDITY $BATTERY
  CONTENT="temp,device=$DEVICE temp=${TEMPERATURE},humidity=${HUMIDITY},battery=${BATTERY} `date +%s`"$'\n'
  CONTENT_LEN=$(echo -en ${CONTENT} | wc -c)
  echo -ne "POST /api/v2/write?u=$TG_USERNAME&p=$TG_PASSWORD&bucket=${TG_BUCKET}&precision=s HTTP/1.0\r\nHost: $TG_HOST\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ${CONTENT_LEN}\r\n\r\n${CONTENT}" | nc -w 15 $TG_HOST $TG_PORT
done < <(/usr/local/bin/goveebttemplogger --passive)

This script goes over goveebttemplogger output and extracts device MAC address and its data. That data is then packed into Telegrafs line format and simply posted into nc as raw HTTP output. Not more difficult than wget or curl.

Wrapping this into a service so it runs in the background is an exercise left to the reader.

Decoding Classless Static Route Option

It has been 7 years since I made Classless Static Route Option Calculator. This was to solve my itch with Mikrotik DHCP but I later expanded it to cover other use cases. Calculator is simple: you put it networks and you get hex output. What else would you want?

Well, how about decoding existing entries? Calculator below takes hex output and gives you networks it represents.