Initial scaffold: NixOS + SvelteKit dashboard + Python renderer

This commit is contained in:
Ruben Hensen
2026-06-07 18:15:06 +02:00
commit 50256fa384
25 changed files with 4491 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Nix
result
result-*
.direnv/
# Node / SvelteKit
dashboard/node_modules/
dashboard/.svelte-kit/
dashboard/build/
dashboard/.env
dashboard/.env.*
!dashboard/.env.example
# Python
__pycache__/
*.pyc
*.pyo
*.so
.venv/
*.egg-info/
.pytest_cache/
# Editors
.vscode/
.idea/
*.swp
# Secrets (handled via sops-nix / LoadCredential, never plaintext in repo)
*.secret
secrets.local.*
+116
View File
@@ -0,0 +1,116 @@
# eink
NixOS-managed Raspberry Pi 4 driving a Waveshare 12.48" e-paper panel as a
calendar / day-planner. CalDAV data comes from the household Stalwart at
`mail.rubenhensen.nl`. The Pi pulls every commit on `main` via
[comin](https://github.com/nlewo/comin) — never touch the SD card after the
first flash.
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Raspberry Pi 4 (NixOS aarch64, hostname: dayplanner) │
│ │
│ systemd: eink-dashboard.service systemd: eink-renderer.service │
│ ┌──────────────────────────┐ ┌──────────────────────────────┐ │
│ │ SvelteKit (Node adapter) │◄──────│ Playwright → Pillow dither │ │
│ │ :3000 │ HTTP │ → Waveshare 12.48" driver │ │
│ │ fetches CalDAV every load│ │ (every REFRESH_SECONDS) │ │
│ └────────────┬─────────────┘ └───────────────┬──────────────┘ │
│ │ │ SPI │
│ ▼ ▼ │
│ mail.rubenhensen.nl Waveshare 12.48" HAT │
│ (Stalwart CalDAV) (1304×984, 4 sub-panels) │
└─────────────────────────────────────────────────────────────────────────┘
Deploy path: laptop → git push → Gitea → comin (60s poll) → Pi
```
## Layout
```
eink/
├── flake.nix # nixpkgs + comin → nixosConfigurations.dayplanner
├── hosts/dayplanner/ # this Pi's NixOS config (users, comin, hardware)
├── modules/ # eink-dashboard.nix + eink-renderer.nix
├── dashboard/ # SvelteKit app rendered headlessly at 1304×984
└── renderer/ # Python loop: screenshot → dither → push
```
## One-time bootstrap
1. **Flash NixOS aarch64 SD image** onto a card and boot the Pi.
- Image: <https://hydra.nixos.org/job/nixos/release-25.11/nixos.sd_image.aarch64-linux/latest>
- On first boot, set a `nixos` user password via the serial console or
monitor + keyboard. Enable SSH manually (`sudo systemctl start sshd`)
and note the Pi's IP.
2. **Add your SSH public key** to `hosts/dayplanner/default.nix` under
`users.users.ruben.openssh.authorizedKeys.keys`.
3. **Push this repo to a Git remote** the Pi can reach (e.g. your Gitea).
Update `services.comin.remotes[0].url` in `hosts/dayplanner/default.nix`
if the URL differs.
4. **First synchronous deploy from your laptop:**
```sh
nix run nixpkgs#nixos-rebuild -- switch \
--flake .#dayplanner \
--target-host nixos@<pi-ip> \
--build-host nixos@<pi-ip> \
--use-remote-sudo
```
(Building on the Pi itself avoids needing a cross-compile setup on your
Mac. Slow but one-time.)
5. **Drop the CalDAV password** on the Pi:
```sh
ssh ruben@<pi-ip> 'sudo install -d -m 700 /var/lib/eink/secrets'
ssh ruben@<pi-ip> 'sudo tee /var/lib/eink/secrets/caldav-password >/dev/null' \
<<< 'your-caldav-app-password'
ssh ruben@<pi-ip> 'sudo chmod 600 /var/lib/eink/secrets/caldav-password'
```
(Long-term replace this with [sops-nix](https://github.com/Mic92/sops-nix)
so the secret lives encrypted in this repo.)
6. **Vendor the Waveshare driver** (see `renderer/waveshare/README.md`).
After this, the steady-state workflow is just:
```sh
git push origin main # comin picks it up within ~60s and switches the Pi
```
## Iterating on the dashboard locally
You don't need the Pi to develop the UI — just open the SvelteKit dev
server in a browser sized to 1304×984:
```sh
cd dashboard
npm install
CALDAV_PASSWORD=... npm run dev -- --host
# open http://localhost:5173 in a browser with viewport 1304×984
```
Chrome devtools → "Toggle device toolbar" → set to 1304×984 to preview
exactly what the panel will see.
## Refresh cadence & panel wear
`eink.renderer.refreshSeconds = 300` (5 min) is the default. The 12.48"
panel has **no partial refresh** — every update is a full flash that
takes ~8s and consumes some panel life. Below 180 s is abusive; 300-600
s is comfortable. Consider scheduling a nightly invert to spread wear.
## Panel variant
Set `eink.renderer.panel = "bwr"` in `hosts/dayplanner/default.nix` if
you have the **12.48inch e-Paper Module (B)** (B/W/Red) instead of the
plain B/W version. The dashboard's red channel is whatever CSS color
matches `rgb(>180, <100, <100)` — see `renderer/render.py:to_bwr`.
## Troubleshooting
- **Comin not pulling:** `journalctl -u comin.service -f` on the Pi.
- **Renderer dry-running:** check `/tmp/eink-last.png` — if it exists,
the screenshot+dither pipeline works and the Waveshare driver is the
remaining piece (see `renderer/waveshare/README.md`).
- **Dashboard 500:** `journalctl -u eink-dashboard.service -f`. Most
common cause: empty `/var/lib/eink/secrets/caldav-password`.
+1965
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "eink-dashboard",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.5.0",
"vite": "^6.0.0"
},
"dependencies": {
"tsdav": "^2.1.0",
"ical.js": "^2.1.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
/* E-paper-friendly base styles.
Constraints:
- Hard 1-bit palette: pure black on pure white. No grays (they dither badly).
- No thin lines below 2px — Floyd-Steinberg dither breaks them up.
- No anti-aliased small fonts. Stick to bold weights at >= 18px.
- No soft shadows; use crisp 2-4px solid borders for separation. */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 1304px;
height: 984px;
background: #ffffff;
color: #000000;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 20px;
font-weight: 500;
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: unset;
text-rendering: geometricPrecision;
}
h1, h2, h3 {
font-weight: 800;
letter-spacing: -0.01em;
}
.border-thick {
border: 3px solid #000000;
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=1304, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}
+112
View File
@@ -0,0 +1,112 @@
import { readFileSync } from 'node:fs';
import { createDAVClient } from 'tsdav';
import ICAL from 'ical.js';
import type { PageServerLoad } from './$types';
const CALDAV_URL = process.env.CALDAV_URL ?? 'https://mail.rubenhensen.nl/dav/';
const CALDAV_USER = process.env.CALDAV_USER ?? 'ruben@rubenhensen.nl';
// systemd LoadCredential drops the password at $CREDENTIALS_DIRECTORY/caldav-password.
// Falls back to env for dev.
function readCalDavPassword(): string {
const dir = process.env.CREDENTIALS_DIRECTORY;
if (dir) {
try {
return readFileSync(`${dir}/caldav-password`, 'utf-8').trim();
} catch {}
}
return process.env.CALDAV_PASSWORD ?? '';
}
interface Event {
start: Date;
end: Date;
summary: string;
allDay: boolean;
}
async function fetchTodayEvents(): Promise<Event[]> {
const password = readCalDavPassword();
if (!password) return [];
const client = await createDAVClient({
serverUrl: CALDAV_URL,
credentials: { username: CALDAV_USER, password },
authMethod: 'Basic',
defaultAccountType: 'caldav'
});
const calendars = await client.fetchCalendars();
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 1);
// Stalwart returns "207 Multi-Status / 404 No resources found" for empty
// time-range queries, which tsdav rejects as an error. Fetch everything
// and filter client-side — the calendar is small enough that this is fine.
const windowStart = ICAL.Time.fromJSDate(start, true);
const windowEnd = ICAL.Time.fromJSDate(end, true);
const events: Event[] = [];
for (const cal of calendars) {
const objects = await client.fetchCalendarObjects({ calendar: cal });
for (const obj of objects) {
if (!obj.data) continue;
const jcal = ICAL.parse(obj.data);
const comp = new ICAL.Component(jcal);
for (const vevent of comp.getAllSubcomponents('vevent')) {
const ev = new ICAL.Event(vevent);
if (ev.isRecurrenceException()) continue;
const push = (s: ICAL.Time, e: ICAL.Time) => {
events.push({
start: s.toJSDate(),
end: e.toJSDate(),
summary: ev.summary,
allDay: s.isDate
});
};
if (!ev.isRecurring()) {
if (ev.endDate.compare(windowStart) <= 0) continue;
if (ev.startDate.compare(windowEnd) >= 0) continue;
push(ev.startDate, ev.endDate);
continue;
}
const iter = ev.iterator();
let next: ICAL.Time | null;
while ((next = iter.next())) {
if (next.compare(windowEnd) >= 0) break;
const occ = ev.getOccurrenceDetails(next);
if (occ.endDate.compare(windowStart) <= 0) continue;
push(occ.startDate, occ.endDate);
}
}
}
}
return events.sort((a, b) => a.start.getTime() - b.start.getTime());
}
export const load: PageServerLoad = async () => {
let events: Event[] = [];
let error: string | null = null;
try {
events = await fetchTodayEvents();
} catch (e) {
if (e instanceof Error) {
error = e.message;
} else if (typeof e === 'object' && e !== null) {
try { error = JSON.stringify(e); } catch { error = String(e); }
} else {
error = String(e);
}
}
return {
events,
error,
renderedAt: new Date().toISOString()
};
};
+107
View File
@@ -0,0 +1,107 @@
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
function fmtTime(d: Date | string): string {
const date = typeof d === 'string' ? new Date(d) : d;
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}
function fmtDate(d: Date | string): string {
const date = typeof d === 'string' ? new Date(d) : d;
return date.toLocaleDateString('en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long'
});
}
const today = new Date();
</script>
<main class="dashboard">
<header class="header">
<h1>{fmtDate(today)}</h1>
<span class="rendered">refreshed {fmtTime(data.renderedAt)}</span>
</header>
<section class="events">
<h2>Today</h2>
{#if data.error}
<p class="error">CalDAV error: {data.error}</p>
{:else if data.events.length === 0}
<p class="empty">Nothing scheduled.</p>
{:else}
<ul>
{#each data.events as ev (`${ev.start}-${ev.summary}`)}
<li class="event">
<span class="time">
{#if ev.allDay}all day{:else}{fmtTime(ev.start)}{fmtTime(ev.end)}{/if}
</span>
<span class="summary">{ev.summary}</span>
</li>
{/each}
</ul>
{/if}
</section>
</main>
<style>
.dashboard {
width: 1304px;
height: 984px;
padding: 40px 56px;
display: flex;
flex-direction: column;
gap: 32px;
}
.header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 4px solid #000;
padding-bottom: 16px;
}
.header h1 {
font-size: 56px;
}
.rendered {
font-size: 18px;
font-weight: 600;
}
.events h2 {
font-size: 36px;
margin-bottom: 16px;
}
.events ul {
list-style: none;
display: flex;
flex-direction: column;
gap: 12px;
}
.event {
display: grid;
grid-template-columns: 220px 1fr;
gap: 24px;
padding: 12px 0;
border-bottom: 2px solid #000;
font-size: 28px;
}
.time {
font-variant-numeric: tabular-nums;
font-weight: 700;
}
.empty, .error {
font-size: 28px;
padding: 16px 0;
}
</style>
+10
View File
@@ -0,0 +1,10 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
};
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});
+23
View File
@@ -0,0 +1,23 @@
{
description = "E-paper dayplanner NixOS config for Pi 4 driving Waveshare 12.48\" e-paper";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
comin = {
url = "github:nlewo/comin";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, comin, ... }: {
nixosConfigurations.dayplanner = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = { inherit self; };
modules = [
comin.nixosModules.comin
./hosts/dayplanner
];
};
};
}
+65
View File
@@ -0,0 +1,65 @@
{ config, lib, pkgs, self, ... }:
{
imports = [
./hardware.nix
../../modules/eink-dashboard.nix
../../modules/eink-renderer.nix
];
networking.hostName = "dayplanner";
networking.domain = "local";
# WiFi or wired. Default to DHCP — set static IP via your router if you want predictability.
networking.networkmanager.enable = true;
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 ];
time.timeZone = "Europe/Amsterdam";
users.users.ruben = {
isNormalUser = true;
extraGroups = [ "wheel" "gpio" "spi" "dialout" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEiY+6AEuKQHyt4Mpg/IbyoIkhg76u3xFPdXGpTsmwxT ruben.hensen@protonmail.com"
];
};
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.PermitRootLogin = "no";
};
# Pull-deploy: every commit on `main` lands on the Pi within ~60 s.
# The remote URL must be reachable from the Pi (push to GitHub/Gitea).
services.comin = {
enable = true;
remotes = [{
name = "origin";
url = "https://git.rubenhensen.nl/ruben/eink.git";
branches.main.name = "main";
}];
hostname = "dayplanner";
};
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
environment.systemPackages = with pkgs; [
git
htop
vim
];
eink.dashboard.enable = true;
eink.dashboard.caldavUser = "ruben";
eink.renderer.enable = true;
eink.renderer.dashboardUrl = "http://127.0.0.1:3000";
system.stateVersion = "25.11";
}
+54
View File
@@ -0,0 +1,54 @@
{ config, lib, pkgs, modulesPath, ... }:
{
# Pi 4 aarch64. Boot from the official NixOS sd-image; we don't generate
# the image from this flake — flash the upstream aarch64 sd-image once,
# then `nixos-rebuild switch --flake .#dayplanner --target-host` swaps
# the running system over and comin takes over from there.
imports = [
"${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
];
# Drop the default channel; we deploy from the flake.
sdImage.compressImage = false;
boot.loader.grub.enable = false;
boot.loader.generic-extlinux-compatible.enable = true;
# The Waveshare 12.48" HAT is driven entirely from userspace Python via
# /dev/spidev*. We only need SPI enabled in the device tree and the
# gpio/spi groups available to the renderer user.
boot.kernelParams = [ "console=serial0,115200" "console=tty1" ];
# Enable SPI0 (CE0/CE1) and the extra GPIOs the HAT needs.
# See: https://www.waveshare.com/wiki/12.48inch_e-Paper_Module
hardware.deviceTree = {
enable = true;
overlays = [
{
name = "spi-on";
dtsText = ''
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2711";
fragment@0 {
target = <&spi0>;
__overlay__ {
status = "okay";
};
};
};
'';
}
];
};
users.groups.spi = {};
users.groups.gpio = {};
services.udev.extraRules = ''
SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"
SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660"
'';
}
+82
View File
@@ -0,0 +1,82 @@
{ config, lib, pkgs, self, ... }:
let
cfg = config.eink.dashboard;
dashboardSrc = self + "/dashboard";
dashboardBuild = pkgs.buildNpmPackage {
pname = "eink-dashboard";
version = "0.1.0";
src = dashboardSrc;
# Re-generate this hash with:
# nix run nixpkgs#prefetch-npm-deps -- dashboard/package-lock.json
npmDepsHash = "sha256-UticV62t7/CYZ1nVZPNxUZn38WhQ4mHmYT3V90D01OU=";
buildPhase = ''
runHook preBuild
npm run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r build package.json $out/
cp -r node_modules $out/
runHook postInstall
'';
};
in
{
options.eink.dashboard = {
enable = lib.mkEnableOption "SvelteKit e-paper dashboard";
port = lib.mkOption {
type = lib.types.port;
default = 3000;
};
caldavUrl = lib.mkOption {
type = lib.types.str;
default = "https://mail.rubenhensen.nl/dav/cal/";
description = "Base URL of the CalDAV server. Stalwart serves CalDAV at /dav/cal/, distinct from CardDAV at /dav/card/.";
};
caldavUser = lib.mkOption {
type = lib.types.str;
default = "ruben@rubenhensen.nl";
};
};
config = lib.mkIf cfg.enable {
systemd.services.eink-dashboard = {
description = "E-paper dashboard (SvelteKit)";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOST = "127.0.0.1";
PORT = toString cfg.port;
CALDAV_URL = cfg.caldavUrl;
CALDAV_USER = cfg.caldavUser;
NODE_ENV = "production";
};
serviceConfig = {
# CalDAV password loaded as a systemd credential. Provision the
# encrypted file under /var/lib/eink/secrets/caldav-password
# (or use sops-nix). See README.
LoadCredential = [ "caldav-password:/var/lib/eink/secrets/caldav-password" ];
DynamicUser = true;
StateDirectory = "eink-dashboard";
ExecStart = "${pkgs.nodejs}/bin/node ${dashboardBuild}/build/index.js";
Restart = "always";
RestartSec = "5s";
};
};
};
}
+79
View File
@@ -0,0 +1,79 @@
{ config, lib, pkgs, self, ... }:
let
cfg = config.eink.renderer;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
pillow
numpy
playwright
requests
# The Waveshare 12.48" lib is not packaged in nixpkgs; we vendor it
# under renderer/waveshare/ and import it as a local module.
spidev
rpi-gpio
]);
in
{
options.eink.renderer = {
enable = lib.mkEnableOption "E-paper renderer (Playwright + Pillow + Waveshare driver)";
dashboardUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:3000";
};
refreshSeconds = lib.mkOption {
type = lib.types.int;
default = 300;
description = "Seconds between full panel refreshes. Below 180 will wear the panel.";
};
panel = lib.mkOption {
type = lib.types.enum [ "bw" "bwr" ];
default = "bw";
description = "Panel variant: bw = 12.48\" B/W, bwr = 12.48\" Module (B) B/W/Red.";
};
};
config = lib.mkIf cfg.enable {
# Playwright needs a Chromium and its system deps. Use the nixpkgs one
# so the binary path is stable across rebuilds (not the npm-downloaded
# browser, which won't find its sandbox libs on NixOS).
systemd.services.eink-renderer = {
description = "E-paper renderer loop";
after = [ "eink-dashboard.service" "network-online.target" ];
wants = [ "eink-dashboard.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
DASHBOARD_URL = cfg.dashboardUrl;
REFRESH_SECONDS = toString cfg.refreshSeconds;
PANEL_VARIANT = cfg.panel;
PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}";
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
PYTHONPATH = "${self}/renderer";
};
serviceConfig = {
Type = "simple";
ExecStart = "${pythonEnv}/bin/python ${self}/renderer/render.py";
Restart = "always";
RestartSec = "10s";
# The Waveshare lib needs raw SPI + GPIO. Run as a dedicated user
# in the spi/gpio groups rather than root.
User = "eink";
Group = "eink";
SupplementaryGroups = [ "spi" "gpio" ];
};
};
users.users.eink = {
isSystemUser = true;
group = "eink";
extraGroups = [ "spi" "gpio" ];
};
users.groups.eink = {};
};
}
+14
View File
@@ -0,0 +1,14 @@
[project]
name = "eink-renderer"
version = "0.1.0"
description = "Screenshot the dashboard, dither it, push it to the Waveshare 12.48\" panel."
requires-python = ">=3.11"
dependencies = [
"playwright>=1.48",
"pillow>=10.4",
"numpy>=2.1",
"requests>=2.32",
# Hardware-only — installed by Nix, not pip on dev machines.
# "spidev>=3.6",
# "RPi.GPIO>=0.7",
]
+144
View File
@@ -0,0 +1,144 @@
"""E-paper renderer loop.
Pipeline:
Playwright screenshot of $DASHBOARD_URL at 1304x984
-> Pillow 1-bit dither (Floyd-Steinberg)
-> Waveshare 12.48" driver push
Tunables come from env: DASHBOARD_URL, REFRESH_SECONDS, PANEL_VARIANT.
"""
from __future__ import annotations
import io
import logging
import os
import signal
import sys
import time
from pathlib import Path
from PIL import Image
from playwright.sync_api import sync_playwright
log = logging.getLogger("eink.renderer")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
WIDTH, HEIGHT = 1304, 984
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://127.0.0.1:3000")
REFRESH_SECONDS = int(os.environ.get("REFRESH_SECONDS", "300"))
PANEL_VARIANT = os.environ.get("PANEL_VARIANT", "bw") # bw | bwr
def screenshot(url: str) -> Image.Image:
"""Headless Chromium screenshot at the panel's native resolution."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
ctx = browser.new_context(
viewport={"width": WIDTH, "height": HEIGHT},
device_scale_factor=1,
)
page = ctx.new_page()
page.goto(url, wait_until="networkidle", timeout=30_000)
png = page.screenshot(full_page=False, type="png", omit_background=False)
browser.close()
return Image.open(io.BytesIO(png)).convert("RGB")
def to_bw(img: Image.Image) -> Image.Image:
"""Floyd-Steinberg dither to 1-bit. Pillow's built-in is C-accelerated."""
return img.convert("L").convert("1", dither=Image.Dither.FLOYDSTEINBERG)
def to_bwr(img: Image.Image) -> tuple[Image.Image, Image.Image]:
"""Separate red plane from B/W plane for the Module (B) variant.
Naive thresholding: pixels with R > 180 and G,B < 100 -> red plane.
Everything else -> dithered B/W.
"""
import numpy as np
arr = np.array(img)
r, g, b = arr[..., 0], arr[..., 1], arr[..., 2]
red_mask = (r > 180) & (g < 100) & (b < 100)
bw_arr = arr.copy()
bw_arr[red_mask] = 255 # remove red from BW plane
bw_img = to_bw(Image.fromarray(bw_arr))
red_img = Image.fromarray((red_mask * 255).astype("uint8"), mode="L").convert("1")
return bw_img, red_img
def push_to_panel_bw(bw: Image.Image) -> None:
"""Push a 1-bit image to the B/W 12.48\" panel.
NOTE: requires the Waveshare 12.48\" Python driver vendored under
renderer/waveshare/. See waveshare/README.md for install steps.
"""
try:
from waveshare import epd12in48 # type: ignore
except ImportError:
log.warning("Waveshare driver not vendored — dry-run mode (no panel push).")
bw.save("/tmp/eink-last.png")
return
epd = epd12in48.EPD()
epd.Init()
epd.display(epd.getbuffer(bw))
epd.EPD_Sleep()
def push_to_panel_bwr(bw: Image.Image, red: Image.Image) -> None:
try:
from waveshare import epd12in48b # type: ignore
except ImportError:
log.warning("Waveshare (B) driver not vendored — dry-run mode (no panel push).")
bw.save("/tmp/eink-last-bw.png")
red.save("/tmp/eink-last-red.png")
return
epd = epd12in48b.EPD()
epd.Init()
epd.display(epd.getbuffer(bw), epd.getbuffer(red))
epd.EPD_Sleep()
def render_once() -> None:
log.info("Screenshot %s", DASHBOARD_URL)
img = screenshot(DASHBOARD_URL)
if PANEL_VARIANT == "bwr":
bw, red = to_bwr(img)
push_to_panel_bwr(bw, red)
else:
push_to_panel_bw(to_bw(img))
log.info("Frame pushed to panel.")
def main() -> None:
stop = False
def handle(*_):
nonlocal stop
stop = True
log.info("Shutdown signal — exiting after current frame.")
signal.signal(signal.SIGTERM, handle)
signal.signal(signal.SIGINT, handle)
while not stop:
started = time.monotonic()
try:
render_once()
except Exception:
log.exception("Render cycle failed; will retry next interval.")
elapsed = time.monotonic() - started
sleep_for = max(0, REFRESH_SECONDS - elapsed)
log.info("Sleeping %.1fs", sleep_for)
for _ in range(int(sleep_for)):
if stop:
return
time.sleep(1)
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
# Waveshare 12.48" driver
The 12.48" driver lives in a **separate repo** from the main Waveshare
e-Paper one — it's at [`waveshareteam/12.48inch-e-paper`](https://github.com/waveshareteam/12.48inch-e-paper).
The big `waveshareteam/e-Paper` repo does **not** contain it.
## Install
Run this from the project root (`~/Repos/eink/`):
```sh
git clone --depth 1 https://github.com/waveshareteam/12.48inch-e-paper.git /tmp/waveshare-12in48
cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48.py renderer/waveshare/
cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48b.py renderer/waveshare/
cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48b_V2.py renderer/waveshare/
cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epdconfig.py renderer/waveshare/
rm -rf /tmp/waveshare-12in48
```
After copying, `renderer/waveshare/` should look like:
```
renderer/waveshare/
├── __init__.py # (already present)
├── README.md # (already present, this file)
├── epd12in48.py # B/W variant driver
├── epd12in48b.py # B/W/Red (Module B) original driver
├── epd12in48b_V2.py # B/W/Red (Module B) v2 driver — newer revision
└── epdconfig.py # SPI + GPIO abstraction (uses spidev + RPi.GPIO)
```
The renderer (`renderer/render.py`) imports `from waveshare import epd12in48`
(B/W) or `from waveshare import epd12in48b` (B/W/Red). For the v2 panel
revision, edit `render.py` to import `epd12in48b_V2` instead.
## Why not vendor it from the start?
It's vendor code with its own license headers — kept out of git until
the user opts in to keep the repo tidy and the license boundary clear.
Add it to `.gitignore` if you'd rather pull it fresh on each deploy.
+2
View File
@@ -0,0 +1,2 @@
# Waveshare 12.48" driver — vendored from
# https://github.com/waveshareteam/e-Paper. See README.md in this dir.
+398
View File
@@ -0,0 +1,398 @@
# /*****************************************************************************
# * | File : epd12in48.py
# * | Author : Waveshare electrices
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-11-01
# * | Info :
# ******************************************************************************/
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import time
import epdconfig
EPD_WIDTH = 1304
EPD_HEIGHT = 984
class EPD(object):
def __init__(self):
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN
self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN
self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN
self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN
self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN
self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN
self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN
self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN
self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN
self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN
self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN
self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN
def Init(self):
print("EPD init...")
epdconfig.module_init()
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
self.Reset()
#panel setting
self.M1_SendCommand(0x00)
self.M1_SendData(0x1f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.S1_SendCommand(0x00)
self.S1_SendData(0x1f)
self.M2_SendCommand(0x00)
self.M2_SendData(0x13)
self.S2_SendCommand(0x00)
self.S2_SendData(0x13)
# booster soft start
self.M1_SendCommand(0x06)
self.M1_SendData(0x17) #A
self.M1_SendData(0x17) #B
self.M1_SendData(0x39) #C
self.M1_SendData(0x17)
self.M2_SendCommand(0x06)
self.M2_SendData(0x17)
self.M2_SendData(0x17)
self.M2_SendData(0x39)
self.M2_SendData(0x17)
#resolution setting
self.M1_SendCommand(0x61)
self.M1_SendData(0x02)
self.M1_SendData(0x88) #source 648
self.M1_SendData(0x01) #gate 492
self.M1_SendData(0xEC)
self.S1_SendCommand(0x61)
self.S1_SendData(0x02)
self.S1_SendData(0x90) #source 656
self.S1_SendData(0x01) #gate 492
self.S1_SendData(0xEC)
self.M2_SendCommand(0x61)
self.M2_SendData(0x02)
self.M2_SendData(0x90) #source 656
self.M2_SendData(0x01) #gate 492
self.M2_SendData(0xEC)
self.S2_SendCommand(0x61)
self.S2_SendData(0x02)
self.S2_SendData(0x88) #source 648
self.S2_SendData(0x01) #gate 492
self.S2_SendData(0xEC)
self.M1S1M2S2_SendCommand(0x15) #DUSPI
self.M1S1M2S2_SendData(0x20)
self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting
self.M1S1M2S2_SendData(0x21) #Border KW
self.M1S1M2S2_SendData(0x07)
self.M1S1M2S2_SendCommand(0x60) #TCON
self.M1S1M2S2_SendData(0x22)
self.M1S1M2S2_SendCommand(0xE3)
self.M1S1M2S2_SendData(0x00)
#temperature
temp = self.M1_ReadTemperature()
self.M1S1M2S2_SendCommand(0xe0) #Cascade setting
self.M1S1M2S2_SendData(0x03)
self.M1S1M2S2_SendCommand(0xe5) #Force temperature
self.M1S1M2S2_SendData(temp)
def display(self, Image):
start = time.clock()
buf = [0x00] * int(self.width * self.height / 8)
image_monocolor = Image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
temp=0;
for y in range(0, imheight):
for x in range(0, imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] < 127: # black
buf[int((x + y*self.width)/8)] &= ~(0x80>>temp)
else: # white
buf[int((x + y*self.width)/8)] |= (0x80>>temp)
temp=temp+1
if(temp==8):
temp=0
#M1 part 648*492
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(buf[y*163 + x])
#S1 part 656*492
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(buf[y*163 + x])
#M2 part 656*492
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(buf[y*163 + x])
#S2 part 648*492
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(buf[y*163 + x])
end = time.clock()
print("use time:%f"%(end - start))
self.TurnOnDisplay()
def clear(self):
"""Clear contents of image buffer"""
start = time.clock()
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(0xff)
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(0xff)
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(0xff)
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(0xff)
end = time.clock()
print("use time:%f"%(end - start))
self.TurnOnDisplay()
""" M1S1M2S2 Write register address and data """
def M1S1M2S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def M1S1M2S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M1M2 Write register address and data """
def M1M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M1S1M2S2_Senddata(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S2 Write register address and data """
def S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M2 Write register address and data """
def M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S1 Write register address and data """
def S1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
def S1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
""" M1 Write register address and data """
def M1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
def M1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
def Reset(self):
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0)
time.sleep(0.01)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
def EPD_Sleep(self):
self.M1S1M2S2_SendCommand(0X02)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0X07)
self.M1S1M2S2_SendData(0xA5)
time.sleep(0.3)
print("module_exit")
epdconfig.module_exit()
def TurnOnDisplay(self):
self.M1M2_SendCommand(0x04)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0x12)
self.M1_ReadBusy()
self.S1_ReadBusy()
self.M2_ReadBusy()
self.S2_ReadBusy()
#Busy
def M1_ReadBusy(self):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
print("M1_ReadBusy")
while(busy):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def M2_ReadBusy(self):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy = not(busy & 0x01)
print("M2_ReadBusy")
while(busy):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy =not(busy & 0x01)
time.sleep(0.2)
def S1_ReadBusy(self):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
print("s1_ReadBusy")
while(busy):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def S2_ReadBusy(self):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
print("S2_ReadBusy")
while(busy):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def M1_ReadTemperature(self):
self.M1_SendCommand(0x40)
self.M1_ReadBusy()
time.sleep(0.3)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
time.sleep(0.01)
# temp = epdconfig.spi_readbyte(0x00)
temp = 25
print("Read Temperature Reg:%d"%temp)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
# temp =0x29
return temp
+525
View File
@@ -0,0 +1,525 @@
# /*****************************************************************************
# * | File : epd12in48.py
# * | Author : Waveshare electrices
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-11-01
# * | Info :
# ******************************************************************************/
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import time
import epdconfig
EPD_WIDTH = 1304
EPD_HEIGHT = 984
class EPD(object):
def __init__(self):
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN
self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN
self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN
self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN
self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN
self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN
self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN
self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN
self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN
self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN
self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN
self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN
def Init(self):
print("EPD init...")
epdconfig.module_init()
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
self.Reset()
#panel setting
self.M1_SendCommand(0x00)
self.M1_SendData(0x2f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.S1_SendCommand(0x00)
self.S1_SendData(0x2f)
self.M2_SendCommand(0x00)
self.M2_SendData(0x23)
self.S2_SendCommand(0x00)
self.S2_SendData(0x23)
# POWER SETTING
self.M1_SendCommand(0x01)
self.M1_SendData(0x07)
self.M1_SendData(0x17) # VGH=20V,VGL=-20V
self.M1_SendData(0x3F) # VDH=15V
self.M1_SendData(0x3F) # VDL=-15V
self.M1_SendData(0x0d)
self.M2_SendCommand(0x01)
self.M2_SendData(0x07)
self.M2_SendData(0x17) # VGH=20V,VGL=-20V
self.M2_SendData(0x3F) # VDH=15V
self.M2_SendData(0x3F) # VDL=-15V
self.M2_SendData(0x0d)
# booster soft start
self.M1_SendCommand(0x06)
self.M1_SendData(0x17) #A
self.M1_SendData(0x17) #B
self.M1_SendData(0x39) #C
self.M1_SendData(0x17)
self.M2_SendCommand(0x06)
self.M2_SendData(0x17)
self.M2_SendData(0x17)
self.M2_SendData(0x39)
self.M2_SendData(0x17)
#resolution setting
self.M1_SendCommand(0x61)
self.M1_SendData(0x02)
self.M1_SendData(0x88) #source 648
self.M1_SendData(0x01) #gate 492
self.M1_SendData(0xEC)
self.S1_SendCommand(0x61)
self.S1_SendData(0x02)
self.S1_SendData(0x90) #source 656
self.S1_SendData(0x01) #gate 492
self.S1_SendData(0xEC)
self.M2_SendCommand(0x61)
self.M2_SendData(0x02)
self.M2_SendData(0x90) #source 656
self.M2_SendData(0x01) #gate 492
self.M2_SendData(0xEC)
self.S2_SendCommand(0x61)
self.S2_SendData(0x02)
self.S2_SendData(0x88) #source 648
self.S2_SendData(0x01) #gate 492
self.S2_SendData(0xEC)
self.M1S1M2S2_SendCommand(0x15) #DUSPI
self.M1S1M2S2_SendData(0x20)
self.M1S1M2S2_SendCommand(0x30) # PLL
self.M1S1M2S2_SendData(0x08)
self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting
self.M1S1M2S2_SendData(0x31)
self.M1S1M2S2_SendData(0x07)
self.M1S1M2S2_SendCommand(0x60)#TCON
self.M1S1M2S2_SendData(0x22)
self.M1_SendCommand(0xE0) #POWER SETTING
self.M1_SendData(0x01)
self.M2_SendCommand(0xE0) #POWER SETTING
self.M2_SendData(0x01)
self.M1S1M2S2_SendCommand(0xE3)
self.M1S1M2S2_SendData(0x00)
self.M1_SendCommand(0x82)
self.M1_SendData(0x1c)
self.M2_SendCommand(0x82)
self.M2_SendData(0x1c)
self.SetLut()
def display(self, BlackImage, RedImage):
start = time.clock()
Blackbuf = [0x00] * int(self.width * self.height / 8)
blackconvert = BlackImage.convert('1')
bimwidth, bimheight = blackconvert.size
Blackpixles = blackconvert.load()
temp=0;
for y in range(0, bimheight):
for x in range(0, bimwidth):
if Blackpixles[x, y] < 127: # black
Blackbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp)
else: # white
Blackbuf[int((x + y*self.width)/8)] |= (0x80>>temp)
temp=temp+1
if(temp==8):
temp=0
Redbuf = [0x00] * int(self.width * self.height / 8)
redconvert = RedImage.convert('1')
rimwidth, rimheight = redconvert.size
Redpixles = redconvert.load()
temp=0;
for y in range(0, rimheight):
for x in range(0, rimwidth):
if Redpixles[x, y] < 127: # black
Redbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp)
else: # white
Redbuf[int((x + y*self.width)/8)] |= (0x80>>temp)
temp=temp+1
if(temp==8):
temp=0
#S2 part 648*492
self.S2_SendCommand(0x10)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(Blackbuf[y*163 + x])
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(~Redbuf[y*163 + x])
#M2 part 656*492
self.M2_SendCommand(0x10)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(Blackbuf[y*163 + x])
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(~Redbuf[y*163 + x])
#M1 part 648*492
self.M1_SendCommand(0x10)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(Blackbuf[y*163 + x])
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(~Redbuf[y*163 + x])
#S1 part 656*492
self.S1_SendCommand(0x10)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(Blackbuf[y*163 + x])
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(~Redbuf[y*163 + x])
end = time.clock()
print("use time: %f"%(end - start))
self.TurnOnDisplay()
def clear(self):
"""Clear contents of image buffer"""
start = time.clock()
self.S2_SendCommand(0x10)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(0xff)
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(0x00)
self.M2_SendCommand(0x10)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(0xff)
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(0x00)
self.M1_SendCommand(0x10)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(0xff)
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(0x00)
self.S1_SendCommand(0x10)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(0xff)
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(0x00)
end = time.clock()
print (end)
print (start)
print("use time: %f" %(end - start))
self.TurnOnDisplay()
def Reset(self):
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0)
time.sleep(0.01)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
def EPD_Sleep(self):
self.M1S1M2S2_SendCommand(0X02)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0X07)
self.M1S1M2S2_SendData(0xA5)
time.sleep(0.3)
print("module_exit")
epdconfig.module_exit()
def TurnOnDisplay(self):
self.M1M2_SendCommand(0x04)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0x12)
self.M1_ReadBusy()
self.S1_ReadBusy()
self.M2_ReadBusy()
self.S2_ReadBusy()
""" M1S1M2S2 Write register address and data """
def M1S1M2S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def M1S1M2S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M1M2 Write register address and data """
def M1M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M1M2_Sendata(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S2 Write register address and data """
def S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M2 Write register address and data """
def M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S1 Write register address and data """
def S1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
def S1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
""" M1 Write register address and data """
def M1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
def M1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
#Busy
def M1_ReadBusy(self):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def M2_ReadBusy(self):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy = not(busy & 0x01)
self.M2_SendCommand(0x71)
while(busy):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy =not(busy & 0x01)
time.sleep(0.2)
def S1_ReadBusy(self):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def S2_ReadBusy(self):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
lut_vcom1 = [
0x00, 0x10, 0x10, 0x01, 0x08, 0x01,
0x00, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x08, 0x01, 0x08, 0x01, 0x06,
0x00, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x00, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_ww1 = [
0x91, 0x10, 0x10, 0x01, 0x08, 0x01,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x08, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bw1 = [
0xA8, 0x10, 0x10, 0x01, 0x08, 0x01,
0x84, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x86, 0x06, 0x01, 0x06, 0x01, 0x05,
0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0xF0, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_wb1 = [
0x91, 0x10, 0x10, 0x01, 0x08, 0x01,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x08, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bb1 = [
0x92, 0x10, 0x10, 0x01, 0x08, 0x01,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x01, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
def SetLut(self):
self.M1S1M2S2_SendCommand(0x20) #vcom
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_vcom1[count])
self.M1S1M2S2_SendCommand(0x21) #red not use
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_ww1[count])
self.M1S1M2S2_SendCommand(0x22) #bw r
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_bw1[count]) # bw=r
self.M1S1M2S2_SendCommand(0x23) #wb w
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_wb1[count]) # wb=w
self.M1S1M2S2_SendCommand(0x24) #bb b
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_bb1[count]) # bb=b
self.M1S1M2S2_SendCommand(0x25) #bb b
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_ww1[count]) # bb=b
+529
View File
@@ -0,0 +1,529 @@
# /*****************************************************************************
# * | File : epd12in48b_V2.py
# * | Author : Waveshare electrices
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2022-09-14
# * | Info :
# ******************************************************************************/
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import time
import epdconfig
EPD_WIDTH = 1304
EPD_HEIGHT = 984
class EPD(object):
def __init__(self):
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN
self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN
self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN
self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN
self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN
self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN
self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN
self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN
self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN
self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN
self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN
self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN
def Init(self):
print("EPD init...")
epdconfig.module_init()
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
self.Reset()
# panel setting for Clear
# self.M1_SendCommand(0x00)
# self.M1_SendData(0x07) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
# self.S1_SendCommand(0x00)
# self.S1_SendData(0x07)
# self.M2_SendCommand(0x00)
# self.M2_SendData(0x07)
# self.S2_SendCommand(0x00)
# self.S2_SendData(0x07)
# panel setting for Display
self.M1_SendCommand(0x00)
self.M1_SendData(0x0f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.S1_SendCommand(0x00)
self.S1_SendData(0x0f)
self.M2_SendCommand(0x00)
self.M2_SendData(0x03)
self.S2_SendCommand(0x00)
self.S2_SendData(0x03)
# booster soft start
self.M1_SendCommand(0x06)
self.M1_SendData(0x17) #A
self.M1_SendData(0x17) #B
self.M1_SendData(0x39) #C
self.M1_SendData(0x17)
self.M2_SendCommand(0x06)
self.M2_SendData(0x17)
self.M2_SendData(0x17)
self.M2_SendData(0x39)
self.M2_SendData(0x17)
#resolution setting
self.M1_SendCommand(0x61)
self.M1_SendData(0x02)
self.M1_SendData(0x88) #source 648
self.M1_SendData(0x01) #gate 492
self.M1_SendData(0xEC)
self.S1_SendCommand(0x61)
self.S1_SendData(0x02)
self.S1_SendData(0x90) #source 656
self.S1_SendData(0x01) #gate 492
self.S1_SendData(0xEC)
self.M2_SendCommand(0x61)
self.M2_SendData(0x02)
self.M2_SendData(0x90) #source 656
self.M2_SendData(0x01) #gate 492
self.M2_SendData(0xEC)
self.S2_SendCommand(0x61)
self.S2_SendData(0x02)
self.S2_SendData(0x88) #source 648
self.S2_SendData(0x01) #gate 492
self.S2_SendData(0xEC)
self.M1S1M2S2_SendCommand(0x15) #DUSPI
self.M1S1M2S2_SendData(0x20)
self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting
self.M1S1M2S2_SendData(0x11)
self.M1S1M2S2_SendData(0x07)
self.M1S1M2S2_SendCommand(0x60)#TCON
self.M1S1M2S2_SendData(0x22)
self.M1S1M2S2_SendCommand(0xE3)
self.M1S1M2S2_SendData(0x00)
self.M1_ReadTemperature()
def display(self, BlackImage, RedImage):
start = time.perf_counter()
Blackbuf = [0x00] * int(self.width * self.height / 8)
blackconvert = BlackImage.convert('1')
bimwidth, bimheight = blackconvert.size
Blackpixles = blackconvert.load()
temp=0
for y in range(0, bimheight):
for x in range(0, bimwidth):
if Blackpixles[x, y] < 127: # black
Blackbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp)
else: # white
Blackbuf[int((x + y*self.width)/8)] |= (0x80>>temp)
temp=temp+1
if(temp==8):
temp=0
Redbuf = [0x00] * int(self.width * self.height / 8)
redconvert = RedImage.convert('1')
rimwidth, rimheight = redconvert.size
Redpixles = redconvert.load()
temp=0
for y in range(0, rimheight):
for x in range(0, rimwidth):
if Redpixles[x, y] < 127: # black
Redbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp)
else: # white
Redbuf[int((x + y*self.width)/8)] |= (0x80>>temp)
temp=temp+1
if(temp==8):
temp=0
#S2 part 648*492
self.S2_SendCommand(0x10)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(Blackbuf[y*163 + x])
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(~Redbuf[y*163 + x])
#M2 part 656*492
self.M2_SendCommand(0x10)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(Blackbuf[y*163 + x])
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(~Redbuf[y*163 + x])
#M1 part 648*492
self.M1_SendCommand(0x10)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(Blackbuf[y*163 + x])
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(~Redbuf[y*163 + x])
#S1 part 656*492
self.S1_SendCommand(0x10)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(Blackbuf[y*163 + x])
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(~Redbuf[y*163 + x])
end = time.perf_counter()
print("use time: %f"%(end - start))
self.TurnOnDisplay()
def clear(self):
"""Clear contents of image buffer"""
start = time.perf_counter()
self.S2_SendCommand(0x10)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(0xff)
self.S2_SendCommand(0x13)
for y in range(0, 492):
for x in range(0, 81):
self.S2_SendData(0x00)
self.M2_SendCommand(0x10)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(0xff)
self.M2_SendCommand(0x13)
for y in range(0, 492):
for x in range(81, 163):
self.M2_SendData(0x00)
self.M1_SendCommand(0x10)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(0xff)
self.M1_SendCommand(0x13)
for y in range(492, 984):
for x in range(0, 81):
self.M1_SendData(0x00)
self.S1_SendCommand(0x10)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(0xff)
self.S1_SendCommand(0x13)
for y in range(492, 984):
for x in range(81, 163):
self.S1_SendData(0x00)
end = time.perf_counter()
print (end)
print (start)
print("use time: %f" %(end - start))
self.TurnOnDisplay()
def Reset(self):
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0)
time.sleep(0.01)
epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1)
time.sleep(0.2)
def EPD_Sleep(self):
self.M1S1M2S2_SendCommand(0X02)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0X07)
self.M1S1M2S2_SendData(0xA5)
time.sleep(0.3)
print("module_exit")
epdconfig.module_exit()
def TurnOnDisplay(self):
self.M1M2_SendCommand(0x04)
time.sleep(0.3)
self.M1S1M2S2_SendCommand(0x12)
self.M1_ReadBusy()
self.S1_ReadBusy()
self.M2_ReadBusy()
self.S2_ReadBusy()
""" M1S1M2S2 Write register address and data """
def M1S1M2S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def M1S1M2S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M1M2 Write register address and data """
def M1M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M1M2_Sendata(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S2 Write register address and data """
def S2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
def S2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
""" M2 Write register address and data """
def M2_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
def M2_SendData(self, val):
epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
""" S1 Write register address and data """
def S1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
def S1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
""" M1 Write register address and data """
def M1_SendCommand(self, cmd):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(cmd)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
def M1_SendData(self, val):
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.spi_writebyte(val)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
#Busy
def M1_ReadBusy(self):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.M1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def M2_ReadBusy(self):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy = not(busy & 0x01)
self.M2_SendCommand(0x71)
while(busy):
self.M2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN)
busy =not(busy & 0x01)
time.sleep(0.2)
def S1_ReadBusy(self):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.S1_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
def S2_ReadBusy(self):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
while(busy):
self.S2_SendCommand(0x71)
busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN)
busy = not(busy & 0x01)
time.sleep(0.2)
lut_vcom1 = [
0x00, 0x10, 0x10, 0x01, 0x08, 0x01,
0x00, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x08, 0x01, 0x08, 0x01, 0x06,
0x00, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x00, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_ww1 = [
0x91, 0x10, 0x10, 0x01, 0x08, 0x01,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x08, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bw1 = [
0xA8, 0x10, 0x10, 0x01, 0x08, 0x01,
0x84, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x86, 0x06, 0x01, 0x06, 0x01, 0x05,
0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0xF0, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_wb1 = [
0x91, 0x10, 0x10, 0x01, 0x08, 0x01,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x08, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bb1 = [
0x92, 0x10, 0x10, 0x01, 0x08, 0x01,
0x80, 0x06, 0x01, 0x06, 0x01, 0x05,
0x84, 0x08, 0x01, 0x08, 0x01, 0x06,
0x04, 0x06, 0x01, 0x06, 0x01, 0x05,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06,
0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01,
0x01, 0x04, 0x05, 0x08, 0x08, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
def SetLut(self):
self.M1S1M2S2_SendCommand(0x20) #vcom
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_vcom1[count])
self.M1S1M2S2_SendCommand(0x21) #red not use
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_ww1[count])
self.M1S1M2S2_SendCommand(0x22) #bw r
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_bw1[count]) # bw=r
self.M1S1M2S2_SendCommand(0x23) #wb w
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_wb1[count]) # wb=w
self.M1S1M2S2_SendCommand(0x24) #bb b
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_bb1[count]) # bb=b
self.M1S1M2S2_SendCommand(0x25) #bb b
for count in range(0, 60):
self.M1S1M2S2_SendData(self.lut_ww1[count]) # bb=b
def M1_ReadTemperature(self):
self.M1_SendCommand(0x40)
self.M1_ReadBusy()
time.sleep(0.3)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 0)
epdconfig.digital_write(self.EPD_S1_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_S2_CS_PIN, 1)
epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1)
time.sleep(0.05)
temp = epdconfig.spi_readbyte(0x00)
epdconfig.digital_write(self.EPD_M1_CS_PIN, 1)
self.M1S1M2S2_SendCommand(0xE0)
self.M1S1M2S2_SendData(0x03)
self.M1S1M2S2_SendCommand(0xE5)
self.M1S1M2S2_SendData(temp)
+99
View File
@@ -0,0 +1,99 @@
# /*****************************************************************************
# * | File : epdconfig.py
# * | Author : Waveshare electrices
# * | Function : Hardware underlying interface
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-11-01
# * | Info :
# ******************************************************************************/
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import time
import os
import logging
import sys
from ctypes import *
EPD_SCK_PIN =11
EPD_MOSI_PIN =10
EPD_M1_CS_PIN =8
EPD_S1_CS_PIN =7
EPD_M2_CS_PIN =17
EPD_S2_CS_PIN =18
EPD_M1S1_DC_PIN =13
EPD_M2S2_DC_PIN =22
EPD_M1S1_RST_PIN =6
EPD_M2S2_RST_PIN =23
EPD_M1_BUSY_PIN =5
EPD_S1_BUSY_PIN =19
EPD_M2_BUSY_PIN =27
EPD_S2_BUSY_PIN =24
find_dirs = [
os.path.dirname(os.path.realpath(__file__)),
'/usr/local/lib',
'/usr/lib',
]
spi = None
for find_dir in find_dirs:
val = int(os.popen('getconf LONG_BIT').read())
logging.debug("System is %d bit"%val)
if val == 64:
so_filename = os.path.join(find_dir, 'DEV_Config_64.so')
else:
so_filename = os.path.join(find_dir, 'DEV_Config_32.so')
if os.path.exists(so_filename):
spi = CDLL(so_filename)
break
if spi is None:
RuntimeError('Cannot find DEV_Config.so')
def digital_write(pin, value):
spi.DEV_Digital_Write(pin, value)
def digital_read(pin):
return spi.DEV_Digital_Read(pin)
def spi_writebyte(value):
spi.DEV_SPI_WriteByte(value)
def delay_ms(delaytime):
time.sleep(delaytime / 1000.0)
def module_init():
spi.DEV_ModuleInit()
def module_exit():
spi.DEV_ModuleExit()
def spi_readbyte(Reg):
return spi.DEV_SPI_ReadByte(Reg)
def delay_ms(delaytime):
time.sleep(delaytime / 1000.0)