ESPHome on the GeekMagic HelloCubic-Lite

I was gifted a GeekMagic HelloCubic-Lite as a tinker toy and of course I could not resist to put ESPHome on it as soon as I got it. The GeekMagic Ultra seems to be officially supported by ESPHome, bot the HelloCubic isn’t. Based on this excellent thread I managed to get it working fairly easily.

While it may be possible to flash ESPHome to the device directly from the original firmware, I decided to immediately open the device and look for any serial pins that I could attach to, and luckily for us the board provides all the necessary pins.

Opening the device is fairly easy. If you look closely to the front you can see a little notch on top of the display. With a small screw driver you can very gently lift the plastic as it’s just held in place with some double sided tape. Just make sure not to slip or scratch the screen.

Once inside you can see the UART pins. You can connect this to any USB-Serial adapter, just make sure to connect the TX side on the adapter with the RX side on the board. Make sure to also connect GND to your adapter.

Once connected over serial, it’s time to flash the first ESPHome firmware. It’s easiest to first flash a very basic firmware with just wifi and OTA functionality.

substitutions:
  plug_name: hellocubic-lite
  plug_id: hellocubic-lite
  ip: 10.0.0.2 # or whatever you want the IP to be
  comment: HelloCubic-Lite

mdns:
  disabled: true

esphome:
  name: ${plug_name}
  comment: ${comment}
esp8266:
  board: nodemcuv2 #esp8285

# WiFi connection
wifi:
  ssid: !secret new_wifi_ssid
  password: !secret new_wifi_password
  fast_connect: false
  reboot_timeout: 6h
  power_save_mode: high
  min_auth_mode: wpa3
  manual_ip:
    static_ip: ${ip}
    gateway: 10.0.0.1
    subnet: 255.255.0.0

api:
  reboot_timeout: 14h
  encryption:
    key: !secret encryption_key

ota:
  platform: esphome
  password: !secret ota_password

In ESPHome, hit the install button and under advanced options, choose download firmware.

After the firmware has compiled, download the firmware image to your pc. We’ll be flashing the device using esptool. I like to install it in a Python virtualenv on linux, but feel free to use whatever flashing tool you prefer (I’m using Linux, so on Windows or Mac you might need different commands or tools).

python3 -m virtualenv .venv
. .venv/bin/activate
pip3 install esptool

With the board connected to the USB-Serial adapter, make sure to connect FLASH to GND before powering up the board via USB-c. A blue light should briefly flash but the screen should not display anything. We’re now in flash mode and it’s time to write our first ESPHome binary to the device.

esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash 0x00000 ~/Downloads/hellocubic-lite-firmware.bin

Once esptool finished flashing the device, unplug it from USB, disconnect the FLASH pin and power it up again. You should see absolutely nothing on the screen, but if everything goes right, the device should come online in ESPHome.

Now it’s time to get the display up and running by adding the following configuration.

spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13

output:
  - platform: esp8266_pwm
    pin: GPIO05
    frequency: 60 Hz
    id: pwm_output
  
light:
  - platform: monochromatic
    output: pwm_output
    name: "Display Backlight"

display:
  - id: geekmagic
    platform: mipi_spi
    model: GEEKMAGIC-SMALLTV
    dimensions:
      height: 240
      width: 240
      offset_height: 0
      offset_width: 0
    dc_pin: GPIO00
    reset_pin: GPIO02
    cs_pin: GPIO15 # dummy, it's wired to GND actually
    spi_mode: mode3 # Important for CS-less design
    color_depth: 8
    update_interval: 30s # Do not set this too low, the device has very little memory
    invert_colors: true
    buffer_size: 20%
    transform:
      mirror_x: true # This mirrors the image left-to-right as the cube will mirror the screen
      mirror_y: false
      swap_xy: false
    show_test_card: true

Flash this image over wifi to your device and if everything goes right, you should see a test card on your cube.

At this point, it’s completely up to your imagination on what you want to show on the cube, but if you want some inspiration, feel free to have a look at my git repository.

lnxsense sensor panel for Linux

A while ago I started creating an alternative hardware monitoring application for Linux written in Java 25. Since it has remote monitoring capabilities, it also opens to door for setting up a sensor panel for your computer using a modern Android phone. In this post I’ll describe how to setup both lnxsense and lnxsense-panel-android.

