{"id":44325091,"url":"https://github.com/eljojo/estrella","last_synced_at":"2026-02-11T07:27:21.593Z","repository":{"id":333741931,"uuid":"1138060581","full_name":"eljojo/estrella","owner":"eljojo","description":"A print server for the StarPRNT protocol","archived":false,"fork":false,"pushed_at":"2026-01-28T04:58:23.000Z","size":4560,"stargazers_count":10,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-28T04:58:27.505Z","etag":null,"topics":["receipt-printer","starprnt","thermal-printer"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eljojo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-20T07:35:57.000Z","updated_at":"2026-01-28T00:22:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/eljojo/estrella","commit_stats":null,"previous_names":["eljojo/estrella"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/eljojo/estrella","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eljojo%2Festrella","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eljojo%2Festrella/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eljojo%2Festrella/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eljojo%2Festrella/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eljojo","download_url":"https://codeload.github.com/eljojo/estrella/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eljojo%2Festrella/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29329493,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-11T06:13:03.264Z","status":"ssl_error","status_checked_at":"2026-02-11T06:12:55.843Z","response_time":97,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["receipt-printer","starprnt","thermal-printer"],"created_at":"2026-02-11T07:27:20.917Z","updated_at":"2026-02-11T07:27:21.584Z","avatar_url":"https://github.com/eljojo.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⭐️ estrella \n\nA Rust library for [Star Micronics TSP650II](https://star-m.jp/eng/products/s_print/tsp650ii/index.html) thermal receipt printers over Bluetooth. Implements the [StarPRNT](https://starmicronics.com/support/download/starprnt-command-specifications/) protocol with a React-inspired component system, an optimizing compiler, and a web UI for photo printing.\n\n![My Desk](https://github.com/user-attachments/assets/30b569ec-d311-492a-9333-d069e9234289)\n\n## Photo Printing\n\nThe web UI supports printing photos with real-time dithered preview:\n\n- **Formats:** JPEG, PNG, GIF, WEBP, HEIC (iPhone photos)\n- **Adjustments:** Rotation, brightness, contrast\n- **Dithering:** Choose algorithm for best results\n- Auto-resize to 576px printer width\n\n\u003cimg width=\"1125\" height=\"1068\" alt=\"Screenshot 2026-01-23 at 18 19 25\" src=\"https://github.com/user-attachments/assets/d8e7779d-7940-47c6-a304-4fc6c7b2992e\" /\u003e\n\n## Image Downloads\n\nDocuments can include images from URLs. When a document is submitted via the JSON API, images are automatically downloaded, cached, resized, and dithered for printing.\n\n```json\n{\"type\": \"image\", \"url\": \"https://example.com/photo.jpg\"}\n```\n\n- **Auto-resize:** Images are scaled to the printer's full width (576 dots) preserving aspect ratio\n- **Max height:** Optional `height` field acts as a cap — if the resized image is taller, it shrinks to fit\n- **Alignment:** Images narrower than paper width are centered by default (`\"align\": \"center\"`). Also accepts `\"left\"` or `\"right\"`\n- **Dithering:** Defaults to Floyd-Steinberg. Set `\"dither\"` to `\"bayer\"`, `\"atkinson\"`, `\"jarvis\"`, or `\"none\"`\n- **Caching:** Downloaded images are cached in memory and shared with photo sessions (30-min TTL), so previewing a document multiple times won't re-download\n\n```json\n{\n  \"type\": \"image\",\n  \"url\": \"https://example.com/photo.jpg\",\n  \"width\": 400,\n  \"height\": 300,\n  \"align\": \"center\",\n  \"dither\": \"atkinson\"\n}\n```\n\n## The Document System\n\nInstead of manually constructing printer escape sequences, Estrella provides a declarative `Document` model. The same types work for both Rust construction and JSON deserialization — one set of types, zero conversion layer.\n\n```rust\nuse estrella::document::*;\n\nlet doc = Document {\n    document: vec![\n        Component::Banner(Banner::new(\"CHURRA MART\")),\n        Component::Text(Text { content: \"2026-01-20 12:00:00\".into(), center: true, ..Default::default() }),\n        Component::Spacer(Spacer::mm(3.0)),\n        Component::Banner(Banner { content: \"TODAY ONLY: 50% OFF\".into(), border: BorderStyle::Double, size: 2, ..Default::default() }),\n        Component::Divider(Divider::default()),\n        Component::LineItem(LineItem::new(\"Espresso\", 4.50)),\n        Component::LineItem(LineItem::new(\"Croissant\", 3.25)),\n        Component::Divider(Divider::default()),\n        Component::Total(Total { amount: 7.75, bold: Some(true), double_width: true, ..Default::default() }),\n        Component::Spacer(Spacer::mm(3.0)),\n        Component::QrCode(QrCode { data: \"https://example.com/rewards\".into(), cell_size: Some(6), ..Default::default() }),\n        Component::Text(Text { content: \"Thank you!\".into(), center: true, bold: true, ..Default::default() }),\n    ],\n    cut: true,\n    ..Default::default()\n};\n\nlet bytes = doc.build();                     // StarPRNT bytes, ready to send\nlet json = serde_json::to_string(\u0026doc)?;     // Same type serializes to JSON\n```\n\n![Demo Receipt](tests/golden/demo_receipt.png)\n\n### Components\n\n| Component | Description |\n|-----------|-------------|\n| `Text` | Styled text (bold, center, invert, size 0–3, optional `font: \"ibm\"`) |\n| `Header` | Pre-styled centered bold header |\n| `Banner` | Framed text with box-drawing borders, auto-sizing (optional `font: \"ibm\"`) |\n| `LineItem` | Left name + right price (e.g., \"Coffee\" ... \"$4.50\") |\n| `Total` | Right-aligned total line |\n| `Divider` | Horizontal line (dashed, solid, double, equals) |\n| `Spacer` | Vertical space in mm, lines, or raw units |\n| `Columns` | Two-column layout (left + right) |\n| `Table` | Table with box-drawing borders, headers, per-column alignment |\n| `Markdown` | Rich text from Markdown (headings, bold, lists) |\n| `Image` | Image from URL (downloaded, cached, dithered, auto-centered) |\n| `Pattern` | Generative art pattern with params |\n| `Canvas` | Absolute-positioned raster compositing with blend modes |\n| `QrCode`, `Pdf417`, `Barcode` | 1D and 2D barcodes |\n| `NvLogo` | Logo from printer's flash memory |\n\n## Dithering Algorithms\n\nThermal printers are binary (black or white), so grayscale images need dithering. Estrella implements four algorithms:\n\n| Algorithm | Characteristics |\n|-----------|-----------------|\n| **Floyd-Steinberg** | Classic error diffusion. Smooth gradients, organic look. Default for photos. |\n| **Atkinson** | Bill Atkinson's Mac algorithm. Higher contrast, loses 25% of error intentionally. |\n| **Jarvis** | Spreads error over 12 neighbors. Smoothest gradients, slightly slower. |\n| **Bayer** | Ordered 8x8 matrix. Fast, deterministic, halftone pattern. Best for patterns. |\n\n| Floyd-Steinberg | Atkinson | Jarvis | Bayer |\n|-----------------|----------|--------|-------|\n| ![Floyd-Steinberg](tests/golden/dither_floyd_steinberg.png) | ![Atkinson](tests/golden/dither_atkinson.png) | ![Jarvis](tests/golden/dither_jarvis.png) | ![Bayer](tests/golden/dither_bayer.png) |\n\n## Pattern Generation\n\n![The web ui allows to preview patterns](https://github.com/user-attachments/assets/7a2d8847-0458-4a55-9044-65cd67a721d2)\n\nProcedural patterns for artistic prints and printer calibration. Each pattern has randomizable parameters.\n\n| ![Ripple](tests/golden/ripple.png) | ![Waves](tests/golden/waves.png) | ![Plasma](tests/golden/plasma.png) |\n|:--:|:--:|:--:|\n| Ripple | Waves | Plasma |\n\n\u003cdetails\u003e\n\u003csummary\u003eMore patterns\u003c/summary\u003e\n\n**Op Art**\n\n| ![Riley](tests/golden/riley.png) | ![Vasarely](tests/golden/vasarely.png) | ![Scintillate](tests/golden/scintillate.png) | ![Moire](tests/golden/moire.png) |\n|:--:|:--:|:--:|:--:|\n| Riley | Vasarely | Scintillate | Moire |\n\n**Organic**\n\n| ![Topography](tests/golden/topography.png) | ![Rings](tests/golden/rings.png) | ![Flowfield](tests/golden/flowfield.png) | ![Mycelium](tests/golden/mycelium.png) |\n|:--:|:--:|:--:|:--:|\n| Topography | Rings | Flowfield | Mycelium |\n\n| ![Erosion](tests/golden/erosion.png) | ![Crystal](tests/golden/crystal.png) | ![Reaction Diffusion](tests/golden/reaction_diffusion.png) | ![Voronoi](tests/golden/voronoi.png) |\n|:--:|:--:|:--:|:--:|\n| Erosion | Crystal | Reaction Diffusion | Voronoi |\n\n**Glitch**\n\n| ![Glitch](tests/golden/glitch.png) | ![Corrupt Barcode](tests/golden/corrupt_barcode.png) | ![Databend](tests/golden/databend.png) | ![Scanline Tear](tests/golden/scanline_tear.png) |\n|:--:|:--:|:--:|:--:|\n| Glitch | Corrupt Barcode | Databend | Scanline Tear |\n\n**Generative**\n\n| ![Attractor](tests/golden/attractor.png) | ![Automata](tests/golden/automata.png) | ![Estrella](tests/golden/estrella.png) |\n|:--:|:--:|:--:|\n| Attractor | Automata | Estrella |\n\n**Textures**\n\n| ![Crosshatch](tests/golden/crosshatch.png) | ![Stipple](tests/golden/stipple.png) | ![Woodgrain](tests/golden/woodgrain.png) | ![Weave](tests/golden/weave.png) |\n|:--:|:--:|:--:|:--:|\n| Crosshatch | Stipple | Woodgrain | Weave |\n\n**Calibration**\n\n| ![Calibration](tests/golden/calibration.png) | ![Microfeed](tests/golden/microfeed.png) | ![Density](tests/golden/density.png) | ![Jitter](tests/golden/jitter.png) |\n|:--:|:--:|:--:|:--:|\n| Calibration | Microfeed | Density | Jitter |\n\n| ![Overburn](tests/golden/overburn.png) |\n|:--:|\n| Overburn |\n\n\u003c/details\u003e\n\n### Pattern Weaving\n\nBlend multiple patterns with DJ-style crossfade transitions:\n\n```bash\nestrella weave ripple plasma waves --length 200mm --crossfade 30mm\n```\n\n![Weave Crossfade](tests/golden/weave_crossfade.png)\n\n## JSON API\n\nThe JSON API uses the same `Document` type as the Rust API — the component structs are all `Serialize + Deserialize`, so JSON documents map directly to Rust types with zero conversion. Useful for automations (e.g. Home Assistant daily briefings).\n\n```bash\ncurl -X POST http://localhost:8080/api/json/print \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"document\": [\n      {\"type\": \"banner\", \"content\": \"GOOD MORNING\"},\n      {\"type\": \"text\", \"content\": \"Monday, January 27\", \"center\": true, \"size\": 0},\n      {\"type\": \"divider\", \"style\": \"double\"},\n      {\"type\": \"text\", \"content\": \" WEATHER \", \"bold\": true, \"invert\": true},\n      {\"type\": \"columns\", \"left\": \"Now\", \"right\": \"6°C Cloudy\"},\n      {\"type\": \"columns\", \"left\": \"High / Low\", \"right\": \"11°C / 3°C\"},\n      {\"type\": \"divider\"},\n      {\"type\": \"text\", \"content\": \" CALENDAR \", \"bold\": true, \"invert\": true},\n      {\"type\": \"columns\", \"left\": \"9:00\", \"right\": \"Standup\"},\n      {\"type\": \"columns\", \"left\": \"11:30\", \"right\": \"Dentist\"},\n      {\"type\": \"divider\"},\n      {\"type\": \"qr_code\", \"data\": \"https://calendar.google.com\"}\n    ],\n    \"cut\": true\n  }'\n```\n\nThe web UI includes a JSON API tab with a live preview editor and a sample daily briefing template.\n\nCanvas components support absolute-positioned compositing with blend modes:\n\n```json\n{\n  \"type\": \"canvas\",\n  \"height\": 100,\n  \"elements\": [\n    {\"type\": \"pattern\", \"name\": \"estrella\", \"height\": 80, \"position\": {\"x\": -43, \"y\": 0}, \"blend_mode\": \"add\"},\n    {\"type\": \"text\", \"content\": \"Hello World\", \"center\": true, \"position\": {\"x\": 7, \"y\": 16}, \"blend_mode\": \"add\"},\n    {\"type\": \"total\", \"amount\": 0, \"position\": {\"x\": 0, \"y\": 34}, \"blend_mode\": \"add\"}\n  ]\n}\n```\n\nElements without `position` stack top-to-bottom (flow mode). Dithering defaults to `\"auto\"` — Atkinson when continuous-tone content is detected, none otherwise.\n\n**Endpoints:**\n- `POST /api/json/preview` — returns a PNG preview\n- `POST /api/json/print` — sends to printer\n\n\u003cdetails\u003e\n\u003csummary\u003eFull component reference\u003c/summary\u003e\n\nEach component in the `\"document\"` array has a `\"type\"` field and type-specific properties:\n\n| Type | Required | Optional (defaults) |\n|------|----------|---------------------|\n| `text` | `content` | `bold`, `underline`, `upperline`, `invert`, `upside_down`, `reduced` (false); `smoothing` (null/auto); `align` (\"left\"), `center`, `right` (false); `size` (1, default Font A — 0=Font B, 2=double, 3=triple, or `[h,w]`); `scale` (null); `double_width`, `double_height` (false); `inline` (false); `font` (null — set `\"ibm\"` for IBM Plex Sans) |\n| `header` | `content` | `variant`: \"normal\" (2x2 centered bold) or \"small\" (1x1) |\n| `banner` | `content` | `size` (3, max expansion 0–3, auto-cascades width); `border`: \"single\"/\"double\"/\"heavy\"/\"shade\"/\"shadow\"; `bold` (true); `padding` (1); `font` (null — set `\"ibm\"` for IBM Plex Sans) |\n| `line_item` | `name`, `price` | `width` (48) |\n| `total` | `amount` | `label` (\"TOTAL:\"), `bold` (true), `double_width` (false), `align` (\"right\") |\n| `divider` | — | `style`: \"dashed\" / \"solid\" / \"double\" / \"equals\"; `width` (48) |\n| `spacer` | one of: `mm`, `lines`, `units` | — |\n| `blank_line` | — | — |\n| `columns` | `left`, `right` | `width` (48), `bold`, `underline`, `invert` (false) |\n| `table` | `rows` | `headers` (null), `border`: \"single\"/\"double\"/\"mixed\"/\"heavy\"/\"shade\" (default: \"single\"); `align` ([] — per-column: \"left\"/\"center\"/\"right\"); `row_separator` (false); `width` (48) |\n| `markdown` | `content` | `show_urls` (false) |\n| `qr_code` | `data` | `cell_size` (4), `error_level` (\"M\"), `align` (\"center\") |\n| `pdf417` | `data` | `module_width` (3), `ecc_level` (2), `align` (\"center\") |\n| `barcode` | `format`, `data` | `height` (80); format: \"code128\" / \"code39\" / \"ean13\" / \"upca\" / \"itf\" |\n| `image` | `url` | `dither` (\"floyd-steinberg\"), `width` (576), `height` (null), `align` (\"center\" — also \"left\", \"right\"; only affects images narrower than paper) |\n| `pattern` | `name` | `height` (500), `params` ({}), `dither` (\"bayer\") |\n| `canvas` | `elements` | `height` (auto), `width` (576), `dither` (\"auto\" — detects continuous-tone content); each element: `position` ({x, y}), `blend_mode` (\"normal\"), `opacity` (1.0) + any component fields |\n| `nv_logo` | `key` | `center` (false), `scale` (1), `scale_x` (1), `scale_y` (1) |\n\n**Text `size`** controls both font selection and character expansion using a 1-indexed model:\n\n| Size | Font | Expansion | Chars/line | Description |\n|------|------|-----------|-----------|-------------|\n| `0` | B (9×24) | none | 64 | Small text |\n| `1` | A (12×24) | none | 48 | Normal (default) |\n| `2` | A | 2× | 24 | Double |\n| `3` | A | 3× | 16 | Triple |\n\nPass a single number for uniform scaling (`\"size\": 2` = double height and width), or an `[h, w]` array for independent control (`\"size\": [3, 1]` = triple height, normal width, 48 chars/line).\n\nThe `banner` component uses the same sizing model but auto-selects the largest width that fits. Given `\"size\": 3`, it tries widths 3→2→1→Font B until the content fits inside the box-drawing frame.\n\n**`cut`** at the top level defaults to `true`. Set to `false` to suppress the paper cut.\n\n\u003c/details\u003e\n\n## How It Works: The Compilation Pipeline\n\nDocument components emit an intermediate representation (IR), which gets optimized before generating StarPRNT bytes:\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                              COMPILATION PIPELINE                           │\n├─────────────────────────────────────────────────────────────────────────────┤\n│                                                                             │\n│   ┌─────────────┐      ┌─────────────┐      ┌─────────────┐      ┌───────┐  │\n│   │  Document   │      │     IR      │      │  Optimizer  │      │ Bytes │  │\n│   │             │      │             │      │             │      │       │  │\n│   │  Text       │      │  Op::Init   │      │  4 passes   │      │ ESC @ │  │\n│   │  LineItem   │ ───► │  Op::Text   │ ───► │  that remove│ ───► │ ...   │  │\n│   │  QrCode     │ emit │  Op::Bold   │      │  redundant  │ gen  │ 1D 69 │  │\n│   │  ...        │      │  Op::Cut    │      │  operations │      │ ...   │  │\n│   │             │      │  ...        │      │             │      │       │  │\n│   └─────────────┘      └─────────────┘      └─────────────┘      └───────┘  │\n│                                                                             │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### Optimizer Passes\n\nThe optimizer runs four passes to eliminate redundant operations:\n\n```\nPass 1: Remove Redundant Init     Pass 2: Collapse Style Toggles\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄     ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\nInit  ←── keep                    SetBold(true)\n...                               Text(\"Hello\")\nInit  ←── remove                  SetBold(false) ┐\n...                               SetBold(true)  ┘── remove pair\nInit  ←── remove                  Text(\"World\")\n\n\nPass 3: Remove Redundant Styles   Pass 4: Merge Adjacent Text\n┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄    ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄\nSetBold(true)                     Text(\"Hello\")  ┐\nText(\"A\")                         Newline        │── merge into\nSetBold(true) ←── remove          Text(\"World\")  ┘   \"Hello\\nWorld\"\nText(\"B\")\n```\n\n**Result:** 11 ops → 6 ops, ~5-8% smaller output with identical visual results.\n\n## Running at Home\n\n### Prerequisites\n\n- **Printer:** Star Micronics TSP650II (or compatible StarPRNT printer)\n- **Connection:** Bluetooth, paired to create `/dev/rfcomm0`\n- **Build:** Nix (recommended) or Rust 1.75+\n\n### Quick Start\n\n```bash\n# With Nix\nnix develop\ncargo run -- serve\n# Open http://localhost:8080\n\n# Without Nix\ncargo build --release\n./target/release/estrella serve --device /dev/rfcomm0\n```\n\n### NixOS Module\n\nFor a proper deployment on NixOS:\n\n```nix\n{\n  inputs.estrella.url = \"github:eljojo/estrella\";\n\n  # Add to your flake outputs:\n  nixpkgs.overlays = [ inputs.estrella.overlays.default ];\n\n  # Enable the service:\n  services.estrella = {\n    enable = true;\n    port = 8080;\n    deviceMac = \"00:11:22:33:44:55\";  # Your printer's Bluetooth MAC\n    # rfcommChannel = 0;  # Optional, defaults to 0\n  };\n}\n```\n\nThe module creates two systemd services:\n- `estrella-rfcomm.service` - Oneshot that sets up the Bluetooth RFCOMM device (runs as root)\n- `estrella.service` - The HTTP daemon (runs unprivileged with DynamicUser)\n\n### Bluetooth Setup\n\n**Important:** The TSP650II ships in SSP (Secure Simple Pairing) mode which doesn't work with Linux. You need the **Star Settings** iOS/Android app to:\n1. Disable \"Auto Connect\" (SSP mode)\n2. Set a PIN code\n\nWithout this step, pairing will silently fail. This was the hardest part to debug.\n\n**Pairing:**\n```bash\nbluetoothctl\n\u003e power on\n\u003e agent on\n\u003e default-agent\n\u003e scan on\n# Find your printer's MAC address (starts with 00:12:F3 for Star)\n\u003e pair XX:XX:XX:XX:XX:XX\n\u003e trust XX:XX:XX:XX:XX:XX\n\u003e connect XX:XX:XX:XX:XX:XX\n```\n\n**Bind rfcomm device (automatic):**\n```bash\nsudo estrella setup-rfcomm XX:XX:XX:XX:XX:XX\n# Connects, verifies with l2ping, and creates /dev/rfcomm0\n```\n\n**Bind rfcomm device (manual):**\n```bash\nsudo rfcomm bind 0 XX:XX:XX:XX:XX:XX 1\n# Creates /dev/rfcomm0\n```\n\n**Permissions:** Add your user to the `dialout` group for `/dev/rfcomm0` access:\n```bash\nsudo usermod -aG dialout $USER\n# Log out and back in\n```\n\n**Debug commands:**\n```bash\nbluetoothctl info XX:XX:XX:XX:XX:XX\nl2ping -c 1 XX:XX:XX:XX:XX:XX\nsdptool browse XX:XX:XX:XX:XX:XX\n```\n\n### CLI Reference\n\n```bash\nestrella print ripple              # Print a pattern\nestrella print ripple --png out.png  # Preview to PNG\nestrella print --list              # List patterns\nestrella serve                     # Start web server\nestrella weave ripple plasma --length 200mm  # Blend patterns\nestrella logo store logo.png       # Store logo in NV memory\nestrella setup-rfcomm XX:XX:XX:XX:XX:XX  # Set up Bluetooth RFCOMM (requires root)\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eLong Print Mode (Buffer Overflow Prevention)\u003c/summary\u003e\n\n### The Problem\n\nThermal printers have limited internal buffers (~100-200KB). Large images can overflow the buffer causing print failures.\n\n### The Solution\n\nEstrella automatically splits large images into multiple independent print jobs (~1000 rows each). Each job completes fully before the next begins, preventing buffer overflow.\n\n### How It Works\n\n```\nLarge Image (e.g., 2000 rows)\n    ↓\nsplit_for_long_print()\n    ↓\nJob 1: Init + Raster(rows 0-999)\n    ↓ [1 second pause]\nJob 2: Init + Raster(rows 1000-1999) + Feed + Cut\n    ↓\nPrinter outputs seamless image\n```\n\n- Each job is a complete, independent StarPRNT program\n- NO feed between jobs (image appears continuous)\n- Feed/Cut only on the final job\n- 1 second pause between jobs lets printer catch up\n\n### Usage\n\nLong print mode is **automatic** when using the web UI or CLI.\n\nFor programmatic use:\n```rust\nlet programs = program.split_for_long_print();\ntransport.send_programs(\u0026programs)?;\n```\n\n### Technical Details\n\n| Parameter | Value |\n|-----------|-------|\n| Chunk size | 1000 rows (~125mm, ~72KB) |\n| Pause between jobs | 1 second |\n| Band mode alignment | 24-row boundaries |\n\n\u003c/details\u003e\n\n## Fonts\n\nThe preview renderer embeds bitmap fonts for receipt rendering:\n\n- **[Spleen](https://github.com/fcambus/spleen)** 12×24 — Font A (48 chars/line). Copyright (c) 2018-2024 Frederic Cambus. BSD 2-Clause license.\n- **[UW ttyp0](https://people.mpi-inf.mpg.de/~uwe/misc/uw-ttyp0/)** 9×18 — Font B/C (64 chars/line, scaled vertically to 9×24 / 9×17). Copyright (c) 2012-2015 Uwe Waldmann. ttyp0 license (MIT-like).\n- **[IBM Plex Sans](https://github.com/IBM/plex)** — Optional TTF font for Text and Banner components (`\"font\": \"ibm\"`). Anti-aliased rendering via `ab_glyph`, dithered to 1-bit. Copyright (c) IBM Corp. Apache 2.0 license.\n\n## Emojis\n\nEmojis obtained from [Emojipedia](https://emojipedia.org/docomo/2006).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feljojo%2Festrella","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feljojo%2Festrella","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feljojo%2Festrella/lists"}