diff --git a/AGENTS.md b/AGENTS.md index 2d7f759..da3ef9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,11 @@ A professional "Octobot Journalist" that generates daily, weekly, and monthly re - **Topics:** IT News, GCP, AI (Anthropic/OpenAI), Hacker News, Regensburg Local News. - **Special Features:** Monday catch-up, Drill-down ("Tell me more"), Gardening (Hochbeet), Supermarket Shopping Guide. +### [memobird](skills/memobird/SKILL.md) +Direct printing to the Memobird thermal printer. Allows sending text, images, and notes to the device on the local network. +- **Features:** Text styling (big, bold, underline), automated 1-bit image conversion, separator lines. +- **Target:** Default targets the static IP `192.168.10.165`. + ### [rvv-regensburg](skills/rvv-regensburg/SKILL.md) Real-time public transport information for Regensburg (RVV). Provides departures, stop finding, and journey planning via direct API access. - **Features:** Real-time departures with delay info, connection search, Stop ID resolver. diff --git a/memobird.skill b/memobird.skill new file mode 100644 index 0000000..d801589 Binary files /dev/null and b/memobird.skill differ diff --git a/skills/memobird/SKILL.md b/skills/memobird/SKILL.md new file mode 100644 index 0000000..1b1dfa8 --- /dev/null +++ b/skills/memobird/SKILL.md @@ -0,0 +1,44 @@ +--- +name: memobird +description: Print text, images, and notes directly to the Memobird thermal printer. Use this when the user wants to print a physical note, a shopping list, or a small image to the Memobird at its local IP. +--- + +# 🐦 Memobird Printer + +Direct printing to the Memobird thermal printer on the local network. + +## 🚀 Usage + +Use the `scripts/memobird_print.py` tool. By default, it targets the static IP `192.168.10.165`. + +### 1. Print simple text +```bash +./scripts/memobird_print.py --text "Hello from Gemini!" +``` + +### 2. Print styled text +```bash +./scripts/memobird_print.py --text "IMPORTANT NOTE" --big --bold +``` + +### 3. Print an image +```bash +./scripts/memobird_print.py --image "path/to/image.png" +``` + +### 4. Print a separator line +```bash +./scripts/memobird_print.py --line THICK +``` + +## 🛠️ Options +- `--text`: The string to print. +- `--image`: Path to a local image file (converted to 1-bit monochrome automatically). +- `--ip`: Override the default IP (192.168.10.165). +- `--big`, `--bold`, `--underline`: Text styling. +- `--line`: Print a line (`THICK`, `THIN`, or `DASH`). + +## 💡 Tips +- **Character Limit:** Thermal paper is narrow (384px wide). Long lines of text will wrap. +- **Image Size:** Large images might fail if they take too long to transfer. Keep them reasonably sized. +- **Connection:** Ensure the printer is powered on and connected to the same WiFi network. diff --git a/skills/memobird/scripts/memobird_print.py b/skills/memobird/scripts/memobird_print.py new file mode 100755 index 0000000..b9360df --- /dev/null +++ b/skills/memobird/scripts/memobird_print.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +import sys +import os +import requests +import json +import base64 +from io import BytesIO +from random import randint +from PIL import Image as PILImg, ImageOps +import argparse + +# Default IP for Moritz's Memobird +DEFAULT_HOST = "192.168.10.165" + +def to_byte_array(img): + imgByteArr = BytesIO() + img.save(imgByteArr, format='bmp') + return imgByteArr.getvalue() + +class MemobirdSender: + def __init__(self, host=DEFAULT_HOST, port='80'): + self.host = host + self.port = port + self.uri = f"http://{self.host}:{self.port}/sys/printer" + + def send(self, text_list, print_id=None): + print_id = print_id or randint(1, 10**6) + payload = { + 'command': 3, + 'content': { + 'textList': text_list + }, + 'encryptFlag': 0, + 'hasHead': 0, + 'hasSignature': 0, + 'hasTail': 0, + 'isFromDirectPrint': False, + 'msgType': 1, + 'pkgCount': 1, + 'pkgNo': 1, + 'printID': print_id, + 'priority': 0, + 'result': 0, + 'scripType': 3 + } + + try: + resp = requests.post(self.uri, json=payload, timeout=10) + resp.raise_for_status() + return resp.status_code + except Exception as e: + print(f"Error sending to Memobird: {e}") + sys.exit(1) + +def create_text_element(text, big=False, bold=False, underline=False): + if not text.endswith('\n'): + text += '\n' + return { + 'encodeType': 0, + 'printType': 1, + 'basetext': base64.b64encode(text.encode("GBK")).decode(), + 'fontSize': 1 + big, + 'bold': 1 * bold, + 'underline': 1 * underline + } + +def create_image_element(path): + img = PILImg.open(path) + img = img.convert(mode='L') + img = ImageOps.flip(img) + img = img.convert(mode='1') + return { + 'encodeType': 0, + 'printType': 5, + 'basetext': base64.b64encode(to_byte_array(img)).decode() + } + +def create_line_element(linetype='THICK'): + codes = {'THICK': 41, 'THIN': 42, 'DASH': 43} + return { + 'encodeType': 0, + 'printType': 4, + 'iconID': codes.get(linetype, 41) + } + +def main(): + parser = argparse.ArgumentParser(description="Print to Memobird") + parser.add_argument("--text", help="Text to print") + parser.add_argument("--image", help="Path to image to print") + parser.add_argument("--ip", default=DEFAULT_HOST, help=f"Memobird IP (default: {DEFAULT_HOST})") + parser.add_argument("--big", action="store_true", help="Use big font") + parser.add_argument("--bold", action="store_true", help="Use bold font") + parser.add_argument("--underline", action="store_true", help="Use underline") + parser.add_argument("--line", choices=['THICK', 'THIN', 'DASH'], help="Print a separator line") + + args = parser.parse_args() + + if not args.text and not args.image and not args.line: + parser.print_help() + sys.exit(1) + + sender = MemobirdSender(host=args.ip) + elements = [] + + if args.line: + elements.append(create_line_element(args.line)) + + if args.text: + elements.append(create_text_element(args.text, big=args.big, bold=args.bold, underline=args.underline)) + + if args.image: + if os.path.exists(args.image): + elements.append(create_image_element(args.image)) + else: + print(f"Error: Image not found: {args.image}") + sys.exit(1) + + status = sender.send(elements) + print(f"Success! Printer returned {status}") + +if __name__ == "__main__": + main()