Skip to content

Commit d54e00a

Browse files
author
lee
committed
Add PICLOCK_TROUBLE_DOTS to invert indicator dots into trouble lights
1 parent f481d84 commit d54e00a

8 files changed

Lines changed: 51 additions & 6 deletions

File tree

‎AGENTS.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ hardware-touching part is isolated:
8383
the loop polls it every tick). It feeds the **upper-left** indicator dot: lit while the
8484
link is up, dark when the link drops or the interface disappears (e.g. firmware crash).
8585
Any read error → down. `PICLOCK_WIFI_DOT` / `PICLOCK_WIFI_IFACE` configure it.
86+
Both indicator dots share a polarity switch, `PICLOCK_TROUBLE_DOTS` (`Config.trouble_dots`,
87+
default off): off = status lights (lit while nominal); on = trouble lights (lit only when
88+
off-nominal, so a healthy clock shows just the time). Applied in `app.py` via `_dot_lit()`.
8689
- [piclock/config.py](piclock/config.py) — `Config` dataclass with `PICLOCK_*` env overrides
8790
(brightness, schedule on/off + times + wake/night brightness, 24h, blink, sync dot +
8891
freshness window, wifi dot + interface, I2C

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Set via environment variables (typically in the systemd unit):
130130
| `PICLOCK_SYNC_FRESH` | `4500` | Seconds since the last successful sync before the sync dot goes dark (~2× timesyncd's max poll interval) |
131131
| `PICLOCK_WIFI_DOT` | `1` | Upper-left dot lit while the Wi-Fi link is up, dark when it drops; `0` disables the dot |
132132
| `PICLOCK_WIFI_IFACE` | `wlan0` | Interface the Wi-Fi dot watches |
133+
| `PICLOCK_TROUBLE_DOTS` | `0` | `1` inverts both dots into trouble lights: dark while nominal, lit only when NTP is stale/unsynced or the Wi-Fi link is down |
133134
| `PICLOCK_I2C_BUS` | `1` | I2C bus number |
134135
| `PICLOCK_I2C_ADDRESS` | `0x70` | HT16K33 address |
135136
| `PICLOCK_TICK` | `0.2` | Loop period, seconds |

‎SOFTWARE_RUNBOOK.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ The commands travel over a Unix socket (`/run/piclock/ctl.sock`, mode 0660) gate
148148

149149
Config via `PICLOCK_*` env (in the unit): `PICLOCK_24H=1`, `PICLOCK_BRIGHTNESS=0..15`,
150150
`PICLOCK_BLINK=0/1`, `PICLOCK_SYNC_DOT=0/1` (lower-left dot lit while NTP-synced, dark when
151-
free-running; on by default), `PICLOCK_DISPLAY=null` (run without hardware), and
151+
free-running; on by default), `PICLOCK_WIFI_DOT=0/1` + `PICLOCK_WIFI_IFACE=wlan0`
152+
(upper-left dot lit while the Wi-Fi link is up, polled from sysfs every tick; on by
153+
default), `PICLOCK_TROUBLE_DOTS=0/1` (`1` inverts both dots into trouble lights — dark
154+
while nominal, lit only on stale/unsynced NTP or a down link; off by default), `PICLOCK_DISPLAY=null` (run without hardware), and
152155
`PICLOCK_WAKE_BRIGHTNESS` / `PICLOCK_NIGHT_BRIGHTNESS` (0-15 levels the optional day/night
153156
schedule applies at its wake and dim edges — see §5; the schedule's on/off switch and times
154157
live in its drop-in, also §5). The unit sets

‎piclock/app.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
_BLANK_TEXT = " "
3434

3535

36+
def _dot_lit(nominal: bool, trouble: bool) -> bool:
37+
"""Indicator polarity: status lights are lit while nominal, trouble lights
38+
only while off-nominal (see Config.trouble_dots)."""
39+
return (not nominal) if trouble else nominal
40+
41+
3642
def run(config: Config | None = None) -> None:
3743
logging.basicConfig(
3844
level=logging.INFO,
@@ -187,12 +193,14 @@ def _handle_signal(signum, _frame):
187193
blink=config.blink_colon,
188194
)
189195
text, colon = state.text, state.colon
190-
sync_dot = state.sync_dot and config.sync_dot and sync_fresh
196+
sync_ok = state.sync_dot and sync_fresh
191197
# Wi-Fi status is not a function of the time, so it joins here
192198
# rather than passing through render(). Polled per tick: it is
193199
# one sysfs read (~140 us), and per-second freshness is the
194200
# point of the indicator.
195-
wifi_dot = config.wifi_dot and wifi.is_wifi_up(config.wifi_iface)
201+
wifi_ok = wifi.is_wifi_up(config.wifi_iface) if config.wifi_dot else True
202+
sync_dot = config.sync_dot and _dot_lit(sync_ok, config.trouble_dots)
203+
wifi_dot = config.wifi_dot and _dot_lit(wifi_ok, config.trouble_dots)
196204
else:
197205
# display off
198206
text, colon, sync_dot, wifi_dot = _BLANK_TEXT, False, False, False

‎piclock/config.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class Config:
9797
# in-process file read, no subprocess.
9898
wifi_dot: bool = True
9999
wifi_iface: str = "wlan0"
100+
# Indicator polarity. Default (False): the dots are *status* lights — lit
101+
# while everything is nominal (synced / link up). True: the dots are
102+
# *trouble* lights — dark while nominal and lit only when something is
103+
# wrong (sync stale or never achieved / link down), so a healthy clock
104+
# shows nothing but the time. Each dot's enable flag still applies.
105+
trouble_dots: bool = False
100106
tick_seconds: float = 0.2 # loop granularity; colon blink derives from wall clock
101107
backend: str = "ht16k33" # "ht16k33" for hardware, "null" for off-Pi dev/testing
102108

@@ -170,6 +176,7 @@ def from_env(cls) -> "Config":
170176
),
171177
wifi_dot=_env_bool("PICLOCK_WIFI_DOT", cls.wifi_dot),
172178
wifi_iface=os.environ.get("PICLOCK_WIFI_IFACE", cls.wifi_iface).strip(),
179+
trouble_dots=_env_bool("PICLOCK_TROUBLE_DOTS", cls.trouble_dots),
173180
tick_seconds=_env_float("PICLOCK_TICK", cls.tick_seconds),
174181
backend=os.environ.get("PICLOCK_DISPLAY", cls.backend).strip().lower(),
175182
ctl_socket=os.environ.get("PICLOCK_CTL_SOCKET", cls.ctl_socket),

