---
title: Printer Client
description: Run the enyDyne order printer on a Raspberry Pi.
---

# Printer Client

`cmd/printer` polls restaurant orders and prints through file or direct USB output. It targets 32-bit ARM Linux. USB mode uses libusb and ESC/POS commands.

## Installation

Run installer as regular Raspberry Pi user:

```bash
curl --proto '=https' --tlsv1.3 -sSf https://docs.enydyne.de/scripts/install-printer.sh | sh
```

Installer downloads latest ARMv7 binary, verifies server-provided SHA-256, installs it to `$HOME/.local/bin/printer`, generates config when missing, and enables system service for startup. It prints config path to edit and reboot command when complete. USB support is statically linked, so target needs no libusb package. `sudo` is used only for service installation.

Installer uses `https://app.enydyne.de` by default. For another deployment, download script and pass server origin as first argument or set `ENYDYNE_ORIGIN`.

## Configuration

Config path is `$XDG_CONFIG_HOME/enydyne/print/config.yaml`. Without `XDG_CONFIG_HOME`, client uses `$HOME/.config/enydyne/print/config.yaml`; without `HOME`, it uses `/etc/enydyne/print/config.yaml`.

Generate example config at resolved path:

```bash
printer init
```

Command prints only absolute created path to stdout. Errors go to stderr. It refuses to overwrite existing config. Edit server URL, output directory, query names, and API keys before starting client.

```yaml
server_url: https://app.enydyne.de/
poll_interval: 10s

printer:
  mode: file
  device: /var/spool/enydyne-print
  character_size: 1 # Reserved; any value is ignored in file mode.
  max_width: 42

queries:
  - name: restaurant-north
    api_key: eny_replace_me
  - name: restaurant-south
    api_key: eny_replace_me_too
```

`server_url` is server origin, not GraphQL path, and defaults to `https://app.enydyne.de/`. Override it for development; loopback HTTP such as `http://127.0.0.1:4321` is accepted. Client appends `/api/graphql`. Each API key selects one restaurant. Query names must be unique and may contain letters, digits, `_`, and `-`.

In file mode, `printer.device` must be absolute directory path. `max_width` must be between 1 and 1024 and limits each output line by Unicode character count. `character_size` does not affect file mode.

### Direct USB

USB mode discovers a USB Printer Class interface by vendor and product ID, claims its bulk OUT endpoint, sends one ESC/POS receipt, then closes connection. Each print reconnects, so unplugged printer can recover on later poll. Example for printer from reference driver:

```yaml
printer:
  mode: usb
  vendor_id: 0x0416
  product_id: 0x5011
  character_size: 1
  max_width: 42
```

`vendor_id` and `product_id` must both be non-zero. `character_size` accepts 1 through 8 and scales both width and height through ESC/POS `GS !`; rendered line width is `max_width / character_size`. Printer must accept UTF-8 receipt text and ESC/POS initialize, character-size, and partial-cut commands.

Service user needs raw USB-device permission. Create udev rule matching configured IDs, replacing values and group when needed:

```bash
printf '%s\n' 'SUBSYSTEM=="usb", ATTR{idVendor}=="0416", ATTR{idProduct}=="5011", MODE="0660", GROUP="'"$(id -gn)"'"' | sudo tee /etc/udev/rules.d/70-enydyne-printer.rules >/dev/null
sudo udevadm control --reload-rules
sudo udevadm trigger
```

USB mode stages receipt under data directory `printed/` before sending and keeps successful receipt copies there. These records prevent repeat output from overlapping polls. Failures before USB transfer are retried after automatic pending-record cleanup. Transfer errors or process interruption during send leave `.pending` record because printer outcome cannot be known. Client blocks that receipt instead of risking duplicate but continues later orders without advancing checkpoint. Check paper, then delete pending record to retry, or rename it without `.pending` suffix to confirm receipt printed.

Protect config because it contains API keys. For default per-user path:

```bash
chmod 600 "${XDG_CONFIG_HOME:-$HOME/.config}/enydyne/print/config.yaml"
```

When neither XDG path nor `HOME` exists, protect global config with `chmod 600 /etc/enydyne/print/config.yaml`.

## Data

Logs, process lock, and polling checkpoint live under `$XDG_DATA_HOME/enydyne/print`. Fallbacks are `$HOME/.local/share/enydyne/print` and then `/var/lib/enydyne/print` when `HOME` is unset.

Keep generated text files in output directory. Their stable names provide duplicate protection when process stops after writing an order but before saving its checkpoint. Moving or deleting files removes this protection for overlapping polls.

Client re-queries one polling interval to tolerate late visibility and modest clock skew. Host still needs working time synchronization. It catches up in ranges no larger than 31 days and subdivides ranges containing over 1,000 orders.

## Updates

Printer checks client registry before first order poll and every hour afterward. It compares SHA-256 of running `/proc/self/exe` with latest `printer-linux-armv7` metadata from configured `server_url`. On mismatch it downloads update, validates size and checksum, keeps current binary as `printer.previous`, atomically replaces executable, and restarts itself.

If restart itself fails, printer automatically restores `printer.previous`. Backup also remains available for manual recovery if a checksum-valid update starts but later fails.

Update checks, downloads, GraphQL polling, and printing run in one sequential event loop. Due update always runs before another poll, and update waits for any current poll to finish, so print jobs and GraphQL requests never overlap binary replacement. Installer-managed `$HOME/.local/bin/printer` is writable by service user and supports self-update.

## Build

Production binary can be queried and downloaded from [client registry](./client-registry.md):

```text
GET /api/clients/printer/linux/armv7/latest
GET /api/clients/printer/linux/armv7/latest/download
```

Local cross-build requires ARM hard-float GCC, ARM libusb development files, and matching `pkg-config` search path. Debian example:

```bash
sudo dpkg --add-architecture armhf
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross libudev-dev:armhf libusb-1.0-0-dev:armhf pkg-config
PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig \
  CGO_ENABLED=1 CGO_LDFLAGS='-Wl,--as-needed -Wl,-Bstatic -lusb-1.0 -Wl,-Bdynamic -ludev -latomic -pthread' CC=arm-linux-gnueabihf-gcc \
  GOOS=linux GOARCH=arm GOARM=7 go build -tags netgo,osusergo \
  -ldflags '-s -w -linkmode external' -o printer ./cmd/printer
```

<!--
Sitemap

URL: https://docs.enydyne.de/index.md
Title: enyDyne API
Description: Integration documentation for the enyDyne restaurant platform.

URL: https://docs.enydyne.de/api-keys.md
Title: API Keys
Description: Create, use, and revoke restaurant API keys.

URL: https://docs.enydyne.de/client-registry.md
Title: Client Registry
Description: Query and download enyDyne client binaries.

URL: https://docs.enydyne.de/graphql.md
Title: GraphQL API
Description: Query restaurant-scoped order data.

URL: https://docs.enydyne.de/printer-client.md
Title: Printer Client
Description: Run the enyDyne order printer on a Raspberry Pi.
-->
