I found a vape pen on the ground already half smashed so I couldn't identify what it was sadly, other than it being pink.
After taking it apart (pliers, force, beer) I looked at the chips under a magnifying glass, and saw it had a SC8F073 MCU^[yeah no idea. Had to look it up. Says it might a UART interface I can dick around with, but not seeing anything], and also had this little ribbon display whose only markings I could see are "12-25 WC294F".
Charlieplexing
It's a 6 pin cable going towards 18 LEDs, so in theory has more than enough (6*5 = 30) configurations to light up each LED.
Turns out because these are what I like to call "Cheap and Cheerful Chinese Chips", they cannot be arsed to put in the required resistors to allow such addressing to take place, so they make use of something called "charlieplexing" which works as follows:
- Only a single pair of anode/cathode pins needed to light up an LED (1 high, 1 low, all others in "high impedance" (i.e. disconnected))
- Since multiple LEDs might share an overlapping pin, you can't have multiple pairs of pins active at the same time, as this will cause spillover to other LEDs
- To show the number "3" , made up of 5 LEDs, you have to cycle through those 5 pairs of pins whilst impeding all others, very very fast to give the illusion of the number "3".
I used a USB to TTL chip from my laptop with Dupont connectors going into an FPC breakout chip (both roughly 5 quid from online retailers), and hooked the display ribbon into it, and got to testing pairs. I got the following (High Low) pair diagram
| 32 | 65 53
| 34 | 56 64 43 52
| 35 | 54 42
| 36 | 61 63 45 51
| 41 | 62 46 26
You might notice:
- The battery (far left) has multiple pin mappings
- The percent symbol (far right) has just one, despite the percent one arguably having a large use case by being paired with numbers...
- There's no common pin grouping for each digit. Digit 1 is a mix of 5/6 high and Digit 2 is a mix of 4/5 high. So I feel like they could have done better.
Anyway after some dicking around, I put this little python "driver" to work and now it can do arbitrary numbers
Code
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)