Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

plotters/hp7585b: bring up the HP 7585B over RS-232

End-to-end pipeline for driving the 7585B drafting plotter alongside the
existing AxiDraw helpers.

What landed:
- Hardware-flow serial sender (CTS/DSR/DTR + Xon/Xoff watchdog, 256-byte
framing on `;` boundaries, configurable per-byte delay).
- Live-limits probe — `OE; OH; OP; OF; OS;` to pull error register, hard
clip, P1/P2, plotter-unit factor, and status off the machine; the same
script can emit a vpype paper profile from the live sheet.
- Native HP-GL composer (`notepat-slide.py`) with EA / RA / CI / EW / EP
/ FP / PM / SL / FT / LT / SI / DI / DT / LB primitives, plus an
SVG-path → polyline flattener (used to render the PALS mark from
bills/invoices/pals.svg as a faceted outline). Every primitive emits
HP-GL and an SVG preview side-by-side so the layout can be eyeballed
on the desktop before plotting.
- vpype TOML profile for `hp7585b` (10 paper sizes, ANSI A–E + ISO
A4–A0, derived from the Aug 1985 spec).
- Fish helpers (`hp7585bplot`, `hp7585bsend`, `hp7585bhome`, etc.)
parallel to the existing `axi*` helpers; top-level README rewritten
for the multi-plotter layout.
- Reference HPGL files captured along the way: a confidence test, a
full-width "aesthetic.computer" wordmark, and the in-progress notepat
keymap slide.

docs/:
- HP 7580/7585B/7586B Interfacing & Programming Manual
(07580-90034, Nov 1985, 406pp) — the authoritative HP-GL reference
- HP 7585B Service Manual (07585-90002, Aug 1983) from bitsavers
- A period sample HPGL test file for cross-checking command syntax

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+2935 -11
+31 -11
plotters/README.md
··· 1 1 # Plotters 2 2 3 - Helper Fish shell functions for controlling an AxiDraw via `axicli`. 3 + Per-plotter helpers and configs. Each plotter has its own subdir with a 4 + README, drivers, and any device profiles it needs. The flat `axi*.fish` 5 + scripts at this level are the AxiDraw helpers (kept top-level for muscle 6 + memory; conceptually they belong to AxiDraw). 4 7 5 - Place each `.fish` file in `~/.config/fish/functions/` or `source` them in your `~/.config/fish/config.fish` to make the commands available in your shell. 8 + ## Devices 6 9 7 - ## Commands 8 - - `axihome` – move to home 9 - - `axipenup` – raise pen 10 - - `axipendown` – lower pen 11 - - `axienable` – enable XY motors 12 - - `axidisable` – disable XY motors 13 - - `axicycle` – run the AxiDraw demo cycle 14 - - `axiplot file.svg [axicli args...]` – plot a file with optional extra `axicli` arguments 10 + - **AxiDraw** — flat fish helpers (`axihome`, `axipenup`, `axipendown`, 11 + `axienable`, `axidisable`, `axicycle`, `axiplot file.svg ...`). Driven via 12 + `axicli`. See [Examples](#axidraw-examples) below. 13 + - **HP 7585B** — `hp7585b/`. A0/E-size 8-pen drafting plotter, driven via 14 + vpype (SVG → HPGL) plus serial helpers. See `hp7585b/README.md`. 15 15 16 - ## Examples 16 + ## Install (fish) 17 + 18 + Symlink the helpers you want into `~/.config/fish/functions/` or `source` 19 + them from `config.fish`. For the AxiDraw helpers: 20 + 21 + ```fish 22 + for f in ~/aesthetic-computer/plotters/axi*.fish 23 + ln -sf $f ~/.config/fish/functions/(basename $f) 24 + end 25 + ``` 26 + 27 + For the HP 7585B helpers: 28 + 29 + ```fish 30 + for f in ~/aesthetic-computer/plotters/hp7585b/hp7585b*.fish 31 + ln -sf $f ~/.config/fish/functions/(basename $f) 32 + end 33 + ``` 34 + 35 + ## AxiDraw examples 36 + 17 37 ``` 18 38 axihome 19 39 axipenup
+4
plotters/hp7585b/.gitignore
··· 1 + # output.png is the user's photo of the plot — copied locally, not for repo 2 + output.png 3 + output.jpg 4 + output.heic
+71
plotters/hp7585b/README.md
··· 1 + # HP 7585B 2 + 3 + A0/E-size 8-pen drafting plotter (1983) driven via HP-GL over RS-232 or HP-IB. 4 + Workflow: SVG → vpype HPGL exporter → serial pipe to the plotter. 5 + 6 + ## One-time setup 7 + 8 + ### 1. Install vpype 9 + 10 + ```fish 11 + pipx install "vpype[all]" # or: brew install pipx && pipx install vpype 12 + ``` 13 + 14 + ### 2. Register this device profile 15 + 16 + Either link the bundled config from your home dir: 17 + 18 + ```fish 19 + ln -sf ~/aesthetic-computer/plotters/hp7585b/vpype.toml ~/.vpype.toml 20 + ``` 21 + 22 + …or pass it explicitly on every invocation: `vpype --config ~/aesthetic-computer/plotters/hp7585b/vpype.toml ...`. 23 + 24 + ### 3. Wire up the plotter 25 + 26 + - Default RS-232: **9600 8N1**, hardware (Xon/Xoff) flow control. DIP-switch 27 + configurable on the rear panel — verify before first plot. 28 + - macOS USB-to-serial adapters appear as `/dev/cu.usbserial-*`. Export 29 + `HP7585B_TTY` (the helpers below read it) so you don't have to repeat the 30 + path: 31 + 32 + ```fish 33 + set -Ux HP7585B_TTY /dev/cu.usbserial-XXXX 34 + ``` 35 + 36 + - Set Metric/Imperial mode on the front panel to match the paper you're 37 + loading (the vpype profile says which is required per size). 38 + 39 + ## Doing a test print 40 + 41 + ```fish 42 + # Convert SVG to HPGL on A3 metric 43 + vpype read test-print.svg \ 44 + write -d hp7585b -p a3 test-print.hpgl 45 + 46 + # Send it 47 + hp7585bplot test-print.hpgl 48 + ``` 49 + 50 + A small canned `test-print.svg` lives next to this README — it draws a frame, 51 + diagonals, a circle, and a couple of pen-swap text labels so you can verify 52 + clip limits and the carousel. 53 + 54 + ## Helpers 55 + 56 + Source these from your fish config (or symlink into `~/.config/fish/functions/`): 57 + 58 + - `hp7585bplot file.hpgl` – cats an HPGL file to `$HP7585B_TTY` with the 59 + right `stty` settings. 60 + - `hp7585bsend "PU;PA0,0;"` – send a raw HP-GL fragment. 61 + - `hp7585bhome` – pen up, pen home (`PU;PA0,0;`). 62 + - `hp7585bpenup` / `hp7585bpendown` – `PU;` / `PD;`. 63 + - `hp7585bpen N` – select pen N from the carousel (`SP N;`), 0 stows. 64 + - `hp7585binit` – issue a fresh `IN;` (reinitialize plotter state). 65 + 66 + ## References 67 + 68 + - HP 7585B Interfacing & Programming Manual (PN 07585-90002, Aug 1983) — 69 + scanned at bitsavers. 70 + - HP Computer Museum — 7585B entry. 71 + - vpype HPGL cookbook — schema for the TOML profile in this directory.
+7
plotters/hp7585b/aesthetic-computer-fullwidth.hpgl
··· 1 + IN;DT*;DI1,0; 2 + SP1;VS20;FS2; 3 + PA-7808,-4436;PD;PA7808,-4436;PA7808,4436;PA-7808,4436;PA-7808,-4436;PU; 4 + SI1.416,2.266; 5 + PA-7647,-453; 6 + LBaesthetic.computer* 7 + PU;PA0,0;SP0;
+13
plotters/hp7585b/aesthetic-computer.hpgl
··· 1 + IN; 2 + DT*; 3 + DI1,0; 4 + SP1; 5 + VS15; 6 + FS2; 7 + PA-15325,-4207;PD;PA15325,-4207;PA15325,4207;PA-15325,4207;PA-15325,-4207;PU; 8 + PA-14400,0;CI600; 9 + PA14400,0;CI600; 10 + SI2.4,3.6; 11 + PA-12960,-1800; 12 + LBaesthetic.computer* 13 + PU;PA0,0;SP0;
+142
plotters/hp7585b/bisect-test.py
··· 1 + #!/usr/bin/env python3 2 + """Send progressively-larger slabs of the slide to find which section 3 + triggers E2. After each slab we read OE; — first non-zero reading 4 + identifies the offending section.""" 5 + 6 + from __future__ import annotations 7 + 8 + import os 9 + import sys 10 + import time 11 + from pathlib import Path 12 + 13 + import serial 14 + 15 + XON, XOFF = 0x11, 0x13 16 + 17 + SECTIONS = [ 18 + ("init", ["IN;DT\x03;DI1,0;LT;SP1;VS25;FS2;\n"]), 19 + ("title", ["SI1.476,2.214;PA-7969,3500;LBnotepat.com keymap\x03\n"]), 20 + ("divider", ["PU;PA-7567,3050;PD;PA7567,3050;PU;\n"]), 21 + ("piano-naturals",[]), # filled below 22 + ("piano-sharps", []), 23 + ("qwerty-row1", []), 24 + ("qwerty-row2", []), 25 + ("qwerty-row3", []), 26 + ("connectors", []), 27 + ("tagline", []), 28 + ("urls", []), 29 + ("park", ["LT;PU;PA0,0;SP0;\n"]), 30 + ] 31 + 32 + # Reuse the actual HPGL we generate by parsing the existing file into the 33 + # section buckets. Simpler: just send the file in halves first. 34 + FILE = Path("/Users/jas/aesthetic-computer/plotters/hp7585b/notepat-slide.hpgl") 35 + data = FILE.read_text() 36 + lines = data.splitlines(keepends=True) 37 + 38 + # Split lines into sections by content heuristics. 39 + naturals, sharps, q1, q2, q3, conns, tag, urls, post = [], [], [], [], [], [], [], [], [] 40 + phase = "header" 41 + for ln in lines: 42 + if ln.startswith("IN;") or ln.startswith("PU;PA-7567"): 43 + continue # init/divider already in SECTIONS 44 + if "LBnotepat.com keymap" in ln: 45 + continue 46 + # piano naturals: y range 1200..2500, the "labelled" lines 47 + if "LB" in ln and "1370" in ln and len(ln.split("LB")[-1]) <= 3: 48 + naturals.append(ln); continue 49 + # piano sharps: double_rect calls — y at 1700..2500 and 1760..2440 50 + if ("EA" in ln) and ("1700" in ln or "1760" in ln or "2500" in ln) and "PA-" in ln and "1200" not in ln: 51 + sharps.append(ln); continue 52 + # QWERTY row 1 ytop=800 53 + if "EA" in ln and "800" in ln and "-100" in ln: 54 + q1.append(ln); continue 55 + if "LB" in ln and "200" in ln and "y" in "": 56 + q1.append(ln); continue 57 + # ... fall back: dump remaining into post 58 + post.append(ln) 59 + 60 + # This heuristic is rough — instead just split file into 4 quarters. 61 + quarters = [data[i:i+len(data)//4] for i in range(0, len(data), len(data)//4)] 62 + 63 + 64 + def open_port(): 65 + s = serial.Serial("/dev/cu.usbserial-110", 9600, bytesize=8, 66 + parity=serial.PARITY_NONE, stopbits=1, 67 + xonxoff=False, rtscts=True, dsrdtr=True, timeout=0.5) 68 + s.setDTR(True); s.setRTS(True) 69 + return s 70 + 71 + 72 + def ask(ser, cmd): 73 + ser.reset_input_buffer() 74 + ser.write(cmd); ser.flush() 75 + deadline = time.time() + 1.0 76 + buf = bytearray() 77 + while time.time() < deadline: 78 + c = ser.read(64) 79 + if c: 80 + for b in c: 81 + if b not in (XON, XOFF): 82 + buf.append(b) 83 + if b"\n" in buf or b"\r" in buf: 84 + break 85 + else: 86 + time.sleep(0.05) 87 + return buf.decode("ascii", errors="replace").strip() 88 + 89 + 90 + def send_slow(ser, payload: bytes): 91 + """Send with 256-byte frames + 2ms/byte + 30ms/frame.""" 92 + i = 0 93 + while i < len(payload): 94 + # cut on ; or ETX boundary, max 256 95 + end = min(i + 256, len(payload)) 96 + if end < len(payload): 97 + cut = payload.rfind(b";", i, end + 1) 98 + if cut > i: 99 + end = cut + 1 100 + chunk = payload[i:end] 101 + for b in chunk: 102 + ser.write(bytes([b])) 103 + time.sleep(0.002) 104 + ser.flush() 105 + time.sleep(0.030) 106 + i = end 107 + 108 + 109 + def main(): 110 + ser = open_port() 111 + try: 112 + # always start with init + clear error 113 + print("clearing error...") 114 + ask(ser, b"OE;") 115 + time.sleep(0.5) 116 + cumulative = b"" 117 + for idx, q in enumerate(quarters, 1): 118 + print(f"\n=== sending quarter {idx}/{len(quarters)} ({len(q)} bytes) ===") 119 + cumulative += q.encode("ascii") 120 + # we resend cumulative each time so plotter state is consistent 121 + # but that's expensive — instead just send the new piece. 122 + send_slow(ser, q.encode("ascii")) 123 + time.sleep(2.0) 124 + err = ask(ser, b"OE;") 125 + print(f" OE; -> {err}") 126 + if err.strip() and err.strip() != "0": 127 + print(f" *** ERROR after quarter {idx} ***") 128 + # save the quarter to disk for inspection 129 + Path(f"/tmp/quarter-{idx}.hpgl").write_text(q) 130 + print(f" payload saved to /tmp/quarter-{idx}.hpgl") 131 + # park the pen 132 + ask(ser, b"PU;PA0,0;SP0;") 133 + return 1 134 + print("\nall quarters clean") 135 + ask(ser, b"PU;PA0,0;SP0;") 136 + finally: 137 + ser.close() 138 + return 0 139 + 140 + 141 + if __name__ == "__main__": 142 + sys.exit(main())
plotters/hp7585b/docs/hp7580-7585-7586b-interfacing-programming-07580-90034-Nov85.pdf

This is a binary file and will not be displayed.

plotters/hp7585b/docs/hp7585b-service-manual-07585-90002-Aug83.pdf

This is a binary file and will not be displayed.