A photo of an Android phone running the lnxsense panel app for Android. The application visualizes multiple sensors like CPU/GPU usage and CPU/GPU temperature using dials. Yellow dials means that a sensor value is reading a higher than normal value.

Please be aware that all of this is still in very early alpha stages. Lnxsense does not yet support reading sensors from AMD CPU’s and AMD GPU’s and the Android app is only available as a debug build for Android 14 and newer and can’t be installed from F-droid (and it will probably never be available on the Play Store either). Please know that you will need some knowledge about Linux (you will need to provide root access) and you will need to sideload an unsigned APK to your phone. If you do not know what sudo implies or what sideloading APK’s implies, please avoid doing it altogether.

Installing lnxsense

In this post I’ll be using the pre-built binary to run lnxsense on my computer. You can download the latest version from Codeberg. You can extract the tarball (e.g. lnxsense-0.1.0.tar.gz) anywhere you want, I decided to go for ~/lnxsense/lnxsense-0.1.0/ .

You will also need to install some dependencies, which may be called slightly differently on different distributions. For Arch based distributions like Arch, Manjaro and CachyOS you can run this commands to install the required dependencies

sudo pacman -Syu jdk-openjdk libcpuid libblockdev libblockdev-smart libblockdev-nvme lib32-lm_sensors smartmontools

For Debian based distributions like Debian, Ubuntu, Mint and Pop!_Os you should run the following command to install the required dependencies.

sudo apt-get update
sudo apt-get install openjdk-25-jdk libcpuid-dev libsensors-dev smartmontools
sudo apt-get install libblockdev-dev libblockdev-nvme-dev libblockdev-smart-dev --no-install-recommends

By default, lnxsense works completely locally and does not allow remote connections to it. Go to the folder where you extracted lnxsense and edit the file launch_server.yaml which you can find in the server folder. Change these lines:

socket:
  # Either UNIX or INET
  socketType: UNIX
  # Where the UI opens the socket if socketType is UNIX
  socketPath: /tmp/lnxsense.socket
  # Listen address if socketType is INET, should never be empty
  socketAddress: "127.0.0.1:9999"

to this:

socket:
  # Either UNIX or INET
  socketType: INET
  # Where the UI opens the socket if socketType is UNIX
  socketPath: /tmp/lnxsense.socket
  # Listen address if socketType is INET, should never be empty
  socketAddress: "0.0.0.0:9999"

Now you can open a terminal, navigate to the directory where you extracted lnxsense and run the following command:

sudo java \
  -XX:+UseCompactObjectHeaders \
  -Xms32M \
  -Xmx32M \
  -XX:+AlwaysPreTouch \
  -XX:MaxMetaspaceSize=64M \
  -XX:+UseZGC \
  -XX:MaxDirectMemorySize=1m \
  -XX:+UseCompressedOops \
  --enable-preview \
  --enable-native-access server,libblockdev,libcpuid,libsensors,nvml \
  --module-path server/lib/ \
  --add-modules server,libblockdev,libcpuid,libsensors,nvml \
  -Dspring.config.location=file://$PWD/server/launch_server.yaml \
  com.pw999.lnxsense.Server
A screenshot of the lnxsense server application running in the console.

This will ask you for your user’s password because the application needs quite some privileges to access all the hardware sensors. You can run it without root privileges (just remove the sudo part in the command) but you won’t have access to for example the Intel CPU’s power usage or your disks health report (S.M.A.R.T.).

Once started you will see a lot of logs appearing but at the end you should see something like

2026-04-05T10:20:31.365+02:00 INFO 15776 --- [lnxsense-server] [e-socket-server] com.pw999.lnxsense.Socket : Starting socket thread
2026-04-05T10:20:31.370+02:00 INFO 15776 --- [lnxsense-server] [e-socket-server] com.pw999.lnxsense.Socket : Server will be listening on Inet socket 0.0.0.0:9999

Installing the Android sensor panel apk

At this point I will assume you already know what sideloading is, how you enable it (which is about to get a lot more difficult to do) and that you know what you’re doing. If you do not know what you’re doing I would advise against enabling developer mode on your phone.

On your phone, please download the latest APK from Codeberg and install it. To connect from your phone to lnxsense you will need the IP address of your computer. I usually run ip a in a shell and look for something like eth0 or enpXsY, but this results may vary on your setup.

