Skip to content

Fix display GPIO contention by switching to MQTT subscriber - #6

Merged
timFinn merged 1 commit into
mainfrom
fix/issue-4-display-mqtt-subscriber
May 10, 2026
Merged

timFinn merged 1 commit into
mainfrom
fix/issue-4-display-mqtt-subscriber

Conversation

@timFinn

@timFinn timFinn commented Apr 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #4. Both weatherhat.service and weatherhat-display.service instantiated WeatherHAT() at startup. Under libgpiod v2 the IO Expander interrupt line (BCM4) is requested exclusively per-process, so the second service to start hit OSError: [Errno 16] Device or resource busy, and the display process driving SPI DC in an unknown state produced banded pixel noise on the LCD instead of the UI.

Architecture decision

Consulted the embedded-engineer and architect agents — both independently recommended Option B (MQTT subscriber display) over merging processes or adding an IPC layer. The embedded side confirmed the pin map is disjoint at the HAT level: only the publisher needs BCM4; display buttons (BCM 5/6/16/24) and display SPI pins don't conflict with anything the publisher owns. The architect side confirmed the display touches WeatherHAT for exactly seven scalar values, all already published to sensors/weather/# — so blast radius is one file plus a retained-flag flip on the publisher.

Changes

  • bin/display-interface.py — full refactor. Removed import weatherhat and SensorDataCollector. Added MqttSensorSubscriber (thread-safe, populates SensorState + history buffers from sensors/weather/# topics on paho's network-loop thread). Added stale/offline banner driven by the existing LWT sensors/weather/status topic and a 2 × PUBLISH_INTERVAL staleness threshold.
  • bin/mqtt-publisher.py — send_payload now publishes with retain=True so new subscribers (including the display after a restart) get the latest value on connect instead of waiting up to PUBLISH_INTERVAL. This is also the correct MQTT pattern for state topics and what Home Assistant expects.
  • weatherhat-display.service — loads config/mqtt.env via EnvironmentFile so the display can reach the broker with the same credentials as the publisher.
  • weatherhat/__init__.py — close() now uses getattr guards so __del__ on a partially-initialized instance doesn't mask the real error with AttributeError: 'WeatherHAT' object has no attribute '_poll_thread' (the secondary traceback seen in the issue report).
  • tests/test_weatherhat.py — regression test: simulate SMBus() raising during __init__ and verify close() on the bare instance does not raise.

Depends on

This works on top of already-deployed Pis (update.sh reinstalls the local package), but for fresh installs it depends on PR #5 (issue #3 fix) landing first so that install-service.sh puts the local weatherhat package in the venv — otherwise from weatherhat import history in display-interface.py fails.

Test plan

  • Tests pass locally: `pytest tests/` → 63 passed, 91% coverage
  • Ruff + isort clean
  • On Pi: both services running simultaneously, no EBUSY, no banded noise, LCD shows live data
  • Kill weatherhat.service → display shows "PUBLISHER OFFLINE" banner within one render tick
  • Restart `weatherhat.service` → display recovers; new subscribers see retained values immediately
  • `mosquitto_sub -v -t 'sensors/weather/#'` shows retained flag on temperature/humidity/etc.
  • `journalctl -u weatherhat-display` no longer shows the EBUSY/AttributeError pair
  • Button navigation (A/B/X/Y) still switches views as expected

What I considered and rejected

  • Option A (merge display into publisher) — throws away the process isolation, couples GUI crash to MQTT availability. Only wins if display must work standalone without network.
  • Option C/D (Unix socket/DBus daemon) — invents a new IPC layer when MQTT already is the project's integration bus.

Both weatherhat.service and weatherhat-display.service instantiated
WeatherHAT() at startup. Under libgpiod v2 the IO Expander interrupt
line (BCM4) is requested exclusively per-process, so the second service
to start got `OSError: [Errno 16] Device or resource busy`, and the
display process driving SPI DC in an unknown state produced banded
pixel noise on the LCD instead of the UI. Reported in issue #4.

The publisher is the hardware owner. The display now subscribes to the
same MQTT topics the rest of the stack (Telegraf, HA, Grafana) already
consumes — no new IPC layer, and button GPIO lines (BCM 5/6/16/24) are
disjoint from the sensor interrupt so they can coexist with the
publisher without contention.

- bin/display-interface.py: replace SensorDataCollector with
  MqttSensorSubscriber (thread-safe, populates SensorState + history
  buffers from sensors/weather/# topics). Add stale/offline banner
  driven by the existing LWT status topic.
- bin/mqtt-publisher.py: publish sensor values with retain=True so new
  subscribers (including the display on restart) get the latest value
  on connect rather than waiting PUBLISH_INTERVAL.
- weatherhat-display.service: load config/mqtt.env so the display can
  reach the broker.
- weatherhat/__init__.py: guard close() with getattr so __del__ on a
  partially-initialized instance doesn't mask the real error with an
  AttributeError about _poll_thread (secondary bug seen in the issue).
- tests/test_weatherhat.py: regression test for the __del__ guard.

Fixes #4
@timFinn
timFinn merged commit e718908 into main May 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

display issues - garbage

1 participant