+945
plotters/hp7585b/docs/reference-test.hpgl
··· 1 + .Y.@;3:.I20;;17:.N;19:;IN;IP;IW;LT;PU;PA0,0;IW0,0,32407,23500; 2 + VS10; 3 + PU;PA21543,2437; 4 + PD;PA21543,21328; 5 + PU;PA16820,21328; 6 + PD;PA16820,2437; 7 + PU;PA12098,2437; 8 + PD;PA12098,21328; 9 + PU;PA7375,21328; 10 + PD;PA7375,2437; 11 + PU;PA2651,16605; 12 + PD;PA26266,16605; 13 + PA26266,21328; 14 + PU;PA21543,11882; 15 + PD;PA2651,11882; 16 + PU;PA2651,7160; 17 + PD;PA26266,7160; 18 + PA26266,2437; 19 + PU;PA30990,2437; 20 + PD;PA30990,21328; 21 + PA2651,21328; 22 + PA2651,2437; 23 + PA30990,2437; 24 + 25 + PU;PA2651,2437; 26 + PD;PA7375,2808; 27 + PU;PA2651,2437; 28 + PD;PA7375,3185; 29 + PU;PA2651,2437; 30 + PD;PA7375,3571; 31 + PU;PA2651,2437; 32 + PD;PA7375,3971; 33 + PU;PA2651,2437; 34 + PD;PA7375,4393; 35 + PU;PA2651,2437; 36 + PD;PA7375,4843; 37 + PU;PA2651,2437; 38 + PD;PA7375,5331; 39 + PU;PA2651,2437; 40 + PD;PA7375,5868; 41 + PU;PA2651,2437; 42 + PD;PA7375,6471; 43 + PU;PA2651,2437; 44 + PD;PA7375,7160; 45 + PU;PA2651,2437; 46 + PD;PA6685,7160; 47 + PU;PA2651,2437; 48 + PD;PA6083,7160; 49 + PU;PA2651,2437; 50 + PD;PA5546,7160; 51 + PU;PA2651,2437; 52 + PD;PA5058,7160; 53 + PU;PA2651,2437; 54 + PD;PA4608,7160; 55 + PU;PA2651,2437; 56 + PD;PA4186,7160; 57 + PU;PA2651,2437; 58 + PD;PA3785,7160; 59 + PU;PA2651,2437; 60 + PD;PA3399,7160; 61 + PU;PA2651,2437; 62 + PD;PA3023,7160; 63 + 64 + PU;PA7138,21328; 65 + PD;PA7138,16605; 66 + PU;PA6902,16605; 67 + PD;PA6902,21328; 68 + PU;PA6902,17078; 69 + PD;PA2651,17078; 70 + PU;PA2651,17314; 71 + PD;PA6902,17314; 72 + PU;PA6902,17550; 73 + PD;PA2651,17550; 74 + PU;PA6430,17550; 75 + PD;PA6430,21328; 76 + PU;PA6194,21328; 77 + PD;PA6194,17550; 78 + PU;PA5958,17550; 79 + PD;PA5958,21328; 80 + PU;PA5958,18022; 81 + PD;PA2651,18022; 82 + PU;PA2651,18258; 83 + PD;PA5958,18258; 84 + PU;PA5958,18494; 85 + PD;PA2651,18494; 86 + PU;PA5485,18494; 87 + PD;PA5485,21328; 88 + PU;PA5249,21328; 89 + PD;PA5249,18494; 90 + PU;PA5013,18494; 91 + PD;PA5013,21328; 92 + PU;PA5013,18967; 93 + PD;PA2651,18967; 94 + PU;PA2651,19203; 95 + PD;PA5013,19203; 96 + PU;PA5013,19439; 97 + PD;PA2651,19439; 98 + PU;PA4541,19439; 99 + PD;PA4541,21328; 100 + PU;PA4305,21328; 101 + PD;PA4305,19439; 102 + PU;PA4068,19439; 103 + PD;PA4068,21328; 104 + PU;PA4068,19911; 105 + PD;PA2651,19911; 106 + PU;PA2651,20148; 107 + PD;PA4068,20148; 108 + PU;PA4068,20384; 109 + PD;PA2651,20384; 110 + PU;PA3596,20384; 111 + PD;PA3596,21328; 112 + PU;PA3360,21328; 113 + PD;PA3360,20384; 114 + PU;PA3124,20384; 115 + PD;PA3124,21328; 116 + PU;PA3124,20856; 117 + PD;PA2651,20856; 118 + 119 + PU;PA21352,19802; 120 + PD;PA18347,16796; 121 + PA18263,16879; 122 + PA21269,19885; 123 + PA21185,19969; 124 + PA18180,16963; 125 + PA18096,17046; 126 + PA21102,20052; 127 + PA21018,20136; 128 + PA18013,17130; 129 + PA17929,17213; 130 + PA20935,20219; 131 + PA20851,20303; 132 + PA17846,17297; 133 + PA17762,17380; 134 + PA20768,20386; 135 + PA20684,20470; 136 + PA17679,17464; 137 + PA17595,17547; 138 + PA20601,20553; 139 + PA20518,20637; 140 + PA17512,17631; 141 + PA17428,17714; 142 + PA20434,20720; 143 + PA20351,20804; 144 + PA17345,17798; 145 + PA17261,17881; 146 + PA20267,20887; 147 + PA20184,20971; 148 + PA17178,17965; 149 + PA17094,18048; 150 + PA20100,21054; 151 + PA20017,21138; 152 + PA17011,18132; 153 + PU;PA18347,21138; 154 + PD;PA21352,18132; 155 + PA21269,18048; 156 + PA18263,21054; 157 + PA18180,20971; 158 + PA21185,17965; 159 + PA21102,17881; 160 + PA18096,20887; 161 + PA18013,20804; 162 + PA21018,17798; 163 + PA20935,17714; 164 + PA17929,20720; 165 + PA17846,20637; 166 + PA20851,17631; 167 + PA20768,17547; 168 + PA17762,20553; 169 + PA17679,20470; 170 + PA20684,17464; 171 + PA20601,17380; 172 + PA17595,20386; 173 + PA17512,20303; 174 + PA20518,17297; 175 + PA20434,17213; 176 + PA17428,20219; 177 + PA17345,20136; 178 + PA20351,17130; 179 + PA20267,17046; 180 + PA17261,20052; 181 + PA17178,19969; 182 + PA20184,16963; 183 + PA20100,16879; 184 + PA17094,19885; 185 + PA17011,19802; 186 + PA20017,16796; 187 + PU;PA19182,4798; 188 + CI118,15; 189 + PU;PA19182,4798; 190 + CI236,15; 191 + PU;PA19182,4798; 192 + CI354,12; 193 + PU;PA19182,4798; 194 + CI473,8; 195 + PU;PA19182,4798; 196 + CI709,7; 197 + PU;PA19182,4798; 198 + CI945,6; 199 + PU;PA19182,4798; 200 + CI1181,5; 201 + PU;PA19182,4798; 202 + CI1417,5; 203 + PU;PA19182,4798; 204 + CI1653,5; 205 + PU;PA19182,4798; 206 + CI1889,5; 207 + PU;PA19182,4798; 208 + CI2126,5; 209 + PU;PA19182,4798; 210 + CI2362,5; 211 + 212 + PU;PA16820,16605; 213 + PD;PA7375,7160; 214 + PU;PA7375,16605; 215 + PD;PA16820,7160; 216 + PU;PA12098,7160; 217 + PD;PA16820,11882; 218 + PA12098,16605; 219 + PA7375,11882; 220 + PA12098,7160; 221 + PU;PA14459,9521; 222 + PD;PA9736,9521; 223 + PA9736,14244; 224 + PA14459,14244; 225 + PA14459,9521; 226 + PU;PA12098,2437; 227 + PD;PA2651,11882; 228 + PA12098,21328; 229 + PA21543,11882; 230 + PA12098,2437; 231 + 232 + PU;PA12098,7160;CI4723,2; 233 + PU;PA12098,16605;CI4723,2; 234 + PU;PA2651,11882;PD; 235 + AA2651,21328,90,2; 236 + AA21543,21328,90,2; 237 + AA21543,2437,90,2; 238 + AA2651,2437,90,2; 239 + 240 + PU;PA7375,16605; 241 + PD;PA7218,16603; 242 + PU;PA7061,16595; 243 + PD;PA6904,16582; 244 + PU;PA6748,16564; 245 + PD;PA6593,16540; 246 + PU;PA6439,16512; 247 + PD;PA6285,16478; 248 + PU;PA6133,16439; 249 + PD;PA5982,16395; 250 + PU;PA5833,16347; 251 + PD;PA5686,16293; 252 + PU;PA5540,16234; 253 + PD;PA5396,16171; 254 + PU;PA5255,16103; 255 + PD;PA5116,16030; 256 + PU;PA4979,15953; 257 + PD;PA4845,15871; 258 + PU;PA4714,15784; 259 + PD;PA4586,15694; 260 + PU;PA4461,15599; 261 + PD;PA4339,15500; 262 + PU;PA4220,15397; 263 + PD;PA4105,15290; 264 + PU;PA3993,15180; 265 + PD;PA3886,15066; 266 + PU;PA3782,14948; 267 + PD;PA3682,14827; 268 + PU;PA3586,14702; 269 + PD;PA3494,14575; 270 + PU;PA3407,14445; 271 + PD;PA3324,14311; 272 + PU;PA3245,14175; 273 + PD;PA3172,14037; 274 + PU;PA3102,13896; 275 + PD;PA3038,13753; 276 + PU;PA2978,13608; 277 + PD;PA2923,13460; 278 + PU;PA2873,13312; 279 + PD;PA2828,13161; 280 + PU;PA2788,13009; 281 + PD;PA2753,12856; 282 + PU;PA2723,12702; 283 + PD;PA2698,12547; 284 + PU;PA2679,12391; 285 + PD;PA2665,12235; 286 + PU;PA2655,12078; 287 + PD;PA2652,11921; 288 + PU;PA2653,11765; 289 + PD;PA2659,11608; 290 + PU;PA2671,11452; 291 + PD;PA2688,11296; 292 + PU;PA2710,11140; 293 + PD;PA2737,10986; 294 + PU;PA2770,10832; 295 + PD;PA2807,10680; 296 + PU;PA2850,10528; 297 + PD;PA2897,10379; 298 + PU;PA2950,10231; 299 + PD;PA3007,10085; 300 + PU;PA3069,9940; 301 + PD;PA3136,9798; 302 + PU;PA3208,9659; 303 + PD;PA3284,9521; 304 + PU;PA3365,9387; 305 + PD;PA3450,9255; 306 + PU;PA3540,9126; 307 + PD;PA3633,9000; 308 + PU;PA3731,8877; 309 + PD;PA3833,8758; 310 + PU;PA3939,8642; 311 + PD;PA4049,8529; 312 + PU;PA4162,8421; 313 + PD;PA4279,8316; 314 + PU;PA4399,8215; 315 + PD;PA4523,8118; 316 + PU;PA4649,8025; 317 + PD;PA4779,7937; 318 + PU;PA4912,7853; 319 + PD;PA5047,7773; 320 + PU;PA5185,7698; 321 + PD;PA5325,7628; 322 + PU;PA5468,7562; 323 + PD;PA5613,7501; 324 + PU;PA5759,7445; 325 + PD;PA5908,7393; 326 + PU;PA6058,7347; 327 + PD;PA6209,7306; 328 + PU;PA6362,7270; 329 + PD;PA6516,7239; 330 + PU;PA6671,7213; 331 + PD;PA6826,7192; 332 + PU;PA6982,7176; 333 + PD;PA7139,7166; 334 + PU;PA7296,7160; 335 + PD;PA7453,7160; 336 + PU;PA7610,7166; 337 + PD;PA7767,7176; 338 + PU;PA7923,7192; 339 + PD;PA8079,7213; 340 + PU;PA8233,7239; 341 + PD;PA8387,7270; 342 + PU;PA8540,7306; 343 + PD;PA8692,7347; 344 + PU;PA8842,7393; 345 + PD;PA8990,7445; 346 + PU;PA9137,7501; 347 + PD;PA9281,7562; 348 + PU;PA9424,7628; 349 + PD;PA9564,7698; 350 + PU;PA9702,7773; 351 + PD;PA9837,7853; 352 + PU;PA9970,7937; 353 + PD;PA10100,8025; 354 + PU;PA10226,8118; 355 + PD;PA10350,8215; 356 + PU;PA10470,8316; 357 + PD;PA10587,8421; 358 + PU;PA10700,8529; 359 + PD;PA10810,8642; 360 + PU;PA10916,8758; 361 + PD;PA11018,8877; 362 + PU;PA11116,9000; 363 + PD;PA11210,9126; 364 + PU;PA11299,9255; 365 + PD;PA11384,9387; 366 + PU;PA11465,9521; 367 + PD;PA11541,9659; 368 + PU;PA11613,9798; 369 + PD;PA11680,9940; 370 + PU;PA11742,10085; 371 + PD;PA11799,10231; 372 + PU;PA11852,10379; 373 + PD;PA11899,10528; 374 + PU;PA11942,10680; 375 + PD;PA11979,10832; 376 + PU;PA12012,10986; 377 + PD;PA12039,11140; 378 + PU;PA12061,11296; 379 + PD;PA12078,11452; 380 + PU;PA12090,11608; 381 + PD;PA12096,11765; 382 + PU;PA12098,11921; 383 + PD;PA12094,12078; 384 + PU;PA12085,12235; 385 + PD;PA12070,12391; 386 + PU;PA12051,12547; 387 + PD;PA12026,12702; 388 + PU;PA11996,12856; 389 + PD;PA11961,13009; 390 + PU;PA11921,13161; 391 + PD;PA11876,13312; 392 + PU;PA11826,13460; 393 + PD;PA11771,13608; 394 + PU;PA11712,13753; 395 + PD;PA11647,13896; 396 + PU;PA11578,14037; 397 + PD;PA11504,14175; 398 + PU;PA11425,14311; 399 + PD;PA11342,14445; 400 + PU;PA11255,14575; 401 + PD;PA11163,14702; 402 + PU;PA11067,14827; 403 + PD;PA10967,14948; 404 + PU;PA10864,15066; 405 + PD;PA10756,15180; 406 + PU;PA10644,15290; 407 + PD;PA10529,15397; 408 + PU;PA10411,15500; 409 + PD;PA10289,15599; 410 + PU;PA10163,15694; 411 + PD;PA10035,15784; 412 + PU;PA9904,15871; 413 + PD;PA9770,15953; 414 + PU;PA9633,16030; 415 + PD;PA9494,16103; 416 + PU;PA9353,16171; 417 + PD;PA9209,16234; 418 + PU;PA9064,16293; 419 + PD;PA8916,16347; 420 + PU;PA8767,16395; 421 + PD;PA8616,16439; 422 + PU;PA8464,16478; 423 + PD;PA8310,16512; 424 + PU;PA8156,16540; 425 + PD;PA8001,16564; 426 + PU;PA7845,16582; 427 + PD;PA7688,16595; 428 + PU;PA7532,16603; 429 + PD;PA7375,16605; 430 + PU;PA16820,16605; 431 + PD;PA16664,16603; 432 + PU;PA16507,16595; 433 + PD;PA16351,16582; 434 + PU;PA16195,16564; 435 + PD;PA16039,16540; 436 + PU;PA15885,16512; 437 + PD;PA15732,16478; 438 + PU;PA15580,16439; 439 + PD;PA15429,16395; 440 + PU;PA15280,16347; 441 + PD;PA15132,16293; 442 + PU;PA14986,16234; 443 + PD;PA14843,16171; 444 + PU;PA14701,16103; 445 + PD;PA14562,16030; 446 + PU;PA14425,15953; 447 + PD;PA14291,15871; 448 + PU;PA14160,15784; 449 + PD;PA14032,15694; 450 + PU;PA13907,15599; 451 + PD;PA13785,15500; 452 + PU;PA13666,15397; 453 + PD;PA13551,15290; 454 + PU;PA13440,15180; 455 + PD;PA13332,15066; 456 + PU;PA13228,14948; 457 + PD;PA13128,14827; 458 + PU;PA13032,14702; 459 + PD;PA12941,14575; 460 + PU;PA12853,14445; 461 + PD;PA12770,14311; 462 + PU;PA12692,14175; 463 + PD;PA12618,14037; 464 + PU;PA12549,13896; 465 + PD;PA12484,13753; 466 + PU;PA12424,13608; 467 + PD;PA12369,13460; 468 + PU;PA12319,13312; 469 + PD;PA12274,13161; 470 + PU;PA12234,13009; 471 + PD;PA12199,12856; 472 + PU;PA12170,12702; 473 + PD;PA12145,12547; 474 + PU;PA12125,12391; 475 + PD;PA12111,12235; 476 + PU;PA12102,12078; 477 + PD;PA12098,11921; 478 + PU;PA12099,11765; 479 + PD;PA12106,11608; 480 + PU;PA12118,11452; 481 + PD;PA12134,11296; 482 + PU;PA12157,11140; 483 + PD;PA12184,10986; 484 + PU;PA12216,10832; 485 + PD;PA12254,10680; 486 + PU;PA12296,10528; 487 + PD;PA12344,10379; 488 + PU;PA12396,10231; 489 + PD;PA12454,10085; 490 + PU;PA12516,9940; 491 + PD;PA12583,9798; 492 + PU;PA12654,9659; 493 + PD;PA12731,9521; 494 + PU;PA12811,9387; 495 + PD;PA12897,9255; 496 + PU;PA12986,9126; 497 + PD;PA13080,9000; 498 + PU;PA13178,8877; 499 + PD;PA13280,8758; 500 + PU;PA13385,8642; 501 + PD;PA13495,8529; 502 + PU;PA13608,8421; 503 + PD;PA13725,8316; 504 + PU;PA13846,8215; 505 + PD;PA13969,8118; 506 + PU;PA14096,8025; 507 + PD;PA14226,7937; 508 + PU;PA14358,7853; 509 + PD;PA14493,7773; 510 + PU;PA14631,7698; 511 + PD;PA14772,7628; 512 + PU;PA14914,7562; 513 + PD;PA15059,7501; 514 + PU;PA15206,7445; 515 + PD;PA15354,7393; 516 + PU;PA15504,7347; 517 + PD;PA15655,7306; 518 + PU;PA15808,7270; 519 + PD;PA15962,7239; 520 + PU;PA16117,7213; 521 + PD;PA16273,7192; 522 + PU;PA16429,7176; 523 + PD;PA16586,7166; 524 + PU;PA16742,7160; 525 + PD;PA16898,7160; 526 + PU;PA17055,7166; 527 + PD;PA17212,7176; 528 + PU;PA17368,7192; 529 + PD;PA17524,7213; 530 + PU;PA17679,7239; 531 + PD;PA17833,7270; 532 + PU;PA17985,7306; 533 + PD;PA18137,7347; 534 + PU;PA18287,7393; 535 + PD;PA18435,7445; 536 + PU;PA18582,7501; 537 + PD;PA18727,7562; 538 + PU;PA18869,7628; 539 + PD;PA19010,7698; 540 + PU;PA19148,7773; 541 + PD;PA19283,7853; 542 + PU;PA19415,7937; 543 + PD;PA19545,8025; 544 + PU;PA19672,8118; 545 + PD;PA19795,8215; 546 + PU;PA19916,8316; 547 + PD;PA20033,8421; 548 + PU;PA20146,8529; 549 + PD;PA20256,8642; 550 + PU;PA20361,8758; 551 + PD;PA20463,8877; 552 + PU;PA20561,9000; 553 + PD;PA20655,9126; 554 + PU;PA20744,9255; 555 + PD;PA20830,9387; 556 + PU;PA20910,9521; 557 + PD;PA20987,9659; 558 + PU;PA21058,9798; 559 + PD;PA21125,9940; 560 + PU;PA21187,10085; 561 + PD;PA21245,10231; 562 + PU;PA21297,10379; 563 + PD;PA21345,10528; 564 + PU;PA21387,10680; 565 + PD;PA21425,10832; 566 + PU;PA21457,10986; 567 + PD;PA21484,11140; 568 + PU;PA21507,11296; 569 + PD;PA21523,11452; 570 + PU;PA21535,11608; 571 + PD;PA21542,11765; 572 + PU;PA21543,11921; 573 + PD;PA21539,12078; 574 + PU;PA21530,12235; 575 + PD;PA21516,12391; 576 + PU;PA21496,12547; 577 + PD;PA21471,12702; 578 + PU;PA21442,12856; 579 + PD;PA21407,13009; 580 + PU;PA21367,13161; 581 + PD;PA21322,13312; 582 + PU;PA21272,13460; 583 + PD;PA21217,13608; 584 + PU;PA21157,13753; 585 + PD;PA21092,13896; 586 + PU;PA21023,14037; 587 + PD;PA20949,14175; 588 + PU;PA20871,14311; 589 + PD;PA20788,14445; 590 + PU;PA20700,14575; 591 + PD;PA20609,14702; 592 + PU;PA20513,14827; 593 + PD;PA20413,14948; 594 + PU;PA20309,15066; 595 + PD;PA20201,15180; 596 + PU;PA20090,15290; 597 + PD;PA19975,15397; 598 + PU;PA19856,15500; 599 + PD;PA19734,15599; 600 + PU;PA19609,15694; 601 + PD;PA19481,15784; 602 + PU;PA19349,15871; 603 + PD;PA19215,15953; 604 + PU;PA19079,16030; 605 + PD;PA18940,16103; 606 + PU;PA18798,16171; 607 + PD;PA18655,16234; 608 + PU;PA18509,16293; 609 + PD;PA18361,16347; 610 + PU;PA18212,16395; 611 + PD;PA18061,16439; 612 + PU;PA17909,16478; 613 + PD;PA17756,16512; 614 + PU;PA17601,16540; 615 + PD;PA17446,16564; 616 + PU;PA17290,16582; 617 + PD;PA17134,16595; 618 + PU;PA16977,16603; 619 + PD;PA16820,16605; 620 + 621 + PU;PA26030,6924; 622 + PD;PA26030,2673; 623 + PA21779,2673; 624 + PA21779,6924; 625 + PA26030,6924; 626 + PU;PA25909,4798; 627 + PD;PA23905,6802; 628 + PA21901,4798; 629 + PA23905,2794; 630 + PA25909,4798; 631 + PU;PA24907,3063; 632 + PD;PA22169,3796; 633 + PA22903,6534; 634 + PA25640,5800; 635 + PA24907,3063; 636 + PU;PA22169,5800; 637 + PD;PA22903,3063; 638 + PA25640,3796; 639 + PA24907,6534; 640 + PA22169,5800; 641 + 642 + PU;PA22016,7632; 643 + PD;PA22488,7632; 644 + PA22488,8104; 645 + PA22016,8104; 646 + PA22016,8577; 647 + PA22488,8577; 648 + PA22488,9049; 649 + PA22016,9049; 650 + PA22016,9521; 651 + PA22488,9521; 652 + PA22488,9994; 653 + PA22016,9994; 654 + PA22016,10466; 655 + PA22488,10466; 656 + PA22488,10938; 657 + PA22016,10938; 658 + PA22016,11411; 659 + PA22488,11411; 660 + PA22488,11882; 661 + PA22016,11882; 662 + PA22016,12354; 663 + PA22488,12354; 664 + PA22488,12827; 665 + PA22016,12827; 666 + PA22016,13299; 667 + PA22488,13299; 668 + PA22488,13771; 669 + PA22016,13771; 670 + PA22016,14244; 671 + PA22488,14244; 672 + PA22488,14716; 673 + PA22016,14716; 674 + PA22016,15188; 675 + PA22488,15188; 676 + PA22488,15661; 677 + PA22016,15661; 678 + PA22016,16133; 679 + PA22488,16133; 680 + PA22488,15661; 681 + PA22016,15661; 682 + PA22016,15188; 683 + PA22488,15188; 684 + PA22488,14716; 685 + PA22016,14716; 686 + PA22016,14244; 687 + PA22488,14244; 688 + PA22488,13771; 689 + PA22016,13771; 690 + PA22016,13299; 691 + PA22488,13299; 692 + PA22488,12827; 693 + PA22016,12827; 694 + PA22016,12354; 695 + PA22488,12354; 696 + PA22488,11882; 697 + PA22016,11882; 698 + PA22016,11411; 699 + PA22488,11411; 700 + PA22488,10938; 701 + PA22016,10938; 702 + PA22016,10466; 703 + PA22488,10466; 704 + PA22488,9994; 705 + PA22016,9994; 706 + PA22016,9521; 707 + PA22488,9521; 708 + PA22488,9049; 709 + PA22016,9049; 710 + PA22016,8577; 711 + PA22488,8577; 712 + PA22488,8104; 713 + PA22016,8104; 714 + PA22016,7632; 715 + 716 + PU;PA22960,7868; 717 + PD;PA22960,8341; 718 + PU;PA22960,8813; 719 + PD;PA22960,9285; 720 + PU;PA22960,9758; 721 + PD;PA22960,10230; 722 + PU;PA22960,10702; 723 + PD;PA22960,11175; 724 + PU;PA22960,11647; 725 + PD;PA22960,12118; 726 + PU;PA22960,12590; 727 + PD;PA22960,13063; 728 + PU;PA22960,13535; 729 + PD;PA22960,14007; 730 + PU;PA22960,14480; 731 + PD;PA22960,14952; 732 + PU;PA22960,15424; 733 + PD;PA22960,15897; 734 + PU;PA23196,15897; 735 + PD;PA23196,15424; 736 + PU;PA23196,14952; 737 + PD;PA23196,14480; 738 + PU;PA23196,14007; 739 + PD;PA23196,13535; 740 + PU;PA23196,13063; 741 + PD;PA23196,12590; 742 + PU;PA23196,12118; 743 + PD;PA23196,11647; 744 + PU;PA23196,11175; 745 + PD;PA23196,10702; 746 + PU;PA23196,10230; 747 + PD;PA23196,9758; 748 + PU;PA23196,9285; 749 + PD;PA23196,8813; 750 + PU;PA23196,8341; 751 + PD;PA23196,7868; 752 + 753 + PU;PA24141,7868; 754 + PD;PA23905,7868; 755 + PA23905,7632; 756 + PA24377,7632; 757 + PA24377,8104; 758 + PA23905,8104; 759 + PA23905,7868; 760 + PA24141,7868; 761 + PU;PA24141,9285; 762 + PD;PA23905,9285; 763 + PA24377,9049; 764 + PA24377,9521; 765 + PA23905,9285; 766 + PA24141,9285; 767 + 768 + PU;PA23999,10891; 769 + PD;AA24141,10704,287,9;PA23999,11104; 770 + AA24141,11291,-287,9;PA23999,10891; 771 + PU;PA23999,12661; 772 + PD;AA24141,12474,287,9;PA23999,12874; 773 + AA24141,13061,-287,9;PA23999,12661; 774 + 775 + PU;PA23905,15661; 776 + PD;PA24141,15897; 777 + PA23905,16133; 778 + PA24141,15897; 779 + PA24377,16133; 780 + PA24141,15897; 781 + PA24377,15661; 782 + PA24141,15897; 783 + PA23905,15661; 784 + PU;PA23905,14480; 785 + PD;PA24141,14480; 786 + PA24141,14716; 787 + PA24141,14480; 788 + PA24377,14480; 789 + PA24141,14480; 790 + PA24141,14244; 791 + PA24141,14480; 792 + PA23905,14480; 793 + 794 + PU;PA25086,7868; 795 + PD;PA24849,7868; 796 + PA24849,7632; 797 + PA25322,7632; 798 + PA25322,8104; 799 + PA24849,8104; 800 + PA24849,7868; 801 + PA25086,7868; 802 + PU;PA25086,9285; 803 + PD;PA24849,9285; 804 + PA25322,9049; 805 + PA25322,9521; 806 + PA24849,9285; 807 + PA25086,9285; 808 + 809 + PU;PA24944,10891; 810 + PD;AA25086,10704,287,9;PA24944,11104; 811 + AA25086,11291,-287,9;PA24944,10891; 812 + PU;PA24944,12661; 813 + PD;AA25086,12474,287,9;PA24944,12874; 814 + AA25086,13061,-287,9;PA24944,12661; 815 + 816 + PU;PA24849,15661; 817 + PD;PA25086,15897; 818 + PA24849,16133; 819 + PA25086,15897; 820 + PA25322,16133; 821 + PA25086,15897; 822 + PA25322,15661; 823 + PA25086,15897; 824 + PA24849,15661; 825 + PU;PA24849,14480; 826 + PD;PA25086,14480; 827 + PA25086,14716; 828 + PA25086,14480; 829 + PA25322,14480; 830 + PA25086,14480; 831 + PA25086,14244; 832 + PA25086,14480; 833 + PA24849,14480; 834 + 835 + PU;PA26030,7868; 836 + PD;PA25794,7868; 837 + PA25794,7632; 838 + PA26266,7632; 839 + PA26266,8104; 840 + PA25794,8104; 841 + PA25794,7868; 842 + PA26030,7868; 843 + PU;PA26030,9285; 844 + PD;PA25794,9285; 845 + PA26266,9049; 846 + PA26266,9521; 847 + PA25794,9285; 848 + PA26030,9285; 849 + 850 + PU;PA25889,10891; 851 + PD;AA26030,10704,287,9;PA25889,11104; 852 + AA26030,11291,-287,9;PA25889,10891; 853 + PU;PA25889,12661; 854 + PD;AA26030,12474,287,9;PA25889,12874; 855 + AA26030,13061,-287,9;PA25889,12661; 856 + 857 + PU;PA25794,15661; 858 + PD;PA26030,15897; 859 + PA25794,16133; 860 + PA26030,15897; 861 + PA26266,16133; 862 + PA26030,15897; 863 + PA26266,15661; 864 + PA26030,15897; 865 + PA25794,15661; 866 + PU;PA25794,14480; 867 + PD;PA26030,14480; 868 + PA26030,14716; 869 + PA26030,14480; 870 + PA26266,14480; 871 + PA26030,14480; 872 + PA26030,14244; 873 + PA26030,14480; 874 + PA25794,14480; 875 + 876 + PU;PA26030,18967; 877 + PD;PA21779,18967; 878 + PU;PA23905,21092; 879 + PD;PA23905,16841; 880 + PU;PA23787,21092; 881 + PD;PA23787,19085; 882 + PA21779,19085; 883 + PU;PA21779,19203; 884 + PD;PA23669,19203; 885 + PA23669,21092; 886 + PU;PA23551,21092; 887 + PD;PA23551,19321; 888 + PA21779,19321; 889 + PU;PA21779,19439; 890 + PD;PA23432,19439; 891 + PA23432,21092; 892 + PU;PA23787,16841; 893 + PD;PA23787,18849; 894 + PA21779,18849; 895 + PU;PA21779,18731; 896 + PD;PA23669,18731; 897 + PA23669,16841; 898 + PU;PA23551,16841; 899 + PD;PA23551,18613; 900 + PA21779,18613; 901 + PU;PA21779,18494; 902 + PD;PA23432,18494; 903 + PA23432,16841; 904 + PU;PA24023,21092; 905 + PD;PA24023,19085; 906 + PA26030,19085; 907 + PU;PA26030,19203; 908 + PD;PA24141,19203; 909 + PA24141,21092; 910 + PU;PA24259,21092; 911 + PD;PA24259,19321; 912 + PA26030,19321; 913 + PU;PA26030,19439; 914 + PD;PA24377,19439; 915 + PA24377,21092; 916 + PU;PA26030,18849; 917 + PD;PA24023,18849; 918 + PA24023,16841; 919 + PU;PA24141,16841; 920 + PD;PA24141,18731; 921 + PA26030,18731; 922 + PU;PA26030,18613; 923 + PD;PA24259,18613; 924 + PA24259,16841; 925 + PU;PA24377,16841; 926 + PD;PA24377,18494; 927 + PA26030,18494; 928 + 929 + PU;PA27000,6747; 930 + di0,1; 931 + SI0.6,1;SMABCDEFGHIJKLMNOPQRSTUVWXYZ; 932 + PU;PA27600,6747; 933 + SI0.6,.5;SMabcdefghijklmnopqrstuvwxyz; 934 + PU;PA28200,6747; 935 + SI0.6,1;LB!"#$%&'()*+,-./:;<=>?[\]`~@ 936 + PU;PA28800,6747; 937 + SI0.6,1;SM��������������������������������; 938 + PU;PA29400,6747; 939 + SI0.6,1;SM��������������������������������; 940 + PU;PA30000,6747; 941 + SI0.6,1;SM1234567890; 942 + 943 + PU;PA30590,2837;di0,1;si.4,.6;smTEST 7 (ISO); 944 + PU;PU;PA0,0; 945 + 
+3
plotters/hp7585b/hp7585bhome.fish
··· 1 + function hp7585bhome 2 + hp7585bsend "PU;PA0,0;" 3 + end
+3
plotters/hp7585b/hp7585binit.fish
··· 1 + function hp7585binit 2 + hp7585bsend "IN;" 3 + end
+14
plotters/hp7585b/hp7585bpen.fish
··· 1 + function hp7585bpen 2 + if test (count $argv) -lt 1 3 + echo "usage: hp7585bpen N (0 = stow, 1-8 = carousel slot)" 4 + return 1 5 + end 6 + 7 + set n $argv[1] 8 + if not string match -qr '^[0-8]$' -- $n 9 + echo "pen number must be 0..8" 10 + return 1 11 + end 12 + 13 + hp7585bsend "SP$n;" 14 + end
+3
plotters/hp7585b/hp7585bpendown.fish
··· 1 + function hp7585bpendown 2 + hp7585bsend "PD;" 3 + end
+3
plotters/hp7585b/hp7585bpenup.fish
··· 1 + function hp7585bpenup 2 + hp7585bsend "PU;" 3 + end
+21
plotters/hp7585b/hp7585bplot.fish
··· 1 + function hp7585bplot 2 + if test (count $argv) -lt 1 3 + echo "usage: hp7585bplot file.hpgl" 4 + return 1 5 + end 6 + 7 + if not set -q HP7585B_TTY 8 + echo "HP7585B_TTY not set. e.g. set -Ux HP7585B_TTY /dev/cu.usbserial-XXXX" 9 + return 1 10 + end 11 + 12 + if not test -e $HP7585B_TTY 13 + echo "device $HP7585B_TTY does not exist" 14 + return 1 15 + end 16 + 17 + # 9600 8N1, Xon/Xoff. -clocal so flow control is honored on macOS. 18 + stty -f $HP7585B_TTY 9600 cs8 -cstopb -parenb -clocal ixon ixoff 19 + 20 + cat $argv[1] > $HP7585B_TTY 21 + end
+14
plotters/hp7585b/hp7585bsend.fish
··· 1 + function hp7585bsend 2 + if test (count $argv) -lt 1 3 + echo 'usage: hp7585bsend "PU;PA0,0;"' 4 + return 1 5 + end 6 + 7 + if not set -q HP7585B_TTY 8 + echo "HP7585B_TTY not set. e.g. set -Ux HP7585B_TTY /dev/cu.usbserial-XXXX" 9 + return 1 10 + end 11 + 12 + stty -f $HP7585B_TTY 9600 cs8 -cstopb -parenb -clocal ixon ixoff 13 + printf "%s" "$argv" > $HP7585B_TTY 14 + end
+123
plotters/hp7585b/notepat-slide.hpgl
··· 1 + IN;DT~;DI1,0;LT;SP1;VS12;FS3; 2 + SL0.200; 3 + SI1.476,2.214;PA-7969,3500;LBnotepat.com keymap~ 4 + SL0.000; 5 + PU;PA-7567,3050;PD;PA7567,3050;PU; 6 + PU;PA7215,3765;PD;PA7293,3758;PA7294,3757;PA7319,3731;PA7326,3698;PA7321,3664;PA7318,3656;PA7324,3659;PA7348,3676;PA7354,3681;PA7375,3697;PA7419,3724;PA7466,3748;PA7517,3737;PA7529,3708;PA7522,3686;PA7521,3685;PA7491,3660;PA7476,3652;PA7460,3643;PA7410,3606;PA7370,3559;PA7367,3540;PA7371,3519;PA7374,3506;PA7388,3462;PA7405,3428;PA7413,3416;PA7415,3412;PA7424,3400;PA7445,3396;PA7491,3412;PA7540,3426;PA7574,3410;PA7584,3377;PA7566,3344;PA7538,3328;PA7435,3298;PA7430,3297;PA7345,3326;PA7318,3362;PA7301,3381;PA7290,3392;PA7273,3393;PA7262,3380;PA7246,3329;PA7221,3238;PA7218,3223;PA7217,3219;PA7212,3196;PA7192,3153;PA7131,3135;PA7093,3181;PA7103,3240;PA7119,3296;PA7146,3400;PA7148,3409;PA7159,3471;PA7164,3504;PA7165,3522;PA7162,3529;PA7153,3531;PA7137,3530;PA7135,3530;PA7118,3529;PA7097,3529;PA7050,3529;PA7026,3532;PA7001,3535;PA6980,3536;PA6970,3534;PA6962,3526;PA6962,3521;PA6965,3511;PA6970,3492;PA6975,3467;PA6988,3398;PA6990,3389;PA7006,3277;PA7006,3254;PA7002,3177;PA6951,3142;PA6905,3178;PA6907,3214;PA6907,3220;PA6910,3242;PA6893,3338;PA6881,3356;PA6865,3352;PA6851,3331;PA6842,3306;PA6838,3295;PA6808,3220;PA6801,3202;PA6800,3199;PA6794,3184;PA6770,3150;PA6729,3139;PA6688,3162;PA6684,3188;PA6690,3213;PA6699,3234;PA6702,3240;PA6705,3247;PA6727,3306;PA6740,3345;PA6771,3447;PA6777,3470;PA6779,3475;PA6782,3496;PA6780,3500;PA6776,3499;PA6759,3485;PA6723,3457;PA6678,3419;PA6662,3406;PA6631,3389;PA6610,3390;PA6592,3404;PA6584,3441;PA6604,3471;PA6624,3489;PA6699,3561;PA6716,3578;PA6755,3615;PA6763,3621;PA6762,3622;PA6742,3709;PA6771,3744;PA6807,3758;PA6874,3743;PA6904,3701;PA6900,3655;PA6898,3649;PA6947,3641;PA6958,3640;PA6969,3639;PA7030,3633;PA7116,3632;PA7157,3638;PA7168,3639;PA7181,3641;PA7173,3649;PA7163,3707;PA7215,3765;PU; 7 + PU;PA7282,3738;PD;PA7221,3744;PA7185,3702;PA7191,3662;PA7199,3654;PA7200,3653;PA7204,3650;PA7206,3648;PA7208,3640;PA7201,3624;PA7184,3619;PA7170,3617;PA7160,3616;PA7119,3611;PA7029,3610;PA6967,3616;PA6956,3618;PA6945,3618;PA6882,3631;PA6880,3631;PA6879,3632;PA6873,3643;PA6874,3651;PA6876,3656;PA6879,3663;PA6882,3695;PA6861,3726;PA6810,3736;PA6784,3726;PA6763,3699;PA6775,3640;PA6787,3629;PA6790,3619;PA6786,3611;PA6782,3607;PA6779,3605;PA6778,3604;PA6771,3599;PA6770,3598;PA6769,3597;PA6733,3565;PA6715,3545;PA6639,3473;PA6620,3455;PA6606,3434;PA6609,3418;PA6618,3411;PA6627,3411;PA6648,3422;PA6663,3435;PA6709,3474;PA6743,3502;PA6761,3516;PA6791,3520;PA6805,3496;PA6801,3469;PA6799,3462;PA6793,3442;PA6762,3337;PA6748,3299;PA6726,3239;PA6722,3229;PA6719,3224;PA6711,3205;PA6706,3185;PA6708,3172;PA6730,3161;PA6756,3167;PA6773,3192;PA6779,3207;PA6780,3209;PA6787,3229;PA6817,3302;PA6820,3312;PA6830,3339;PA6852,3371;PA6888,3377;PA6915,3344;PA6932,3242;PA6930,3216;PA6929,3211;PA6927,3180;PA6951,3164;PA6980,3182;PA6984,3252;PA6983,3277;PA6968,3385;PA6966,3393;PA6952,3464;PA6948,3488;PA6944,3503;PA6943,3504;PA6939,3517;PA6942,3535;PA6956,3551;PA6978,3558;PA7003,3557;PA7028,3554;PA7051,3551;PA7095,3551;PA7118,3551;PA7133,3552;PA7135,3552;PA7155,3553;PA7175,3547;PA7187,3525;PA7186,3501;PA7181,3467;PA7170,3405;PA7168,3396;PA7141,3290;PA7125,3234;PA7115,3181;PA7136,3156;PA7175,3167;PA7190,3202;PA7195,3224;PA7196,3228;PA7200,3244;PA7224,3335;PA7241,3387;PA7262,3413;PA7302,3411;PA7318,3397;PA7335,3376;PA7364,3338;PA7425,3319;PA7433,3320;PA7530,3348;PA7551,3361;PA7561,3377;PA7556,3396;PA7540,3404;PA7502,3392;PA7500,3391;PA7450,3374;PA7409,3383;PA7394,3403;PA7393,3405;PA7387,3416;PA7367,3453;PA7352,3502;PA7349,3514;PA7345,3539;PA7350,3569;PA7395,3623;PA7448,3662;PA7465,3671;PA7466,3672;PA7479,3679;PA7503,3698;PA7506,3709;PA7501,3722;PA7473,3727;PA7430,3705;PA7388,3679;PA7369,3664;PA7362,3659;PA7334,3639;PA7323,3635;PA7309,3631;PA7297,3628;PA7294,3628;PA7288,3629;PA7282,3637;PA7284,3645;PA7286,3648;PA7300,3671;PA7303,3698;PA7299,3721;PA7282,3738;PU; 8 + PA-6300,1200;EA-5400,2500; 9 + SI0.550,0.825;PA-6015,1370;LBC~ 10 + PA-5400,1200;EA-4500,2500; 11 + SI0.550,0.825;PA-5115,1370;LBD~ 12 + PA-4500,1200;EA-3600,2500; 13 + SI0.550,0.825;PA-4215,1370;LBE~ 14 + PA-3600,1200;EA-2700,2500; 15 + SI0.550,0.825;PA-3315,1370;LBF~ 16 + PA-2700,1200;EA-1800,2500; 17 + SI0.550,0.825;PA-2415,1370;LBG~ 18 + PA-1800,1200;EA-900,2500; 19 + SI0.550,0.825;PA-1515,1370;LBA~ 20 + PA-900,1200;EA0,2500; 21 + SI0.550,0.825;PA-615,1370;LBB~ 22 + PA0,1200;EA900,2500; 23 + SI0.550,0.825;PA285,1370;LBC~ 24 + PA900,1200;EA1800,2500; 25 + SI0.550,0.825;PA1185,1370;LBD~ 26 + PA1800,1200;EA2700,2500; 27 + SI0.550,0.825;PA2085,1370;LBE~ 28 + PA2700,1200;EA3600,2500; 29 + SI0.550,0.825;PA2985,1370;LBF~ 30 + PA3600,1200;EA4500,2500; 31 + SI0.550,0.825;PA3885,1370;LBG~ 32 + PA4500,1200;EA5400,2500; 33 + SI0.550,0.825;PA4785,1370;LBA~ 34 + PA5400,1200;EA6300,2500; 35 + SI0.550,0.825;PA5685,1370;LBB~ 36 + FT3,80,45;PA-5670,1700;RA-5130,2500;EA-5130,2500;FT; 37 + FT3,80,45;PA-4770,1700;RA-4230,2500;EA-4230,2500;FT; 38 + FT3,80,45;PA-2970,1700;RA-2430,2500;EA-2430,2500;FT; 39 + FT3,80,45;PA-2070,1700;RA-1530,2500;EA-1530,2500;FT; 40 + FT3,80,45;PA-1170,1700;RA-630,2500;EA-630,2500;FT; 41 + FT3,80,45;PA630,1700;RA1170,2500;EA1170,2500;FT; 42 + FT3,80,45;PA1530,1700;RA2070,2500;EA2070,2500;FT; 43 + FT3,80,45;PA3330,1700;RA3870,2500;EA3870,2500;FT; 44 + FT3,80,45;PA4230,1700;RA4770,2500;EA4770,2500;FT; 45 + FT3,80,45;PA5130,1700;RA5670,2500;EA5670,2500;FT; 46 + PA-6950,-100;EA-5650,800; 47 + SI0.850,1.275;PA-6555,200;LBQ~ 48 + PA-5550,-100;EA-4250,800; 49 + SI0.850,1.275;PA-5155,200;LBW~ 50 + PA-4150,-100;EA-2850,800; 51 + PA-4060,-10;EA-2940,710; 52 + SI0.850,1.275;PA-3755,200;LBE~ 53 + PA-2750,-100;EA-1450,800; 54 + SI0.850,1.275;PA-2355,200;LBR~ 55 + PA-1350,-100;EA-50,800; 56 + SI0.850,1.275;PA-955,200;LBT~ 57 + PA50,-100;EA1350,800; 58 + SI0.850,1.275;PA445,200;LBY~ 59 + PA1450,-100;EA2750,800; 60 + SI0.850,1.275;PA1845,200;LBU~ 61 + PA2850,-100;EA4150,800; 62 + SI0.850,1.275;PA3245,200;LBI~ 63 + PA4250,-100;EA5550,800; 64 + SI0.850,1.275;PA4645,200;LBO~ 65 + PA5650,-100;EA6950,800; 66 + SI0.850,1.275;PA6045,200;LBP~ 67 + PA-6600,-1150;EA-5300,-250; 68 + PA-6510,-1060;EA-5390,-340; 69 + SI0.850,1.275;PA-6205,-850;LBA~ 70 + PA-5200,-1150;EA-3900,-250; 71 + SI0.850,1.275;PA-4805,-850;LBS~ 72 + PA-3800,-1150;EA-2500,-250; 73 + PA-3710,-1060;EA-2590,-340; 74 + SI0.850,1.275;PA-3405,-850;LBD~ 75 + PA-2400,-1150;EA-1100,-250; 76 + PA-2310,-1060;EA-1190,-340; 77 + SI0.850,1.275;PA-2005,-850;LBF~ 78 + PA-1000,-1150;EA300,-250; 79 + PA-910,-1060;EA210,-340; 80 + SI0.850,1.275;PA-605,-850;LBG~ 81 + PA400,-1150;EA1700,-250; 82 + SI0.850,1.275;PA795,-850;LBH~ 83 + PA1800,-1150;EA3100,-250; 84 + SI0.850,1.275;PA2195,-850;LBJ~ 85 + PA3200,-1150;EA4500,-250; 86 + SI0.850,1.275;PA3595,-850;LBK~ 87 + PA4600,-1150;EA5900,-250; 88 + SI0.850,1.275;PA4995,-850;LBL~ 89 + PA6000,-1150;EA7300,-250; 90 + SI0.850,1.275;PA6395,-850;LB'~ 91 + PA-6150,-2200;EA-4850,-1300; 92 + SI0.850,1.275;PA-5755,-1900;LBZ~ 93 + PA-4750,-2200;EA-3450,-1300; 94 + SI0.850,1.275;PA-4355,-1900;LBX~ 95 + PA-3350,-2200;EA-2050,-1300; 96 + PA-3260,-2110;EA-2140,-1390; 97 + SI0.850,1.275;PA-2955,-1900;LBC~ 98 + PA-1950,-2200;EA-650,-1300; 99 + SI0.850,1.275;PA-1555,-1900;LBV~ 100 + PA-550,-2200;EA750,-1300; 101 + PA-460,-2110;EA660,-1390; 102 + SI0.850,1.275;PA-155,-1900;LBB~ 103 + PA850,-2200;EA2150,-1300; 104 + SI0.850,1.275;PA1245,-1900;LBN~ 105 + PA2250,-2200;EA3550,-1300; 106 + SI0.850,1.275;PA2645,-1900;LBM~ 107 + PA3650,-2200;EA4950,-1300; 108 + SI0.850,1.275;PA4045,-1900;LB,~ 109 + PA5050,-2200;EA6350,-1300; 110 + SI0.850,1.275;PA5445,-1900;LB.~ 111 + PA6450,-2200;EA7750,-1300; 112 + SI0.850,1.275;PA6845,-1900;LB/~ 113 + LT2,0.3; 114 + PU;PA-5850,1850;PD;PA-2700,-1750;PU; 115 + PU;PA-4950,1850;PD;PA-3150,-700;PU; 116 + PU;PA-4050,1850;PD;PA-3500,350;PU; 117 + PU;PA-3150,1850;PD;PA-1750,-700;PU; 118 + PU;PA-2250,1850;PD;PA-350,-700;PU; 119 + LT; 120 + SI0.590,0.885;PA-7969,-2700;LBC D E F G A B play the notes they name~ 121 + SI0.500,0.750;PA-2550,-3300;LBnotepat lives in:~ 122 + SI0.501,0.752;PA-7969,-3700;LBnotepat.com prompt.ac/menuband aesthetic.computer~ 123 + LT;PU;PA0,0;SP0;
+629
plotters/hp7585b/notepat-slide.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + notepat keymap slide — built entirely from native HP-GL primitives. 4 + 5 + Emits: 6 + - notepat-slide.hpgl (sendable to the 7585B) 7 + - notepat-slide.svg (preview, written to the user's Desktop) 8 + 9 + Primitives used (all stock HP-GL, no extensions): 10 + IN, SP, VS, FS initialise / pen / speed / force 11 + PA, PU, PD absolute moves, pen down/up 12 + EA edge rectangle absolute (key outlines) 13 + RA fill rectangle absolute (black piano keys, highlights) 14 + CI circle (chip dots) 15 + LT line type (dashed connectors) 16 + SI, DI character size / direction 17 + LB, DT label / label terminator 18 + 19 + The plotter unit is 0.025 mm. Coordinate origin is the centre of the 20 + plotter's hard-clip area (the 7585B reports P1/P2 centred). 21 + 22 + Layout zones (plotter units, ±X / ±Y): 23 + title 4000 .. 3300 24 + piano 3100 .. 1500 25 + connectors 1400 .. 900 26 + qwerty 800 .. -2200 27 + tagline -2400 .. -2900 28 + chips -3100 .. -3800 29 + """ 30 + 31 + from __future__ import annotations 32 + 33 + import argparse 34 + import os 35 + import sys 36 + import time 37 + import webbrowser 38 + from pathlib import Path 39 + 40 + import serial 41 + 42 + 43 + XON, XOFF = 0x11, 0x13 44 + 45 + 46 + # --------------------------------------------------------------------------- 47 + # Live limits query 48 + # --------------------------------------------------------------------------- 49 + def _ask(ser: serial.Serial, cmd: bytes) -> str: 50 + ser.reset_input_buffer() 51 + ser.write(cmd) 52 + ser.flush() 53 + deadline = time.time() + 1.0 54 + buf = bytearray() 55 + while time.time() < deadline: 56 + c = ser.read(64) 57 + if c: 58 + for b in c: 59 + if b not in (XON, XOFF): 60 + buf.append(b) 61 + if b"\n" in buf or b"\r" in buf: 62 + break 63 + else: 64 + time.sleep(0.05) 65 + return buf.decode("ascii", errors="replace").strip() 66 + 67 + 68 + def _ints(s: str) -> list[int]: 69 + out = [] 70 + for tok in s.replace(",", " ").split(): 71 + try: 72 + out.append(int(tok)) 73 + except ValueError: 74 + pass 75 + return out 76 + 77 + 78 + def query_limits(port: str, baud: int) -> tuple[tuple[int, int, int, int], tuple[int, int]]: 79 + ser = serial.Serial( 80 + port=port, 81 + baudrate=baud, 82 + bytesize=serial.EIGHTBITS, 83 + parity=serial.PARITY_NONE, 84 + stopbits=serial.STOPBITS_ONE, 85 + xonxoff=False, 86 + rtscts=True, 87 + dsrdtr=True, 88 + timeout=0.5, 89 + ) 90 + ser.setDTR(True) 91 + ser.setRTS(True) 92 + try: 93 + pp = _ints(_ask(ser, b"OP;")) 94 + of = _ints(_ask(ser, b"OF;")) 95 + finally: 96 + ser.close() 97 + if len(pp) != 4 or len(of) < 2: 98 + raise SystemExit("plotter did not respond to OP;/OF;") 99 + return (pp[0], pp[1], pp[2], pp[3]), (of[0], of[1]) 100 + 101 + 102 + # --------------------------------------------------------------------------- 103 + # Composer that emits HPGL + SVG simultaneously 104 + # --------------------------------------------------------------------------- 105 + class Composer: 106 + """Each primitive call appends both an HP-GL string and an SVG element.""" 107 + 108 + def __init__(self, p1p2: tuple[int, int, int, int], factor: tuple[int, int]): 109 + self.p1p2 = p1p2 110 + self.fx, self.fy = factor 111 + # Explicit *printable* label terminator. Our 7585B firmware doesn't 112 + # accept the optional suppress flag on DT (E2 = wrong # of params), 113 + # so we use plain `DT~;` and accept that '~' draws as a small visible 114 + # mark after each label. Better visible than runaway labels. 115 + self.hpgl: list[str] = ["IN;DT~;DI1,0;LT;SP1;VS12;FS3;"] 116 + self.svg: list[str] = [] 117 + self._line_type: int | None = None # current LT setting (None = solid) 118 + 119 + x1, y1, x2, y2 = p1p2 120 + self.w_units = x2 - x1 121 + self.h_units = y2 - y1 122 + # SVG viewBox in mm (drawing space). Origin at center, y-up like HPGL. 123 + self.svg_w_mm = self.w_units / self.fx 124 + self.svg_h_mm = self.h_units / self.fy 125 + 126 + # --- coord helpers --- 127 + def _to_svg_xy(self, x: int, y: int) -> tuple[float, float]: 128 + # plotter is y-up at center; SVG is y-down at top-left. 129 + sx = (x - self.p1p2[0]) / self.fx 130 + sy = (self.p1p2[3] - y) / self.fy 131 + return sx, sy 132 + 133 + def set_line_type(self, lt: int | None) -> None: 134 + if lt == self._line_type: 135 + return 136 + if lt is None: 137 + self.hpgl.append("LT;") 138 + else: 139 + # Pattern length in percent of P1-P2 diagonal. 0.3% ≈ 1.4mm 140 + # on this sheet — very fine tics. 141 + self.hpgl.append(f"LT{lt},0.3;") 142 + self._line_type = lt 143 + 144 + # --- primitives --- 145 + def edge_rect(self, x1: int, y1: int, x2: int, y2: int) -> None: 146 + self.set_line_type(None) 147 + self.hpgl.append(f"PA{x1},{y1};EA{x2},{y2};") 148 + sx1, sy1 = self._to_svg_xy(x1, y1) 149 + sx2, sy2 = self._to_svg_xy(x2, y2) 150 + self.svg.append( 151 + f'<rect x="{min(sx1, sx2):.3f}" y="{min(sy1, sy2):.3f}" ' 152 + f'width="{abs(sx2 - sx1):.3f}" height="{abs(sy2 - sy1):.3f}" ' 153 + f'fill="none" stroke="#111" stroke-width="0.4"/>' 154 + ) 155 + 156 + def double_rect(self, x1: int, y1: int, x2: int, y2: int, inset: int = 80) -> None: 157 + """Outer + inner offset rect — for highlighting without filling.""" 158 + self.edge_rect(x1, y1, x2, y2) 159 + self.edge_rect(x1 + inset, y1 + inset, x2 - inset, y2 - inset) 160 + 161 + # ---- richer HP-GL primitives ---- 162 + def set_fill(self, fill_type: int, spacing: int = 0, angle_deg: int = 0) -> None: 163 + """FT n[,sp,a]; — set the fill pattern used by FP / RA / WG. 164 + type 1 = solid, 2 = parallel hatch, 3 = cross-hatch, 4 = dots. 165 + spacing is in plotter units; angle is degrees.""" 166 + if spacing > 0: 167 + self.hpgl.append(f"FT{fill_type},{spacing},{angle_deg};") 168 + else: 169 + self.hpgl.append(f"FT{fill_type};") 170 + 171 + def hatched_rect( 172 + self, x1: int, y1: int, x2: int, y2: int, spacing: int = 80, angle: int = 45 173 + ) -> None: 174 + """Cross-hatched rectangle — outline + hatched fill via FT3 + RA.""" 175 + self.set_line_type(None) 176 + # FT 3 = cross-hatch, then RA fills, then EA edges. 177 + self.hpgl.append( 178 + f"FT3,{spacing},{angle};" 179 + f"PA{x1},{y1};RA{x2},{y2};EA{x2},{y2};FT;" 180 + ) 181 + sx1, sy1 = self._to_svg_xy(x1, y1) 182 + sx2, sy2 = self._to_svg_xy(x2, y2) 183 + self.svg.append( 184 + f'<defs><pattern id="h{len(self.svg)}" patternUnits="userSpaceOnUse" ' 185 + f'width="2" height="2" patternTransform="rotate({angle})">' 186 + f'<line x1="0" y1="0" x2="0" y2="2" stroke="#111" stroke-width="0.3"/>' 187 + f'<line x1="0" y1="0" x2="2" y2="0" stroke="#111" stroke-width="0.3"/>' 188 + f'</pattern></defs>' 189 + f'<rect x="{min(sx1, sx2):.3f}" y="{min(sy1, sy2):.3f}" ' 190 + f'width="{abs(sx2 - sx1):.3f}" height="{abs(sy2 - sy1):.3f}" ' 191 + f'fill="url(#h{len(self.svg) - 1})" stroke="#111" stroke-width="0.4"/>' 192 + ) 193 + 194 + def edge_polygon(self, points: list[tuple[int, int]]) -> None: 195 + """EP — outline a polygon defined via PM.""" 196 + if len(points) < 3: 197 + return 198 + self.set_line_type(None) 199 + cmd = [f"PA{points[0][0]},{points[0][1]};PM0;"] 200 + for x, y in points[1:]: 201 + cmd.append(f"PA{x},{y};") 202 + cmd.append("PM2;EP;") 203 + self.hpgl.append("".join(cmd)) 204 + svg_pts = " ".join( 205 + f"{self._to_svg_xy(x, y)[0]:.3f},{self._to_svg_xy(x, y)[1]:.3f}" 206 + for x, y in points 207 + ) 208 + self.svg.append( 209 + f'<polygon points="{svg_pts}" fill="none" stroke="#111" stroke-width="0.4"/>' 210 + ) 211 + 212 + def fill_polygon( 213 + self, 214 + points: list[tuple[int, int]], 215 + fill_type: int = 3, 216 + spacing: int = 80, 217 + angle: int = 45, 218 + ) -> None: 219 + """FP — fill a polygon defined via PM, with FT controlling pattern.""" 220 + if len(points) < 3: 221 + return 222 + self.set_line_type(None) 223 + cmd = [ 224 + f"FT{fill_type},{spacing},{angle};", 225 + f"PA{points[0][0]},{points[0][1]};PM0;", 226 + ] 227 + for x, y in points[1:]: 228 + cmd.append(f"PA{x},{y};") 229 + cmd.append("PM2;FP;EP;FT;") 230 + self.hpgl.append("".join(cmd)) 231 + svg_pts = " ".join( 232 + f"{self._to_svg_xy(x, y)[0]:.3f},{self._to_svg_xy(x, y)[1]:.3f}" 233 + for x, y in points 234 + ) 235 + self.svg.append( 236 + f'<polygon points="{svg_pts}" fill="#888" fill-opacity="0.35" ' 237 + f'stroke="#111" stroke-width="0.4"/>' 238 + ) 239 + 240 + def edge_wedge( 241 + self, cx: int, cy: int, r: int, start_deg: float, sweep_deg: float 242 + ) -> None: 243 + """EW r,start,sweep; — outline a pie-slice. Centre is current pen pos.""" 244 + self.set_line_type(None) 245 + self.hpgl.append(f"PA{cx},{cy};EW{r},{start_deg:.2f},{sweep_deg:.2f};") 246 + sx, sy = self._to_svg_xy(cx, cy) 247 + rx = r / self.fx 248 + # SVG arc path 249 + import math 250 + a1 = math.radians(start_deg) 251 + a2 = math.radians(start_deg + sweep_deg) 252 + x1s = sx + rx * math.cos(a1) 253 + y1s = sy - rx * math.sin(a1) 254 + x2s = sx + rx * math.cos(a2) 255 + y2s = sy - rx * math.sin(a2) 256 + large = 1 if abs(sweep_deg) > 180 else 0 257 + sweep = 0 if sweep_deg > 0 else 1 # SVG arcs Y-down 258 + self.svg.append( 259 + f'<path d="M {sx:.3f} {sy:.3f} L {x1s:.3f} {y1s:.3f} ' 260 + f'A {rx:.3f} {rx:.3f} 0 {large} {sweep} {x2s:.3f} {y2s:.3f} Z" ' 261 + f'fill="none" stroke="#111" stroke-width="0.4"/>' 262 + ) 263 + 264 + def set_slant(self, slant: float) -> None: 265 + """SL tan(angle); — character slant. 0 = upright; ~0.4 = italic-ish.""" 266 + self.hpgl.append(f"SL{slant:.3f};") 267 + 268 + def svg_polylines( 269 + self, 270 + svg_path: str, 271 + cx: int, 272 + cy: int, 273 + target_size: int, 274 + bezier_samples: int = 6, 275 + ) -> None: 276 + """Load an SVG file, flatten every <path> into polylines (sampling 277 + beziers), scale to fit `target_size` plotter units in the longer 278 + axis, centre at (cx, cy), and emit as point-to-point PA/PD strokes. 279 + """ 280 + import svgelements 281 + 282 + svg = svgelements.SVG.parse(svg_path) 283 + polylines: list[list[tuple[float, float]]] = [] 284 + current: list[tuple[float, float]] = [] 285 + 286 + def flush() -> None: 287 + nonlocal current 288 + if len(current) >= 2: 289 + polylines.append(current) 290 + current = [] 291 + 292 + for el in svg.elements(): 293 + if not isinstance(el, svgelements.Path): 294 + continue 295 + for seg in el: 296 + if isinstance(seg, svgelements.Move): 297 + flush() 298 + current = [(seg.end.x, seg.end.y)] 299 + elif isinstance(seg, svgelements.Line): 300 + current.append((seg.end.x, seg.end.y)) 301 + elif isinstance(seg, (svgelements.CubicBezier, svgelements.QuadraticBezier, svgelements.Arc)): 302 + # Faceted approximation — one straight line per curve segment. 303 + # No bezier sampling: just draw start→end. Cleaner, smaller, 304 + # and the polygonal look is intentional for the plot aesthetic. 305 + current.append((seg.end.x, seg.end.y)) 306 + elif isinstance(seg, svgelements.Close): 307 + if current: 308 + current.append(current[0]) 309 + flush() 310 + flush() 311 + 312 + if not polylines: 313 + return 314 + 315 + # bounding box, scale, centre 316 + xs = [x for pl in polylines for x, _ in pl] 317 + ys = [y for pl in polylines for _, y in pl] 318 + sw = max(xs) - min(xs) 319 + sh = max(ys) - min(ys) 320 + scale = target_size / max(sw, sh) 321 + scx = (min(xs) + max(xs)) / 2 322 + scy = (min(ys) + max(ys)) / 2 323 + 324 + def xform(p: tuple[float, float]) -> tuple[int, int]: 325 + x, y = p 326 + px = cx + (x - scx) * scale 327 + py = cy - (y - scy) * scale # flip Y for HPGL (y-up) 328 + return int(round(px)), int(round(py)) 329 + 330 + # de-duplicate consecutive points within ~1 plotter unit 331 + for pl in polylines: 332 + tpts: list[tuple[int, int]] = [] 333 + for p in pl: 334 + tp = xform(p) 335 + if tpts and abs(tp[0] - tpts[-1][0]) + abs(tp[1] - tpts[-1][1]) < 2: 336 + continue 337 + tpts.append(tp) 338 + if len(tpts) < 2: 339 + continue 340 + self.set_line_type(None) 341 + cmd = [f"PU;PA{tpts[0][0]},{tpts[0][1]};PD;"] 342 + for x, y in tpts[1:]: 343 + cmd.append(f"PA{x},{y};") 344 + cmd.append("PU;") 345 + self.hpgl.append("".join(cmd)) 346 + svg_pts = " ".join( 347 + f"{self._to_svg_xy(x, y)[0]:.3f},{self._to_svg_xy(x, y)[1]:.3f}" 348 + for x, y in tpts 349 + ) 350 + self.svg.append( 351 + f'<polyline points="{svg_pts}" fill="none" stroke="#111" stroke-width="0.4"/>' 352 + ) 353 + 354 + def line(self, x1: int, y1: int, x2: int, y2: int, dashed: bool = False) -> None: 355 + self.set_line_type(2 if dashed else None) 356 + self.hpgl.append(f"PU;PA{x1},{y1};PD;PA{x2},{y2};PU;") 357 + sx1, sy1 = self._to_svg_xy(x1, y1) 358 + sx2, sy2 = self._to_svg_xy(x2, y2) 359 + dasharray = ' stroke-dasharray="2 2"' if dashed else "" 360 + self.svg.append( 361 + f'<line x1="{sx1:.3f}" y1="{sy1:.3f}" x2="{sx2:.3f}" y2="{sy2:.3f}" ' 362 + f'stroke="#111" stroke-width="0.3"{dasharray}/>' 363 + ) 364 + 365 + def circle(self, cx: int, cy: int, r: int, fill: bool = False) -> None: 366 + self.set_line_type(None) 367 + # never fill — just an outline. 368 + self.hpgl.append(f"PA{cx},{cy};CI{r};") 369 + sx, sy = self._to_svg_xy(cx, cy) 370 + rx = r / self.fx 371 + self.svg.append( 372 + f'<circle cx="{sx:.3f}" cy="{sy:.3f}" r="{rx:.3f}" ' 373 + f'fill="none" stroke="#111" stroke-width="0.3"/>' 374 + ) 375 + 376 + def label( 377 + self, 378 + cx: int, 379 + cy: int, 380 + text: str, 381 + char_w_cm: float, 382 + anchor: str = "middle", 383 + ) -> None: 384 + """Draw a horizontal label centered (anchor=middle) or left (start) 385 + at (cx, cy). cy is the baseline. Auto-shrinks the character size if 386 + the label would otherwise start beyond the plot's hard-clip limit.""" 387 + self.set_line_type(None) 388 + spacing = 1.5 389 + char_h_cm = char_w_cm * 1.5 390 + 391 + # plot bounds in plotter units (P1..P2 with a small inner margin) 392 + x1 = int(self.p1p2[0] * 0.99) 393 + x2 = int(self.p1p2[2] * 0.99) 394 + avail = x2 - x1 395 + 396 + text_w = int(len(text) * char_w_cm * spacing * 10 * self.fx) 397 + if text_w > avail: 398 + scale = avail / text_w 399 + char_w_cm *= scale 400 + # Floor: the 7585B refuses SI values below ~0.5 cm with E3. 401 + char_w_cm = max(char_w_cm, 0.5) 402 + char_h_cm = char_w_cm * 1.5 403 + text_w = int(len(text) * char_w_cm * spacing * 10 * self.fx) 404 + 405 + if anchor == "middle": 406 + x = cx - text_w // 2 407 + elif anchor == "end": 408 + x = cx - text_w 409 + else: 410 + x = cx 411 + # clamp to plot area so PA can never go off-limit 412 + x = max(x1, min(x, x2 - text_w)) 413 + 414 + # Strip any '~' (our terminator) or '\x03' from label content. 415 + safe = text.replace("~", "").replace("\x03", "") 416 + self.hpgl.append( 417 + f"SI{char_w_cm:.3f},{char_h_cm:.3f};" 418 + f"PA{x},{cy};LB{safe}~" 419 + ) 420 + sx, sy = self._to_svg_xy(cx, cy) 421 + font_px = char_h_cm * 10 # in mm 422 + self.svg.append( 423 + f'<text x="{sx:.3f}" y="{sy:.3f}" font-size="{font_px:.2f}" ' 424 + f'font-family="Helvetica, Arial, sans-serif" ' 425 + f'text-anchor="{anchor}" fill="#111">{_xml_escape(text)}</text>' 426 + ) 427 + 428 + def end(self) -> None: 429 + self.hpgl.append("LT;PU;PA0,0;SP0;") 430 + 431 + # --- output --- 432 + def hpgl_bytes(self) -> bytes: 433 + return ("\n".join(self.hpgl) + "\n").encode("ascii") 434 + 435 + def svg_doc(self) -> str: 436 + body = "\n ".join(self.svg) 437 + return ( 438 + f'<?xml version="1.0" encoding="UTF-8"?>\n' 439 + f'<svg xmlns="http://www.w3.org/2000/svg" ' 440 + f'width="{self.svg_w_mm:.2f}mm" height="{self.svg_h_mm:.2f}mm" ' 441 + f'viewBox="0 0 {self.svg_w_mm:.2f} {self.svg_h_mm:.2f}">\n' 442 + f' <rect x="0" y="0" width="{self.svg_w_mm:.2f}" ' 443 + f'height="{self.svg_h_mm:.2f}" fill="#fafafa"/>\n' 444 + f' {body}\n' 445 + f'</svg>\n' 446 + ) 447 + 448 + 449 + def _xml_escape(s: str) -> str: 450 + return ( 451 + s.replace("&", "&amp;") 452 + .replace("<", "&lt;") 453 + .replace(">", "&gt;") 454 + .replace('"', "&quot;") 455 + ) 456 + 457 + 458 + # --------------------------------------------------------------------------- 459 + # Slide composition 460 + # --------------------------------------------------------------------------- 461 + NATURALS = list("CDEFGABCDEFGAB") # 14 white keys = 2 octaves 462 + # black-key positions: True if a sharp sits *above* this natural's right edge. 463 + HAS_SHARP_RIGHT = [True, True, False, True, True, True, False] * 2 # C# D# (skip E) F# G# A# (skip B) 464 + 465 + 466 + def compose_slide( 467 + p1p2: tuple[int, int, int, int], 468 + factor: tuple[int, int], 469 + title: str = "notepat.com keymap", 470 + tagline: str = "C D E F G A B play the notes they name", 471 + ) -> Composer: 472 + c = Composer(p1p2, factor) 473 + x1, y1, x2, y2 = p1p2 474 + 475 + # ---- title (with a touch of slant for character) ---- 476 + c.set_slant(0.20) # ~11° forward slant 477 + c.label(0, 3500, title, char_w_cm=1.6) 478 + c.set_slant(0.0) # back to upright for the rest 479 + 480 + # divider under header 481 + c.line(int(x1 * 0.94), 3050, int(x2 * 0.94), 3050) 482 + 483 + # ---- PALS logo in top-right corner — real SVG flattened to polylines ---- 484 + logo_cx = int(x2 * 0.88) 485 + logo_cy = 3450 486 + pals_svg = "/Users/jas/aesthetic-computer/bills/invoices/pals.svg" 487 + c.svg_polylines(pals_svg, logo_cx, logo_cy, target_size=1000, bezier_samples=5) 488 + 489 + # ---- piano (2 octaves) ---- 490 + n_white = 14 491 + white_w = 900 492 + white_h = 1300 493 + piano_top = 2500 494 + piano_bot = piano_top - white_h # = 1200 495 + piano_x_start = -(n_white * white_w) // 2 496 + black_w = 540 497 + black_h = 800 498 + 499 + # remember each white key's geometric centre for connector use 500 + white_centers: dict[str, tuple[int, int]] = {} 501 + piano_mid_y = (piano_top + piano_bot) // 2 502 + octave = 0 503 + for i, note in enumerate(NATURALS): 504 + kx1 = piano_x_start + i * white_w 505 + kx2 = kx1 + white_w 506 + c.edge_rect(kx1, piano_bot, kx2, piano_top) 507 + # natural label inside, lower part — bigger so it's actually legible 508 + c.label((kx1 + kx2) // 2, piano_bot + 170, note, char_w_cm=0.55) 509 + # tag this key 510 + if note == "C" and i > 0: 511 + octave = 1 512 + white_centers[f"{note}{octave}"] = ((kx1 + kx2) // 2, piano_mid_y) 513 + 514 + # "black" keys — cross-hatched (FT3) at 80-unit spacing / 45°, so they 515 + # read as the sharps without scribbling a solid block of ink. 516 + for i, has in enumerate(HAS_SHARP_RIGHT): 517 + if not has or i >= n_white - 1: 518 + continue 519 + kx1 = piano_x_start + i * white_w + white_w - black_w // 2 520 + kx2 = kx1 + black_w 521 + c.hatched_rect(kx1, piano_top - black_h, kx2, piano_top, spacing=80, angle=45) 522 + 523 + # ---- dashed connectors from C-D-E-F-G (lower octave) to QWERTY ---- 524 + qwerty_top = 800 525 + qwerty_bot = -2200 526 + 527 + # First-octave naturals and their QWERTY home keys 528 + note_to_qwerty = { 529 + "C": ("C", -1), # row, with index in row 530 + "D": ("D", -1), 531 + "E": ("E", -1), 532 + "F": ("F", -1), 533 + "G": ("G", -1), 534 + } 535 + 536 + # ---- QWERTY ---- 537 + rows = [ 538 + ("0", list("QWERTYUIOP")), 539 + ("1", list("ASDFGHJKL'")), 540 + ("2", list("ZXCVBNM,./")), 541 + ] 542 + cap_w = 1300 543 + cap_h = 900 544 + gap = 100 545 + slot = cap_w + gap 546 + row_offsets = [0, 350, 800] 547 + row_y_top = [qwerty_top, qwerty_top - cap_h - 150, qwerty_top - 2 * (cap_h + 150)] 548 + 549 + qwerty_centers: dict[str, tuple[int, int]] = {} 550 + notes_set = set("CDEFGAB") 551 + for r_idx, (rname, keys) in enumerate(rows): 552 + n = len(keys) 553 + row_w = n * slot - gap 554 + row_x_start = -row_w // 2 + row_offsets[r_idx] 555 + ytop = row_y_top[r_idx] 556 + ybot = ytop - cap_h 557 + for i, k in enumerate(keys): 558 + kx1 = row_x_start + i * slot 559 + kx2 = kx1 + cap_w 560 + if k in notes_set: 561 + # highlight via double-stroked outline — no fill. 562 + c.double_rect(kx1, ybot, kx2, ytop, inset=90) 563 + else: 564 + c.edge_rect(kx1, ybot, kx2, ytop) 565 + cap_cx = (kx1 + kx2) // 2 566 + cap_cy = (ytop + ybot) // 2 567 + c.label(cap_cx, ybot + cap_h // 3, k, char_w_cm=0.85) 568 + qwerty_centers[k] = (cap_cx, cap_cy) 569 + 570 + # ---- connectors: centre-of-piano-key → centre-of-qwerty-cap ---- 571 + for note in "CDEFG": 572 + if note in qwerty_centers and f"{note}0" in white_centers: 573 + px, py = white_centers[f"{note}0"] 574 + qx, qy = qwerty_centers[note] 575 + c.line(px, py, qx, qy, dashed=True) 576 + 577 + # ---- tagline ---- 578 + c.label(0, -2700, tagline, char_w_cm=0.7) 579 + 580 + # ---- URL row — short enough to fit at min SI 0.5 ---- 581 + c.label(0, -3300, "notepat lives in:", char_w_cm=0.5) 582 + c.label(0, -3700, "notepat.com prompt.ac/menuband aesthetic.computer", char_w_cm=0.55) 583 + 584 + c.end() 585 + return c 586 + 587 + 588 + # --------------------------------------------------------------------------- 589 + # CLI 590 + # --------------------------------------------------------------------------- 591 + def main() -> int: 592 + p = argparse.ArgumentParser() 593 + p.add_argument("--port", default=os.environ.get("HP7585B_TTY", "")) 594 + p.add_argument("--baud", type=int, default=9600) 595 + p.add_argument("--no-query", action="store_true", 596 + help="skip plotter query, use last-known ANSI B P1/P2") 597 + p.add_argument("--out-hpgl", default=str(Path(__file__).parent / "notepat-slide.hpgl")) 598 + p.add_argument("--out-svg", default=str(Path.home() / "Desktop" / "notepat-slide-preview.svg")) 599 + p.add_argument("--open", action="store_true", 600 + help="open the SVG preview in the default app") 601 + args = p.parse_args() 602 + 603 + if args.no_query or not args.port: 604 + # last-known ANSI B landscape sheet from earlier OH;/OP; 605 + p1p2 = (-8050, -4574, 8050, 4574) 606 + factor = (40, 40) 607 + sys.stderr.write(f"using cached limits {p1p2} factor={factor}\n") 608 + else: 609 + p1p2, factor = query_limits(args.port, args.baud) 610 + sys.stderr.write(f"queried limits {p1p2} factor={factor}\n") 611 + 612 + comp = compose_slide(p1p2, factor) 613 + 614 + Path(args.out_hpgl).write_bytes(comp.hpgl_bytes()) 615 + Path(args.out_svg).write_text(comp.svg_doc()) 616 + 617 + sys.stderr.write( 618 + f"wrote HPGL: {args.out_hpgl} ({len(comp.hpgl_bytes())} B)\n" 619 + f"wrote SVG: {args.out_svg}\n" 620 + ) 621 + 622 + if args.open: 623 + webbrowser.open(f"file://{args.out_svg}") 624 + 625 + return 0 626 + 627 + 628 + if __name__ == "__main__": 629 + sys.exit(main())
+257
plotters/hp7585b/query-limits.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Probe the HP 7585B for its actual limits and emit a usable test pattern. 4 + 5 + The plotter knows what sheet is loaded and what hard-clip limits apply far 6 + better than any TOML guess. This script: 7 + 8 + 1. opens the serial port, 9 + 2. sends OE; OH; OP; OF; OS; (all read-only, no movement), 10 + 3. parses the responses, 11 + 4. prints a human summary, 12 + 5. optionally writes a hand-rolled HPGL test pattern sized to fit *inside* 13 + the reported P1/P2 area with margin (`--write-test out.hpgl`), 14 + 6. optionally writes a vpype paper profile snippet to merge into vpype.toml 15 + (`--write-toml`). 16 + 17 + Usage: 18 + query-limits.py --port /dev/cu.usbserial-110 19 + query-limits.py --port /dev/cu.usbserial-110 --write-test safe.hpgl 20 + query-limits.py --port /dev/cu.usbserial-110 --write-toml > paper.toml 21 + """ 22 + 23 + from __future__ import annotations 24 + 25 + import argparse 26 + import os 27 + import sys 28 + import time 29 + 30 + import serial 31 + 32 + XON, XOFF = 0x11, 0x13 33 + 34 + 35 + def _ask(ser: serial.Serial, cmd: bytes, settle_s: float = 1.0) -> str: 36 + ser.reset_input_buffer() 37 + ser.write(cmd) 38 + ser.flush() 39 + deadline = time.time() + settle_s 40 + buf = bytearray() 41 + while time.time() < deadline: 42 + chunk = ser.read(64) 43 + if chunk: 44 + for b in chunk: 45 + if b not in (XON, XOFF): 46 + buf.append(b) 47 + # plotter terminates output strings with CR / LF; bail early 48 + if b"\n" in buf or b"\r" in buf: 49 + break 50 + else: 51 + time.sleep(0.05) 52 + return buf.decode("ascii", errors="replace").strip() 53 + 54 + 55 + def _ints(s: str) -> list[int]: 56 + out: list[int] = [] 57 + for tok in s.replace(",", " ").split(): 58 + try: 59 + out.append(int(tok)) 60 + except ValueError: 61 + pass 62 + return out 63 + 64 + 65 + def _decode_status(byte: int) -> str: 66 + flags = [] 67 + if byte & 0x01: 68 + flags.append("pen-down") 69 + if byte & 0x02: 70 + flags.append("p1p2-changed") 71 + if byte & 0x04: 72 + flags.append("digitized-point") 73 + if byte & 0x08: 74 + flags.append("initialized") 75 + if byte & 0x10: 76 + flags.append("ready") 77 + if byte & 0x20: 78 + flags.append("error") 79 + return ",".join(flags) or "<none>" 80 + 81 + 82 + def query(port: str, baud: int) -> dict: 83 + ser = serial.Serial( 84 + port=port, 85 + baudrate=baud, 86 + bytesize=serial.EIGHTBITS, 87 + parity=serial.PARITY_NONE, 88 + stopbits=serial.STOPBITS_ONE, 89 + xonxoff=False, 90 + rtscts=True, 91 + dsrdtr=True, 92 + timeout=0.5, 93 + ) 94 + ser.setDTR(True) 95 + ser.setRTS(True) 96 + try: 97 + info = {} 98 + info["id"] = _ask(ser, b"OI;") 99 + info["error_raw"] = _ask(ser, b"OE;") 100 + info["hardclip_raw"] = _ask(ser, b"OH;") 101 + info["p1p2_raw"] = _ask(ser, b"OP;") 102 + info["factor_raw"] = _ask(ser, b"OF;") 103 + info["status_raw"] = _ask(ser, b"OS;") 104 + finally: 105 + ser.close() 106 + 107 + info["error"] = _ints(info["error_raw"])[0] if _ints(info["error_raw"]) else None 108 + hc = _ints(info["hardclip_raw"]) 109 + pp = _ints(info["p1p2_raw"]) 110 + of = _ints(info["factor_raw"]) 111 + st = _ints(info["status_raw"]) 112 + if len(hc) == 4: 113 + info["hardclip"] = (hc[0], hc[1], hc[2], hc[3]) 114 + if len(pp) == 4: 115 + info["p1p2"] = (pp[0], pp[1], pp[2], pp[3]) 116 + if len(of) >= 2: 117 + info["factor"] = (of[0], of[1]) 118 + if st: 119 + info["status"] = st[0] 120 + info["status_flags"] = _decode_status(st[0]) 121 + return info 122 + 123 + 124 + def summarize(info: dict) -> str: 125 + lines = [] 126 + lines.append(f"identifier: {info.get('id', '?')}") 127 + err = info.get("error") 128 + lines.append(f"error reg: {err} {'(clear)' if err == 0 else '(SET — investigate)'}") 129 + if "hardclip" in info: 130 + x1, y1, x2, y2 = info["hardclip"] 131 + fx, fy = info.get("factor", (40, 40)) 132 + lines.append( 133 + f"hard clip: X={x1}..{x2} Y={y1}..{y2} units" 134 + f" ({(x2 - x1) / fx:.1f} x {(y2 - y1) / fy:.1f} mm)" 135 + ) 136 + if "p1p2" in info: 137 + x1, y1, x2, y2 = info["p1p2"] 138 + fx, fy = info.get("factor", (40, 40)) 139 + lines.append( 140 + f"P1/P2: ({x1},{y1}) ({x2},{y2}) units" 141 + f" ({(x2 - x1) / fx:.1f} x {(y2 - y1) / fy:.1f} mm)" 142 + ) 143 + if "factor" in info: 144 + fx, fy = info["factor"] 145 + lines.append(f"factor: {fx}, {fy} units/mm (= {1000 / fx:.3f} um/unit)") 146 + if "status" in info: 147 + lines.append(f"status: 0x{info['status']:02X} [{info['status_flags']}]") 148 + return "\n".join(lines) 149 + 150 + 151 + def build_test_hpgl(p1p2: tuple[int, int, int, int], pen: int = 1) -> bytes: 152 + """Hand-rolled test pattern sized to fit inside P1/P2 with 5% margin. 153 + 154 + Frame + diagonals + centered crosshair + centered circle. All coordinates 155 + are clipped to (P1+5%) .. (P2-5%) so we cannot run off-limit. 156 + """ 157 + x1, y1, x2, y2 = p1p2 158 + mx = int((x2 - x1) * 0.05) 159 + my = int((y2 - y1) * 0.05) 160 + fx1, fy1 = x1 + mx, y1 + my 161 + fx2, fy2 = x2 - mx, y2 - my 162 + cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 163 + # circle radius = 35% of the smaller axis 164 + r = int(min((fx2 - fx1), (fy2 - fy1)) * 0.35 / 2) 165 + 166 + cmds = [] 167 + cmds.append(f"IN;SP{pen};") 168 + # frame 169 + cmds.append( 170 + f"PA{fx1},{fy1};PD;PA{fx2},{fy1};PA{fx2},{fy2};" 171 + f"PA{fx1},{fy2};PA{fx1},{fy1};PU;" 172 + ) 173 + # diagonals 174 + cmds.append( 175 + f"PA{fx1},{fy1};PD;PA{fx2},{fy2};PU;" 176 + f"PA{fx1},{fy2};PD;PA{fx2},{fy1};PU;" 177 + ) 178 + # crosshair 179 + cmds.append( 180 + f"PA{fx1},{cy};PD;PA{fx2},{cy};PU;" 181 + f"PA{cx},{fy1};PD;PA{cx},{fy2};PU;" 182 + ) 183 + # circle 184 + cmds.append(f"PA{cx},{cy};CI{r};PU;") 185 + # park 186 + cmds.append("PA0,0;SP0;") 187 + return ("\n".join(cmds) + "\n").encode("ascii") 188 + 189 + 190 + def build_toml(info: dict, name: str = "current") -> str: 191 + if "hardclip" not in info or "factor" not in info: 192 + return "# could not query plotter — no profile emitted\n" 193 + x1, y1, x2, y2 = info["hardclip"] 194 + fx, fy = info["factor"] 195 + paper_x_mm = (x2 - x1) / fx 196 + paper_y_mm = (y2 - y1) / fy 197 + return f"""# Generated from a live OH; query against the connected 7585B. 198 + # This reflects whatever sheet is currently loaded — re-run query-limits.py 199 + # whenever you change paper. 200 + 201 + [[device.hp7585b.paper]] 202 + name = "{name}" 203 + paper_size = ["{paper_x_mm:.2f}mm", "{paper_y_mm:.2f}mm"] 204 + x_range = [{x1}, {x2}] 205 + y_range = [{y1}, {y2}] 206 + y_axis_up = true 207 + origin_location = ["{paper_x_mm / 2:.2f}mm", "{paper_y_mm / 2:.2f}mm"] 208 + final_pu_params = "0,0" 209 + info = "auto-generated from plotter OH; query" 210 + """ 211 + 212 + 213 + def main() -> int: 214 + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) 215 + p.add_argument( 216 + "--port", 217 + default=os.environ.get("HP7585B_TTY", ""), 218 + help="serial device (defaults to $HP7585B_TTY)", 219 + ) 220 + p.add_argument("--baud", type=int, default=9600) 221 + p.add_argument( 222 + "--write-test", 223 + metavar="OUT.HPGL", 224 + help="write a hand-rolled, in-bounds test plot", 225 + ) 226 + p.add_argument( 227 + "--write-toml", 228 + action="store_true", 229 + help="emit a vpype paper profile to stdout based on the current sheet", 230 + ) 231 + p.add_argument( 232 + "--pen", type=int, default=1, help="pen number for the test plot" 233 + ) 234 + args = p.parse_args() 235 + 236 + if not args.port: 237 + p.error("no --port and HP7585B_TTY not set") 238 + 239 + info = query(args.port, args.baud) 240 + print(summarize(info), file=sys.stderr) 241 + 242 + if args.write_test: 243 + if "p1p2" not in info: 244 + print("no P1/P2 reading — refusing to generate test", file=sys.stderr) 245 + return 2 246 + with open(args.write_test, "wb") as f: 247 + f.write(build_test_hpgl(info["p1p2"], pen=args.pen)) 248 + print(f"wrote {args.write_test}", file=sys.stderr) 249 + 250 + if args.write_toml: 251 + sys.stdout.write(build_toml(info)) 252 + 253 + return 0 254 + 255 + 256 + if __name__ == "__main__": 257 + sys.exit(main())
+6
plotters/hp7585b/safe-test.hpgl
··· 1 + IN;SP1; 2 + PA-14993,-4116;PD;PA14993,-4116;PA14993,4116;PA-14993,4116;PA-14993,-4116;PU; 3 + PA-14993,-4116;PD;PA14993,4116;PU;PA-14993,4116;PD;PA14993,-4116;PU; 4 + PA-14993,0;PD;PA14993,0;PU;PA0,-4116;PD;PA0,4116;PU; 5 + PA0,0;CI1440;PU; 6 + PA0,0;SP0;
+228
plotters/hp7585b/serial-tx.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + HP 7585B serial driver with monitoring + safe chunking. 4 + 5 + - Xon/Xoff hardware flow control (kernel-level via pyserial). 6 + - Chunks on HP-GL `;` boundaries, max 256 bytes/frame (the plotter's 7 + documented per-frame ceiling). Never splits mid-command. 8 + - Per-byte delay (default 1 ms) to keep tiny machines happy. 9 + - Reader thread prints anything the plotter sends back, prefixed [<-]. 10 + - Raw mode (`-c "OI;"`) for ad-hoc commands; file mode (`-f file.hpgl`) 11 + for prepared plots. 12 + 13 + usage: 14 + serial-tx.py --port /dev/cu.usbserial-110 -c "OI;" --listen 2 15 + serial-tx.py --port /dev/cu.usbserial-110 -f test-print.hpgl 16 + """ 17 + 18 + from __future__ import annotations 19 + 20 + import argparse 21 + import os 22 + import sys 23 + import threading 24 + import time 25 + from pathlib import Path 26 + 27 + import serial 28 + 29 + 30 + XON = 0x11 31 + XOFF = 0x13 32 + 33 + 34 + def _reader( 35 + ser: serial.Serial, stop: threading.Event, can_send: threading.Event 36 + ) -> None: 37 + while not stop.is_set(): 38 + try: 39 + data = ser.read(64) 40 + except serial.SerialException: 41 + return 42 + if not data: 43 + continue 44 + 45 + # Strip Xon/Xoff out of the user-visible stream and act on them. 46 + passthrough = bytearray() 47 + for b in data: 48 + if b == XOFF: 49 + if can_send.is_set(): 50 + sys.stdout.write("[<-] <Xoff>\n") 51 + sys.stdout.flush() 52 + can_send.clear() 53 + elif b == XON: 54 + if not can_send.is_set(): 55 + sys.stdout.write("[<-] <Xon>\n") 56 + sys.stdout.flush() 57 + can_send.set() 58 + else: 59 + passthrough.append(b) 60 + if passthrough: 61 + try: 62 + text = bytes(passthrough).decode("ascii", errors="replace") 63 + except Exception: 64 + text = repr(bytes(passthrough)) 65 + sys.stdout.write(f"[<-] {text}") 66 + sys.stdout.flush() 67 + 68 + 69 + def _chunks(payload: bytes, limit: int) -> list[bytes]: 70 + """Split payload into <= `limit`-byte frames on `;` boundaries. 71 + 72 + Walks forward, taking as many full HP-GL commands as fit before each 73 + cut. Falls back to a hard split if a single command exceeds `limit` 74 + (rare — long PA polylines from vpype can do this). 75 + """ 76 + out: list[bytes] = [] 77 + i = 0 78 + n = len(payload) 79 + while i < n: 80 + end = min(i + limit, n) 81 + if end < n: 82 + cut = payload.rfind(b";", i, end + 1) 83 + if cut > i: 84 + end = cut + 1 # include the ';' 85 + out.append(payload[i:end]) 86 + i = end 87 + return out 88 + 89 + 90 + def send( 91 + port: str, 92 + payload: bytes, 93 + baud: int, 94 + frame_limit: int, 95 + byte_delay_ms: float, 96 + chunk_delay_ms: float, 97 + listen_after_s: float, 98 + ) -> None: 99 + ser = serial.Serial( 100 + port=port, 101 + baudrate=baud, 102 + bytesize=serial.EIGHTBITS, 103 + parity=serial.PARITY_NONE, 104 + stopbits=serial.STOPBITS_ONE, 105 + xonxoff=False, # plotter is in hardwire-handshake mode 106 + rtscts=True, # honor the plotter's CTS line 107 + dsrdtr=True, # assert DTR; honor DSR 108 + timeout=0.5, 109 + write_timeout=30, 110 + ) 111 + # Make sure modem control lines are asserted from our side. 112 + ser.setDTR(True) 113 + ser.setRTS(True) 114 + stop = threading.Event() 115 + can_send = threading.Event() 116 + can_send.set() # start in "go" state; the plotter Xoffs us when full 117 + reader = threading.Thread( 118 + target=_reader, args=(ser, stop, can_send), daemon=True 119 + ) 120 + reader.start() 121 + 122 + def _wait_xon() -> None: 123 + while not can_send.wait(timeout=0.5): 124 + if stop.is_set(): 125 + return 126 + 127 + try: 128 + # drain anything already buffered (boot banner, prior responses) 129 + ser.reset_input_buffer() 130 + 131 + frames = _chunks(payload, frame_limit) 132 + total = len(payload) 133 + sent = 0 134 + byte_pause = byte_delay_ms / 1000.0 135 + chunk_pause = chunk_delay_ms / 1000.0 136 + 137 + for idx, frame in enumerate(frames, 1): 138 + _wait_xon() 139 + sys.stdout.write( 140 + f"[->] frame {idx}/{len(frames)} ({len(frame)} B, " 141 + f"{sent}/{total} sent)\n" 142 + ) 143 + sys.stdout.flush() 144 + if byte_pause > 0: 145 + for b in frame: 146 + _wait_xon() 147 + ser.write(bytes([b])) 148 + time.sleep(byte_pause) 149 + else: 150 + ser.write(frame) 151 + ser.flush() 152 + sent += len(frame) 153 + if chunk_pause > 0 and idx < len(frames): 154 + time.sleep(chunk_pause) 155 + 156 + sys.stdout.write(f"[->] done, {sent} B sent\n") 157 + sys.stdout.flush() 158 + 159 + if listen_after_s > 0: 160 + time.sleep(listen_after_s) 161 + finally: 162 + stop.set() 163 + time.sleep(0.1) 164 + ser.close() 165 + 166 + 167 + def main() -> int: 168 + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) 169 + p.add_argument( 170 + "--port", 171 + default=os.environ.get("HP7585B_TTY", ""), 172 + help="serial device (defaults to $HP7585B_TTY)", 173 + ) 174 + p.add_argument("--baud", type=int, default=9600) 175 + p.add_argument( 176 + "--frame-bytes", 177 + type=int, 178 + default=256, 179 + help="max bytes per frame (plotter limit; default 256)", 180 + ) 181 + p.add_argument( 182 + "--byte-delay-ms", 183 + type=float, 184 + default=2.0, 185 + help="delay between bytes in ms (default 2.0; set 0 to disable)", 186 + ) 187 + p.add_argument( 188 + "--chunk-delay-ms", 189 + type=float, 190 + default=10.0, 191 + help="delay between frames in ms (default 10)", 192 + ) 193 + p.add_argument( 194 + "--listen", 195 + type=float, 196 + default=1.0, 197 + metavar="SECONDS", 198 + help="seconds to keep listening after the last byte is sent", 199 + ) 200 + src = p.add_mutually_exclusive_group(required=True) 201 + src.add_argument("-c", "--command", help='inline HP-GL, e.g. "OI;"') 202 + src.add_argument("-f", "--file", help="path to .hpgl file") 203 + 204 + args = p.parse_args() 205 + if not args.port: 206 + p.error("no --port and HP7585B_TTY not set") 207 + if not Path(args.port).exists(): 208 + p.error(f"{args.port} does not exist") 209 + 210 + if args.command is not None: 211 + payload = args.command.encode("ascii") 212 + else: 213 + payload = Path(args.file).read_bytes() 214 + 215 + send( 216 + port=args.port, 217 + payload=payload, 218 + baud=args.baud, 219 + frame_limit=args.frame_bytes, 220 + byte_delay_ms=args.byte_delay_ms, 221 + chunk_delay_ms=args.chunk_delay_ms, 222 + listen_after_s=args.listen, 223 + ) 224 + return 0 225 + 226 + 227 + if __name__ == "__main__": 228 + sys.exit(main())
+1
plotters/hp7585b/test-print.hpgl
··· 1 + IN;DF;PS4;SP1;PU200,200;PR;PD0,11280;PU11080,0;PD0,-11280,-11080,0;PU0,0;PD7811,11280;PU-4542,0;PD7811,-11280;PU;SP2;PU-5540,10400;PD4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,-1,4,0,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,0,4,-1,4,0,3,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,0,4,-1,4,-1,4,0,3,-1,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,0,3,-1,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,-1,3,-1,4,0,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,0,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-2,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-2,4,-1,4,-1,4,-1,4,-1,3,-1,4,-2,4,-1,4,-1,4,-1,4,-2,3,-1,4,-1,4,-1,4,-1,4,-2,3,-1,4,-1,4,-1,4,-2,4,-1,3,-1,4,-2,4,-1,4,-1,3,-2,4,-1,4,-1,4,-2,4,-1,3,-1,4,-2,4,-1,4,-1,3,-2,4,-1,4,-1,4,-2,3,-1,4,-2,4,-1,4,-1,3,-2,4,-1,4,-2,4,-1,3,-2,4,-1,4,-1,3,-2,4,-1,4,-2,4,-1,3,-2,4,-1,4,-2,3,-1,4,-2,4,-1,4,-2,3,-1,4,-2,4,-2,3,-1,4,-2,4,-1,3,-2,4,-1,4,-2,3,-2,4,-1,4,-2,3,-1,4,-2,4,-2,3,-1,4,-2,4,-1,3,-2,4,-2,4,-1,3,-2,4,-2,4,-1,3,-2,4,-2,3,-1,4,-2,4,-2,3,-2,4,-1,4,-2,3,-2,4,-1,3,-2,4,-2,4,-2,3,-1,4,-2,3,-2,4,-2,4,-1,3,-2,4,-2,3,-2,4,-2,3,-1,4,-2,4,-2,3,-2,4,-2,3,-2,4,-1,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,4,-2,3,-1,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-2,3,-3,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-3,4,-2,3,-2,3,-2,4,-2,3,-2,4,-3,3,-2,3,-2,4,-2,3,-2,3,-3,4,-2,3,-2,3,-2,4,-3,3,-2,3,-2,3,-2,4,-3,3,-2,3,-2,4,-2,3,-3,3,-2,3,-2,4,-3,3,-2,3,-2,4,-2,3,-3,3,-2,3,-2,4,-3,3,-2,3,-2,3,-3,4,-2,3,-3,3,-2,3,-2,3,-3,4,-2,3,-2,3,-3,3,-2,4,-3,3,-2,3,-2,3,-3,3,-2,3,-3,4,-2,3,-3,3,-2,3,-3,3,-2,3,-2,4,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,4,-2,3,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,3,-3,3,-2,4,-3,3,-2,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-3,3,-2,2,-3,3,-3,3,-3,3,-2,3,-3,3,-3,3,-3,3,-2,3,-3,3,-3,2,-3,3,-2,3,-3,3,-3,3,-3,3,-3,3,-2,2,-3,3,-3,3,-3,3,-3,3,-3,3,-2,2,-3,3,-3,3,-3,3,-3,3,-3,2,-3,3,-2,3,-3,3,-3,2,-3,3,-3,3,-3,3,-3,2,-3,3,-3,3,-3,3,-2,2,-3,3,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,2,-3,3,-4,2,-3,3,-3,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-3,2,-4,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-4,2,-3,2,-3,3,-3,2,-3,3,-3,2,-4,3,-3,2,-3,3,-3,2,-3,2,-3,3,-4,2,-3,3,-3,2,-3,2,-4,3,-3,2,-3,2,-3,3,-3,2,-4,3,-3,2,-3,2,-3,3,-4,2,-3,2,-3,3,-3,2,-4,2,-3,2,-3,3,-4,2,-3,2,-3,3,-3,2,-4,2,-3,2,-3,3,-4,2,-3,2,-3,2,-3,3,-4,2,-3,2,-3,2,-4,3,-3,2,-3,2,-4,2,-3,2,-3,3,-4,2,-3,2,-4,2,-3,2,-3,2,-4,3,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,3,-3,2,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,1,-3,2,-4,2,-3,2,-4,2,-4,2,-3,2,-4,2,-3,1,-4,2,-3,2,-4,2,-3,2,-4,2,-4,1,-3,2,-4,2,-3,2,-4,2,-3,1,-4,2,-4,2,-3,2,-4,1,-3,2,-4,2,-4,2,-3,1,-4,2,-3,2,-4,1,-4,2,-3,2,-4,2,-4,1,-3,2,-4,2,-3,1,-4,2,-4,2,-3,1,-4,2,-4,2,-3,1,-4,2,-4,1,-3,2,-4,2,-4,1,-3,2,-4,1,-4,2,-3,2,-4,1,-4,2,-3,1,-4,2,-4,1,-3,2,-4,2,-4,1,-3,2,-4,1,-4,2,-4,1,-3,2,-4,1,-4,2,-3,1,-4,2,-4,1,-4,2,-3,1,-4,1,-4,2,-3,1,-4,2,-4,1,-4,2,-3,1,-4,1,-4,2,-4,1,-3,2,-4,1,-4,1,-4,2,-3,1,-4,1,-4,2,-4,1,-3,1,-4,2,-4,1,-4,1,-4,2,-3,1,-4,1,-4,2,-4,1,-3,1,-4,2,-4,1,-4,1,-4,1,-3,2,-4,1,-4,1,-4,1,-4,1,-3,2,-4,1,-4,1,-4,1,-4,2,-4,1,-3,1,-4,1,-4,1,-4,1,-4,2,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,2,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,0,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,0,-4,1,-3,1,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,1,-3,0,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,1,-3,0,-4,1,-4,1,-4,0,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-3,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,-1,-3,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-3,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-3,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,0,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-2,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-1,-4,-1,-4,-2,-4,-1,-3,-1,-4,-1,-4,-2,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-2,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-2,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-2,-4,-1,-4,-2,-3,-1,-4,-1,-4,-2,-3,-1,-4,-2,-4,-1,-4,-2,-3,-1,-4,-2,-4,-1,-3,-2,-4,-1,-4,-2,-4,-1,-3,-2,-4,-2,-4,-1,-3,-2,-4,-1,-4,-2,-3,-1,-4,-2,-4,-2,-3,-1,-4,-2,-4,-1,-3,-2,-4,-2,-4,-1,-3,-2,-4,-1,-4,-2,-3,-2,-4,-1,-4,-2,-3,-2,-4,-1,-4,-2,-3,-2,-4,-1,-3,-2,-4,-2,-4,-2,-3,-1,-4,-2,-4,-2,-3,-1,-4,-2,-3,-2,-4,-2,-4,-1,-3,-2,-4,-2,-3,-2,-4,-1,-4,-2,-3,-2,-4,-2,-3,-2,-4,-1,-3,-2,-4,-2,-4,-2,-3,-2,-4,-2,-3,-1,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-4,-1,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-3,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-3,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-3,-3,-2,-4,-2,-3,-2,-4,-2,-3,-3,-3,-2,-4,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-3,-2,-4,-2,-3,-3,-3,-2,-3,-3,-3,-2,-4,-2,-3,-3,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-2,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-3,-2,-2,-3,-3,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-3,-3,-2,-2,-3,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-2,-3,-3,-2,-3,-3,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-3,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-4,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-3,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-3,-2,-3,-3,-4,-2,-3,-3,-3,-2,-3,-3,-3,-2,-3,-2,-4,-3,-3,-2,-3,-3,-3,-2,-4,-2,-3,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-3,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-3,-2,-4,-3,-3,-2,-3,-2,-4,-2,-3,-3,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-3,-3,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-3,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-1,-4,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-2,-3,-2,-4,-1,-3,-2,-4,-2,-3,-2,-4,-2,-4,-2,-3,-1,-4,-2,-3,-2,-4,-2,-3,-1,-4,-2,-4,-2,-3,-2,-4,-2,-3,-1,-4,-2,-4,-2,-3,-2,-4,-1,-3,-2,-4,-2,-4,-1,-3,-2,-4,-2,-4,-2,-3,-1,-4,-2,-3,-2,-4,-1,-4,-2,-3,-2,-4,-1,-4,-2,-3,-2,-4,-1,-4,-2,-3,-1,-4,-2,-4,-2,-3,-1,-4,-2,-4,-1,-3,-2,-4,-2,-4,-1,-3,-2,-4,-1,-4,-2,-3,-1,-4,-2,-4,-2,-3,-1,-4,-2,-4,-1,-4,-2,-3,-1,-4,-2,-4,-1,-3,-2,-4,-1,-4,-2,-4,-1,-3,-2,-4,-1,-4,-1,-3,-2,-4,-1,-4,-2,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-2,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-2,-4,-1,-4,-1,-3,-2,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-2,-4,-1,-4,-1,-3,-1,-4,-2,-4,-1,-4,-1,-4,-1,-3,-1,-4,-2,-4,-1,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-2,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-2,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-3,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,0,-3,-1,-4,-1,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-3,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-3,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-3,-1,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,-1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,0,-4,1,-4,0,-3,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,0,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,0,-4,1,-4,1,-4,0,-3,1,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,0,-3,1,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,0,-4,1,-4,1,-4,1,-4,1,-3,1,-4,0,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,0,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-4,2,-3,1,-4,1,-4,1,-4,1,-4,1,-4,1,-3,1,-4,2,-4,1,-4,1,-4,1,-4,1,-3,1,-4,2,-4,1,-4,1,-4,1,-3,2,-4,1,-4,1,-4,1,-4,1,-4,2,-3,1,-4,1,-4,1,-4,2,-4,1,-3,1,-4,2,-4,1,-4,1,-3,2,-4,1,-4,1,-4,2,-4,1,-3,1,-4,2,-4,1,-4,1,-3,2,-4,1,-4,1,-4,2,-3,1,-4,2,-4,1,-4,1,-3,2,-4,1,-4,2,-4,1,-3,2,-4,1,-4,1,-3,2,-4,1,-4,2,-4,1,-3,2,-4,1,-4,2,-3,1,-4,2,-4,1,-4,2,-3,1,-4,2,-4,2,-3,1,-4,2,-4,1,-3,2,-4,1,-4,2,-3,2,-4,1,-4,2,-3,1,-4,2,-4,2,-3,1,-4,2,-4,1,-3,2,-4,2,-4,1,-3,2,-4,2,-4,1,-3,2,-4,2,-3,1,-4,2,-4,2,-3,2,-4,1,-4,2,-3,2,-4,1,-3,2,-4,2,-4,2,-3,1,-4,2,-3,2,-4,2,-4,2,-3,1,-4,2,-3,2,-4,2,-3,1,-4,2,-4,2,-3,2,-4,2,-3,2,-4,1,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-4,2,-3,1,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,2,-4,2,-3,2,-3,2,-4,2,-3,3,-4,2,-3,2,-3,2,-4,2,-3,2,-4,2,-3,3,-3,2,-4,2,-3,2,-3,2,-4,2,-3,3,-3,2,-4,2,-3,2,-4,2,-3,2,-3,3,-4,2,-3,2,-3,2,-4,3,-3,2,-3,2,-3,2,-4,3,-3,2,-3,2,-4,2,-3,3,-3,2,-3,2,-4,3,-3,2,-3,2,-4,2,-3,3,-3,2,-3,2,-4,3,-3,2,-3,2,-3,3,-4,2,-3,3,-3,2,-3,2,-3,3,-4,2,-3,2,-3,3,-3,2,-4,3,-3,2,-3,2,-3,3,-3,2,-3,3,-4,2,-3,3,-3,2,-3,3,-3,2,-3,2,-4,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-4,2,-3,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-4,2,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,2,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,2,-3,3,-3,3,-3,3,-3,2,-2,3,-3,3,-3,3,-3,2,-3,3,-3,3,-3,3,-3,2,-3,3,-3,3,-2,3,-3,2,-3,3,-3,3,-3,3,-3,3,-3,2,-2,3,-3,3,-3,3,-3,3,-3,3,-3,2,-2,3,-3,3,-3,3,-3,3,-3,3,-2,3,-3,2,-3,3,-3,3,-2,3,-3,3,-3,3,-3,3,-2,3,-3,3,-3,3,-3,2,-2,3,-3,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-2,4,-3,3,-2,3,-3,3,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,3,-2,4,-3,3,-2,3,-3,3,-2,3,-3,3,-2,3,-3,4,-2,3,-2,3,-3,3,-2,3,-3,3,-2,4,-3,3,-2,3,-3,3,-2,3,-2,4,-3,3,-2,3,-3,3,-2,3,-2,4,-3,3,-2,3,-2,3,-3,3,-2,4,-3,3,-2,3,-2,3,-3,4,-2,3,-2,3,-3,3,-2,4,-2,3,-2,3,-3,4,-2,3,-2,3,-3,3,-2,4,-2,3,-2,3,-3,4,-2,3,-2,3,-2,3,-3,4,-2,3,-2,3,-2,4,-3,3,-2,3,-2,4,-2,3,-2,4,-2,3,-3,3,-2,4,-2,3,-2,3,-2,4,-2,3,-3,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-3,3,-2,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-2,4,-1,3,-2,4,-2,4,-2,3,-2,4,-2,3,-2,4,-2,3,-1,4,-2,3,-2,4,-2,3,-2,4,-2,4,-1,3,-2,4,-2,3,-2,4,-1,3,-2,4,-2,4,-2,3,-2,4,-1,3,-2,4,-2,4,-2,3,-1,4,-2,3,-2,4,-1,4,-2,3,-2,4,-2,4,-1,3,-2,4,-2,3,-1,4,-2,4,-2,3,-1,4,-2,4,-2,3,-1,4,-2,4,-1,3,-2,4,-2,4,-1,3,-2,4,-1,4,-2,3,-2,4,-1,4,-2,3,-1,4,-2,4,-1,3,-2,4,-2,4,-1,3,-2,4,-1,4,-2,4,-1,3,-2,4,-1,4,-2,3,-1,4,-2,4,-1,4,-2,3,-1,4,-1,4,-2,3,-1,4,-2,4,-1,4,-2,3,-1,4,-1,4,-2,4,-1,3,-2,4,-1,4,-1,4,-2,3,-1,4,-1,4,-2,4,-1,3,-1,4,-2,4,-1,4,-1,4,-2,3,-1,4,-1,4,-2,4,-1,3,-1,4,-2,4,-1,4,-1,4,-1,3,-2,4,-1,4,-1,4,-1,4,-1,4,-2,3,-1,4,-1,4,-1,4,-2,4,-1,3,-1,4,-1,4,-1,4,-1,4,-2,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-2,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,3,-1,4,0,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,-1,4,0,4,-1,3,-1,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,-1,3,0,4,-1,4,-1,4,0,4,-1,4,-1,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,-1,3,0,4,-1,4,-1,4,0,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,-1,3,0,4,-1,4,0,4,0,4,-1,4,0,4,-1,4,0,4,-1,4,0,4,0,4,-1,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,-1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,1,4,0,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,0,4,1,4,0,4,1,3,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,1,4,0,4,1,4,0,4,1,4,1,4,0,4,1,3,1,4,0,4,1,4,1,4,0,4,1,4,1,4,0,4,1,4,1,4,1,4,0,4,1,4,1,4,0,4,1,3,1,4,1,4,1,4,0,4,1,4,1,4,1,4,0,4,1,4,1,4,1,4,1,4,1,3,0,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,0,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,2,4,1,3,1,4,1,4,1,4,1,4,1,4,1,3,2,4,1,4,1,4,1,4,1,4,1,3,2,4,1,4,1,4,1,4,2,4,1,3,1,4,1,4,1,4,2,4,1,3,1,4,1,4,2,4,1,4,1,3,2,4,1,4,1,4,2,3,1,4,1,4,2,4,1,4,1,3,2,4,1,4,1,4,2,3,1,4,1,4,2,4,1,3,2,4,1,4,1,4,2,3,1,4,2,4,1,4,2,3,1,4,1,4,2,3,1,4,2,4,1,4,2,3,1,4,2,4,1,3,2,4,1,4,2,4,1,3,2,4,2,4,1,3,2,4,1,4,2,3,1,4,2,4,2,3,1,4,2,4,1,3,2,4,2,4,1,3,2,4,1,4,2,3,2,4,1,4,2,3,2,4,1,4,2,3,2,4,1,3,2,4,2,4,2,3,1,4,2,4,2,3,1,4,2,3,2,4,2,4,1,3,2,4,2,3,2,4,2,4,1,3,2,4,2,3,2,4,1,3,2,4,2,4,2,3,2,4,2,3,1,4,2,3,2,4,2,3,2,4,2,4,2,3,2,4,1,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,3,2,4,3,3,2,4,2,3,2,3,2,4,2,3,2,4,3,3,2,3,2,4,2,3,2,3,2,4,3,3,2,4,2,3,2,3,2,4,2,3,3,3,2,4,2,3,2,3,3,4,2,3,2,3,2,3,3,4,2,3,2,3,2,4,3,3,2,3,2,3,3,4,2,3,2,3,2,4,3,3,2,3,2,3,3,4,2,3,2,3,3,3,2,4,3,3,2,3,2,3,3,3,2,4,2,3,3,3,2,3,3,4,2,3,2,3,3,3,2,3,3,3,2,4,3,3,2,3,3,3,2,3,2,3,3,4,2,3,3,3,2,3,3,3,2,3,3,3,2,4,3,3,2,3,3,3,2,3,3,3,2,3,3,3,3,3,2,3,3,4,2,3,3,3,2,3,3,3,3,3,2,3,3,3,2,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,3,3,3,2,2,3,3,3,3,3,3,3,3,3,2,3,3,2,3,3,3,3,3,3,3,3,2,3,3,2,3,3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,2,3,3,3,2,3,3,3,3,3,2,3,3,4,2,3,3,3,2,3,3,3,3,3,2,3,3,3,2,3,3,3,2,3,3,4,2,3,3,3,2,3,3,3,2,3,3,3,2,4,3,3,2,3,2,3,3,3,2,3,3,4,2,3,3,3,2,3,3,3,2,3,2,4,3,3,2,3,3,3,2,4,2,3,3,3,2,3,2,3,3,4,2,3,3,3,2,3,2,4,3,3,2,3,2,3,3,4,2,3,2,3,2,4,3,3,2,3,2,3,3,4,2,3,2,3,2,4,3,3,2,3,2,3,2,4,3,3,2,3,2,4,2,3,3,3,2,4,2,3,2,4,2,3,3,3,2,4,2,3,2,3,2,4,2,3,3,3,2,4,2,3,2,4,2,3,2,3,2,4,2,3,3,4,2,3,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,2,4,2,3,1,4,2,4,2,3,2,4,2,3,2,4,2,3,2,4,1,3,2,4,2,3,2,4,2,4,2,3,1,4,2,3,2,4,2,3,2,4,1,4,2,3,2,4,2,3,1,4,2,4,2,3,2,4,1,3,2,4,2,4,1,3,2,4,2,4,2,3,1,4,2,3,2,4,1,4,2,3,2,4,1,4,2,3,2,4,1,4,2,3,1,4,2,4,2,3,1,4,2,4,1,3,2,4,2,4,1,3,2,4,1,4,2,3,1,4,2,4,2,3,1,4,2,4,1,4,2,3,1,4,2,4,1,3,2,4,1,4,2,4,1,3,2,4,1,4,1,3,2,4,1,4,2,4,1,3,2,4,1,4,1,4,2,3,1,4,2,4,1,4,1,3,2,4,1,4,1,4,2,3,1,4,1,4,2,4,1,4,1,3,2,4,1,4,1,4,2,3,1,4,1,4,2,4,1,4,1,3,1,4,2,4,1,4,1,4,1,4,1,3,2,4,1,4,1,4,1,4,2,3,1,4,1,4,1,4,1,4,1,4,2,3,1,4,1,4,1,4,1,4,1,4,1,3,1,4,2,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,3,1,4,1,4,0,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,0,3,1,4,1,4,1,4,1,4,1,4,0,4,1,4,1,4,1,4,0,4,1,4,1,4,1,3,1,4,0,4,1,4,1,4,0,4,1,4,1,4,1,4,0,4,1,4,1,4,0,4,1,4,1,4,0,4,1,3,1,4,0,4,1,4,1,4,0,4,1,4,0,4,1,4,1,4,0,4,1,4,0,4,1,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,1,4,0,3,1,4,0,4,1,4,0,4,0,4,1,4,0,4,1,4,0,4,1,4,0,4,0,4,1,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,1,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0;PU0,-5200;PD0,5600;PU-2800,-2800;PD5600,0;PA;PU0,0;SP0;IN;
+39
plotters/hp7585b/test-print.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!-- 3 + HP 7585B test print. Sized for ISO A3 (420x297 mm). Convert with: 4 + vpype read test-print.svg write -d hp7585b -p a3 test-print.hpgl 5 + Then send: hp7585bplot test-print.hpgl 6 + 7 + Verifies: frame clipping, full-extent travel (diagonals), curve smoothness 8 + (circle), pen accuracy (crosshair), and color/pen swap if multiple pens 9 + are loaded — vpype layers map 1:1 to pen carousel slots. 10 + --> 11 + <svg xmlns="http://www.w3.org/2000/svg" 12 + width="420mm" height="297mm" 13 + viewBox="0 0 420 297" 14 + stroke-width="0.4" fill="none"> 15 + 16 + <!-- Layer 1 (pen 1): outer frame + diagonals at 10 mm margin --> 17 + <g inkscape:groupmode="layer" inkscape:label="1" stroke="black" 18 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"> 19 + <rect x="10" y="10" width="400" height="277" /> 20 + <line x1="10" y1="10" x2="410" y2="287" /> 21 + <line x1="410" y1="10" x2="10" y2="287" /> 22 + </g> 23 + 24 + <!-- Layer 2 (pen 2): centered circle + crosshair --> 25 + <g inkscape:groupmode="layer" inkscape:label="2" stroke="red" 26 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"> 27 + <circle cx="210" cy="148.5" r="60" /> 28 + <line x1="140" y1="148.5" x2="280" y2="148.5" /> 29 + <line x1="210" y1="78.5" x2="210" y2="218.5" /> 30 + </g> 31 + 32 + <!-- Layer 3 (pen 3): label, hand-stroked so it actually plots --> 33 + <g inkscape:groupmode="layer" inkscape:label="3" stroke="blue" 34 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 35 + font-family="monospace" font-size="6"> 36 + <text x="20" y="25">AC * HP 7585B * vpype HPGL test</text> 37 + <text x="20" y="282">a3 metric * pen carousel 1-3</text> 38 + </g> 39 + </svg>
+152
plotters/hp7585b/vpype.toml
··· 1 + # HP 7585B vpype HPGL device profile. 2 + # 3 + # Drop this into ~/.vpype.toml (or pass --config /path/to/this/file on every 4 + # invocation). vpype merges device tables from the user config into its 5 + # bundled set, so this adds `hp7585b` without touching anything shipped. 6 + # 7 + # Spec sources: 8 + # • plotter unit = 0.025 mm (40 units / mm) — HP 7585B service spec 9 + # • pen carousel = 8 — HP 7585B service spec 10 + # • plot-area margins = 5 / 5 / 5 / 29 mm — HP 7585B I&P manual, 11 + # Table 1-2 supplemental 12 + # • paper range = 203×267 mm → 927×1190 mm cut sheet — HP 7585B service spec 13 + # 14 + # Plot-area numbers below are derived from (paper_long − 34 mm, paper_short − 15 + # 10 mm) × 40 units/mm. They are nominal — verify against your actual sheet 16 + # load and adjust if the plotter clips. Origin is bottom-left of plottable 17 + # area (HP plotters are y-up natively), and the long edge is X. 18 + 19 + [device.hp7585b] 20 + name = "HP 7585B" 21 + plotter_unit_length = "0.025mm" 22 + pen_count = 8 23 + info = """ 24 + HP 7585B drafting plotter (1983, A0/E-size, 8-pen carousel). 25 + Default RS-232: 9600 8N1, hardware (Xon/Xoff) flow control. 26 + Set DIP switches on the rear panel before first use. Always run a confidence 27 + test (front panel) once after powering on. 28 + """ 29 + 30 + # ---------- ISO sizes ---------- 31 + 32 + [[device.hp7585b.paper]] 33 + name = "a4" 34 + aka_names = ["A4"] 35 + paper_size = ["297mm", "210mm"] 36 + x_range = [0, 10520] 37 + y_range = [0, 8000] 38 + y_axis_up = true 39 + origin_location = ["5mm", "205mm"] 40 + final_pu_params = "0,0" 41 + set_ps = 4 42 + info = "ISO A4. Long edge along X. Plotter must be in Metric mode." 43 + 44 + [[device.hp7585b.paper]] 45 + name = "a3" 46 + aka_names = ["A3"] 47 + paper_size = ["420mm", "297mm"] 48 + x_range = [0, 15440] 49 + y_range = [0, 11480] 50 + y_axis_up = true 51 + origin_location = ["5mm", "292mm"] 52 + final_pu_params = "0,0" 53 + set_ps = 4 54 + info = "ISO A3. Plotter must be in Metric mode." 55 + 56 + [[device.hp7585b.paper]] 57 + name = "a2" 58 + aka_names = ["A2"] 59 + paper_size = ["594mm", "420mm"] 60 + x_range = [0, 22400] 61 + y_range = [0, 16400] 62 + y_axis_up = true 63 + origin_location = ["5mm", "415mm"] 64 + final_pu_params = "0,0" 65 + set_ps = 4 66 + info = "ISO A2. Plotter must be in Metric mode." 67 + 68 + [[device.hp7585b.paper]] 69 + name = "a1" 70 + aka_names = ["A1"] 71 + paper_size = ["841mm", "594mm"] 72 + x_range = [0, 32280] 73 + y_range = [0, 23360] 74 + y_axis_up = true 75 + origin_location = ["5mm", "589mm"] 76 + final_pu_params = "0,0" 77 + set_ps = 4 78 + info = "ISO A1. Plotter must be in Metric mode." 79 + 80 + [[device.hp7585b.paper]] 81 + name = "a0" 82 + aka_names = ["A0"] 83 + paper_size = ["1189mm", "841mm"] 84 + x_range = [0, 46200] 85 + y_range = [0, 33240] 86 + y_axis_up = true 87 + origin_location = ["5mm", "836mm"] 88 + final_pu_params = "0,0" 89 + set_ps = 4 90 + info = "ISO A0 (full E-size capacity). Plotter must be in Metric mode." 91 + 92 + # ---------- ANSI / ARCH sizes ---------- 93 + 94 + [[device.hp7585b.paper]] 95 + name = "a" 96 + aka_names = ["ansi_a", "letter"] 97 + paper_size = ["11in", "8.5in"] 98 + x_range = [0, 9816] 99 + y_range = [0, 8236] 100 + y_axis_up = true 101 + origin_location = ["5mm", "210.9mm"] 102 + final_pu_params = "0,0" 103 + set_ps = 4 104 + info = "ANSI A / US Letter. Plotter must be in Imperial mode." 105 + 106 + [[device.hp7585b.paper]] 107 + name = "b" 108 + aka_names = ["ansi_b", "tabloid"] 109 + paper_size = ["17in", "11in"] 110 + x_range = [0, 15912] 111 + y_range = [0, 10776] 112 + y_axis_up = true 113 + origin_location = ["5mm", "274.4mm"] 114 + final_pu_params = "0,0" 115 + set_ps = 4 116 + info = "ANSI B / Tabloid. Plotter must be in Imperial mode." 117 + 118 + [[device.hp7585b.paper]] 119 + name = "c" 120 + aka_names = ["ansi_c"] 121 + paper_size = ["22in", "17in"] 122 + x_range = [0, 20992] 123 + y_range = [0, 16872] 124 + y_axis_up = true 125 + origin_location = ["5mm", "426.8mm"] 126 + final_pu_params = "0,0" 127 + set_ps = 4 128 + info = "ANSI C. Plotter must be in Imperial mode." 129 + 130 + [[device.hp7585b.paper]] 131 + name = "d" 132 + aka_names = ["ansi_d"] 133 + paper_size = ["34in", "22in"] 134 + x_range = [0, 33184] 135 + y_range = [0, 21952] 136 + y_axis_up = true 137 + origin_location = ["5mm", "553.8mm"] 138 + final_pu_params = "0,0" 139 + set_ps = 4 140 + info = "ANSI D. Plotter must be in Imperial mode." 141 + 142 + [[device.hp7585b.paper]] 143 + name = "e" 144 + aka_names = ["ansi_e"] 145 + paper_size = ["44in", "34in"] 146 + x_range = [0, 43344] 147 + y_range = [0, 34144] 148 + y_axis_up = true 149 + origin_location = ["5mm", "858.6mm"] 150 + final_pu_params = "0,0" 151 + set_ps = 4 152 + info = "ANSI E (full capacity). Plotter must be in Imperial mode."
+13
plotters/hp7585b/wordmark.hpgl
··· 1 + IN;DT; 2 + DI1,0; 3 + SP1; 4 + PA-15325,-4207;PD;PA15325,-4207;PA15325,4207;PA-15325,4207;PA-15325,-4207;PU; 5 + SP1; 6 + SI2.171,3.474; 7 + PA-1172,841; 8 + LBAESTHETIC COMPUTER 9 + SP1; 10 + SI0.544,0.762; 11 + PA-621,-1145; 12 + LBHP 7585B * FIRST PLOT * 2026-04-30 13 + PU;PA0,0;SP0;
+213
plotters/hp7585b/wordmark.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Generate a wordmark plot fitted to the live plotter limits. 4 + 5 + Queries OH;/OP; off the plotter, then composes: 6 + - frame at 92% of the P1/P2 box 7 + - big title (HP-GL LB), centered horizontally, set above mid-line 8 + - subtitle (smaller LB), centered horizontally, set below mid-line 9 + 10 + Output goes to stdout (or --out file) as HPGL, ready to feed to serial-tx.py. 11 + 12 + Usage: 13 + wordmark.py --port /dev/cu.usbserial-110 --title "AESTHETIC COMPUTER" \ 14 + --subtitle "HP 7585B · FIRST PLOT · 2026-04-30" --out plot.hpgl 15 + """ 16 + 17 + from __future__ import annotations 18 + 19 + import argparse 20 + import os 21 + import sys 22 + 23 + import serial 24 + 25 + XON, XOFF = 0x11, 0x13 26 + 27 + 28 + def _ask(ser: serial.Serial, cmd: bytes) -> str: 29 + import time 30 + 31 + ser.reset_input_buffer() 32 + ser.write(cmd) 33 + ser.flush() 34 + deadline = time.time() + 1.0 35 + buf = bytearray() 36 + while time.time() < deadline: 37 + chunk = ser.read(64) 38 + if chunk: 39 + for b in chunk: 40 + if b not in (XON, XOFF): 41 + buf.append(b) 42 + if b"\n" in buf or b"\r" in buf: 43 + break 44 + else: 45 + time.sleep(0.05) 46 + return buf.decode("ascii", errors="replace").strip() 47 + 48 + 49 + def _ints(s: str) -> list[int]: 50 + out = [] 51 + for tok in s.replace(",", " ").split(): 52 + try: 53 + out.append(int(tok)) 54 + except ValueError: 55 + pass 56 + return out 57 + 58 + 59 + def query(port: str, baud: int) -> tuple[tuple[int, int, int, int], tuple[int, int]]: 60 + ser = serial.Serial( 61 + port=port, 62 + baudrate=baud, 63 + bytesize=serial.EIGHTBITS, 64 + parity=serial.PARITY_NONE, 65 + stopbits=serial.STOPBITS_ONE, 66 + xonxoff=False, 67 + rtscts=True, 68 + dsrdtr=True, 69 + timeout=0.5, 70 + ) 71 + ser.setDTR(True) 72 + ser.setRTS(True) 73 + try: 74 + pp = _ints(_ask(ser, b"OP;")) 75 + of = _ints(_ask(ser, b"OF;")) 76 + finally: 77 + ser.close() 78 + if len(pp) != 4 or len(of) < 2: 79 + raise SystemExit("plotter did not respond to OP;/OF;") 80 + return (pp[0], pp[1], pp[2], pp[3]), (of[0], of[1]) 81 + 82 + 83 + def _label_width_mm(text: str, char_cm_w: float) -> float: 84 + # HP-GL default char spacing: cell width = 1.5 * char_width. 85 + return len(text) * char_cm_w * 1.5 * 10.0 86 + 87 + 88 + def fit_size(text: str, max_mm: float, target_mm: float) -> float: 89 + """Pick char width in cm so the label is at most max_mm wide and ideally 90 + target_mm. Return char width (cm).""" 91 + spacing = 1.5 92 + cw_cm = target_mm / (len(text) * spacing * 10.0) 93 + if _label_width_mm(text, cw_cm) > max_mm: 94 + cw_cm = max_mm / (len(text) * spacing * 10.0) 95 + return cw_cm 96 + 97 + 98 + def build( 99 + p1p2: tuple[int, int, int, int], 100 + factor: tuple[int, int], 101 + title: str, 102 + subtitle: str, 103 + pen_title: int = 1, 104 + pen_sub: int = 1, 105 + pen_frame: int = 1, 106 + ) -> bytes: 107 + x1, y1, x2, y2 = p1p2 108 + fx, fy = factor 109 + 110 + # 92% frame 111 + fx1 = int(x1 * 0.92) 112 + fy1 = int(y1 * 0.92) 113 + fx2 = int(x2 * 0.92) 114 + fy2 = int(y2 * 0.92) 115 + 116 + # available width for text in mm, leaving 5% inside frame 117 + avail_w_mm = (fx2 - fx1) / fx * 0.90 118 + avail_h_mm = (fy2 - fy1) / fy 119 + 120 + # title: ~80% of width, height ~ 35% of avail height 121 + title_cw_cm = fit_size(title, avail_w_mm, avail_w_mm * 0.85) 122 + title_ch_cm = title_cw_cm * 1.6 # taller than wide for presence 123 + 124 + # subtitle: smaller — height roughly 25-30% of title 125 + sub_cw_cm = fit_size(subtitle, avail_w_mm * 0.7, avail_w_mm * 0.45) 126 + sub_ch_cm = sub_cw_cm * 1.4 127 + 128 + title_w_units = int(_label_width_mm(title, title_cw_cm) * fx / 10) 129 + sub_w_units = int(_label_width_mm(subtitle, sub_cw_cm) * fx / 10) 130 + 131 + title_h_units = int(title_ch_cm * 10 * fy) 132 + sub_h_units = int(sub_ch_cm * 10 * fy) 133 + 134 + # vertical layout: title above midline, subtitle below 135 + gap = int(avail_h_mm * 0.05 * fy) 136 + title_y = gap // 2 137 + sub_y = -title_h_units // 2 - gap - sub_h_units 138 + 139 + # …actually simpler: title baseline above center, subtitle baseline below. 140 + title_baseline = int(avail_h_mm * 0.10 * fy) 141 + sub_baseline = -int(avail_h_mm * 0.10 * fy) - sub_h_units 142 + 143 + cmds: list[str] = [] 144 + cmds.append("IN;DT\x03;") # default terminator = ETX (0x03) 145 + cmds.append("DI1,0;") # horizontal labels 146 + 147 + # Frame 148 + cmds.append(f"SP{pen_frame};") 149 + cmds.append( 150 + f"PA{fx1},{fy1};PD;PA{fx2},{fy1};PA{fx2},{fy2};" 151 + f"PA{fx1},{fy2};PA{fx1},{fy1};PU;" 152 + ) 153 + 154 + # Title 155 + cmds.append(f"SP{pen_title};") 156 + cmds.append(f"SI{title_cw_cm:.3f},{title_ch_cm:.3f};") 157 + cmds.append(f"PA{-title_w_units // 2},{title_baseline};") 158 + cmds.append(f"LB{title}\x03") 159 + 160 + # Subtitle 161 + cmds.append(f"SP{pen_sub};") 162 + cmds.append(f"SI{sub_cw_cm:.3f},{sub_ch_cm:.3f};") 163 + cmds.append(f"PA{-sub_w_units // 2},{sub_baseline};") 164 + cmds.append(f"LB{subtitle}\x03") 165 + 166 + # park 167 + cmds.append("PU;PA0,0;SP0;") 168 + 169 + return ("\n".join(cmds) + "\n").encode("ascii") 170 + 171 + 172 + def main() -> int: 173 + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) 174 + p.add_argument( 175 + "--port", 176 + default=os.environ.get("HP7585B_TTY", ""), 177 + help="serial device (defaults to $HP7585B_TTY)", 178 + ) 179 + p.add_argument("--baud", type=int, default=9600) 180 + p.add_argument("--title", default="AESTHETIC COMPUTER") 181 + p.add_argument("--subtitle", default="HP 7585B") 182 + p.add_argument("--out", help="write HPGL here (default stdout)") 183 + p.add_argument("--pen-title", type=int, default=1) 184 + p.add_argument("--pen-sub", type=int, default=1) 185 + p.add_argument("--pen-frame", type=int, default=1) 186 + args = p.parse_args() 187 + if not args.port: 188 + p.error("no --port and HP7585B_TTY not set") 189 + 190 + p1p2, factor = query(args.port, args.baud) 191 + sys.stderr.write( 192 + f"plotter P1/P2 = {p1p2}, factor = {factor}\n" 193 + ) 194 + hpgl = build( 195 + p1p2, 196 + factor, 197 + title=args.title, 198 + subtitle=args.subtitle, 199 + pen_title=args.pen_title, 200 + pen_sub=args.pen_sub, 201 + pen_frame=args.pen_frame, 202 + ) 203 + if args.out: 204 + with open(args.out, "wb") as f: 205 + f.write(hpgl) 206 + sys.stderr.write(f"wrote {len(hpgl)} bytes to {args.out}\n") 207 + else: 208 + sys.stdout.buffer.write(hpgl) 209 + return 0 210 + 211 + 212 + if __name__ == "__main__": 213 + sys.exit(main())