When you launch the app on your phone you will be greeted with the following screen, here you can either enter the IP address of your machine or pick one that you saved previously. After entering the IP address you can either directly connect to it or save it as profile using the bookmark looking thing next to the Connect button.

A screenshot of the lnxsense panel app for Android showing the initial connection screen.

Once connected to the server, you can see all the live sensor data from your computer:

A screenshot of the lnxsense panel app for Android showing all the available sensors (of the computer running lnxsense) and their current readings.

You can tap on a sensor to get a small line graph of the sensor’s values with the min/max/avg values of that sensor. Tapping the line graph will maximize it.

A screenshot of the lnxsense panel app for Android showing the mini line graph for the CPU's frequency along with the min/max/avg values of the CPU frequency.

You can also long press sensors to assign them to one of the dials. This can also be done via the configuration screen (tap the cogwheel on the top of the app) where you can also set the number of rows and columns for the dial screen.

A screenshot of the lnxsense panel app for Android showing the assign to dial shot modal.

Once you’ve assigned the sensors you want to monitor in the panel you can open the dial view using the button with all those squares at the top of the application.

A screenshot of the lnxsense panel app for Android showing the dials for different sensors.

Now you can monitoring your systems hardware sensors from an Android phone while playing games on Linux or doing other full-screen activities. As said in the introduction, it’s all very, very early alpha stage and while it works on my computer, results may vary depending on your setup.

lnxsense, a system monitoring tool for Linux

Ever since I got my AMD Athlon XP 2500+, I’ve been into overclocking. While my overclocking activities were limited at the time (as a student I couldn’t risk burning up my CPU or motherboard), I made sure that ever since, none of my desktops ran at stock speeds. Even my trusty Intel 2500K that I’m writing this blog on still hums along at 4,4Ghz all core.

Overclocking has always been a Windows thing though, and for good reason; in 2009 the Linux market share was only 0,6%, while Windows dominated the market with a 95% market share. With such a dominating OS, motherboards manufacturers focused fully on (usually terrible) software which allowed you to overclock and monitor your system without leaving Windows. The overclocking community didn’t stop there either, tools like 8rdavcore (apparently ported from Linux), setfsb, MemSet, CPU-Tweaker and many more made it possible to overclock and tweak your system to the max. Combined with a lot of monitoring software like HWInfo, Aida64, SpeedFan, CPU-z and benchmarks like 3Dmark, Sisoft Sandra, Cinebench, and it was clear: overclocking belonged to Windows.

Fast forward to 2025, and things have changed; Linux has a market share of 3% while Windows has dropped to 66%. OCCT is now also available on Linux, GreenWithEnvy makes it easier to overclock NVIDIA gpu’s and benchmarks like y-cruncher, 7-zip and Geekbench run fine on Linux. But when it comes to graphical monitoring applications, we only have Psensor or xsensors. Both work fine but it can still be better.

A screenshot showing xsensors and psensor side by side
Xsensors and PSensor side by side

This is where I want to change a couple of things and after this years release of Java 25 and its Foreign Function and Memory API, I can finally work in a language I love while using C libraries like libsensors, libcpuid, the NVIDIA management API and many more.

After returning from Devoxx I decided to create a Linux alternative to Open Hardware Monitor, HWMonitor and HWInfo and that’s how lnxsense was born. It’s a still in early alpha stages and what it can show depends heavily on what the underlying libraries can return (e.g. NVIDIA’s nvml doesn’t even have an option to get the hotspot temperature or actual fan RPM). Even so, I’m already really happy with what it can do.

lnxsense showing different metrics like cpu usage, power draw, GPU frequency.

In it’s very early stage it supports (when running the back-end server as root)

  • CPU Frequencies (as reported by the Linux kernel)
  • CPU Utilization
  • Memory Utilization
  • Core temperatures
  • Intel requested VCore (the VID)
  • Intel Core multipliers
  • Intel Throttling reasons
  • Intel RAPL Power Management information like PP0, PP1 and Platform power limits and usage
  • NVIDIA Clocks, Utilization, Temperature and Fan speed (in % because why would nvml expose the actual fan speed), P-state and current PCIe speed
  • SMART and NVMe log
  • Blockdevice IOPS and read/write speed
  • Remote monitoring using sockets

