almanach

module
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT

README ΒΆ

πŸ–¨οΈ Almanach β€” Thermal Printer Studio

Almanach is a self-hosted tool for designing, rendering, and printing pages on 58mm thermal paper. It pairs a React-based layout editor (Almanach Studio) with a Go rendering service that uses Chrome headless to produce pixel-perfect 1-bit bitmaps, then sends them to an ESP32-powered thermal printer.

What's in the Box

almanach/
β”œβ”€β”€ cmd/almanach-render-service/  ← Go binary entrypoint
β”œβ”€β”€ internal/app/                 ← CLI verbs, HTTP API, Chrome renderer, bitmap packing, printer client
β”œβ”€β”€ internal/app/doc/             ← Embedded help pages (layout DSL reference, tutorials)
β”œβ”€β”€ internal/web/                 ← go:embed target for bundled SPA assets
β”œβ”€β”€ web/                          ← Almanach Studio React SPA source
β”‚   └── src/almanach-studio.jsx   ← Layout engine, block renderers, themes, image upload
β”‚   └── src/fonts-embedded.css   ← Google Fonts woff2 (base64) β€” no network needed at runtime
β”œβ”€β”€ firmware/atoms3r/             ← ESP-IDF firmware for M5Stack AtomS3R Lite printer endpoint
β”œβ”€β”€ examples/
β”‚   β”œβ”€β”€ layouts/                  ← Sample YAML layouts (9 examples)
β”‚   β”œβ”€β”€ rendered/                 ← Pre-rendered PNG previews with inspect metadata
β”‚   └── bundles/                  ← ZIP bundles with layout + image assets
└── Dockerfile                    ← Single-container image: Go binary + Chrome headless-shell

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  YAML/JSON   │───▢│  Go Server   │───▢│  Chrome   │───▢│  1-bit       β”‚
β”‚  Layout      β”‚    β”‚  (serve)     β”‚    β”‚  Headless  β”‚    β”‚  Bitmap      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                          β”‚                                     β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                      β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
                   β”‚  Studio SPA  β”‚                      β”‚  ESP32      β”‚
                   β”‚  (React UI)  β”‚                      β”‚  Printer    β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. You write a layout in YAML (or JSON) β€” a list of blocks like title, weather, quote, image.
  2. The Go server navigates Chrome to the Studio SPA, injects your layout JSON, waits for fonts/images to load, and screenshots the paper element.
  3. The screenshot is converted to a packed 1-bit bitmap (384px wide = 48 bytes/row).
  4. The bitmap is sent to the ESP32 printer firmware via HTTP POST (/api/print/bitmap).
  5. The ESP32 streams the bitmap to the K118 thermal mechanism over UART.

Quick Start

Build
# Build web assets and bundle into Go binary
make build
# β€” or manually:
CGO_ENABLED=1 go build -tags=embed -o dist/almanach-render-service ./cmd/almanach-render-service

CGO_ENABLED=1 is required β€” go-sqlite3 (indirect dep via glazed) needs CGO. The repo's Dockerfile originally used CGO_ENABLED=0 which crashes at startup.

Print a layout
almanach-render-service print \
  --layout examples/layouts/02-daily-briefing.yaml \
  --printer-ip 192.168.0.126 \
  --feed-lines 3 \
  --output yaml
Preview without printing
almanach-render-service render \
  --layout examples/layouts/02-daily-briefing.yaml \
  --out /tmp/preview.png
Start the web UI
almanach-render-service serve --port 8199
# Open http://localhost:8199/almanach

Almanach Studio UI

CLI Commands

Command Description
serve Start the HTTP API server + Studio web UI
render Render a layout once β†’ PNG or raw bitmap
inspect Render + emit DOM metrics (for debugging cutoffs)
print Render + send bitmap to the ESP32 printer
ble-provision Pair the printer over BLE (native Go or Web Bluetooth)
setup Start the setup wizard for first-time BLE pairing

HTTP API

Endpoint Method Description
GET /health GET Health check (printer IP, version)
GET /almanach GET Almanach Studio SPA
GET /almanach/bundle.js GET React bundle
GET /almanach/fonts.css GET Embedded Google Fonts (base64 woff2)
POST /api/render POST Render a layout β†’ PNG or bitmap
POST /api/render-and-print POST Render + send to printer
GET /setup GET BLE pairing wizard
Print via HTTP
# Convert YAML β†’ JSON and POST
cat layout.yaml | python3 -c "import sys,yaml,json; print(json.dumps(yaml.safe_load(sys.stdin)))" | \
  curl -X POST http://localhost:8199/api/render-and-print \
    -H "Content-Type: application/json" \
    -d @-

Layout Format

Layouts are YAML or JSON objects describing a thermal paper page as an ordered list of blocks:

