Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 257 lines 7.8 kB view raw
1#!/usr/bin/env python3 2""" 3Probe the HP 7585B for its actual limits and emit a usable test pattern. 4 5The plotter knows what sheet is loaded and what hard-clip limits apply far 6better 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 17Usage: 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 23from __future__ import annotations 24 25import argparse 26import os 27import sys 28import time 29 30import serial 31 32XON, XOFF = 0x11, 0x13 33 34 35def _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 55def _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 65def _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 82def 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 124def 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 151def 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 190def 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]] 202name = "{name}" 203paper_size = ["{paper_x_mm:.2f}mm", "{paper_y_mm:.2f}mm"] 204x_range = [{x1}, {x2}] 205y_range = [{y1}, {y2}] 206y_axis_up = true 207origin_location = ["{paper_x_mm / 2:.2f}mm", "{paper_y_mm / 2:.2f}mm"] 208final_pu_params = "0,0" 209info = "auto-generated from plotter OH; query" 210""" 211 212 213def 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 256if __name__ == "__main__": 257 sys.exit(main())