If you want to try it out, you can download a release version from Codeberg. Just be sure to read the INSTALL.md, it’s still in early development, so it’s not a one-click experience and definitely not production-ready.

// 2025/12/15: I decided to rename the project from HWJinfo to lnxsense, it just makes more sense, doesn’t it ?

Ansible: VARIABLE IS NOT DEFINED!

So, I let my certificates expire (again) and thus I had to re-run all my Ansible playbooks to roll out my new self-signed certificates on all my severs and the reality was that a lot of my playbooks didn’t run or didn’t survive the galaxy update I ran a couple of weeks before this happened.

The weirdest thing of all was that the Postgres role that I use failed on an assert for a variable that 100% exists and which has worked before.

TASK [robertdebock.postgres : assert | Test postgres_hba_entries] *************************************************************************************************************************************************************************************************************
fatal: [postgresql-01]: FAILED! => changed=false 
  assertion: postgres_hba_entries is defined
  evaluated_to: false
  msg: Assertion failed

Running an ansible.builtin.debug in a pre-task did confirm that the variable “did not exist”

  pre_tasks:
    - name: Debug
      ansible.builtin.debug:
        var: postgres_hba_entries
TASK [Debug] ******************************************************************************************************************************************************************************************************************************************************************
ok: [postgresql-01] => 
  postgres_hba_entries: VARIABLE IS NOT DEFINED!

Even with the verbosity set to 6 there was no sign of anything being wrong. While debugging other variables, I noticed the same behavior when trying to output the value of postgres_listen_addresses: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}" while gathering facts was disabled.

As it turns out, if you’re using an unknown variable to create another variable, then it will simply not exist, even if you’re using it in a map like postgres_hba_entries. So in the below example, the non existing DOES_NOT_EXIST variable will result in the complete map missing from the environment.

postgres_hba_entries:
  - type: local
    database: all
    user: all
    method: peer
  - type: host
    database: all
    user: all
    address: 127.0.0.1/32
    method: ident
  - type: hostssl
    address: all
    database: "{{ DOES_NOT_EXIST }}"
    method: md5
    user: all

This is on some ways pretty good, because you don’t want to roll out only half of your config without knowing it. On the other hand it’s pretty annoying there’s absolutely no feedback about what is going on (even though I can come up with many reasons why it is so).

Using a read-only SMB share in a root-less Immich setup with SELinux enabled

I recently wanted to switch from Google Photos to Immich and while doing so I stumbled across some difficulties while adding the photo’s on my NAS as an external library. In the past 20+ years I organized my library by hand without relying on any tools, so I did not want Immich to make any changes to my photo library, hence I mounted the Samba share as read-only.

//nas.internal/photo /mnt/photo cifs credentials=/root/samba.cred,ro,nodev,noexec,nosuid,gid=0,dir_mode=0777,file_mode=0444 0 0

If I try to add a folder from this share as an external library I get the following error: “Lacking read permissions for folder”

Disabling SELinux would fix the issue, but even if the instance is not publicly available, it’s still a bad idea to disable any security measures. So we need to tell SELinux it’s fine for the container to access the share. Usually this is done by appending :z to the volume:

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.ml.yml
    #   service: vaapi # 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
      - /var/immich/media/:/usr/src/app/upload:z
      - /etc/localtime:/etc/localtime:ro
      - /mnt/photo:/usr/src/app/external:z
    env_file:
      - immich.env
    ports:
      - '2283:2283'
    depends_on:
      - database
    restart: always
    healthcheck:
      disable: false

But simply adding “:z” in the Docker compose file won’t work for two reasons:

  1. The user does not have any root privileges to change the SELinux context
  2. The filesystem is mounted read-only and changing the context is a write operation

Luckily, we can mount the SMB share with an SELinux content which will allow the container to access the files:

//nas.internal/photo /mnt/photo cifs credentials=/root/samba.cred,ro,nodev,noexec,nosuid,gid=0,dir_mode=0777,file_mode=0444,context="system_u:object_r:container_file_t:s0" 0 0

To apply the changes we need to unmount/remount the share

# stop the immich containers first
podman compose down
# Remount (mount -o remount won't work, remount can't change permissions)
sudo umount /mnt/photo
sudo mount /mnt/photo

If we now restart Immich and add the Samba share we can see that it can access the files: