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:

  1. 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))
  2. 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
  3. 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)

you are viewing a single comment's thread
view the rest of the comments
[–] 4 points 5 hours ago (2 children)

Excuse me, sir, this is dull_mens_club, not awesome_mens_club. I'll have to ask you to leave.

JK! This is awesome! I love recovering e-waste, though usually I just collect the li-ion cells from wayside vapes. Regarding how this thing is driven, I'm continually saddened that display manufacturers don't bother with the tiny amount of extra SRAM needed to support a persistent image.

For one project of mine in the distant past -- using a device similar to an Alpha Smart -- the display did have its own memory but the board designers couldn't be bothered to run the MISO line from the display to the CPU. So even though I could write to the display memory, I still had to keep a full copy of the rendered graphics in CPU memory, which was significant. All this made worse because the display chip itself supported a reasonable set of manipulations, like scrolling and shifting the display. But alas, none of it could be used effectively.

  • source
  • hideshow 2 child comments
  • [–] [S] 4 points 3 hours ago (1 child)

    Chalk another mark towards what were they thinking when they put this wonder together?

    Also I think we really need an electronics community on the lemmyverse. I tried looking for one but found nothing, and just know that I want to share more in this direction

  • source
  • parent
  • hideshow 1 child comment