No description
  • Python 96.7%
  • Nix 3.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-06-03 06:07:31 -06:00
.forgejo/workflows chore(deps): update forgejo.kainnef.com/mfenniak/debian-latest-forgejo-runner docker digest to ae8308d (#96) 2026-06-03 06:07:31 -06:00
component fix: use hours & minutes for time display, all days (#60) 2025-12-28 21:39:46 -07:00
data fix: replace env canada data source for sun with Home Assistant (#37) 2025-09-02 09:45:32 -06:00
draw fixups 2025-02-22 19:15:13 -07:00
fonts Convert to using PIL text drawing w/ space line breaking 2023-09-16 19:56:03 -06:00
icons Add washer/dryer component and data source 2025-05-29 13:32:19 -06:00
.envrc Move CalendarDataResolver to data & DI 2023-10-07 19:44:22 -06:00
.gitignore Rip out unmaintained dependency_injector; also now mypy checking on all object creation is accurate 2024-07-01 15:01:21 +02:00
config.py Rip out unmaintained dependency_injector; also now mypy checking on all object creation is accurate 2024-07-01 15:01:21 +02:00
di.py chore: change to non-winter config (#75) 2026-04-10 13:38:24 -06:00
displaybase.py Rip out unmaintained dependency_injector; also now mypy checking on all object creation is accurate 2024-07-01 15:01:21 +02:00
flake.lock chore(deps): lock file maintenance (#93) 2026-06-01 00:27:53 -06:00
flake.nix chore(deps): lock file maintenance (#67) 2026-03-02 19:10:51 -07:00
main.py Change default mqtt 2024-09-27 09:12:31 -06:00
mosquitto_test_cmds.md Add washer/dryer component and data source 2025-05-29 13:32:19 -06:00
mqtt.py fix bug when wind_speed is None 2025-03-04 08:27:18 -07:00
pixelperfectpi.py Reimplement layout with flexbox & stretchable library (#6) 2024-07-09 12:47:12 +00:00
README.md end-to-end deploy test 2024-11-30 15:07:54 -07:00
renovate.json5 chore: centralize renovate config 2025-05-06 20:55:11 -06:00
service.py Cleanup unused imports 2023-10-08 08:39:32 -06:00
setup.py chore(deps): lock file maintenance (#67) 2026-03-02 19:10:51 -07:00
stretchable-Cargo.lock.patch Add stretchable package for flexbox/grid layout algorithms 2024-07-03 13:19:22 +02:00
stretchable-py.typed.patch First test panel w/ stretchable-based flexbox layout 2024-07-03 22:51:43 +02:00

pixelperfectpi

pixelperfectpi is an application for the Raspberry Pi that drives an LED matrix panel in order to create a custom digital clock.

This app isn't really intended to be re-used -- it isn't very configurable. It was just built for my own use, and the code is shared so that anyone else who tries to do the same thing has some starting point, or hints and shortcuts.

Features:

  • Displays the time.
  • Reads and displays AQI and temperature from a local PurpleAir sensor.
  • Displays the next weather forecast sourced from Environment Canada.
  • Displays the next three calendar events from an iCal feed.

Example installation:

https://forgejo.kainnef.com/attachments/833dadf3-50b3-411b-8ca6-23dc693d35e2

Running

Dependencies

Using a nix devshell (nix develop) or direnv (direnv allow) will install all the required dependencies to run pixelperfectpi either emulated on a desktop, or on a raspberry pi.

Config

For development, you can copy config_template.py to config.py and fill in the config values to get an easy-to-reproduce testing environment, either on a desktop or a Pi.

Performing all configuration through cmdline options is the approach I've taken for a permanent install. See the NixOS configuration below for an example.

Emulation

If you run pixelperfectpi on any device that isn't detected as a Raspberry Pi (via /proc/cpuinfo), it will use https://github.com/ty-porter/RGBMatrixEmulator to run an emulator that can be loaded in a web browser, pygame, a console, etc. This is a handy way to do development. The emulator_config.json generated by RGBMatrixEmulator can be tweaked to run in different modes; see RGBMatrixEmulator's README for more information.

LED Matrix

In order to get the Raspberry Pi -> LED Matrix wired up and working, follow the instructions at https://github.com/hzeller/rpi-rgb-led-matrix.

NixOS Configuration

Although this is a typicalish Python application, I've deployed it on my Raspberry Pi through NixOS. After installing NixOS (https://nix.dev/tutorials/nixos/installing-nixos-on-a-raspberry-pi), the following flake configuration can be used to set up this app and have it autostart on boot, and set up the Raspberry Pi hardware correctly:

flake.nix

{
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-unstable";
    pixelperfectpi.url = "git+https://forgejo.kainnef.com/mfenniak/pixelperfectpi.git";
    nixos-hardware.url = "github:NixOS/nixos-hardware";
  };

  outputs = { self, nixpkgs, nixos-hardware, pixelperfectpi, ... }: {
    nixosConfigurations.nixclock = nixpkgs.lib.nixosSystem rec {
      system = "aarch64-linux";
      modules = [
        ./nixclock.nix
        nixos-hardware.nixosModules.raspberry-pi-4
        { nixpkgs.overlays = [ pixelperfectpi.overlays.${system}.default ]; }
      ];
    };
  };
}

nixclock.nix

{ config, pkgs, lib, ... }:

{
  boot = {
    kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
    initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" ];
    loader = {
      grub.enable = false;
      generic-extlinux-compatible.enable = true;
    };

    kernelParams = [
      # per https://github.com/hzeller/rpi-rgb-led-matrix#cpu-use...
      "isolcpus=3"
      # https://github.com/NixOS/nixpkgs/issues/122993
      "iomem=relaxed"
      "strict-devmem=0"
    ];

    blacklistedKernelModules = [
      # disable snd module: https://github.com/hzeller/rpi-rgb-led-matrix#use-minimal-raspbian-distribution
      "snd_bcm2835"
    ];
  };

  fileSystems = {
    "/" = {
      device = "/dev/disk/by-label/NIXOS_SD";
      fsType = "ext4";
      options = [ "noatime" ];
    };
  };

  networking = {
    hostName = "nixclock";
    wireless = {
      enable = true;
      networks."...yourSID...".psk = "...yourPSK..."; # FIXME: there's probably a better way than hardcoding a pwd here
      interfaces = [ "wlan0" ];
    };
  };

  environment.systemPackages = with pkgs; [ vim ];

  services.openssh.enable = true;

  time.timeZone = "America/Edmonton";

  # nix settings
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  nix.settings.trusted-users = [ "root" ];
  nix.settings.auto-optimise-store = true;
  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-older-than 30d";
  };

  systemd.services.pixelperfectpi = {
    description = "pixelperfectpi";
    wants = [ "network-online.target" ];
    after = [ "network-online.target" ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      Restart = "always";
      # NOTE: any % in the iCal URL needs to be escaped as %% due to systemd's usage of it as a special char;
      # Google Calendar's private shared URLs have % embedded in them
      ExecStart = ''
        ${pkgs.pixelperfectpi}/bin/pixelperfectpi.py \
          --led-rows=32 \
          --led-cols=64 \
          --led-gpio-mapping=regular \
          --led-slowdown-gpio=4 \
          --font-path ${pkgs.pixelperfectpi}/fonts \
          --ical-url="...your-ical-url..."
        '';
    };
  };

  hardware.enableRedistributableFirmware = true;
  system.stateVersion = "23.11";
}