first version of morning edition in python

This commit is contained in:
2026-04-03 22:57:19 -05:00
parent 8fbb905022
commit acd7b95dfc
7 changed files with 992 additions and 0 deletions

9
CLAUDE.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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()