I bought a radio … an old one.

Browsing though secondhand shops, and retro art shops, I came across a cool old piece of history.

I was planning to remove the internals (first checking if its really broken .. it is), and make a radio which plays audio fragments from the 50-60s.
(Music and News)

  • Playing old media using a raspberry
  • Using the original knobs and lights (more on this later)
  • While tuning in on channels, static sound will be heard and country selection will be a used also for music/news selection
  • Maybe I’ll use one of the turning knobs as decade selector (like the old band selector 25m/31m etc)
Back of the radio

Type H265A01.
This one was made in de 50s in Nederlandsche Seintoestellen Fabriek (N.S.F.).
This factory was standing within 100 meters where I currently live

Another interesting thing: It has a Magic Eye (Philips MiniWatt EM4)!
This little thingy works like a mini CRT device.

A magic eye EM4 is a vintage vacuum tube (introduced around 1938–1939) used as a visual tuning indicator in old European radios and test equipment. It glows with a green or yellow-green phosphor pattern that narrows or expands based on signal strength, helping users find the exact radio frequency

Due too the high voltages needed for this device, I’ll be storing this in a safe place, and make a modern alternative using a microcontroller and a round display.

Complete config Immich/Powertools and Kiosk

For ML i’m using a second machine with GPU.

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
    volumes:
      # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
    # Example tag: ${IMMICH_VERSION:-release}-cuda
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
    #   file: hwaccel.ml.yml
    #   service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false

  immich-kiosk:
    image: ghcr.io/damongolding/immich-kiosk:latest
    container_name: immich-kiosk
    environment:
        LANG: "en_GB"
        TZ: "Europe/Amsterdam"
        # Required settings
        KIOSK_IMMICH_API_KEY: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx"
        KIOSK_IMMICH_URL: "https://mypix.henriaanstoot.nl/"
        # External url for image links/QR codes
        KIOSK_IMMICH_EXTERNAL_URL: ""
        # Clock
        KIOSK_SHOW_TIME: true
        KIOSK_TIME_FORMAT: 24
        KIOSK_SHOW_DATE: false
        KIOSK_DATE_FORMAT: YYYY/MM/DD
        KIOSK_CLOCK_SOURCE: client
        # Kiosk behaviour
        KIOSK_DURATION: 60
        KIOSK_DISABLE_SCREENSAVER: false
        KIOSK_OPTIMIZE_IMAGES: false
        KIOSK_USE_GPU: true
        KIOSK_BURN_IN_INTERVAL: 0
        KIOSK_BURN_IN_DURATION: 30
        KIOSK_BURN_IN_OPACITY: 70
        # Asset sources
        KIOSK_SHOW_ARCHIVED: false
        KIOSK_ALBUMS: ""
        KIOSK_ALBUM_ORDER: random
        KIOSK_EXCLUDED_ALBUMS: "ALBUM_ID,ALBUM_ID,ALBUM_ID"
        KIOSK_PEOPLE: ""
        KIOSK_REQUIRE_ALL_PEOPLE: false
        KIOSK_EXCLUDED_PEOPLE: "PERSON_ID,PERSON_ID,PERSON_ID"
        KIOSK_DATES: ""
        KIOSK_TAGS: ""
        KIOSK_EXCLUDED_TAGS: "TAG_VALUE,TAG_VALUE,TAG_VALUE"
        KIOSK_RATING: -1
        KIOSK_EXCLUDED_PARTNERS: "PARTNER_ID"
        KIOSK_MEMORIES: false
        KIOSK_BLACKLIST: "ASSET_ID,ASSET_ID,ASSET_ID"
        # FILTER
        KIOSK_FILTER_DATE: ""
        KIOSK_FILTER_NEWEST: 0
        KIOSK_FILTER_EXCLUDE_FACES: false
        KIOSK_FILTER_FAVORITES: false
        # UI
        KIOSK_SHOW_PROGRESS_BAR: false
        KIOSK_DISABLE_NAVIGATION: false
        KIOSK_DISABLE_UI: false
        KIOSK_FRAMELESS: false
        KIOSK_HIDE_CURSOR: false
        KIOSK_FONT_SIZE: 100
        KIOSK_BACKGROUND_BLUR: true
        KIOSK_BACKGROUND_BLUR_AMOUNT: 10
        KIOSK_THEME: fade
        KIOSK_LAYOUT: single
        KIOSK_SHOW_USER: false
        # Sleep mode
        # KIOSK_SLEEP_START: 22
        # KIOSK_SLEEP_END: 7
        # KIOSK_SLEEP_DIM_SCREEN: false
        # Transistion options
        KIOSK_TRANSITION: none
        KIOSK_FADE_TRANSITION_DURATION: 1
        KIOSK_CROSS_FADE_TRANSITION_DURATION: 1
        # Image display settings
        KIOSK_IMAGE_FIT: contain
        KIOSK_IMAGE_EFFECT: zoom
        KIOSK_IMAGE_EFFECT_AMOUNT: 120
        KIOSK_USE_ORIGINAL_IMAGE: false
        # Video
        KIOSK_SHOW_VIDEOS: true
        KIOSK_LIVE_PHOTOS: false
        KIOSK_LIVE_PHOTO_LOOP_DELAY: 0
        KIOSK_SHOW_ANIMATED_GIFS: false
        # Image metadata
        KIOSK_SHOW_IMAGE_RATING: true
        KIOSK_SHOW_OWNER: false
        KIOSK_SHOW_ALBUM_NAME: true
        KIOSK_SHOW_PERSON_NAME: false
        KIOSK_SHOW_PERSON_AGE: false
        KIOSK_SHOW_IMAGE_TIME: true
        KIOSK_IMAGE_TIME_FORMAT: 24
        KIOSK_SHOW_IMAGE_DATE: true
        KIOSK_IMAGE_DATE_FORMAT: YYYY-MM-DD
        KIOSK_SHOW_IMAGE_DESCRIPTION: true
        KIOSK_SHOW_IMAGE_CAMERA: false
        KIOSK_SHOW_IMAGE_EXIF: true
        KIOSK_SHOW_IMAGE_LOCATION: true
        KIOSK_HIDE_COUNTRIES: "HIDDEN_COUNTRY,HIDDEN_COUNTRY"
        KIOSK_SHOW_IMAGE_ID: false
        KIOSK_SHOW_IMAGE_QR: false
        KIOSK_SHOW_MORE_INFO: true
        KIOSK_SHOW_MORE_INFO_IMAGE_LINK: true
        KIOSK_SHOW_MORE_INFO_QR_CODE: true
        # More info actions
        KIOSK_LIKE_BUTTON_ACTION: favorite
        KIOSK_HIDE_BUTTON_ACTION: tag
        # Kiosk settings
        KIOSK_PORT: 3000
        KIOSK_BEHIND_PROXY: false
        KIOSK_DISABLE_URL_QUERIES: false
        KIOSK_DISABLE_CONFIG_ENDPOINT: false
        KIOSK_ENABLE_URL_BUILDER: false
        KIOSK_WATCH_CONFIG: false
        KIOSK_FETCHED_ASSETS_SIZE: 1000
        KIOSK_HTTP_TIMEOUT: 20
        KIOSK_PASSWORD: ""
        KIOSK_CACHE: true
        KIOSK_PREFETCH: true
        KIOSK_ASSET_WEIGHTING: true
    ports:
      - 3000:3000
    restart: always
    healthcheck:
      test: ["CMD", "/kiosk", "--healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s


  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
      # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
      # DB_STORAGE_TYPE: 'HDD'
    volumes:
      # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb
    restart: always
  power-tools:
    container_name: immich_power_tools
    image: ghcr.io/immich-power-tools/immich-power-tools:latest
    volumes:
      - immich-power-tools-data:/app/data
    ports:
      - "8001:3000"
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  model-cache:
  immich-power-tools-data:

Onkyo old to new

Our Onkyo was giving problems, some channels are breaking up.
To have some channels back, we needed to crank up the volume for a few seconds.

So I thought .. preamps!

So I opened the Onkyo and started investigating.

We used it as a 5.1 surround sound system. So I’ve got PreAmp channels to spare, so lets switch the boards!


Well, that didn’t work.
Reading forums, it COULD be the sound chip, on the main board.
Because of the heat over the years, it could losing contact with the main PCB.

So I put my heatgun to work, but to no avail.
One of the square chips on the right in first photo.

The AMP being 15+ years old, we decided to get a new one.
Researching best amp’s in 2026 … in our price range.

Onkyo TX-RZ50

Amazing …

Using the DIRAC sound setup, perfect again.

Using the same IP, our Home Assistant setup mostly worked without changing much.

Cheap yellow display

I’ve posted about these a while ago. (OpenHASP)

I bought some new ones.
These will be used for serial logging, plotting data and terminal emulation for a 6502 system.

I’ve got some M5Stack credit card sized keyboards, so next to do is implement these instead of using the onscreen keyboard.

Immich Workflow Filters

UPDATE 20260729 : There are EXIF filters in workflows now!

Put your media in the correct directories using workflows.

Below the regex for a Samsung phone

Image
^\d{8}_\d{6}\.(jpg|jpeg|heic|png)$

Video
^\d{8}_\d{6}\.(mp4|mov|mkv)$

WhatsApp Image
^IMG-\d{8}-WA\d+\.(jpg|jpeg|png|webp)$

WhatsApp Video
^VID-\d{8}-WA\d+\.(mp4|3gp)$

Signal Image
^(signal-|Signal-).*\.(jpg|jpeg|png|webp)$

Signal Video
^(signal-|Signal-).*\.(mp4|mov)$

Flashing a new ROM on an Android tablet.

It has been a long time, since I flashed the last one.
Some things have changed.

I’ve flashed tablets and phones, now a tablet again.

The small tablet was really slow, and could not be upgraded to support a new webkit version.

So I installed a minimal Lineage OS on it.

Now it can be used as a immich web kiosk.

Some notes for myself

adb reboot bootloader
adb fastboot oem unlock
fastboot flash recovery twrp-3.7.0_9-1-flo_followmsi.img # this didn´t work, needed an older version
twrp-3.3.0-0-flo.img worked
adb push lanchon-repit-20210221-system=max-cache=16M+wipe-flo.zip /tmp/lanchon-repit-20210221-system=max-cache=16M+wipe-flo.zip
also lineage-20.0-20230830-UNOFFICIAL-flo.zip
adb install -r F-Droid.apk
adb install -r Rotation+Control_1.1_APKPure.apk # This because trebuchet rotate is broken.

While waiting for flashing, I installed nerdfonts, and patched them with cool icons. Then I installed superfile, a terminal file manager. Nice!

Dual PICO UART test with CirquitPython

Test for UART serial communication for my whack-a-mole game

CODE for Client

import board
import busio
import time

uart = busio.UART(
    tx=board.GP16,
    rx=board.GP17,
    baudrate=115200,
    timeout=1,
)

time.sleep(2)

commands = ["PING", "HELLO", "TEST"]
while True:
    for cmd in commands:
        print("Sending:", cmd)
        uart.write((cmd + "\n").encode())

        response = uart.readline()

        if response:
            print("Response:", response.decode().strip())
        else:
            print("No response")

        time.sleep(1)

CODE for server

import board
import busio
import digitalio
import time

# UART pins
uart = busio.UART(
    tx=board.GP0,
    rx=board.GP1,
    baudrate=115200,
    timeout=0.1
)

# Onboard LED
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

print("UART LED server ready")

while True:
    data = uart.readline()

    if data:
        command = data.decode("utf-8").strip()
        print("Received:", command)

        if command == "PING":
            # Blink LED
            led.value = True
            time.sleep(0.2)
            led.value = False

            # Reply
            uart.write(b"PONG\n")

        else:
            uart.write(b"UNKNOWN\n")

    time.sleep(0.01)

New CO2 sensor

Today I got, amongst other goodies a new CO2 sensor in the mail.

This is a MH-Z19C CO2/Temperature sensor.
(Easy to implement with ESPHome)

Calibrate using HD 7 seconds to GND
(Do this in a well ventilated room)

YAML Code

# Board: Wemos D1 Mini (ESP8266) (Wemos)
# Definition: definitions/boards/d1_mini/manifest.yaml

esphome:
  name: newco2
  friendly_name: NewCO2

esp8266:
  board: d1_mini

logger:

api:
  encryption:
    key: "c/vSFcxi1YgYlXtvsjXXXXXXXXXXXXXXXXXXXXXX="

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: NewCO2 Fallback Hotspot
    password: "tepe9XXXXXXXXXXX"

captive_portal:

uart:
  - baud_rate: 9600
    rx_pin: 4
    tx_pin: 5
    id: uart_1

sensor:
  - platform: mhz19
    uart_id: uart_1
    co2:
      id: sensor_mhz19_1_co2
      name: CO2
    temperature:
      id: sensor_mhz19_1_temperature
      name: Temperature
    id: sensor_mhz19_1
    automatic_baseline_calibration: true
    detection_range: "5000ppm"
    
switch:
  - platform: template
    name: "MH-Z19 ABC"
    optimistic: true
    on_turn_on:
      mhz19.abc_enable: sensor_mhz19_1
    on_turn_off:
      mhz19.abc_disable: sensor_mhz19_1

light:
  - platform: status_led
    name: status
    id: light_status_led_1
    pin: GPIO12

Blender plugin for 8 potmeter input

This weekend I saw a corridor crew video about controlling CGI objects using input devices.

This made me think of a half finished project I did a while ago.
So lets finish it.

(I know, dirty screen .. too much outside hacking)

8Angle M5 thingy using I2C (Pin D1 and D2)

CODE for wemos

Very simple code to read pots using I2C and printing values on the serial output using the wemos.
Only needs 5V,GND,SDA,SCL (D1 D2)

279,206,520,1023,1023,60,300,985,0
Output example 8 potmeters 0-1023 value and last value 0 or 1 for the switch

Serial plotter example below

#include "m5angle8.h"

M5ANGLE8 MM;

void setup()
{
  Serial.begin(115200);

  Serial.println();
  delay(100);

  Wire.begin();
  MM.begin();
}


void loop()
{
  for (int ch = 0; ch < 8; ch++)
  {
    Serial.print(MM.analogRead(ch, 10));
    Serial.print(",");
    delay(1);
  }
  Serial.print(MM.inputSwitch());
  Serial.print("\n");
  delay(100);
}

Plugin for Blender

filename unit8angle_blender/__init__.py

made into a zip you can install using

zip -r myplugin.zip unit8angle_blender

Blender edit > preferences > add-ons > Install from disk

import bpy
import serial
import serial.tools.list_ports

ser = None


def open_serial():
    global ser

    if ser:
        return

    # Change this to your COM port
    ser = serial.Serial("/dev/ttyUSB0",115200,timeout=0)


class SERIAL_OT_start(bpy.types.Operator):
    bl_idname = "wm.unit8_start"
    bl_label = "Start Unit8"

    _timer = None

    def modal(self, context, event):

        if event.type == 'TIMER':

            global ser

            if ser and ser.in_waiting:

                line = ser.readline().decode(errors="ignore").strip()

                try:

                    values = list(map(int,line.split(",")))

                    if len(values) >= 8:

                        obj = context.active_object

                        if obj:

                            obj.location.x = values[0] / 100.0
                            obj.location.y = values[1] / 100.0
                            obj.location.z = values[2] / 100.0

#                            obj.location.x = values[0] / 1023.0
#                            obj.location.y = values[1] / 1023.0
#                            obj.location.z = values[2] / 1023.0

                            obj.rotation_euler.x = values[3] / 1023.0 * 6.28318
                            obj.rotation_euler.y = values[4] / 1023.0 * 6.28318
                            obj.rotation_euler.z = values[5] / 1023.0 * 6.28318

                            s = 0.1 + values[6] / 1023.0 * 3.0
                            obj.scale = (s,s,s)

                except Exception:
                    pass

        return {'PASS_THROUGH'}

    def execute(self, context):

        open_serial()

        wm = context.window_manager

        self._timer = wm.event_timer_add(0.01, window=context.window)

        wm.modal_handler_add(self)

        return {'RUNNING_MODAL'}


class SERIAL_PT_panel(bpy.types.Panel):
    bl_label = "Unit8Angle"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Unit8'

    def draw(self, context):
        self.layout.operator("wm.unit8_start")


classes = (
    SERIAL_OT_start,
    SERIAL_PT_panel,
)


def register():
    for c in classes:
        bpy.utils.register_class(c)


def unregister():
    for c in reversed(classes):
        bpy.utils.unregister_class(c)