import json
import time
from pathlib import Path
from typing import Iterable
from gpiozero import InputDevice, OutputDevice
GPIO_PINS = (13, 19, 26, 16, 20, 21)
##FFCB_PINS = (1, 3, 5, 2, 4, 6) # Physical FFC interface
DISP_PINS = (6, 4, 2, 5, 3, 1) # Display pin numbers
Mapping = dict[str, dict[str, list[int]]] # Type
class PinMap:
"""Load display segment definitions and map them to display pins."""
def __init__(self, gpio_pins: Iterable[int] = GPIO_PINS,
display_pins: Iterable[int] = DISP_PINS,
segment_file: str | Path = "out_segments.json"):
self.gpio_to_display = dict(zip(gpio_pins, display_pins))
self.display_to_gpio = {display: gpio
for gpio, display in self.gpio_to_display.items()}
self.segments = self._load_segments(segment_file)
def _load_segments(self, filename: str | Path) -> Mapping:
with open(filename, encoding="utf-8") as file:
raw_segments = json.load(file)
segments: Mapping = {}
for name, pairs in raw_segments.items():
display_name, _, part = name.partition(".")
part = part or "main"
display_pairs = [[self.gpio_to_display[gpio] for gpio in pair]
for pair in pairs]
# Preserve the original behaviour: use the first pair.
segments.setdefault(display_name, {})[part] = display_pairs[0]
return segments
def to_gpio_pairs(self, display_pairs: Iterable[tuple[int, int]]) -> list[tuple[int, int]]:
return [(self.display_to_gpio[anode],
self.display_to_gpio[cathode])
for anode, cathode in display_pairs]
class CharlieplexDisplay:
"""Drive a Charlieplexed display using gpiozero."""
def __init__(self, pin_map: PinMap, gpio_pins: Iterable[int] = GPIO_PINS):
self.pin_map = pin_map
self.gpio_pins = tuple(gpio_pins)
self.devices: dict[int, InputDevice | OutputDevice] = {}
def __enter__(self) -> "CharlieplexDisplay":
self.high_impedance()
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
self.close()
def _close_devices(self) -> None:
for device in self.devices.values():
device.close()
self.devices.clear()
def high_impedance(self) -> None:
"""Release all GPIO pins and configure them as floating inputs."""
self._close_devices()
self.devices = {pin: InputDevice(pin, pull_up=None, active_state=True)
for pin in self.gpio_pins}
def close(self) -> None:
self._close_devices()
def drive_pair(self, anode: int, cathode: int) -> None:
"""Drive one LED: anode HIGH, cathode LOW, all others floating."""
valid_pins = set(self.gpio_pins)
if anode == cathode:
raise ValueError("Anode and cathode must be different")
if anode not in valid_pins or cathode not in valid_pins:
raise ValueError(
f"Pins must be in {sorted(valid_pins)}; "
f"got {anode}, {cathode}"
)
self.high_impedance()
# Replace the two input devices with outputs.
self.devices[anode].close()
self.devices[cathode].close()
self.devices[anode] = OutputDevice(anode, initial_value=False)
self.devices[cathode] = OutputDevice(cathode, initial_value=False)
# Set sink first, then source.
self.devices[cathode].off()
self.devices[anode].on()
def light_single_pair(self, anode: int, cathode: int, duration: float = 1) -> bool:
"""Light one LED for duration seconds."""
try:
self.drive_pair(anode, cathode)
time.sleep(duration)
return True
except Exception as exc:
print(f"Error lighting {anode} -> {cathode}: {exc}")
return False
finally:
self.high_impedance()
def light_pairs_multi(self, display_pairs: Iterable[tuple[int, int]], loops: int,
on_time: float, off_time: float) -> bool:
"""Multiplex a sequence of display-pin LED pairs."""
gpio_pairs=self.pin_map.to_gpio_pairs(display_pairs)
if not gpio_pairs:
return False
try:
for _ in range(loops):
for anode, cathode in gpio_pairs:
self.light_single_pair(anode, cathode, on_time)
time.sleep(off_time)
return True
except Exception as exc:
print(f"Error in display: {exc}")
return False
finally:
self.high_impedance()
def make_number(display: CharlieplexDisplay, leds: dict, value: int, loops: int = 100,
show_percent: bool = False, show_battery: bool = False) -> bool:
if not 0 <= value <= 199:
print("Value must be between 0 and 199")
return False
digits = f"{value:3d}"
pairs: list[tuple[int, int]] = []
for index, digit in enumerate(digits, start=1):
pairs.extend(leds[f"dig{index}"][digit])
if show_percent:pairs.extend(leds["percent"])
if show_battery:pairs.extend(leds["battery"])
return display.light_pairs_multi(pairs, loops=loops, on_time=1e-4, off_time=0)
pin_map = PinMap()
dp = pin_map.segments
# Make the LED mappings a bit more legible?
DIGITS = {
" ": (), "0": ("tm", "tr", "tl", "br", "bm", "bl"),
"1": ("br", "tr"), "2": ("tm", "tr", "bl", "bm", "cm"),
"3": ("tm", "tr", "bm", "br", "cm"), "4": ("tl", "cm", "tr", "br"),
"5": ("tm", "br", "bm", "tl", "cm"), "6": ("tl", "cm", "tm", "br", "bm", "bl"),
"7": ("tm", "br", "tr"), "8": ("cm", "tr", "tl", "tm", "br", "bm", "bl"),
"9": ("cm", "tl", "tm", "tr", "br", "bm"),
}
SEGMENT_NAMES = {
"tm": "top.middle", "tr": "top.right", "tl": "top.left", "cm": "center.middle",
"br": "bottom.right", "bm": "bottom.middle", "bl": "bottom.left",
}
def make_digit_map(display: dict) -> dict[str, list[tuple[int, int]]]:
def segment(name: str) -> tuple[int, int]:
return display[SEGMENT_NAMES[name]]
return {digit: [segment(name) for name in names]
for digit, names in DIGITS.items()}
leds = {
"battery": [dp["battery"]["main"]],
"percent": [dp["percent"]["main"]],
"dig1": {
" ": [], "0": [], ## Empty arrays we can add in
"1": [dp["dig1"]["bottom"], dp["dig1"]["top"]],
},
"dig2": make_digit_map(dp["dig2"]),
"dig3": make_digit_map(dp["dig3"]),
}
# display = CharlieplexDisplay(pin_map)
# print(leds["dig2"]["3"])
# print(display.light_pairs_multi(leds["dig2"]["3"], 100, 1e-3, 1e-3))
with CharlieplexDisplay(pin_map) as display:
value = 199 ## the literal max value
while value > 0:
make_number(display, leds, value,
loops=3, show_percent=True, show_battery=False)
value -= 1
## And then add the battery display at zero
make_number(display, leds, 0,
loops=300, show_percent=False, show_battery=True)