‎systemd/piclock.service‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ Environment=PICLOCK_CTL_SOCKET=/run/piclock/ctl.sock
3030
# Keep each value on its own line and put comments on their own `#` lines.
3131
# PICLOCK_BRIGHTNESS: 0-15; 0 = dimmest, for a dark bedroom.
3232
Environment=PICLOCK_BRIGHTNESS=0
33-
# PICLOCK_24H: 1 = 24-hour time (the default), 0 = 12-hour (leading-zero hour
34-
# blanked, no PM indicator). Uncomment for 12-hour.
35-
# Environment=PICLOCK_24H=0
33+
# PICLOCK_24H: 1 = 24-hour time, 0 = 12-hour (leading-zero hour blanked, no PM
34+
# indicator). Flip to 1 (or comment out) to go back to 24-hour.
35+
Environment=PICLOCK_24H=0
3636
# PICLOCK_BLINK=1 makes the colon blink at 1 Hz (default: steady).
3737
# Environment=PICLOCK_BLINK=1
38+
# PICLOCK_TROUBLE_DOTS=1 inverts the two indicator dots into trouble lights:
39+
# dark while Wi-Fi is up and NTP is fresh, lit only when something is wrong.
40+
# Default 0 = status lights (lit while nominal).
41+
# Environment=PICLOCK_TROUBLE_DOTS=1
3842
# PICLOCK_WAKE_BRIGHTNESS / PICLOCK_NIGHT_BRIGHTNESS: 0-15 levels the optional
3943
# day/night schedule applies at its wake and dim edges (defaults: 15 = full,
4044
# 0 = dimmest lit level).

‎tests/test_app.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
from piclock.app import _dot_lit
4+
5+
6+
@pytest.mark.parametrize(
7+
"nominal, trouble, lit",
8+
[
9+
(True, False, True), # status light: lit while nominal
10+
(False, False, False),
11+
(True, True, False), # trouble light: dark while nominal
12+
(False, True, True),
13+
],
14+
)
15+
def test_dot_polarity(nominal, trouble, lit):
16+
assert _dot_lit(nominal, trouble) is lit

‎tests/test_config.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,19 @@ def test_indicator_dot_defaults():
118118
assert cfg.sync_fresh_seconds == 4500.0
119119
assert cfg.wifi_dot is True
120120
assert cfg.wifi_iface == "wlan0"
121+
assert cfg.trouble_dots is False
121122

122123

123124
def test_from_env_reads_indicator_dot_settings(monkeypatch):
124125
monkeypatch.setenv("PICLOCK_SYNC_FRESH", "600")
125126
monkeypatch.setenv("PICLOCK_WIFI_DOT", "off")
126127
monkeypatch.setenv("PICLOCK_WIFI_IFACE", "wlan1")
128+
monkeypatch.setenv("PICLOCK_TROUBLE_DOTS", "1")
127129
cfg = Config.from_env()
128130
assert cfg.sync_fresh_seconds == 600.0
129131
assert cfg.wifi_dot is False
130132
assert cfg.wifi_iface == "wlan1"
133+
assert cfg.trouble_dots is True
131134

132135

133136
@pytest.mark.parametrize("window", [0, -1.0])

0 commit comments

Comments
 (0)