first version of morning edition in python
This commit is contained in:
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project
|
||||
|
||||
**morningedition** — a morning edition printable newspaper generator.
|
||||
|
||||
This repository is in early/empty state. No build system, dependencies, or code structure has been established yet.
|
||||
56
diagnose.py
Normal file
56
diagnose.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Printer diagnostic — runs several test prints to identify character width
|
||||
and font settings on the Epson TM-T88V.
|
||||
"""
|
||||
import subprocess, sys, time
|
||||
|
||||
PRINTER = "receipt"
|
||||
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
|
||||
INIT = ESC + b'\x40' # ESC @ — initialize
|
||||
FONT_A = ESC + b'\x4d\x00' # Font A (12x24 dots) — expected 42 chars
|
||||
FONT_B = ESC + b'\x4d\x01' # Font B ( 9x17 dots) — expected 56 chars
|
||||
SIZE_NORM = GS + b'\x21\x00' # GS ! 0 — force 1x1 character size
|
||||
CUT = GS + b'\x56\x00' # full cut
|
||||
|
||||
RULER_42 = "123456789012345678901234567890123456789012"
|
||||
RULER_56 = "12345678901234567890123456789012345678901234567890123456"
|
||||
|
||||
def send(data: bytes):
|
||||
r = subprocess.run(["lpr", "-P", PRINTER, "-o", "raw"],
|
||||
input=data, capture_output=True)
|
||||
if r.returncode != 0:
|
||||
print("lpr error:", r.stderr.decode(), file=sys.stderr)
|
||||
|
||||
def test(label, extra_init=b"", font=FONT_A, ruler=RULER_42):
|
||||
body = (
|
||||
f"\n--- TEST: {label} ---\n"
|
||||
f"Font: {'A (expect 42)' if font == FONT_A else 'B (expect 56)'}\n"
|
||||
f"{ruler}\n"
|
||||
f"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^\n"
|
||||
f"The quick brown fox jumps over the lazy dog\n"
|
||||
f"\n"
|
||||
).encode("ascii")
|
||||
send(INIT + extra_init + font + body + b"\n\n" + CUT)
|
||||
print(f" Printed: {label}")
|
||||
time.sleep(3)
|
||||
|
||||
print("Running printer diagnostics -- watch the paper!\n")
|
||||
|
||||
print("Test 1: Font A, no GS ! override (baseline -- your current setup)")
|
||||
test("Font A baseline", extra_init=b"", font=FONT_A, ruler=RULER_42)
|
||||
|
||||
print("Test 2: Font A + GS ! 0x00 (force normal 1x1 character size)")
|
||||
test("Font A + GS!00", extra_init=SIZE_NORM, font=FONT_A, ruler=RULER_42)
|
||||
|
||||
print("Test 3: Font B (smaller -- expect 56 chars/line)")
|
||||
test("Font B baseline", extra_init=b"", font=FONT_B, ruler=RULER_56)
|
||||
|
||||
print("Test 4: Font B + GS ! 0x00")
|
||||
test("Font B + GS!00", extra_init=SIZE_NORM, font=FONT_B, ruler=RULER_56)
|
||||
|
||||
print("\nDone. Count where the ruler wraps on each strip.")
|
||||
print("Report which test gave 42+ chars and we'll lock that in.")
|
||||
105
diagnose2.py
Normal file
105
diagnose2.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Printer diagnostic v2 — tests print area width, character spacing,
|
||||
and direct USB device access on Epson TM-T88V.
|
||||
"""
|
||||
import subprocess, sys, time, glob
|
||||
|
||||
PRINTER = "receipt"
|
||||
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
|
||||
INIT = ESC + b'\x40'
|
||||
FONT_A = ESC + b'\x4d\x00'
|
||||
FONT_B = ESC + b'\x4d\x01'
|
||||
SIZE_NORM = GS + b'\x21\x00' # GS ! 0 — 1x1 character size
|
||||
SP_ZERO = ESC + b'\x20\x00' # ESC SP 0 — zero right-side char spacing
|
||||
# GS L nL nH — set left margin to 0
|
||||
MARGIN_L = GS + b'\x4c\x00\x00'
|
||||
# GS W nL nH — set print area width
|
||||
# 576 dots = full 80mm printable width (203dpi): 576 = 0x0240 -> nL=0x40 nH=0x02
|
||||
WIDTH_576 = GS + b'\x57\x40\x02'
|
||||
# 512 dots — conservative full width
|
||||
WIDTH_512 = GS + b'\x57\x00\x02'
|
||||
# 480 dots (what 42 chars x ~11.4 dots rounds to)
|
||||
WIDTH_480 = GS + b'\x57\xe0\x01'
|
||||
|
||||
CUT = GS + b'\x56\x00'
|
||||
|
||||
RULER_42 = "123456789|123456789|123456789|123456789|12"
|
||||
RULER_56 = "123456789|123456789|123456789|123456789|123456789|123456"
|
||||
|
||||
def send_lpr(data: bytes):
|
||||
r = subprocess.run(["lpr", "-P", PRINTER, "-o", "raw"],
|
||||
input=data, capture_output=True)
|
||||
if r.returncode != 0:
|
||||
print(" lpr error:", r.stderr.decode().strip(), file=sys.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
def send_usb(data: bytes):
|
||||
"""Try writing directly to the USB device, bypassing CUPS entirely."""
|
||||
devices = sorted(glob.glob("/dev/usb/lp*"))
|
||||
if not devices:
|
||||
print(" No /dev/usb/lp* devices found", file=sys.stderr)
|
||||
return False
|
||||
dev = devices[0]
|
||||
print(f" Writing directly to {dev}")
|
||||
try:
|
||||
with open(dev, "wb") as f:
|
||||
f.write(data)
|
||||
return True
|
||||
except PermissionError:
|
||||
print(f" Permission denied on {dev} -- try: sudo chmod a+rw {dev}", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" USB write error: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def strip(label, init_seq, font=FONT_A, ruler=RULER_42, via_usb=False):
|
||||
body = (
|
||||
f"\n[{label}]\n"
|
||||
f"{ruler}\n"
|
||||
f"The quick brown fox jumps over...\n"
|
||||
f"\n"
|
||||
).encode("ascii")
|
||||
data = init_seq + font + body + b"\n\n" + CUT
|
||||
ok = send_usb(data) if via_usb else send_lpr(data)
|
||||
status = "OK" if ok else "FAILED"
|
||||
print(f" {status}: {label}")
|
||||
time.sleep(3)
|
||||
|
||||
print("=== Diagnostic v2: print area & spacing tests ===\n")
|
||||
|
||||
print("Test 1: Baseline (same as v1 test 1) via lpr")
|
||||
strip("T1 Font-A baseline lpr",
|
||||
INIT + SIZE_NORM)
|
||||
|
||||
print("Test 2: ESC SP 0 (zero right-side character spacing)")
|
||||
strip("T2 Font-A + SP=0",
|
||||
INIT + SIZE_NORM + SP_ZERO)
|
||||
|
||||
print("Test 3: GS L 0 + GS W 576 (full printable area)")
|
||||
strip("T3 Font-A + margin=0 width=576",
|
||||
INIT + SIZE_NORM + SP_ZERO + MARGIN_L + WIDTH_576)
|
||||
|
||||
print("Test 4: GS W 512")
|
||||
strip("T4 Font-A + width=512",
|
||||
INIT + SIZE_NORM + SP_ZERO + MARGIN_L + WIDTH_512)
|
||||
|
||||
print("Test 5: Font B + full area reset")
|
||||
strip("T5 Font-B + SP=0 + width=576",
|
||||
INIT + SIZE_NORM + SP_ZERO + MARGIN_L + WIDTH_576,
|
||||
font=FONT_B, ruler=RULER_56)
|
||||
|
||||
print("\nTest 6: Direct USB write (bypasses CUPS entirely) -- Font A")
|
||||
strip("T6 Font-A direct USB",
|
||||
INIT + SIZE_NORM + SP_ZERO,
|
||||
via_usb=True)
|
||||
|
||||
print("\nDone.")
|
||||
print("Key question: does Test 6 (direct USB) give more chars than Test 1?")
|
||||
print("If yes -> CUPS is modifying the raw data even with -o raw.")
|
||||
print("If all same -> printer NV memory has a constrained print area.")
|
||||
print("Report what you see on each strip.")
|
||||
112
fixwidth.py
Normal file
112
fixwidth.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TM-T88V paper width fix tool.
|
||||
Writes directly to /dev/usb/lp0, bypassing CUPS entirely.
|
||||
|
||||
Step 1: Direct USB test print (safe, no NV writes)
|
||||
Step 2: NV paper-width reset to 80mm (writes to printer NV memory)
|
||||
"""
|
||||
import sys, time
|
||||
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
|
||||
INIT = ESC + b'\x40'
|
||||
FONT_A = ESC + b'\x4d\x00'
|
||||
FONT_B = ESC + b'\x4d\x01'
|
||||
SIZE_NORM = GS + b'\x21\x00'
|
||||
SP_ZERO = ESC + b'\x20\x00'
|
||||
# GS W 576 = full 80mm printable area (576 = 0x0240)
|
||||
WIDTH_576 = GS + b'\x57\x40\x02'
|
||||
# GS W 512
|
||||
WIDTH_512 = GS + b'\x57\x00\x02'
|
||||
CUT = GS + b'\x56\x00'
|
||||
|
||||
DEV = "/dev/usb/lp0"
|
||||
|
||||
RULER42 = "123456789|123456789|123456789|123456789|12"
|
||||
RULER56 = "123456789|123456789|123456789|123456789|123456789|123456"
|
||||
|
||||
def write_dev(data: bytes):
|
||||
with open(DEV, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
def test_print():
|
||||
"""Step 1 — safe diagnostic, no NV changes."""
|
||||
body = (
|
||||
"\n[DIRECT USB - Font A + GS W 576]\n"
|
||||
f"{RULER42}\n"
|
||||
"The quick brown fox jumps over the lazy dog\n"
|
||||
"\n"
|
||||
"[DIRECT USB - Font B + GS W 576]\n"
|
||||
f"{RULER56}\n"
|
||||
"The quick brown fox jumps over the lazy dog\n"
|
||||
"\n"
|
||||
).encode("ascii")
|
||||
|
||||
data = (INIT + SIZE_NORM + SP_ZERO + WIDTH_576 +
|
||||
FONT_A + body +
|
||||
b"\n\n" + CUT)
|
||||
write_dev(data)
|
||||
print("Test print sent directly via USB (no CUPS).")
|
||||
print("Count the ruler chars on each line and report back.")
|
||||
|
||||
# ── NV paper-width commands ───────────────────────────────────────────────────
|
||||
# GS ( E — Set customized value (persists in NV across power cycles)
|
||||
# Format: GS ( E pL pH fn [data...]
|
||||
# pL pH = little-endian length of (fn + data)
|
||||
#
|
||||
# TM-T88V firmware 30.xx:
|
||||
# fn=2 : Set paper type
|
||||
# data : 0x00 = 80mm/RP80, 0x01 = 58mm/RP58
|
||||
#
|
||||
# This writes to NV — takes effect after power cycle.
|
||||
def gs_e(fn: int, data: bytes) -> bytes:
|
||||
payload = bytes([fn]) + data
|
||||
pL = len(payload) & 0xFF
|
||||
pH = (len(payload) >> 8) & 0xFF
|
||||
return GS + b'\x28\x45' + bytes([pL, pH]) + payload
|
||||
|
||||
NV_80MM = gs_e(0x02, b'\x00') # fn=2, param=0x00 → 80mm paper
|
||||
NV_58MM = gs_e(0x02, b'\x01') # fn=2, param=0x01 → 58mm paper (for reference)
|
||||
|
||||
def nv_fix():
|
||||
"""Step 2 — write 80mm paper width to NV memory, then test."""
|
||||
print("Sending NV paper-width reset to 80mm...")
|
||||
# Send the NV command alone first, then reinit and test
|
||||
data = (
|
||||
INIT +
|
||||
NV_80MM + # set 80mm in NV
|
||||
INIT + # re-init so new setting takes effect now
|
||||
SIZE_NORM + SP_ZERO + WIDTH_576 + FONT_A +
|
||||
(
|
||||
"\n[NV SET: 80mm - if this is 42 chars wide, it worked]\n"
|
||||
f"{RULER42}\n"
|
||||
"The quick brown fox jumps over the lazy dog\n"
|
||||
"\n"
|
||||
).encode("ascii") +
|
||||
b"\n\n" + CUT
|
||||
)
|
||||
write_dev(data)
|
||||
print("NV write + test strip sent.")
|
||||
print("If the ruler shows 42 chars: power-cycle the printer to confirm it sticks.")
|
||||
print("If still 30 chars: the fn=2/0x00 command didn't apply; tell me and we'll try alternate byte sequences.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "test"
|
||||
try:
|
||||
if cmd == "test":
|
||||
test_print()
|
||||
elif cmd == "fix":
|
||||
nv_fix()
|
||||
elif cmd == "fix58":
|
||||
# Emergency restore to 58mm if needed
|
||||
write_dev(INIT + NV_58MM + INIT + b"\n[restored to 58mm]\n\n" + CUT)
|
||||
print("Restored to 58mm NV setting.")
|
||||
else:
|
||||
print("Usage: fixwidth.py [test|fix|fix58]")
|
||||
except PermissionError:
|
||||
print(f"Permission denied on {DEV}")
|
||||
print(f"Run: sudo chmod a+rw {DEV} then retry")
|
||||
except FileNotFoundError:
|
||||
print(f"{DEV} not found — run: sudo modprobe usblp")
|
||||
70
fixwidth2.py
Normal file
70
fixwidth2.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TM-T88V paper width NV fix — attempt 2.
|
||||
Tries multiple memory switch group/value combinations.
|
||||
Writes directly to /dev/usb/lp0.
|
||||
"""
|
||||
import sys, time
|
||||
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
|
||||
INIT = ESC + b'\x40'
|
||||
FONT_A = ESC + b'\x4d\x00'
|
||||
CUT = GS + b'\x56\x00'
|
||||
DEV = "/dev/usb/lp0"
|
||||
|
||||
RULER = "123456789|123456789|123456789|123456789|12"
|
||||
|
||||
def write_dev(data: bytes):
|
||||
with open(DEV, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
def gs_e(fn: int, data: bytes) -> bytes:
|
||||
"""Build GS ( E pL pH fn [data]"""
|
||||
payload = bytes([fn]) + data
|
||||
pL = len(payload) & 0xFF
|
||||
pH = (len(payload) >> 8) & 0xFF
|
||||
return GS + b'\x28\x45' + bytes([pL, pH]) + payload
|
||||
|
||||
def strip(label: str, nv_cmd: bytes):
|
||||
body = (
|
||||
f"\n[{label}]\n"
|
||||
f"{RULER}\n"
|
||||
).encode("ascii")
|
||||
write_dev(INIT + nv_cmd + INIT + FONT_A + body + b"\n\n" + CUT)
|
||||
time.sleep(4)
|
||||
|
||||
# ── Memory switch (fn=3): GS ( E 03 00 03 <group> <value> ──────────────────
|
||||
# group 1-8 control different settings; one of them holds paper width.
|
||||
# Trying both 0x00 (all bits clear = 80mm) and 0x80 (bit7 = 80mm candidate).
|
||||
MSW_TESTS = [
|
||||
# (label, group, value)
|
||||
("fn3 grp1 val=0x00", 3, 1, 0x00),
|
||||
("fn3 grp2 val=0x00", 3, 2, 0x00),
|
||||
("fn3 grp3 val=0x00", 3, 3, 0x00),
|
||||
("fn3 grp4 val=0x00", 3, 4, 0x00),
|
||||
("fn3 grp5 val=0x00", 3, 5, 0x00),
|
||||
("fn3 grp6 val=0x00", 3, 6, 0x00),
|
||||
("fn3 grp7 val=0x00", 3, 7, 0x00),
|
||||
("fn3 grp8 val=0x00", 3, 8, 0x00),
|
||||
# Same groups, value=0x80 in case the width bit is bit 7
|
||||
("fn3 grp6 val=0x80", 3, 6, 0x80),
|
||||
("fn3 grp7 val=0x80", 3, 7, 0x80),
|
||||
# Customized values (fn=5): GS ( E 03 00 05 <id> <value>
|
||||
("fn5 id=0x01 val=0", 5, 0x01, 0x00),
|
||||
("fn5 id=0x02 val=0", 5, 0x02, 0x00),
|
||||
("fn5 id=0x03 val=0", 5, 0x03, 0x00),
|
||||
("fn5 id=0x04 val=0", 5, 0x04, 0x00),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Sending NV test strips. Watch for the first one that prints 42 chars wide.")
|
||||
print("Each strip has its label printed on it. Power-cycle after the winning one.\n")
|
||||
for entry in MSW_TESTS:
|
||||
label, fn, param, val = entry
|
||||
nv = gs_e(fn, bytes([param, val]))
|
||||
print(f" Trying: {label}")
|
||||
strip(label, nv)
|
||||
print("\nDone. Tell me which label was the first to print 42 chars wide.")
|
||||
print("If none worked, all strips will be 30 chars and we need a different approach.")
|
||||
552
morningedition.py
Executable file
552
morningedition.py
Executable file
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Morning Edition - Daily receipt printer for Bridgeland, Cypress TX
|
||||
Prints to Epson TM-T88V via CUPS printer 'receipt'
|
||||
Designed for 80mm paper (42 chars wide)
|
||||
|
||||
Usage:
|
||||
morningedition.py -- print to receipt printer
|
||||
morningedition.py --preview -- dump to stdout for testing
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import datetime
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────
|
||||
PRINTER = "receipt"
|
||||
WIDTH = 30 # printer NV is locked to 58mm mode (30 chars); change to 42 after fixing printer to 80mm
|
||||
ZIPCODE = "77433"
|
||||
CITY_LINE = "Bridgeland, Cypress TX 77433"
|
||||
NAME = "Rich"
|
||||
|
||||
# ── ESC/POS commands ────────────────────────────────────────────────────────
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
|
||||
INIT = ESC + b'\x40' # Initialize printer
|
||||
FONT_A = ESC + b'\x4d\x00' # Font A 12x24 — 42 chars/line at 80mm
|
||||
BOLD_ON = ESC + b'\x45\x01'
|
||||
BOLD_OFF = ESC + b'\x45\x00'
|
||||
ALIGN_L = ESC + b'\x61\x00'
|
||||
ALIGN_C = ESC + b'\x61\x01'
|
||||
CUT = GS + b'\x56\x00' # Full cut
|
||||
|
||||
# ── Weather ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_weather():
|
||||
try:
|
||||
url = f"https://wttr.in/{ZIPCODE}?format=j1"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "morningedition/1.0"})
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
data = json.loads(resp.read().decode())
|
||||
|
||||
cc = data["current_condition"][0]
|
||||
day = data["weather"][0]
|
||||
ast = day.get("astronomy", [{}])[0]
|
||||
|
||||
return {
|
||||
"temp_f" : cc["temp_F"],
|
||||
"feels_f" : cc["FeelsLikeF"],
|
||||
"desc" : cc["weatherDesc"][0]["value"],
|
||||
"humidity" : cc["humidity"],
|
||||
"wind_mph" : cc["windspeedMiles"],
|
||||
"wind_dir" : cc["winddir16Point"],
|
||||
"visibility" : cc["visibility"],
|
||||
"uv" : cc["uvIndex"],
|
||||
"hi_f" : day["maxtempF"],
|
||||
"lo_f" : day["mintempF"],
|
||||
"sunrise" : ast.get("sunrise", "?"),
|
||||
"sunset" : ast.get("sunset", "?"),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[weather error] {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def weather_art(desc):
|
||||
d = desc.lower()
|
||||
if any(w in d for w in ["sunny", "clear"]):
|
||||
return [
|
||||
r" \ | / ",
|
||||
r" .---. ",
|
||||
r" --- ( * * * ) --- ",
|
||||
r" `---' ",
|
||||
r" / | \ ",
|
||||
]
|
||||
elif "partly" in d:
|
||||
return [
|
||||
r" \ .---. ",
|
||||
r" --- ( sun ) ",
|
||||
r" `---' )-. ",
|
||||
r" .-~~~{ cloudy } ",
|
||||
r" `-.__________.-' ",
|
||||
]
|
||||
elif any(w in d for w in ["overcast", "cloudy"]):
|
||||
return [
|
||||
r" .-~~~~-. ",
|
||||
r" .-{ }-. ",
|
||||
r"{ overcast } ",
|
||||
r" `-.________.-' ",
|
||||
r" ",
|
||||
]
|
||||
elif any(w in d for w in ["thunder", "storm"]):
|
||||
return [
|
||||
r" .-~~~~-. ",
|
||||
r" .-{ STORM }-. ",
|
||||
r" `-.________.-' ",
|
||||
r" /\ ",
|
||||
r" / \ ",
|
||||
]
|
||||
elif any(w in d for w in ["rain", "shower", "drizzle"]):
|
||||
return [
|
||||
r" .-~~~~-. ",
|
||||
r" .-{ rain }-. ",
|
||||
r" `-.________.-' ",
|
||||
r" ' ' ' ' ' ' ' ",
|
||||
r" ' ' ' ' ' ' ' ",
|
||||
]
|
||||
elif any(w in d for w in ["fog", "mist", "haze"]):
|
||||
return [
|
||||
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
|
||||
r" ~ foggy / hazy ~ ",
|
||||
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
|
||||
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
|
||||
r" ~ ~ ~ ~ ~ ~ ~ ~ ",
|
||||
]
|
||||
else:
|
||||
return [
|
||||
r" \ | / ",
|
||||
r" .---. ",
|
||||
r" --- ( ) --- ",
|
||||
r" `---' ",
|
||||
r" / | \ ",
|
||||
]
|
||||
|
||||
|
||||
def uv_label(uv):
|
||||
uv = int(uv)
|
||||
if uv <= 2: return f"{uv} - Low"
|
||||
if uv <= 5: return f"{uv} - Moderate"
|
||||
if uv <= 7: return f"{uv} - High"
|
||||
if uv <= 10: return f"{uv} - Very High"
|
||||
return f"{uv} - Extreme!"
|
||||
|
||||
|
||||
def get_weather_tip(weather):
|
||||
if not weather:
|
||||
return "Have a safe and productive day!"
|
||||
desc = weather["desc"].lower()
|
||||
temp = int(weather["temp_f"])
|
||||
if any(w in desc for w in ["thunder", "storm"]):
|
||||
return "Stay safe! Monitor weather alerts today."
|
||||
if any(w in desc for w in ["rain", "shower", "drizzle"]):
|
||||
return "Grab an umbrella -- Houston rain waits for no one!"
|
||||
if any(w in desc for w in ["fog", "mist"]):
|
||||
return "Allow extra drive time -- visibility may be low."
|
||||
if temp >= 95:
|
||||
return "Stay hydrated! Carry water everywhere today."
|
||||
if temp <= 45:
|
||||
return "Layer up -- don't let the cold catch you off guard."
|
||||
if int(weather.get("uv", 0)) >= 7:
|
||||
return "High UV today -- sunscreen before you head out!"
|
||||
if int(weather.get("humidity", 0)) >= 85:
|
||||
return "Steamy one today -- moisture is high out there."
|
||||
return "Make it a great day -- you've got this!"
|
||||
|
||||
|
||||
# ── News ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def fetch_rss_headlines(url, count=5):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "morningedition/1.0"})
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
tree = ET.fromstring(resp.read())
|
||||
items = tree.findall(".//item")[:count]
|
||||
headlines = []
|
||||
for item in items:
|
||||
t = item.find("title")
|
||||
if t is not None and t.text:
|
||||
# Strip CDATA, collapse whitespace, ASCII-ify
|
||||
text = t.text.strip()
|
||||
text = text.encode("ascii", errors="replace").decode("ascii")
|
||||
headlines.append(text)
|
||||
return headlines
|
||||
except Exception as e:
|
||||
print(f"[rss error {url}] {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
# ── Rotating daily content ───────────────────────────────────────────────────
|
||||
|
||||
QUOTES = [
|
||||
("The best time to plant a tree was 20 years ago. The second best time is now.",
|
||||
"Chinese Proverb"),
|
||||
("Every morning we are born again. What we do today matters most.",
|
||||
"Buddha"),
|
||||
("With the new day comes new strength and new thoughts.",
|
||||
"Eleanor Roosevelt"),
|
||||
("You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose.",
|
||||
"Dr. Seuss"),
|
||||
("Make each day your masterpiece.",
|
||||
"John Wooden"),
|
||||
("Do something today that your future self will thank you for.",
|
||||
"Sean Patrick Flanery"),
|
||||
("Start where you are. Use what you have. Do what you can.",
|
||||
"Arthur Ashe"),
|
||||
("Success is the sum of small efforts repeated day in and day out.",
|
||||
"Robert Collier"),
|
||||
("Believe you can and you're halfway there.",
|
||||
"Theodore Roosevelt"),
|
||||
("The secret of getting ahead is getting started.",
|
||||
"Mark Twain"),
|
||||
("What lies behind us and before us are tiny matters compared to what lies within us.",
|
||||
"Ralph Waldo Emerson"),
|
||||
("The only way to do great work is to love what you do.",
|
||||
"Steve Jobs"),
|
||||
("Life is 10% what happens to you and 90% how you react to it.",
|
||||
"Charles R. Swindoll"),
|
||||
("In the middle of every difficulty lies opportunity.",
|
||||
"Albert Einstein"),
|
||||
("It always seems impossible until it's done.",
|
||||
"Nelson Mandela"),
|
||||
("Don't watch the clock; do what it does. Keep going.",
|
||||
"Sam Levenson"),
|
||||
("Opportunities don't happen, you create them.",
|
||||
"Chris Grosser"),
|
||||
("Wake up determined. Go to bed satisfied.",
|
||||
"Unknown"),
|
||||
("Your imagination is your preview of life's coming attractions.",
|
||||
"Albert Einstein"),
|
||||
("The future belongs to those who believe in the beauty of their dreams.",
|
||||
"Eleanor Roosevelt"),
|
||||
("It does not matter how slowly you go as long as you do not stop.",
|
||||
"Confucius"),
|
||||
("Hardships often prepare ordinary people for an extraordinary destiny.",
|
||||
"C.S. Lewis"),
|
||||
]
|
||||
|
||||
JOKES = [
|
||||
("Why don't scientists trust atoms?",
|
||||
"Because they make up everything!"),
|
||||
("Why did the scarecrow win an award?",
|
||||
"He was outstanding in his field!"),
|
||||
("Why can't you give Elsa a balloon?",
|
||||
"Because she'll let it go!"),
|
||||
("What do you call fake spaghetti?",
|
||||
"An impasta!"),
|
||||
("Why did the bicycle fall over?",
|
||||
"Because it was two-tired!"),
|
||||
("What's a skeleton's least favorite room?",
|
||||
"The living room!"),
|
||||
("Why do cows wear bells?",
|
||||
"Because their horns don't work!"),
|
||||
("Why don't eggs tell jokes?",
|
||||
"They'd crack each other up!"),
|
||||
("What do you call a fish without eyes?",
|
||||
"A fsh!"),
|
||||
("Why did the math book look so sad?",
|
||||
"Because it had too many problems!"),
|
||||
("What do you get when you cross a snowman and a vampire?",
|
||||
"Frostbite!"),
|
||||
("Why couldn't the leopard play hide and seek?",
|
||||
"Because he was always spotted!"),
|
||||
("What do you call cheese that isn't yours?",
|
||||
"Nacho cheese!"),
|
||||
("Why did the golfer bring extra socks?",
|
||||
"In case he got a hole in one!"),
|
||||
("What has ears but cannot hear?",
|
||||
"A cornfield!"),
|
||||
("Why did the coffee file a police report?",
|
||||
"It got mugged!"),
|
||||
("What do you call a pile of cats?",
|
||||
"A meowtain!"),
|
||||
("How do trees access the internet?",
|
||||
"They log in!"),
|
||||
("Why was the belt arrested?",
|
||||
"It was holding up a pair of pants!"),
|
||||
("What do you call a sleeping dinosaur?",
|
||||
"A dino-snore!"),
|
||||
("Why did the stadium get hot after the game?",
|
||||
"All the fans left!"),
|
||||
("What do you call a bear with no teeth?",
|
||||
"A gummy bear!"),
|
||||
("Why did the tomato turn red?",
|
||||
"Because it saw the salad dressing!"),
|
||||
]
|
||||
|
||||
FACTS = [
|
||||
"A group of flamingos is called a 'flamboyance.'",
|
||||
"Honey never spoils. Edible honey was found in 3,000-year-old Egyptian tombs.",
|
||||
"Octopuses have THREE hearts and blue blood.",
|
||||
"Crows can recognize and remember human faces.",
|
||||
"Bananas are technically berries. Strawberries are not.",
|
||||
"A day on Venus is longer than a year on Venus.",
|
||||
"Wombats are the only animals that produce cube-shaped droppings.",
|
||||
"Sharks existed before trees. Sharks: 450M yrs. Trees: 350M yrs.",
|
||||
"Butterflies taste with their feet.",
|
||||
"The shortest war in history lasted 38-45 minutes (Anglo-Zanzibar War, 1896).",
|
||||
"Oxford University is older than the Aztec Empire.",
|
||||
"A snail can sleep for 3 years at a stretch.",
|
||||
"Cleopatra lived closer in time to the Moon landing than to the pyramids.",
|
||||
"Nintendo was founded in 1889 -- as a playing card company.",
|
||||
"The scent of rain on dry earth is called 'petrichor.'",
|
||||
"Humans share about 60% of their DNA with bananas.",
|
||||
"Sloths can hold their breath longer than dolphins -- up to 40 minutes.",
|
||||
"A bolt of lightning is 5x hotter than the surface of the Sun.",
|
||||
"The human nose can detect over 1 TRILLION different scents.",
|
||||
"Goats have rectangular pupils to give them near-360-degree vision.",
|
||||
"A group of owls is called a 'parliament.'",
|
||||
"There are more possible chess games than atoms in the observable universe.",
|
||||
"Dolphins sleep with one eye open.",
|
||||
"Polar bear fur is actually transparent, not white.",
|
||||
"Sea otters hold hands while sleeping so they don't drift apart.",
|
||||
"An ostrich's eye is bigger than its brain.",
|
||||
"Male seahorses carry and give birth to the young.",
|
||||
"Platypuses don't have stomachs -- food goes straight to the intestine.",
|
||||
"A group of crows is called a 'murder.'",
|
||||
"The fingerprints of a koala are virtually identical to human fingerprints.",
|
||||
"You can't hum while holding your nose closed.",
|
||||
"Cats have fewer toes on their back paws than their front paws.",
|
||||
]
|
||||
|
||||
HOUSTON_TIPS = [
|
||||
"IAH tip: Terminal C is usually least crowded for TSA.",
|
||||
"Beltway 8 (Sam Houston Tollway) runs 24/7 -- great I-10 bypass.",
|
||||
"Bridgeland: Lakeland Activity Center open 5am-10pm weekdays.",
|
||||
"H-E-B on Fry Rd is restocked overnight -- mornings = freshest produce.",
|
||||
"Houston weather reminder: if you don't like it, wait 20 minutes.",
|
||||
"Buc-ee's on I-10 in Katy -- best beaver nuggets in Texas. Just saying.",
|
||||
"Bluebonnet season peaks late March -- drive FM 1093 for the show.",
|
||||
"Houston Medical Center is the largest in the world. Right in our backyard.",
|
||||
"Buffalo Bayou Park has great sunrise walks if you're up early.",
|
||||
"Hike-and-bike trails around Bridgeland Lake -- 60+ miles of paths.",
|
||||
"Discovery Green downtown hosts free outdoor events most weekends.",
|
||||
"NASA Johnson Space Center is 45 min southeast -- worth a visit!",
|
||||
"Hermann Park's McGovern Lake paddle boats open at 10am weekends.",
|
||||
]
|
||||
|
||||
ON_THIS_DAY = {
|
||||
# month-day: (year, event)
|
||||
"04-03": (1973, "First handheld cell phone call made by Martin Cooper."),
|
||||
"07-04": (1776, "United States Declaration of Independence adopted."),
|
||||
"07-20": (1969, "Apollo 11 lands on the Moon."),
|
||||
"12-17": (1903, "Wright Brothers make first successful airplane flight."),
|
||||
"01-01": (2000, "Y2K passes without incident -- the world breathes a sigh of relief."),
|
||||
"02-14": (1876, "Alexander Graham Bell applies for the telephone patent."),
|
||||
"03-14": (1879, "Albert Einstein is born in Ulm, Germany."),
|
||||
"10-31": (1517, "Martin Luther posts 95 Theses, sparking the Reformation."),
|
||||
"11-22": (1963, "JFK assassinated in Dallas, TX."),
|
||||
"05-05": (1961, "Alan Shepard becomes first American in space."),
|
||||
"06-06": (1944, "D-Day: Allied forces land at Normandy, France."),
|
||||
"08-06": (1945, "Atomic bomb dropped on Hiroshima, Japan."),
|
||||
"09-11": (2001, "Terrorist attacks on the World Trade Center, Pentagon."),
|
||||
"11-09": (1989, "Berlin Wall falls."),
|
||||
"12-25": (0000, "Merry Christmas! Go spend time with family!"),
|
||||
}
|
||||
|
||||
|
||||
# ── Formatting helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def ctr(text, w=WIDTH):
|
||||
return text.center(w)[:w]
|
||||
|
||||
def trunc(text, w=WIDTH):
|
||||
"""Truncate to fit on one line, no wrapping."""
|
||||
text = text.replace("\n", " ")
|
||||
if len(text) > w:
|
||||
return text[:w-3] + "..."
|
||||
return text
|
||||
|
||||
def wrap(text, w=WIDTH):
|
||||
return textwrap.wrap(text.replace("\n", " "), w)
|
||||
|
||||
def section(title):
|
||||
return [
|
||||
"-" * WIDTH,
|
||||
ctr(f"[ {title} ]"),
|
||||
"-" * WIDTH,
|
||||
]
|
||||
|
||||
def bullet_line(text, w=WIDTH, prefix="* "):
|
||||
"""Single-line bullet — truncate to fit, no wrapping."""
|
||||
return trunc(prefix + text, w)
|
||||
|
||||
|
||||
# ── Receipt builder ──────────────────────────────────────────────────────────
|
||||
|
||||
def build_receipt(now):
|
||||
L = []
|
||||
|
||||
doy = now.timetuple().tm_yday
|
||||
wday = now.weekday() # 0=Mon .. 6=Sun
|
||||
is_weekend = wday >= 5
|
||||
month_day = now.strftime("%m-%d")
|
||||
|
||||
# ── Masthead ──────────────────────────────────────────────────────────
|
||||
L += ["=" * WIDTH]
|
||||
L += [ctr("* * * MORNING EDITION * * *")]
|
||||
L += ["=" * WIDTH]
|
||||
L += [ctr(" __ __ ___ ____ _ _")]
|
||||
L += [ctr("| \\/ || __|| _ \\| \\| |")]
|
||||
L += [ctr("| |\\/| || _| | |_) | ' |")]
|
||||
L += [ctr("|_| |_||___||____/|_|\\_|")]
|
||||
L += ["=" * WIDTH]
|
||||
L += [ctr(f"Good morning, {NAME}!")]
|
||||
L += ["=" * WIDTH]
|
||||
L += [""]
|
||||
|
||||
# ── Date / Location ───────────────────────────────────────────────────
|
||||
L += [ctr(CITY_LINE)]
|
||||
L += [ctr(now.strftime("%A, %B %d, %Y"))]
|
||||
L += [ctr(now.strftime("%-I:%M %p"))]
|
||||
L += [ctr(f"Day {doy} of {now.year}")]
|
||||
if is_weekend:
|
||||
L += [""]
|
||||
L += [ctr("* * * WEEKEND EDITION * * *")]
|
||||
L += [""]
|
||||
|
||||
# ── On This Day ───────────────────────────────────────────────────────
|
||||
if month_day in ON_THIS_DAY:
|
||||
yr, evt = ON_THIS_DAY[month_day]
|
||||
L += section("ON THIS DAY")
|
||||
L += [""]
|
||||
if yr:
|
||||
L += [ctr(f"In {yr}:")]
|
||||
for line in wrap(evt):
|
||||
L += [line]
|
||||
L += [""]
|
||||
|
||||
# ── Weather ───────────────────────────────────────────────────────────
|
||||
L += section("WEATHER - BRIDGELAND")
|
||||
L += [""]
|
||||
|
||||
weather = get_weather()
|
||||
if weather:
|
||||
art = weather_art(weather["desc"])
|
||||
for a in art:
|
||||
L += [ctr(a.strip())]
|
||||
L += [""]
|
||||
L += [ctr(weather["desc"].upper())]
|
||||
L += [""]
|
||||
L += [ctr(f"Now: {weather['temp_f']}F Feels: {weather['feels_f']}F")]
|
||||
L += [ctr(f"Today: Hi {weather['hi_f']}F / Lo {weather['lo_f']}F")]
|
||||
L += [ctr(f"Humidity:{weather['humidity']}% UV: {uv_label(weather['uv'])}")]
|
||||
L += [ctr(f"Wind: {weather['wind_mph']} mph {weather['wind_dir']}")]
|
||||
L += [ctr(f"Vis: {weather['visibility']} mi")]
|
||||
L += [ctr(f"Rise: {weather['sunrise']} Set: {weather['sunset']}")]
|
||||
L += [""]
|
||||
tip = get_weather_tip(weather)
|
||||
for line in wrap(f">> {tip} <<"):
|
||||
L += [ctr(line)]
|
||||
else:
|
||||
L += [ctr("Weather unavailable")]
|
||||
L += [ctr("Check weather.gov for updates")]
|
||||
|
||||
L += [""]
|
||||
|
||||
# ── Houston News ──────────────────────────────────────────────────────
|
||||
L += section("HOUSTON NEWS - ABC13")
|
||||
L += [""]
|
||||
hou_headlines = fetch_rss_headlines("https://abc13.com/feed/", 5)
|
||||
if hou_headlines:
|
||||
for h in hou_headlines:
|
||||
L += [bullet_line(h)]
|
||||
else:
|
||||
L += [" (Unable to fetch headlines)"]
|
||||
L += [""]
|
||||
|
||||
# ── World News ────────────────────────────────────────────────────────
|
||||
L += section("WORLD NEWS - BBC")
|
||||
L += [""]
|
||||
bbc_headlines = fetch_rss_headlines("https://feeds.bbci.co.uk/news/rss.xml", 5)
|
||||
if bbc_headlines:
|
||||
for h in bbc_headlines:
|
||||
L += [bullet_line(h)]
|
||||
else:
|
||||
L += [" (Unable to fetch headlines)"]
|
||||
L += [""]
|
||||
|
||||
# ── Daily Quote ───────────────────────────────────────────────────────
|
||||
L += section("THOUGHT FOR THE DAY")
|
||||
L += [""]
|
||||
q_text, q_author = QUOTES[doy % len(QUOTES)]
|
||||
for line in wrap(f'"{q_text}"'):
|
||||
L += [line]
|
||||
L += [f" -- {q_author}"]
|
||||
L += [""]
|
||||
|
||||
# ── Joke ──────────────────────────────────────────────────────────────
|
||||
L += section("MORNING CHUCKLE")
|
||||
L += [""]
|
||||
setup, punchline = JOKES[doy % len(JOKES)]
|
||||
for line in wrap(f"Q: {setup}"):
|
||||
L += [line]
|
||||
L += [""]
|
||||
for line in wrap(f"A: {punchline}"):
|
||||
L += [line]
|
||||
L += [""]
|
||||
|
||||
# ── Fun Fact ──────────────────────────────────────────────────────────
|
||||
L += section("DID YOU KNOW?")
|
||||
L += [""]
|
||||
fact = FACTS[doy % len(FACTS)]
|
||||
for line in wrap(fact):
|
||||
L += [line]
|
||||
L += [""]
|
||||
|
||||
# ── Local Houston Tip ─────────────────────────────────────────────────
|
||||
L += section("LOCAL TIP")
|
||||
L += [""]
|
||||
for line in wrap(HOUSTON_TIPS[doy % len(HOUSTON_TIPS)]):
|
||||
L += [line]
|
||||
L += [""]
|
||||
|
||||
# ── Footer ────────────────────────────────────────────────────────────
|
||||
L += ["=" * WIDTH]
|
||||
if is_weekend:
|
||||
L += [ctr("Enjoy your weekend, Rich!")]
|
||||
else:
|
||||
L += [ctr("Make today count!")]
|
||||
L += [ctr("Bridgeland Strong!")]
|
||||
L += ["=" * WIDTH]
|
||||
L += [ctr(now.strftime("Printed %-I:%M %p"))]
|
||||
L += [""]
|
||||
L += [""]
|
||||
L += [""] # feed before cut
|
||||
|
||||
text = "\n".join(L)
|
||||
|
||||
# Wrap text in ESC/POS framing: init → Font A → content → feed → cut
|
||||
body = text.encode("ascii", errors="replace")
|
||||
return INIT + FONT_A + ALIGN_L + body + b"\n\n\n" + CUT
|
||||
|
||||
|
||||
# ── Print ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def print_receipt(data: bytes):
|
||||
cmd = ["lpr", "-P", PRINTER, "-o", "raw"]
|
||||
result = subprocess.run(cmd, input=data, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
print(f"lpr error: {result.stderr.decode()}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
now = datetime.datetime.now()
|
||||
data = build_receipt(now)
|
||||
|
||||
if "--preview" in sys.argv:
|
||||
# Strip ESC/POS bytes for preview — just print the text portion
|
||||
sys.stdout.buffer.write(data[len(INIT + FONT_A + ALIGN_L):-len(b"\n\n\n" + CUT)])
|
||||
sys.stdout.buffer.write(b"\n")
|
||||
else:
|
||||
print_receipt(data)
|
||||
print(f"Morning Edition printed at {now.strftime('%I:%M %p')} -- have a great day, {NAME}!")
|
||||
88
readmsw.py
Normal file
88
readmsw.py
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Read TM-T88V memory switch values back from the printer via USB.
|
||||
GS ( E fn=4 asks the printer to transmit its current memory switch settings.
|
||||
"""
|
||||
import sys, time, select
|
||||
|
||||
ESC = b'\x1b'
|
||||
GS = b'\x1d'
|
||||
DEV = "/dev/usb/lp0"
|
||||
|
||||
INIT = ESC + b'\x40'
|
||||
CUT = GS + b'\x56\x00'
|
||||
|
||||
def gs_e(fn: int, data: bytes = b'') -> bytes:
|
||||
payload = bytes([fn]) + data
|
||||
pL = len(payload) & 0xFF
|
||||
pH = (len(payload) >> 8) & 0xFF
|
||||
return GS + b'\x28\x45' + bytes([pL, pH]) + payload
|
||||
|
||||
# fn=4: Transmit memory switch (all groups)
|
||||
QUERY_MSW = gs_e(4, b'')
|
||||
# fn=6: Transmit customized values
|
||||
QUERY_CUSTOM = gs_e(6, b'')
|
||||
|
||||
try:
|
||||
f = open(DEV, "r+b", buffering=0)
|
||||
except PermissionError:
|
||||
print(f"Permission denied on {DEV} -- run: sudo chmod a+rw {DEV}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Querying memory switches (fn=4)...")
|
||||
f.write(INIT + QUERY_MSW)
|
||||
f.flush()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Try to read response
|
||||
response = b''
|
||||
deadline = time.time() + 2.0
|
||||
while time.time() < deadline:
|
||||
r, _, _ = select.select([f], [], [], 0.2)
|
||||
if r:
|
||||
chunk = f.read(64)
|
||||
if chunk:
|
||||
response += chunk
|
||||
else:
|
||||
if response:
|
||||
break
|
||||
|
||||
if response:
|
||||
print(f"Memory switch response ({len(response)} bytes):")
|
||||
print(" hex:", response.hex())
|
||||
print(" dec:", list(response))
|
||||
print()
|
||||
# GS ( E fn=4 response format: header + 8 bytes (one per MSW group)
|
||||
# Find the data bytes after any header
|
||||
for i, b in enumerate(response):
|
||||
print(f" byte[{i}] = 0x{b:02x} ({b:08b}b)")
|
||||
else:
|
||||
print("No response to fn=4 query.")
|
||||
|
||||
print()
|
||||
print("Querying customized values (fn=6)...")
|
||||
f.write(QUERY_CUSTOM)
|
||||
f.flush()
|
||||
time.sleep(0.5)
|
||||
|
||||
response2 = b''
|
||||
deadline = time.time() + 2.0
|
||||
while time.time() < deadline:
|
||||
r, _, _ = select.select([f], [], [], 0.2)
|
||||
if r:
|
||||
chunk = f.read(64)
|
||||
if chunk:
|
||||
response2 += chunk
|
||||
else:
|
||||
if response2:
|
||||
break
|
||||
|
||||
if response2:
|
||||
print(f"Customized value response ({len(response2)} bytes):")
|
||||
print(" hex:", response2.hex())
|
||||
for i, b in enumerate(response2):
|
||||
print(f" byte[{i}] = 0x{b:02x} ({b:08b}b)")
|
||||
else:
|
||||
print("No response to fn=6 query.")
|
||||
|
||||
f.close()
|
||||
Reference in New Issue
Block a user