almanach_studio_version: 1
theme: minimal
paperWidth: 384
bodyScale: 1.3
feedLines: 3
blocks:
  - id: title-1
    type: title
    data:
      text: DAILY BRIEFING
      subtitle: Thursday, May 14, 2026
  - id: date-1
    type: date
    data:
      date: May 14, 2026
      day: Thursday
  - id: weather-1
    type: weather
    data:
      temp: 18Β°C
      condition: Clear morning
      high: 23Β°C
      low: 12Β°C
  - id: quote-1
    type: quote
    data:
      text: Simplicity is prerequisite for reliability.
      author: Dijkstra
Block Types
Type Description
title Page heading with subtitle
date Date line under the title
divider Visual separator (line, dots, wave, leaves)
weather Compact current conditions
plan Time-ordered checklist (with done/undone)
news Short headlines
note Italic callout
quote Centered quotation
word Vocabulary/word-of-the-day
history Dated historical facts
did Fun facts list
habits Weekly habit tracker grid
mood Daily mood/energy/sleep tracker
reading Current book + reading queue
reflection Daily journal footer
image Embedded photo or illustration

See almanach-render-service help layout-dsl-reference for the complete field reference.

Themes
Theme Style
classic Double-line ornate frame, serif fonts
minimal Clean, no frame, DM Sans + EB Garamond
botanical ❦ corner decorations, serif fonts
notebook Lined paper, handwriting fonts
ledger Vintage ledger, boxed sections
space ✦ star decorations, sans-serif
Images

Use the image block to embed photos or illustrations. For headless/CLI rendering, embed as data URLs so no network fetch is needed:

- id: img-1
  type: image
  data:
    label: Fox
    src: data:image/png;base64,iVBOR...
    alt: A fox engraving
    caption: Vintage illustration
    height: 100
    fit: cover
    border: true
    grayscale: true

To convert a local image:

python3 -c "import base64,sys; print('data:image/png;base64,' + base64.b64encode(open(sys.argv[1],'rb').read()).decode())" image.png

Example Renders

Minimal

01-minimal

Daily Briefing

02-daily-briefing

Knowledge Strip

03-knowledge-strip

Tracker Journal

04-tracker-journal

Image Block

08-image-block

Paper Shell Preview (with zigzag edges)

daily-briefing-shell

Printing Architecture

Segmented Printing

The ESP32 httpd cannot reliably receive >~38 KiB in a single HTTP request. Bitmaps exceeding this limit are automatically split into horizontal segments and sent as sequential POSTs. Only the last segment includes the paper feed. This is handled transparently β€” no action needed from the user.

Large bitmap (111 KiB)
    β”œβ”€β”€ Segment 1 (36 KiB) β†’ POST /api/print/bitmap (X-Feed: 0)
    β”œβ”€β”€ Segment 2 (36 KiB) β†’ POST /api/print/bitmap (X-Feed: 0)
    β”œβ”€β”€ Segment 3 (36 KiB) β†’ POST /api/print/bitmap (X-Feed: 0)
    └── Segment 4 ( 3 KiB) β†’ POST /api/print/bitmap (X-Feed: 3)
Paper Feed

Feed is implemented as baked blank raster rows appended to the bitmap body. The firmware also receives an X-Feed header as a supplementary signal (issues ESC d n), but the primary feed mechanism is the blank rows since ESC d n alone is not visually reliable on the K118 mechanism.

3 feed lines Γ— 24 pixels/line = 72 blank rows β‰ˆ 3.4 KiB at 384px width.

ESP32 Printer API
POST http://<PRINTER_IP>/api/print/bitmap
Headers:
  X-Width: 384
  X-Height: <height>
  X-Feed: <feedLines>
  Content-Type: application/octet-stream
Body: packed 1-bit bitmap (MSB first, 48 bytes/row at 384px)

Docker Deployment

Build & Run
docker build -t almanach-render-service .
docker run -p 8199:8199 \
  -e ALMANACH_PRINTER_IP=192.168.0.126 \
  almanach-render-service

The Docker image bundles the Go binary + Chrome headless-shell in a single container. Google Fonts are embedded as base64 woff2 in the SPA bundle β€” no network access needed at render time, ensuring pixel-identical output regardless of the container's network.

Published Image

GitOps deployments publish and consume the repository-scoped GHCR package:

ghcr.io/go-go-golems/almanach:sha-<commit>

Do not use the old ghcr.io/go-go-golems/almanach-render-service package for deployments. That package is obsolete/private and was replaced by ghcr.io/go-go-golems/almanach so GitHub Actions can publish the image with repository-scoped package permissions.

Docker Compose
ALMANACH_PRINTER_IP=192.168.0.126 docker compose up

BLE Pairing

The printer uses ESP-IDF BLE provisioning with Security 1. Pair when the printer is new or after a physical reset.

