Skip to content

ausil/i2c-display

I2C Display Controller

A Go application for Single Board Computers that controls OLED and TFT displays, showing system stats and network information with rotating pages. Supports both I2C (OLED) and SPI (TFT colour) displays.

Works with any SBC that provides I2C or SPI devices:

  • Raspberry Pi (all models)
  • Radxa (Rock 3C, Rock 5B, Rock 4, etc.)
  • Orange Pi (all models)
  • Pine64 (all models)
  • Banana Pi, Odroid, and any other Linux SBC

Supported Displays

Fully Working ✅

  • SSD1306 - 128x64, 128x32, or 96x16 monochrome OLED (I2C)

    • Most common I2C OLED display
    • Full support via periph.io
    • Types: ssd1306, ssd1306_128x64, ssd1306_128x32, ssd1306_96x16
  • ST7735 - Color TFT LCD (SPI)

    • White-on-black rendering, RGB565 colour
    • Types: st7735 / st7735_128x160 (1.8"), st7735_128x128 (1.44"), st7735_160x80 (0.96" Waveshare)
  • UCTRONICS - 0.96" 160x80 colour TFT (I2C, Pi Rack Pro SKU_RM0004)

    • Onboard MCU bridges I2C to the internal ST7735 — no SPI, DC or RST pins needed
    • Fixed address 0x18; dimensions auto-set to 160x80
    • Type: uctronics_colour

Framework Ready (Drivers Needed) 🔧

  • SH1106 - 128x64 monochrome (similar to SSD1306) — Types: sh1106, sh1106_128x64
  • SSD1327 - 128x128 / 96x96 4-bit grayscale OLED — Types: ssd1327, ssd1327_128x128, ssd1327_96x96
  • SSD1331 - 96x64 16-bit color OLED — Types: ssd1331, ssd1331_96x64

These types are recognized and dimensions auto-set, but return a helpful error message explaining the driver is not yet implemented.

See DISPLAY_TYPES.md for detailed information and how to add new display drivers.

Features

  • System Monitoring: Display disk usage, RAM usage, and CPU temperature
  • Network Information: Show IP addresses for configured network interfaces
  • Rotating Pages: Automatically cycle through information pages
  • Flexible Configuration: JSON-based configuration with multiple search paths and hot reload
  • Systemd Integration: Run as a system service with automatic start
  • Hardware Abstraction: Mock display for testing without physical hardware
  • Comprehensive Testing: 75% test coverage with CI/CD support
  • Structured Logging: JSON and console logging with configurable levels
  • Prometheus Metrics: Optional metrics endpoint for monitoring
  • Error Handling: Automatic retry with exponential backoff for I2C errors
  • Health Monitoring: Component health tracking and status reporting
  • Screen Saver: Dim or blank display on idle, with active-hours scheduling and manual wake
  • Config Validation: Validate configuration without running the service

Requirements

  • Go 1.24 or later (for building from source)
  • Supported display (see Supported Displays section)
  • Any Linux-based SBC with I2C or SPI support

Hardware Setup

Wiring — I2C (SSD1306)

SSD1306 Pin SBC Pin Description
VCC 3.3V Power
GND GND Ground
SCL I2C SCL Clock
SDA I2C SDA Data

Wiring — SPI (ST7735)

ST7735 Pin SBC Pin Description
VCC 3.3V Power
GND GND Ground
SCL/SCK SPI SCLK SPI Clock
SDA/MOSI SPI MOSI SPI Data
CS SPI CS0 (CE0) Chip Select
DC/RS Any GPIO (e.g. GPIO24) Data/Command select
RST Any GPIO (e.g. GPIO25) Reset (optional but recommended)

Enable I2C

Raspberry Pi:

sudo raspi-config
# Select: Interface Options -> I2C -> Enable
sudo reboot

Radxa (Rock 3C, Rock 5B, etc.):

# I2C is usually enabled by default in modern images
# Verify with: ls /dev/i2c-*

Orange Pi / Pine64 / Other SBCs:

# Most modern Linux images have I2C enabled by default
# Check your board's documentation if I2C devices are not present
# Verify with: ls /dev/i2c-*

Verify I2C is working:

# Install i2c-tools if not already installed
# For Debian/Ubuntu/Raspberry Pi OS:
sudo apt-get install -y i2c-tools

# For Fedora/RHEL/CentOS:
sudo dnf install -y i2c-tools
# or: sudo yum install -y i2c-tools

# Detect I2C devices (replace '1' with your I2C bus number if different)
sudo i2cdetect -y 1
# You should see your display address (typically 0x3C or 0x3D)

Finding your I2C bus:

# List all I2C buses
ls /dev/i2c-*
# Common values: /dev/i2c-0, /dev/i2c-1, /dev/i2c-3, etc.
# Use the bus number in your config.json

Installation

Fedora (Recommended for Fedora users)

i2c-display is available directly from the Fedora repositories:

sudo dnf install i2c-display

# Enable and start
sudo systemctl enable --now i2c-display.service

This installs everything you need — binary, config, and systemd service. No building required.

For ARM/aarch64 Single Board Computers

Build and install packages locally on your SBC (Raspberry Pi, Radxa, Orange Pi, Pine64, etc.):

Building packages locally on ARM is fast and integrates better with your system's package manager.

Debian/Ubuntu/Raspberry Pi OS (DEB Package)
# Install build dependencies
sudo apt-get update
sudo apt-get install -y git golang debhelper devscripts dh-golang

# Clone and build
git clone https://github.com/ausil/i2c-display.git
cd i2c-display
make deb

# Install the package
sudo apt install ../i2c-display_*_arm64.deb  # or *_armhf.deb for 32-bit

# Enable and start
sudo systemctl enable --now i2c-display.service

The package automatically:

  • Installs the binary to /usr/bin/i2c-displayd
  • Creates config at /etc/i2c-display/config.json
  • Installs and enables the systemd service
  • Can be removed with: sudo apt remove i2c-display
Fedora/RHEL on ARM (RPM Package)

On Fedora, the easiest option is sudo dnf install i2c-display (see above). To build from source instead:

# Install build dependencies
sudo dnf install -y git golang rpm-build systemd-rpm-macros

# Clone and build
git clone https://github.com/ausil/i2c-display.git
cd i2c-display
make rpm

# Install the package
sudo dnf install rpm-build/RPMS/*/i2c-display-*.aarch64.rpm

# Enable and start
sudo systemctl enable --now i2c-display.service

The package automatically:

  • Installs the binary to /usr/bin/i2c-displayd
  • Creates config at /etc/i2c-display/config.json
  • Installs and enables the systemd service
  • Can be removed with: sudo dnf remove i2c-display

Alternative: Pre-built Binaries

If you prefer not to build packages, you can use pre-built binaries:

# Download for ARM64 (e.g., Raspberry Pi 4, Rock 5B)
wget https://github.com/ausil/i2c-display/releases/latest/download/i2c-displayd-linux-arm64
chmod +x i2c-displayd-linux-arm64
sudo mv i2c-displayd-linux-arm64 /usr/bin/i2c-displayd

# Download for ARMv7 (e.g., Raspberry Pi 2/3)
# wget https://github.com/ausil/i2c-display/releases/latest/download/i2c-displayd-linux-armv7
# chmod +x i2c-displayd-linux-armv7
# sudo mv i2c-displayd-linux-armv7 /usr/bin/i2c-displayd

# Download for RISC-V 64-bit
# wget https://github.com/ausil/i2c-display/releases/latest/download/i2c-displayd-linux-riscv64
# chmod +x i2c-displayd-linux-riscv64
# sudo mv i2c-displayd-linux-riscv64 /usr/bin/i2c-displayd

# Create config directory and download config
sudo mkdir -p /etc/i2c-display
sudo wget -O /etc/i2c-display/config.json \
  https://raw.githubusercontent.com/ausil/i2c-display/main/configs/config.example.json

# Download and install systemd service
sudo wget -O /etc/systemd/system/i2c-display.service \
  https://raw.githubusercontent.com/ausil/i2c-display/main/systemd/i2c-display.service

# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable --now i2c-display.service

Note: Using packages is recommended as they integrate with your system's package manager for easier updates and removal.

For x86_64 Systems

Fedora users can install directly from the repos: sudo dnf install i2c-display

Other distros — build packages locally or use the pre-built binary from GitHub Releases:

Build RPM locally (RHEL, Rocky Linux, AlmaLinux, CentOS)
# Install build dependencies
sudo dnf install -y git golang rpm-build systemd-rpm-macros

# Clone and build
git clone https://github.com/ausil/i2c-display.git
cd i2c-display
make rpm

# Install the package
sudo dnf install rpm-build/RPMS/*/i2c-display-*.x86_64.rpm

# Enable and start
sudo systemctl enable --now i2c-display.service
Build DEB locally (Debian, Ubuntu, Linux Mint, Pop!_OS)
# Install build dependencies
sudo apt-get update
sudo apt-get install -y git golang debhelper devscripts dh-golang

# Clone and build
git clone https://github.com/ausil/i2c-display.git
cd i2c-display
make deb

# Install the package
sudo apt install ../i2c-display_*_amd64.deb

# Enable and start
sudo systemctl enable --now i2c-display.service

Or use the pre-built binary:

wget https://github.com/ausil/i2c-display/releases/latest/download/i2c-displayd-linux-amd64
chmod +x i2c-displayd-linux-amd64
sudo mv i2c-displayd-linux-amd64 /usr/bin/i2c-displayd
# Then follow manual setup steps from ARM binary installation above

Building from Source

Quick Install:

git clone https://github.com/ausil/i2c-display.git
cd i2c-display
sudo ./scripts/install.sh

Manual Build:

# Build the binary
make build

# Install
sudo make install

# Enable and start the service
sudo systemctl enable i2c-display.service
sudo systemctl start i2c-display.service

Configuration

The configuration file is searched in the following order:

  1. Path specified with -config flag
  2. $I2C_DISPLAY_CONFIG_PATH environment variable
  3. /etc/i2c-display/config.json (system-wide)
  4. $HOME/.config/i2c-display/config.json (user-specific)
  5. ./config.json (current directory)

Example Configuration

See configs/config.example.json for a complete example:

{
  "display": {
    "type": "ssd1306",
    "i2c_bus": "/dev/i2c-1",
    "i2c_address": "0x3C",
    "rotation": 0
  },
  "pages": {
    "rotation_interval": "5s",
    "refresh_interval": "1s"
  },
  "system_info": {
    "hostname_display": "short",
    "disk_path": "/",
    "temperature_source": "/sys/class/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  },
  "network": {
    "auto_detect": true,
    "interface_filter": {
      "include": ["eth0", "wlan0", "usb0"],
      "exclude": ["lo", "docker*", "veth*"]
    },
    "show_ipv4": true,
    "show_ipv6": false,
    "max_interfaces_per_page": 3
  },
  "logging": {
    "level": "info",
    "output": "stdout",
    "json": false
  },
  "metrics": {
    "enabled": false,
    "address": ":9090"
  }
}

Configuration Options

Display

  • type: Display controller type (default: ssd1306)
    • ssd1306 or ssd1306_128x64 - Standard 128x64 OLED (I2C)
    • ssd1306_128x32 - Compact 128x32 OLED (I2C)
    • ssd1306_96x16 - Small 96x16 OLED (I2C)
    • st7735 / st7735_128x160 - 1.8" 128x160 TFT (SPI)
    • st7735_128x128 - 1.44" 128x128 TFT (SPI)
    • st7735_160x80 - 0.96" 160x80 TFT (SPI, e.g. Waveshare)
    • uctronics_colour - 0.96" 160x80 colour TFT on UCTRONICS Pi Rack Pro (I2C, address 0x18 auto-set)
    • See DISPLAY_TYPES.md for all supported types

I2C displays only:

  • i2c_bus: I2C bus device path (default: /dev/i2c-1)

    • Find your bus: ls /dev/i2c-*
    • Common values: /dev/i2c-0, /dev/i2c-1, /dev/i2c-3
  • i2c_address: I2C device address in hexadecimal (default: 0x3C)

    • Detect with: sudo i2cdetect -y 1
    • Common addresses: 0x3C or 0x3D

SPI displays only:

  • spi_bus: SPI bus identifier (e.g. SPI0.0)

    • Find your bus: ls /dev/spidev*
    • SPI0.0 corresponds to /dev/spidev0.0
  • dc_pin: GPIO pin name for the Data/Command line (required)

    • Example: GPIO24
  • rst_pin: GPIO pin name for the hardware reset line (optional but recommended)

    • Example: GPIO25
  • rotation: Display rotation in 90° increments (default: 0)

    • 0 - Normal orientation
    • 1 - Rotated 90° clockwise
    • 2 - Rotated 180° (upside down)
    • 3 - Rotated 270° clockwise (90° counter-clockwise)
  • lines: Content line mode for 128×32 displays (default: 0 / auto)

    • 0 or 2 — standard mode: hostname header + separator + one metric per rotating page
    • 4 — compact mode: mirrors the 128×64 layout (header + separator + 3 content lines + load graph) using a 5×7 font so all information fits in the 32 pixel height
    • Ignored on displays taller than 32 pixels
  • width / height: Display dimensions in pixels (optional)

    • Automatically set based on display type - no need to specify
    • Only needed for custom/unsupported displays

Pages

  • rotation_interval: How often to rotate between pages

    • Format: Duration string (e.g., "5s", "30s", "2m")
    • Default: "5s"
  • refresh_interval: How often to update data on current page

    • Format: Duration string (e.g., "1s", "500ms")
    • Default: "1s"

System Info

  • hostname_display: How to display the hostname

    • "short" - Only hostname (e.g., raspberrypi)
    • "full" - Full FQDN (e.g., raspberrypi.local)
  • disk_path: Filesystem path to monitor (default: "/")

    • Examples: "/", "/home", "/mnt/data"
  • temperature_source: Path to CPU temperature sensor

    • Raspberry Pi: /sys/class/thermal/thermal_zone0/temp
    • Radxa Rock 5B: /sys/class/thermal/thermal_zone0/temp
    • Orange Pi: /sys/class/thermal/thermal_zone0/temp or /sys/devices/virtual/thermal/thermal_zone0/temp
    • Pine64: Check ls /sys/class/thermal/thermal_zone*/temp
    • Leave empty ("") to disable temperature display
  • temperature_unit: Display unit for temperature

    • "celsius" - Display in °C
    • "fahrenheit" - Display in °F

Finding your temperature sensor:

# List all thermal zones
for zone in /sys/class/thermal/thermal_zone*/temp; do
  echo "$zone: $(cat $zone)"
done

Network

  • auto_detect: Automatically find network interfaces (default: true)

    • Set to false to manually specify interfaces
  • interface_filter.include: Patterns for interfaces to show

    • Supports wildcards: ["eth*", "wlan*", "usb*"]
    • Default: ["eth0", "wlan0", "usb0"]
  • interface_filter.exclude: Patterns for interfaces to hide

    • Supports wildcards: ["lo", "docker*", "veth*"]
    • Useful to hide virtual interfaces
    • Default: ["lo", "docker*", "veth*"]
  • show_ipv4: Display IPv4 addresses (default: true)

  • show_ipv6: Display IPv6 addresses (default: false)

  • max_interfaces_per_page: Maximum network interfaces per page (default: 3)

Example interface configurations:

Show only Ethernet
"network": {
  "auto_detect": true,
  "interface_filter": {
    "include": ["eth*", "en*"],
    "exclude": ["lo", "wlan*", "docker*", "veth*"]
  }
}
Show WiFi and Ethernet
"network": {
  "auto_detect": true,
  "interface_filter": {
    "include": ["eth*", "wlan*", "en*", "wl*"],
    "exclude": ["lo", "docker*", "veth*"]
  }
}

Screen Saver (Optional)

Power saving feature to dim or blank the display after inactivity or outside configured hours.

  • enabled: Enable screen saver (default: false)

  • mode: Screen saver behavior

    • "dim" - Reduce brightness
    • "blank" - Turn off display completely
    • "off" - No screen saver
  • idle_timeout: Time before activating screen saver (ignored when active_hours.enabled is true)

    • Format: Duration string (e.g., "5m", "30m", "1h")
    • Default: "5m"
  • dim_brightness: Brightness level when dimmed (0-255)

    • Default: 50
  • normal_brightness: Normal operating brightness (0-255)

    • Default: 255
  • wake_duration: How long a manual wake keeps the display on

    • Format: Duration string (e.g., "30s", "2m")
    • Default: "30s"
  • active_hours: Time window during which the display is always kept on

    • enabled: Enable active hours (default: false)
    • start: Start of active window in HH:MM 24-hour format (e.g., "08:00")
    • end: End of active window in HH:MM 24-hour format (e.g., "22:00")
    • Overnight ranges are supported (e.g., "22:00" to "06:00")
    • When active hours are enabled, idle_timeout is not required

Waking the display manually:

When the screensaver is active you can wake the display for wake_duration via:

# Via HTTP (requires metrics server enabled)
curl -X POST http://localhost:9090/wake

# Via signal
systemctl kill -s SIGUSR1 i2c-display.service
# or: kill -USR1 $(pidof i2c-displayd)

Example — dim at night, always on during the day:

"screensaver": {
  "enabled": true,
  "mode": "dim",
  "dim_brightness": 30,
  "normal_brightness": 255,
  "wake_duration": "30s",
  "active_hours": {
    "enabled": true,
    "start": "08:00",
    "end": "22:00"
  }
}

Example — idle timeout with manual wake:

"screensaver": {
  "enabled": true,
  "mode": "blank",
  "idle_timeout": "10m",
  "dim_brightness": 0,
  "normal_brightness": 255,
  "wake_duration": "60s"
}

Logging

  • level: Log level verbosity

    • "debug" - Very verbose, includes all details
    • "info" - Normal operation information
    • "warn" - Warnings only
    • "error" - Errors only
    • Default: "info"
  • output: Where to send logs

    • "stdout" - Standard output
    • "stderr" - Standard error
    • Default: "stdout"
  • json: Log format

    • true - JSON format (good for log aggregation)
    • false - Human-readable console format
    • Default: false

Metrics (Optional)

Prometheus-compatible metrics endpoint for monitoring.

  • enabled: Enable metrics endpoint (default: false)

  • address: HTTP server address and port

    • Format: "host:port" or ":port"
    • Examples: ":9090", "127.0.0.1:9090", "0.0.0.0:9090"
    • Default: ":9090"

When enabled, metrics are available at http://address/metrics

Example metrics:

  • Display update count and errors
  • I2C communication metrics
  • Page rotation statistics
  • System resource usage

Platform-Specific Configuration Examples

Raspberry Pi with SSD1306 128x64
{
  "display": {
    "type": "ssd1306_128x64",
    "i2c_bus": "/dev/i2c-1",
    "i2c_address": "0x3C",
    "rotation": 0
  },
  "system_info": {
    "temperature_source": "/sys/class/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  },
  "network": {
    "auto_detect": true,
    "interface_filter": {
      "include": ["eth0", "wlan0"],
      "exclude": ["lo", "docker*"]
    }
  }
}
Radxa Rock 5B
{
  "display": {
    "type": "ssd1306_128x64",
    "i2c_bus": "/dev/i2c-7",
    "i2c_address": "0x3C",
    "rotation": 0
  },
  "system_info": {
    "temperature_source": "/sys/class/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  },
  "network": {
    "auto_detect": true,
    "interface_filter": {
      "include": ["eth*", "wlan*", "en*"],
      "exclude": ["lo", "docker*", "veth*"]
    }
  }
}
Orange Pi with Smaller Display (128x32)
{
  "display": {
    "type": "ssd1306_128x32",
    "i2c_bus": "/dev/i2c-0",
    "i2c_address": "0x3C",
    "rotation": 0,
    "lines": 2
  },
  "pages": {
    "rotation_interval": "10s",
    "refresh_interval": "2s"
  },
  "system_info": {
    "temperature_source": "/sys/devices/virtual/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  }
}

Use "lines": 4 to mirror the 128×64 layout with all metrics on one pass using a compact 5×7 font.

Pine64 with Screen Saver
{
  "display": {
    "type": "ssd1306_128x64",
    "i2c_bus": "/dev/i2c-1",
    "i2c_address": "0x3C",
    "rotation": 0
  },
  "screensaver": {
    "enabled": true,
    "mode": "dim",
    "idle_timeout": "5m",
    "dim_brightness": 30,
    "normal_brightness": 255
  }
}
ST7735 0.96" TFT (160x80, Waveshare) on Raspberry Pi
{
  "display": {
    "type": "st7735_160x80",
    "spi_bus": "SPI0.0",
    "dc_pin": "GPIO24",
    "rst_pin": "GPIO25",
    "rotation": 0
  },
  "system_info": {
    "temperature_source": "/sys/class/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  }
}

Enable SPI on Raspberry Pi: raspi-config → Interface Options → SPI → Enable

ST7735 1.44" TFT (128x128) on Raspberry Pi
{
  "display": {
    "type": "st7735_128x128",
    "spi_bus": "SPI0.0",
    "dc_pin": "GPIO24",
    "rst_pin": "GPIO25",
    "rotation": 0
  },
  "system_info": {
    "temperature_source": "/sys/class/thermal/thermal_zone0/temp",
    "temperature_unit": "celsius"
  }
}
Server/Headless Setup with Metrics
{
  "display": {
    "type": "ssd1306_128x64",
    "i2c_bus": "/dev/i2c-1",
    "i2c_address": "0x3C",
    "rotation": 0
  },
  "logging": {
    "level": "info",
    "output": "stdout",
    "json": true
  },
  "metrics": {
    "enabled": true,
    "address": ":9090"
  }
}

Access metrics: curl http://localhost:9090/metrics

Usage

Run as Service

# Start service
sudo systemctl start i2c-display.service

# Stop service
sudo systemctl stop i2c-display.service

# Restart service
sudo systemctl restart i2c-display.service

# Check status
sudo systemctl status i2c-display.service

# View logs
sudo journalctl -u i2c-display.service -f

Run Manually

# With default config search
./bin/i2c-displayd

# With specific config
./bin/i2c-displayd -config /path/to/config.json

# With mock display (for testing)
./bin/i2c-displayd -mock -config configs/config.example.json

# Validate configuration without running
./bin/i2c-displayd -validate-config -config /path/to/config.json

# Reload configuration (send SIGHUP to running process)
sudo systemctl reload i2c-display.service
# Or: sudo kill -HUP $(pidof i2c-displayd)

# Wake the display (if screensaver is active)
sudo systemctl kill -s SIGUSR1 i2c-display.service
# Or: sudo kill -USR1 $(pidof i2c-displayd)

Development

Building

# Build for current architecture
make build

# Build for Raspberry Pi (32-bit ARM)
make build-arm7

# Build for Raspberry Pi 4 / Rock 3C (64-bit ARM)
make build-arm64

# Build for RISC-V 64-bit
make build-riscv64

# Build all architectures (amd64, arm7, arm64, riscv64)
make build-all

Testing

# Run unit tests
make test

# Run with mock display (no hardware needed)
make run-mock

# Run hardware tests (requires actual display)
make test-hardware

Project Structure

i2c-display/
├── cmd/
│   └── i2c-displayd/       # Main application entry point
├── internal/
│   ├── config/             # Configuration loading and validation
│   ├── display/            # Display abstraction layer and drivers
│   │   ├── ssd1306.go      # SSD1306 I2C OLED driver
│   │   ├── st7735.go       # ST7735 SPI TFT driver
│   │   ├── uctronics.go    # UCTRONICS colour TFT driver
│   │   ├── factory.go      # Display factory
│   │   └── mock.go         # Mock display for testing
│   ├── renderer/           # Page rendering and layout
│   │   ├── layout.go       # Adaptive layout for different display sizes
│   │   ├── system_page.go  # System stats page (disk, RAM, CPU temp)
│   │   ├── network_page.go # Network interfaces page
│   │   ├── load_graph_page.go # Rolling load average graph page
│   │   ├── icons.go        # Bitmap icons for metrics
│   │   ├── text.go         # Text drawing helpers and color functions
│   │   └── smallfont.go    # Compact 5×7 bitmap font for 128×32 lines=4 mode
│   ├── stats/              # System statistics collectors
│   ├── rotation/           # Page rotation manager
│   ├── screensaver/        # Screen saver (dim/blank on idle)
│   ├── health/             # Component health tracking
│   ├── metrics/            # Prometheus metrics endpoint
│   ├── logger/             # Structured logging (zerolog)
│   └── retry/              # Retry with exponential backoff
├── configs/                # Example configurations per display type
├── systemd/                # Systemd service file
├── scripts/                # Installation/uninstallation scripts
├── testdata/               # Test fixtures
├── Makefile
└── README.md

Display Layout

All pages show the hostname centered at the top, separated from content by a horizontal rule. Metric text is color-coded green/yellow/red based on usage thresholds (on colour displays).

Page 1: System Stats

┌──────────────────────────┐
│        hostname          │  ← centered header
├──────────────────────────┤
│ [disk]  45.2% (12.5/27.6GB) │
│ [mem]   62.8% (2.5/4.0GB)   │
│ [cpu]   45.2C               │
└──────────────────────────┘

Page 2: Load Average Graph

┌──────────────────────────┐
│        hostname          │
├──────────────────────────┤
│ 1m:0.42 5m:0.38 15m:0.31│
│  ▂▃▄▃▂▁▂▃▅▄▃▂▁▂▃▄▃▂▁▂  │  ← rolling bar graph
│  ░░░░░░░░░░░░░░░░░░░░░  │    color: green/yellow/red
└──────────────────────────┘

Page 3+: Network Interfaces

┌──────────────────────────┐
│        hostname          │
├──────────────────────────┤
│ eth0: 192.168.1.100      │
│ wlan0: 10.0.0.50         │
│ usb0: 172.16.0.1         │
│                 Page 3/4 │
└──────────────────────────┘

Small Displays (128×32)

128×32 displays have two layout modes controlled by display.lines:

lines=2 (default) — one metric per rotating page, full 32 px text:

┌──────────────────────────┐
│        hostname          │
├──────────────────────────┤
│ [disk] 45.2% (12.5/27.6GB) │
└──────────────────────────┘

(separate pages for disk, RAM, CPU temp, load avg, network)

lines=4 — mirrors the 128×64 layout using a compact 5×7 font:

┌──────────────────────────┐
│       hostname           │  ← 7 px header
├──────────────────────────┤  ← separator at row 7
│ D:45% 12.5/27.6G         │  ← row 8
│ R:63% 2.5/4.0G           │  ← row 16
│ C:45.2C                  │  ← row 24
└──────────────────────────┘

(same page rotation as 128×64: system → load graph → network)

Troubleshooting

Display Not Working

  1. Check I2C is enabled:

    ls /dev/i2c-*
  2. Check I2C address:

    sudo i2cdetect -y 1
  3. Verify permissions:

    sudo usermod -a -G i2c $USER
    # Log out and back in
  4. Check service logs:

    sudo journalctl -u i2c-display.service -n 50

ST7735 Display Not Working

  1. Ensure SPI is enabled:

    ls /dev/spidev*
    # Should show /dev/spidev0.0 (or similar)
  2. On Raspberry Pi, enable SPI via raspi-config → Interface Options → SPI

  3. Verify GPIO pin numbers match your wiring:

    # Check available GPIO names
    gpio readall   # if wiringpi installed
    # or check /sys/class/gpio/
  4. Check permissions:

    sudo usermod -a -G spi $USER
    # Log out and back in

Temperature Not Showing

Different SBCs have different temperature sensor paths:

  • Raspberry Pi: /sys/class/thermal/thermal_zone0/temp
  • Rock 3C: /sys/class/thermal/thermal_zone0/temp or /sys/devices/virtual/thermal/thermal_zone0/temp

Update temperature_source in your config accordingly.

Network Interfaces Not Showing

Check your interface filter settings in the config. Use:

ip link show

to see available interfaces and adjust the include patterns.

Monitoring

Prometheus Metrics

Enable metrics in your configuration:

{
  "metrics": {
    "enabled": true,
    "address": "127.0.0.1:9090"
  }
}

Available metrics:

  • i2c_display_refresh_total - Total display refreshes
  • i2c_display_refresh_errors_total - Display errors by type
  • i2c_display_refresh_latency_seconds - Refresh latency histogram
  • i2c_display_i2c_errors_total - I2C communication errors
  • i2c_display_cpu_temperature_celsius - Current CPU temperature
  • i2c_display_memory_used_percent - Memory usage percentage
  • i2c_display_disk_used_percent - Disk usage percentage
  • i2c_display_network_interfaces_count - Number of network interfaces
  • i2c_display_current_page - Current page number
  • i2c_display_page_rotation_total - Total page rotations

Access metrics: curl http://127.0.0.1:9090/metrics

Wake endpoint:

When the screensaver is enabled, a POST to /wake temporarily wakes the display:

curl -X POST http://127.0.0.1:9090/wake

Logging

Structured logging with contextual information:

# Console output (human-readable)
{"level":"info","time":"2026-02-09T12:00:00Z","message":"Display service running"}

# JSON output (for log aggregation)
{
  "level":"info",
  "display_type":"ssd1306",
  "bus":"/dev/i2c-1",
  "time":"2026-02-09T12:00:00Z",
  "message":"Initializing display hardware"
}

Uninstallation

sudo ./scripts/uninstall.sh

Or manually:

sudo systemctl stop i2c-display.service
sudo systemctl disable i2c-display.service
sudo make uninstall

License

BSD 3-Clause License. See LICENSE for details.

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass: make test
  5. Submit a pull request

Acknowledgments

  • Built with periph.io for hardware abstraction
  • Uses basicfont for standard text rendering
  • Compact 5×7 bitmap font derived from the Adafruit GFX classic glyph table

Support

For issues, questions, or contributions, please visit: https://github.com/ausil/i2c-display