Forwarding Makefile Targets to a Script

I love make files. There is something special when you just run make and all gets built automatically. Even better, you can use multiple targets to chain a few operations together, e.g., make clean test debug All this is available to you under Linux. Under Windows, all this magic is gone.

For Windows, most of the time I see either a separate script handling build tasks, or nothing at all. A separate script is not a bad solution but it does introduce a potential difference between builds. Assuming you have Git installed, the easiest way out is to simply forward Makefile entries to the bash script. Something like this:

.PHONY: clean test debug release

clean:
	@./Make.sh clean

test:
	@./Make.sh test

debug:
	@./Make.sh debug

release:
	@./Make.sh release

And honestly, this is probably good enough. If you are on linux, you use make debug and on Windows you use Make.sh debug. For years now I used this approach whenever I needed things to work on both Linux and Windows. But there were issues - mainly with target “chaining”.

For example, if you want to run clean as a prerequisite to release, you can do that in Makefile.

…

clean:
	@./Make.sh clean

release: clean
	@./Make.sh release

This will, under Linux, do what you expect it. But, under Windows, this is not enough. So, alternatively, you might leave Makefile as-is and do the chaining in Make.sh. And that works on Windows but, under Linux, it will double call to clean, i.e.,

make clean release

will translate into

./Make.sh clean    # first call doing only clean
./Move.sh release  # second call internally does clean again

It’s not the worst issue out there and god knows I lived with it for a long time. What I need was to just forward whatever arguments I receive in make command to my Make.sh script. Reading GNU make documentation did point toward MAKECMDGOALS special variable that was exactly what I needed. It even pointed to last resort %:: syntax. So, the following Makefile looked to be all I needed.

%::
	@./Make.sh $(MAKECMDGOALS)

Only if life was that easy. This last-resort rule will unfortunately call script once for each target given to make. I.e., the final call in our example would be:

./Make.sh clean release
./Move.sh clean release

And there is no fool-proof way I found to prevent the second run. You cannot set a variable, you cannot really detect which argument you’re forwarding, you cannot exit. You could write in file that you are already running but that gets messy when task is cancelled.

I spent quite a lot of time messing with this but I never found a generic way. But, I finally managed to find something incredibly close.

all clean run test debug release &:
	@./Make.sh $(MAKECMDGOALS)

As long as you list all targets, listing only one or all of them will lead to the same command. And, because they are all grouped together, it will run it only one. It’s not ideal because I do need to keep target list in two places, but that list is not likely to change.

If you want to check my whole build script, you can check my GitHub.

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.