Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214#!/usr/bin/env python3"""Generate a wordmark plot fitted to the live plotter limits.
Queries OH;/OP; off the plotter, then composes: - frame at 92% of the P1/P2 box - big title (HP-GL LB), centered horizontally, set above mid-line - subtitle (smaller LB), centered horizontally, set below mid-line
Output goes to stdout (or --out file) as HPGL, ready to feed to serial-tx.py.
Usage: wordmark.py --port /dev/cu.usbserial-110 --title "AESTHETIC COMPUTER" \ --subtitle "HP 7585B · FIRST PLOT · 2026-04-30" --out plot.hpgl"""
from __future__ import annotations
import argparseimport osimport sys
import serial
XON, XOFF = 0x11, 0x13
def _ask(ser: serial.Serial, cmd: bytes) -> str: import time
ser.reset_input_buffer() ser.write(cmd) ser.flush() deadline = time.time() + 1.0 buf = bytearray() while time.time() < deadline: chunk = ser.read(64) if chunk: for b in chunk: if b not in (XON, XOFF): buf.append(b) if b"\n" in buf or b"\r" in buf: break else: time.sleep(0.05) return buf.decode("ascii", errors="replace").strip()
def _ints(s: str) -> list[int]: out = [] for tok in s.replace(",", " ").split(): try: out.append(int(tok)) except ValueError: pass return out
def query(port: str, baud: int) -> tuple[tuple[int, int, int, int], tuple[int, int]]: ser = serial.Serial( port=port, baudrate=baud, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, xonxoff=False, rtscts=True, dsrdtr=True, timeout=0.5, ) ser.setDTR(True) ser.setRTS(True) try: pp = _ints(_ask(ser, b"OP;")) of = _ints(_ask(ser, b"OF;")) finally: ser.close() if len(pp) != 4 or len(of) < 2: raise SystemExit("plotter did not respond to OP;/OF;") return (pp[0], pp[1], pp[2], pp[3]), (of[0], of[1])
def _label_width_mm(text: str, char_cm_w: float) -> float: # HP-GL default char spacing: cell width = 1.5 * char_width. return len(text) * char_cm_w * 1.5 * 10.0
def fit_size(text: str, max_mm: float, target_mm: float) -> float: """Pick char width in cm so the label is at most max_mm wide and ideally target_mm. Return char width (cm).""" spacing = 1.5 cw_cm = target_mm / (len(text) * spacing * 10.0) if _label_width_mm(text, cw_cm) > max_mm: cw_cm = max_mm / (len(text) * spacing * 10.0) return cw_cm
def build( p1p2: tuple[int, int, int, int], factor: tuple[int, int], title: str, subtitle: str, pen_title: int = 1, pen_sub: int = 1, pen_frame: int = 1,) -> bytes: x1, y1, x2, y2 = p1p2 fx, fy = factor
# 92% frame fx1 = int(x1 * 0.92) fy1 = int(y1 * 0.92) fx2 = int(x2 * 0.92) fy2 = int(y2 * 0.92)
# available width for text in mm, leaving 5% inside frame avail_w_mm = (fx2 - fx1) / fx * 0.90 avail_h_mm = (fy2 - fy1) / fy
# title: ~80% of width, height ~ 35% of avail height title_cw_cm = fit_size(title, avail_w_mm, avail_w_mm * 0.85) title_ch_cm = title_cw_cm * 1.6 # taller than wide for presence
# subtitle: smaller — height roughly 25-30% of title sub_cw_cm = fit_size(subtitle, avail_w_mm * 0.7, avail_w_mm * 0.45) sub_ch_cm = sub_cw_cm * 1.4
title_w_units = int(_label_width_mm(title, title_cw_cm) * fx / 10) sub_w_units = int(_label_width_mm(subtitle, sub_cw_cm) * fx / 10)
title_h_units = int(title_ch_cm * 10 * fy) sub_h_units = int(sub_ch_cm * 10 * fy)
# vertical layout: title above midline, subtitle below gap = int(avail_h_mm * 0.05 * fy) title_y = gap // 2 sub_y = -title_h_units // 2 - gap - sub_h_units
# …actually simpler: title baseline above center, subtitle baseline below. title_baseline = int(avail_h_mm * 0.10 * fy) sub_baseline = -int(avail_h_mm * 0.10 * fy) - sub_h_units
cmds: list[str] = [] cmds.append("IN;DT\x03;") # default terminator = ETX (0x03) cmds.append("DI1,0;") # horizontal labels
# Frame cmds.append(f"SP{pen_frame};") cmds.append( f"PA{fx1},{fy1};PD;PA{fx2},{fy1};PA{fx2},{fy2};" f"PA{fx1},{fy2};PA{fx1},{fy1};PU;" )
# Title cmds.append(f"SP{pen_title};") cmds.append(f"SI{title_cw_cm:.3f},{title_ch_cm:.3f};") cmds.append(f"PA{-title_w_units // 2},{title_baseline};") cmds.append(f"LB{title}\x03")
# Subtitle cmds.append(f"SP{pen_sub};") cmds.append(f"SI{sub_cw_cm:.3f},{sub_ch_cm:.3f};") cmds.append(f"PA{-sub_w_units // 2},{sub_baseline};") cmds.append(f"LB{subtitle}\x03")
# park cmds.append("PU;PA0,0;SP0;")
return ("\n".join(cmds) + "\n").encode("ascii")
def main() -> int: p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument( "--port", default=os.environ.get("HP7585B_TTY", ""), help="serial device (defaults to $HP7585B_TTY)", ) p.add_argument("--baud", type=int, default=9600) p.add_argument("--title", default="AESTHETIC COMPUTER") p.add_argument("--subtitle", default="HP 7585B") p.add_argument("--out", help="write HPGL here (default stdout)") p.add_argument("--pen-title", type=int, default=1) p.add_argument("--pen-sub", type=int, default=1) p.add_argument("--pen-frame", type=int, default=1) args = p.parse_args() if not args.port: p.error("no --port and HP7585B_TTY not set")
p1p2, factor = query(args.port, args.baud) sys.stderr.write( f"plotter P1/P2 = {p1p2}, factor = {factor}\n" ) hpgl = build( p1p2, factor, title=args.title, subtitle=args.subtitle, pen_title=args.pen_title, pen_sub=args.pen_sub, pen_frame=args.pen_frame, ) if args.out: with open(args.out, "wb") as f: f.write(hpgl) sys.stderr.write(f"wrote {len(hpgl)} bytes to {args.out}\n") else: sys.stdout.buffer.write(hpgl) return 0
if __name__ == "__main__": sys.exit(main())