CLI pairing
# Verify BLE connection
almanach-render-service ble-provision \
  --implementation native \
  --action version \
  --service-name ALM_0F2320 --pop alm-0f2320

# Provision WiFi
almanach-render-service ble-provision \
  --implementation native \
  --action provision \
  --service-name ALM_0F2320 --pop alm-0f2320 \
  --ssid YOUR_WIFI --passphrase YOUR_PASSWORD
Web Bluetooth pairing
  1. almanach-render-service setup --port 8199
  2. Open http://localhost:8199/setup in Chrome
  3. Enter PoP, WiFi credentials, click "Find BLE printer"
  4. Printer IP is saved to ~/.config/almanach/render-service/state.json

Configuration

Variable Default Description
ALMANACH_PORT 8199 HTTP listen port
ALMANACH_WEB_DIR ./web/dist SPA static files (empty β†’ embedded bundle)
ALMANACH_PRINTER_IP (empty) ESP32 printer IP
ALMANACH_CHROME_PATH (auto) Chrome binary (local mode)
CHROME_WS_URL (empty) WebSocket URL for remote Chrome (Docker mode)
ALMANACH_PAPER_WIDTH 384 Paper width in pixels
ALMANACH_FONT_SCALE 1.4 Body font scale multiplier
ALMANACH_DEFAULT_FEED 3 Feed lines after printing
ALMANACH_DEFAULT_THEME minimal Default theme key

Firmware

The firmware/atoms3r/ directory contains the ESP-IDF firmware that runs on the M5Stack AtomS3R Lite β€” the small ESP32-S3 device that connects to the K118 thermal printer mechanism and exposes the bitmap printing API over WiFi.

Hardware
  • MCU: M5Stack AtomS3R Lite (ESP32-S3)
  • Printer: K118 thermal printer mechanism (58mm paper, 384 dots/line)
  • Connection: UART at 9600 baud (TX=GPIO8, RX=GPIO7, CTS=GPIO6)
  • Power: USB-C (AtomS3R) + dedicated 5V/2A for printer mechanism
What the firmware does
  • Runs an HTTP server with bitmap printing, text printing, and status endpoints
  • Receives packed 1-bit bitmaps and streams them row-by-row to the K118 over UART
  • Buffers the full bitmap body before starting UART output (direct streaming caused visible stripes due to timing gaps)
  • Supports BLE provisioning (ESP-IDF Security 1) for WiFi setup without a serial connection
  • Includes the Almanach Studio SPA as embedded assets (served at /almanach in SoftAP mode when offline)
  • Exposes printer control endpoints: density, speed, graphics mode, temperature, status
Firmware HTTP API
Endpoint Method Description
POST /api/print/bitmap POST Print a 1-bit bitmap (headers: X-Width, X-Height, X-Feed)
POST /api/print/text POST Print plain text ({"text": "..."})
GET /api/status GET WiFi + printer state JSON
GET /api/printer/status GET K118 real-time status byte
GET /api/printer/temp GET Printhead temperature
GET /api/printer/baud GET Current UART baud rate
POST /api/printer/density POST Set print density (0–15)
POST /api/printer/speed POST Set print speed
POST /api/printer/graphics-mode POST Enable/disable graphics mode
GET / GET Status dashboard page
GET /almanach GET Almanach Studio SPA
Build & Flash
cd firmware/atoms3r
./build.sh /dev/ttyACM0 build
./build.sh /dev/ttyACM0 flash-monitor

Requires ESP-IDF 5.4.x. The build.sh script wraps idf.py build and idf.py flash monitor.

Firmware Documentation

The firmware/atoms3r/docs/ directory contains detailed engineering notes:

  • 01-k118-command-discoveries.md β€” Practical discoveries from the Chinese K118 command spec (baud, status, bitmap, density, speed, graphics mode, temperature, flow control)
  • 02-bitmap-stripes-flow-control.md β€” Full record of the bitmap stripe/pause investigation (why full-body buffering is needed, why chunk streaming and 5-row banding caused visible seams)
  • 03-original-arduino-firmware-command-inventory.md β€” Inventory of every printer command used by the original M5Stack Arduino firmware
  • PROJ-SToMS3R-... β€” Obsidian project report with architecture, command surface, and research links

License

MIT

Directories ΒΆ

Path Synopsis
cmd
build-web command
build-web builds the React frontend and copies the output to internal/web/embed/public/ for embedding via //go:embed.
build-web builds the React frontend and copies the output to internal/web/embed/public/ for embedding via //go:embed.
gen
internal
app
layoutpb
Package layoutpb is the Go side of the Almanach Layout DSL v2 contract.
Package layoutpb is the Go side of the Almanach Layout DSL v2 contract.
web

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL