Compare commits
No commits in common. "4575584b9d670dc75b5649589284d81955a1e2a3" and "da8ab4e448172d93bc75734f9abd540dbe3dca37" have entirely different histories.
4575584b9d
...
da8ab4e448
13
README.md
13
README.md
@ -8,19 +8,6 @@ Simple homework printer using a thermal printer. Practice makes perfect!
|
|||||||
pip install python-escpos[all] --user
|
pip install python-escpos[all] --user
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chess module
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install chess
|
|
||||||
```
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
|
|
||||||
Consult python-escpos docs:
|
|
||||||
- [Usage](https://python-escpos.readthedocs.io/en/latest/user/usage.html)
|
|
||||||
- [Available profiles](https://python-escpos.readthedocs.io/en/latest/printer_profiles/available-profiles.html)
|
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@ -4,9 +4,6 @@ class Job:
|
|||||||
def get_name(self):
|
def get_name(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def print_body(self, p):
|
def print_body(self, p):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import random
|
import random
|
||||||
try:
|
|
||||||
import chess
|
|
||||||
except ImportError:
|
|
||||||
chess = None
|
|
||||||
from .base import Job
|
from .base import Job
|
||||||
|
|
||||||
class ChessPuzzleJob(Job):
|
class ChessPuzzleJob(Job):
|
||||||
@ -10,12 +6,32 @@ class ChessPuzzleJob(Job):
|
|||||||
return "SACHOVE ULOHY"
|
return "SACHOVE ULOHY"
|
||||||
|
|
||||||
def print_body(self, p):
|
def print_body(self, p):
|
||||||
if chess is None:
|
# (FEN, Instruction, Solution)
|
||||||
p.text("CHYBA: Neni nainstalovana knihovna 'chess'.\n")
|
puzzles = [
|
||||||
p.text("Spust: pip install chess\n\n")
|
("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4", "Bily na tahu. Mat 1. tahem.", "Qxf7# (Scholar's Mate)"),
|
||||||
return
|
("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR b KQkq - 1 3", "Cerny na tahu. Mat 1. tahem.", "Qh4# (Fool's Mate)"),
|
||||||
|
("6k1/5ppp/8/8/8/8/5PPP/4R1K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Re8# (Back Rank Mate)"),
|
||||||
|
("3r2k1/p4ppp/1p6/8/8/8/P4PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rxd8#"),
|
||||||
|
("6rk/5p1p/3N4/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Nf7# (Smothered Mate)"),
|
||||||
|
("8/4N1pk/8/7R/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh5# (Anastasia's Mate)"),
|
||||||
|
("7k/R7/5N2/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh7# (Arabian Mate)"),
|
||||||
|
("2kr4/1pp5/B7/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Ba6# (Boden's Mate)"),
|
||||||
|
("7k/6pp/6P1/7Q/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qh7#"),
|
||||||
|
("6k1/5p1p/5PpQ/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qg7# (Lolli's Mate)"),
|
||||||
|
("3k4/5ppp/8/6B1/8/8/5PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rd8# (Opera Mate)"),
|
||||||
|
("6k1/5P2/8/8/8/8/5PPP/6KR w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh8# (Anderssen's Mate)"),
|
||||||
|
("7k/5ppp/8/7Q/8/2B5/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qxh7# (Damiano's Bishop Mate)"),
|
||||||
|
("4k3/8/4K3/8/8/8/8/5Q2 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qf7# (Dovetail Mate)"),
|
||||||
|
("2rkr3/8/2Q5/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qd7# (Swallow's Tail Mate)"),
|
||||||
|
("3rkr2/8/4Q3/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qe6# (Epaulette Mate)"),
|
||||||
|
("7k/6p1/8/8/2B5/8/5PPP/R5K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh1# (Greco's Mate)"),
|
||||||
|
("6k1/5pP1/5N2/8/8/8/5PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rd8# (Hook Mate)"),
|
||||||
|
("8/8/8/8/8/5K2/6Q1/7k w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qg2#"),
|
||||||
|
("k7/1R6/8/8/8/8/8/R5K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Ra8# (Ladder Mate)")
|
||||||
|
]
|
||||||
|
|
||||||
fen, instruction, solution = self.generate_puzzle()
|
puzzle = random.choice(puzzles)
|
||||||
|
fen, instruction, solution = puzzle
|
||||||
|
|
||||||
p.text(f"{instruction}\n\n")
|
p.text(f"{instruction}\n\n")
|
||||||
self.print_board(p, fen)
|
self.print_board(p, fen)
|
||||||
@ -28,37 +44,6 @@ class ChessPuzzleJob(Job):
|
|||||||
p.text("Reseni (naskenuj):\n")
|
p.text("Reseni (naskenuj):\n")
|
||||||
p.qr(solution, size=6, native=True)
|
p.qr(solution, size=6, native=True)
|
||||||
|
|
||||||
def generate_puzzle(self):
|
|
||||||
"""
|
|
||||||
Simulates a random game until a 'Mate in 1' situation arises.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
board = chess.Board()
|
|
||||||
# Play up to 120 ply (60 moves)
|
|
||||||
for _ in range(120):
|
|
||||||
if board.is_game_over():
|
|
||||||
break
|
|
||||||
|
|
||||||
legal_moves = list(board.legal_moves)
|
|
||||||
random.shuffle(legal_moves)
|
|
||||||
|
|
||||||
# Check if any move leads to immediate checkmate
|
|
||||||
for move in legal_moves:
|
|
||||||
board.push(move)
|
|
||||||
if board.is_checkmate():
|
|
||||||
# Found a puzzle!
|
|
||||||
board.pop() # Revert to state before mate
|
|
||||||
solution = board.san(move)
|
|
||||||
turn = "Bily" if board.turn == chess.WHITE else "Cerny"
|
|
||||||
return board.fen(), f"{turn} na tahu. Mat jednim tahem.", solution
|
|
||||||
board.pop()
|
|
||||||
|
|
||||||
# No mate found, play a random move to progress the game
|
|
||||||
if legal_moves:
|
|
||||||
board.push(random.choice(legal_moves))
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
def print_board(self, p, fen):
|
def print_board(self, p, fen):
|
||||||
board_str = fen.split(' ')[0]
|
board_str = fen.split(' ')[0]
|
||||||
rows = board_str.split('/')
|
rows = board_str.split('/')
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
CZECH_WORDS = [
|
|
||||||
"AUTO", "BABIČKA", "BANÁN", "BARVA", "BÁSNIČKA", "BATOH", "BAVLNA", "BAZÉN",
|
|
||||||
"BEDNA", "BERAN", "BLÁTO", "BLESK", "BLOUDIT", "BOBŘÍK", "BODLÁK", "BOTY",
|
|
||||||
"BOUDA", "BOUŘKA", "BRÁNA", "BRATR", "BROUK", "BRUSLE", "BUBEN", "BUNDA",
|
|
||||||
"CESTA", "CIHLA", "CIRKUS", "CITRON", "CUKR", "ČEPICE", "ČERVENÁ", "ČESNEK",
|
|
||||||
"ČÍSLO", "ČLOVĚK", "DÁREK", "DATEL", "DCERA", "DĚDEČEK", "DEŠTNÍK", "DIVADLO",
|
|
||||||
"DOKTOR", "DOMOV", "DOPIS", "DORT", "DRAK", "DUBEN", "DŮM", "DVEŘE",
|
|
||||||
"DŽBÁN", "FARA", "FIALA", "FILM", "FLÉTNA", "FOTBAL", "GUMA", "HAVRAN",
|
|
||||||
"HLAVA", "HLÍNA", "HNÍZDO", "HODINY", "HOLKA", "HOLUB", "HORA", "HOUSKA",
|
|
||||||
"HRAD", "HRÁBĚ", "HRANA", "HRAČKA", "HRNEK", "HRUŠKA", "HUDBA", "CHATA",
|
|
||||||
"CHLÉB", "CHLAP", "CHYBA", "JABLKO", "JAHODA", "JARO", "JAZYK", "JEHLA",
|
|
||||||
"JEZERO", "JÍDLO", "JITRO", "KABÁT", "KAČNA", "KAKAO", "KÁMEN", "KAPSÁŘ",
|
|
||||||
"KARTA", "KÁVA", "KILO", "KLADIVO", "KLÍČ", "KLOBOUK", "KLUK", "KNIHA",
|
|
||||||
"KOČKA", "KOLO", "KOMÁR", "KONEC", "KOPEC", "KOŘEN", "KOST", "KOŠILE",
|
|
||||||
"KOZA", "KRÁL", "KRÁVA", "KRESLIT", "KREV", "KRK", "KRUH", "KUCHYNĚ",
|
|
||||||
"KVĚTINA", "LÁSKA", "LAVICE", "LEDEN", "LES", "LÉTO", "LEV", "LÍSTEK",
|
|
||||||
"LOĎ", "LOUKA", "LŽÍCE", "MÁMA", "MAPA", "MASO", "MĚSTO", "METR",
|
|
||||||
"MLÉKO", "MLÝN", "MODRÁ", "MOŘE", "MOST", "MOTÝL", "MRAK", "MRKEV",
|
|
||||||
"MUŽ", "MYŠ", "NÁDRAŽÍ", "NOHA", "NOC", "NOS", "NOVINY", "NŮŽKY",
|
|
||||||
"OBCHOD", "OBĚD", "OBRAZ", "OCAS", "OKNO", "OKO", "OLEJ", "OPICE",
|
|
||||||
"OREL", "OVOCE", "PÁTEK", "PES", "PÍSEŇ", "PIVO", "PLÁČ", "PLOT",
|
|
||||||
"POLE", "POLÉVKA", "POMOC", "POSTEL", "POTOK", "PRÁCE", "PRAHA", "PRASE",
|
|
||||||
"PRST", "PTÁK", "RÁDIO", "RADOST", "RAKETA", "RÁNO", "RUKA", "RYBA",
|
|
||||||
"ŘEKA", "ŘEPA", "SÁŇKY", "SEDLO", "SESTRA", "SEŠIT", "SKLO", "SLON",
|
|
||||||
"SLUNCE", "SNÍH", "SOVA", "SRDCE", "STROM", "STŮL", "SVĚTLO", "ŠKOLA",
|
|
||||||
"ŠATY", "ŠNEK", "TÁTA", "TELEVIZE", "TETA", "TMA", "TRAVÁ", "TRH",
|
|
||||||
"TUŽKA", "UCHO", "ULICE", "ÚNOR", "ÚSTA", "VAJÍČKO", "VÁNOCE", "VČELA",
|
|
||||||
"VEČEŘE", "VEJCE", "VELRYBA", "VESNICE", "VĚTR", "VODA", "VOJÁK", "VOLANT",
|
|
||||||
"VRÁNA", "VRATA", "VLASY", "VLAK", "VLK", "ZÁHADA", "ZAHRADA", "ZÁMEK",
|
|
||||||
"ZIMA", "ZLATO", "ZUB", "ZVON", "ŽÁBA", "ŽÁROVKA", "ŽENA", "ŽIDLE", "ŽIVOT",
|
|
||||||
# special letters to make sure we have them covered:
|
|
||||||
"WEB", "PIXEL", "TEQUILA", "BRAWL", "BOX", "QÍK"
|
|
||||||
]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import random
|
|
||||||
from .base import Job
|
|
||||||
|
|
||||||
class DecimalDivisionJob(Job):
|
|
||||||
def get_name(self):
|
|
||||||
return "DELENI (2 DES. MISTA)"
|
|
||||||
|
|
||||||
def print_body(self, p):
|
|
||||||
p.text("Vypocitej na 2 desetinna mista:\n\n")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
exercises = 2;
|
|
||||||
for i in range(1, exercises + 1):
|
|
||||||
# Ensure it's not too easy (avoid 1)
|
|
||||||
divisor = random.randint(2, 39)
|
|
||||||
# Dividend between 10 and 100
|
|
||||||
dividend = random.randint(10, 1000)
|
|
||||||
|
|
||||||
p.text(f"{i}) {dividend} : {divisor} = ______\n\n")
|
|
||||||
|
|
||||||
# Calculate result rounded to 2 decimal places
|
|
||||||
res = dividend / divisor
|
|
||||||
results.append(f"{i}) {res:.2f}")
|
|
||||||
|
|
||||||
p.text("\n\n\n\n") # add space for calculations
|
|
||||||
|
|
||||||
p.text("Reseni (naskenuj):\n")
|
|
||||||
# Join results for the QR code
|
|
||||||
qr_data = "\n".join(results)
|
|
||||||
p.qr(qr_data, size=6, native=True)
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import random
|
|
||||||
import unicodedata
|
|
||||||
from .base import Job
|
|
||||||
from jobs.czech_words import CZECH_WORDS
|
|
||||||
|
|
||||||
class DivisionCipherJob(Job):
|
|
||||||
def __init__(self):
|
|
||||||
self.secret = "TAJENKA"
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return "TAJENKA DELENIM"
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
print("\n--- Configure Division Cipher ---")
|
|
||||||
phrase = input("Enter secret phrase (default: Random): ").strip().upper()
|
|
||||||
|
|
||||||
raw_secret = phrase if phrase else random.choice(CZECH_WORDS)
|
|
||||||
|
|
||||||
# Remove accents to ensure mapping to A-Z works
|
|
||||||
nfkd_form = unicodedata.normalize('NFKD', raw_secret)
|
|
||||||
only_ascii = "".join([c for c in nfkd_form if not unicodedata.combining(c)])
|
|
||||||
|
|
||||||
# Keep only A-Z
|
|
||||||
self.secret = "".join([c for c in only_ascii.upper() if 'A' <= c <= 'Z'])
|
|
||||||
|
|
||||||
if not self.secret:
|
|
||||||
self.secret = "TAJENKA"
|
|
||||||
|
|
||||||
def print_body(self, p):
|
|
||||||
secret = self.secret
|
|
||||||
|
|
||||||
p.text("Vylusti tajenku!\n")
|
|
||||||
p.text("Vysledek deleni je poradi pismena\n")
|
|
||||||
p.text("v abecede (A=1, B=2, C=3...).\n\n")
|
|
||||||
|
|
||||||
problems = []
|
|
||||||
for char in secret:
|
|
||||||
# A=1, B=2...
|
|
||||||
target = ord(char) - ord('A') + 1
|
|
||||||
|
|
||||||
# Generate Dividend / Divisor = target
|
|
||||||
# Ensure divisor is small enough for mental math
|
|
||||||
divisor = random.randint(2, 9)
|
|
||||||
dividend = target * divisor
|
|
||||||
|
|
||||||
problems.append((dividend, divisor))
|
|
||||||
|
|
||||||
# Print problems
|
|
||||||
for i, (dividend, divisor) in enumerate(problems):
|
|
||||||
p.text(f"{i+1}) {dividend} : {divisor} = ___\n")
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
# Print slots for solution
|
|
||||||
p.text("Tajenka: " + " ".join(["___"] * len(secret)) + "\n\n")
|
|
||||||
|
|
||||||
# Print helper key
|
|
||||||
p.text("Napoveda:\n")
|
|
||||||
p.text("1=A 2=B 3=C 4=D 5=E 6=F 7=G 8=H\n")
|
|
||||||
p.text("9=I 10=J 11=K 12=L 13=M 14=N 15=O\n")
|
|
||||||
p.text("16=P 17=Q 18=R 19=S 20=T 21=U 22=V\n")
|
|
||||||
p.text("23=W 24=X 25=Y 26=Z\n")
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
class FlushJob:
|
|
||||||
def get_name(self):
|
|
||||||
return "Flush Printer Queue"
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self, printer):
|
|
||||||
# Send NUL bytes to push any buffered data without printing visible characters
|
|
||||||
printer._raw(b'\x00\x00')
|
|
||||||
# Send a newline to ensure any line-buffered data is processed
|
|
||||||
printer.text("\n")
|
|
||||||
47
jobs/joke.py
47
jobs/joke.py
@ -1,47 +0,0 @@
|
|||||||
import textwrap
|
|
||||||
from jobs.joke_sources import JednorozecJokeSource, BestPageJokeSource
|
|
||||||
|
|
||||||
class JokeJob:
|
|
||||||
def __init__(self):
|
|
||||||
self.sources = [
|
|
||||||
JednorozecJokeSource(),
|
|
||||||
BestPageJokeSource()
|
|
||||||
]
|
|
||||||
self.selected_source = self.sources[0]
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return "Random Joke"
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
print("\nSelect Joke Source:")
|
|
||||||
for i, source in enumerate(self.sources):
|
|
||||||
print(f" [{i + 1}] {source.get_name()}")
|
|
||||||
|
|
||||||
choice = input(f"Choice [{self.sources.index(self.selected_source) + 1}]: ").strip()
|
|
||||||
if choice:
|
|
||||||
try:
|
|
||||||
idx = int(choice) - 1
|
|
||||||
if 0 <= idx < len(self.sources):
|
|
||||||
self.selected_source = self.sources[idx]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self, printer):
|
|
||||||
try:
|
|
||||||
joke = self.selected_source.fetch_joke()
|
|
||||||
|
|
||||||
if joke:
|
|
||||||
# Wrap text to avoid word splitting (assuming ~42 chars for 80mm paper)
|
|
||||||
wrapped_joke = "\n".join([textwrap.fill(line, width=42) for line in joke.splitlines()])
|
|
||||||
|
|
||||||
printer.text(f"Joke from {self.selected_source.get_name()}:\n")
|
|
||||||
printer.text("--------------------------------\n\n")
|
|
||||||
printer.text(wrapped_joke)
|
|
||||||
printer.text("\n\n")
|
|
||||||
else:
|
|
||||||
printer.text("Sorry, could not extract any jokes from the website.\n")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
printer.text(f"Error fetching joke: {e}\n")
|
|
||||||
|
|
||||||
printer.cut()
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import random
|
|
||||||
|
|
||||||
class JokeSource:
|
|
||||||
def get_name(self):
|
|
||||||
return "Generic Source"
|
|
||||||
|
|
||||||
def fetch_joke(self):
|
|
||||||
"""Returns a single joke string or None."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
class JednorozecJokeSource(JokeSource):
|
|
||||||
def get_name(self):
|
|
||||||
return "vtipy.jednorozec.cz"
|
|
||||||
|
|
||||||
def fetch_joke(self):
|
|
||||||
url = "https://vtipy.jednorozec.cz/"
|
|
||||||
try:
|
|
||||||
# Add a User-Agent to be polite and avoid basic blocking
|
|
||||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; PrintServer/1.0)'}
|
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.content, 'html.parser')
|
|
||||||
|
|
||||||
jokes = []
|
|
||||||
|
|
||||||
# Strategy 1: Look for specific classes often used in blogs/joke sites
|
|
||||||
# We look for divs that might contain the joke text
|
|
||||||
potential_classes = ['post', 'entry', 'hentry', 'joke', 'vtip']
|
|
||||||
for class_name in potential_classes:
|
|
||||||
elements = soup.find_all(class_=lambda x: x and class_name in x.split())
|
|
||||||
if elements:
|
|
||||||
for el in elements:
|
|
||||||
for br in el.find_all("br"):
|
|
||||||
br.replace_with("\n")
|
|
||||||
text = el.get_text()
|
|
||||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
||||||
text = "\n".join(lines)
|
|
||||||
# Filter out very short texts (titles, metadata) and ensure safety limit
|
|
||||||
if len(text) > 20 and len(lines) <= 20:
|
|
||||||
jokes.append(text)
|
|
||||||
if jokes:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Strategy 2: Fallback to all paragraphs if no specific container found
|
|
||||||
if not jokes:
|
|
||||||
for p in soup.find_all('p'):
|
|
||||||
for br in p.find_all("br"):
|
|
||||||
br.replace_with("\n")
|
|
||||||
text = p.get_text()
|
|
||||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
||||||
text = "\n".join(lines)
|
|
||||||
if len(text) > 50 and len(lines) <= 20: # Assume jokes are somewhat long paragraphs
|
|
||||||
jokes.append(text)
|
|
||||||
|
|
||||||
if jokes:
|
|
||||||
return random.choice(jokes)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
class BestPageJokeSource(JokeSource):
|
|
||||||
def get_name(self):
|
|
||||||
return "bestpage.cz"
|
|
||||||
|
|
||||||
def fetch_joke(self):
|
|
||||||
url = "https://bestpage.cz/vtipy/"
|
|
||||||
try:
|
|
||||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; PrintServer/1.0)'}
|
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
|
||||||
# Older sites often use windows-1250 or iso-8859-2
|
|
||||||
response.encoding = response.apparent_encoding
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.content, 'html.parser')
|
|
||||||
|
|
||||||
jokes = []
|
|
||||||
|
|
||||||
# Bestpage is an older site, often using tables or simple paragraphs
|
|
||||||
for el in soup.find_all(['p', 'div', 'td']):
|
|
||||||
for br in el.find_all("br"):
|
|
||||||
br.replace_with("\n")
|
|
||||||
text = el.get_text()
|
|
||||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
||||||
text = "\n".join(lines)
|
|
||||||
|
|
||||||
if 50 < len(text) < 1000 and len(lines) <= 20:
|
|
||||||
jokes.append(text)
|
|
||||||
|
|
||||||
if jokes:
|
|
||||||
return random.choice(jokes)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
31
jobs/maze.py
31
jobs/maze.py
@ -3,33 +3,15 @@ from PIL import Image, ImageDraw
|
|||||||
from .base import Job
|
from .base import Job
|
||||||
|
|
||||||
class MazeJob(Job):
|
class MazeJob(Job):
|
||||||
def __init__(self):
|
|
||||||
self.width = 14
|
|
||||||
self.height = 32
|
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return "BLUDISTE"
|
return "BLUDISTE"
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
print("\nSelect Difficulty:")
|
|
||||||
print(" [1] Easy")
|
|
||||||
print(" [2] Medium")
|
|
||||||
print(" [3] Hard")
|
|
||||||
choice = input("Choice [2]: ").strip()
|
|
||||||
|
|
||||||
if choice == '1':
|
|
||||||
self.height = 8
|
|
||||||
elif choice == '3':
|
|
||||||
self.height = 32
|
|
||||||
else:
|
|
||||||
self.height = 18
|
|
||||||
|
|
||||||
def print_body(self, p):
|
def print_body(self, p):
|
||||||
# Width and Height in cells.
|
# Width and Height in cells.
|
||||||
# Total width in chars = 2 * w + 1.
|
# Total width in chars = 2 * w + 1.
|
||||||
# w=15 -> 31 chars (Fits comfortably on 80mm printers, tight on 58mm)
|
# w=15 -> 31 chars (Fits comfortably on 80mm printers, tight on 58mm)
|
||||||
w = self.width
|
w = 14
|
||||||
h = self.height
|
h = 32
|
||||||
|
|
||||||
maze = self.generate_maze(w, h)
|
maze = self.generate_maze(w, h)
|
||||||
|
|
||||||
@ -56,17 +38,12 @@ class MazeJob(Job):
|
|||||||
|
|
||||||
for r in range(rows):
|
for r in range(rows):
|
||||||
for c in range(cols):
|
for c in range(cols):
|
||||||
x = c * cell_size
|
|
||||||
y = r * cell_size
|
|
||||||
|
|
||||||
# Draw walls as black rectangles
|
# Draw walls as black rectangles
|
||||||
if maze[r][c] == '#':
|
if maze[r][c] == '#':
|
||||||
|
x = c * cell_size
|
||||||
|
y = r * cell_size
|
||||||
# fill=0 means Black in '1' mode
|
# fill=0 means Black in '1' mode
|
||||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||||
elif maze[r][c] == 'S':
|
|
||||||
draw.text((x + 5, y + 2), "S", fill=0)
|
|
||||||
elif maze[r][c] == 'E':
|
|
||||||
draw.text((x + 5, y + 2), "E", fill=0)
|
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|||||||
@ -1,239 +0,0 @@
|
|||||||
import random
|
|
||||||
from collections import deque
|
|
||||||
import time
|
|
||||||
|
|
||||||
class MazeMultitargetJob:
|
|
||||||
def __init__(self):
|
|
||||||
self.options = []
|
|
||||||
self.correct_index = 0
|
|
||||||
self.width = 18 * 2 + 1
|
|
||||||
self.height = 20 * 2 + 1
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return "Maze with Multiple Endings"
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
print("\n--- Configure Maze Options ---")
|
|
||||||
self.options = []
|
|
||||||
print("Enter labels for the endings (empty line to finish):")
|
|
||||||
while True:
|
|
||||||
label = input(f"Option {chr(65 + len(self.options))}: ").strip()
|
|
||||||
if not label:
|
|
||||||
if len(self.options) < 2:
|
|
||||||
print("Please enter at least 2 options.")
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
self.options.append(label)
|
|
||||||
if len(self.options) >= 26:
|
|
||||||
break
|
|
||||||
|
|
||||||
print("\nWhich option is the correct one?")
|
|
||||||
for i, opt in enumerate(self.options):
|
|
||||||
print(f" [{chr(65 + i)}] {opt}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
choice = input("Correct option (letter): ").strip().upper()
|
|
||||||
if len(choice) == 1:
|
|
||||||
idx = ord(choice) - 65
|
|
||||||
if 0 <= idx < len(self.options):
|
|
||||||
self.correct_index = idx
|
|
||||||
break
|
|
||||||
print("Invalid selection.")
|
|
||||||
|
|
||||||
def run(self, printer):
|
|
||||||
# 1. Generate Perfect Maze (DFS)
|
|
||||||
# Grid: 1 = Wall, 0 = Path
|
|
||||||
grid = [[1 for _ in range(self.width)] for _ in range(self.height)]
|
|
||||||
|
|
||||||
def get_neighbors(r, c, dist=2):
|
|
||||||
ns = []
|
|
||||||
for dr, dc in [(-dist, 0), (dist, 0), (0, -dist), (0, dist)]:
|
|
||||||
nr, nc = r + dr, c + dc
|
|
||||||
if 0 < nr < self.height and 0 < nc < self.width:
|
|
||||||
ns.append((nr, nc))
|
|
||||||
return ns
|
|
||||||
|
|
||||||
# Start carving from (1, 1)
|
|
||||||
start_pos = (1, 1)
|
|
||||||
grid[start_pos[0]][start_pos[1]] = 0
|
|
||||||
stack = [start_pos]
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
current = stack[-1]
|
|
||||||
r, c = current
|
|
||||||
neighbors = get_neighbors(r, c)
|
|
||||||
unvisited = []
|
|
||||||
for nr, nc in neighbors:
|
|
||||||
if grid[nr][nc] == 1:
|
|
||||||
unvisited.append((nr, nc))
|
|
||||||
|
|
||||||
if unvisited:
|
|
||||||
nr, nc = random.choice(unvisited)
|
|
||||||
# Remove wall between
|
|
||||||
wr, wc = (r + nr) // 2, (c + nc) // 2
|
|
||||||
grid[wr][wc] = 0
|
|
||||||
grid[nr][nc] = 0
|
|
||||||
stack.append((nr, nc))
|
|
||||||
else:
|
|
||||||
stack.pop()
|
|
||||||
|
|
||||||
def find_path(start, end, current_grid):
|
|
||||||
q = deque([start])
|
|
||||||
came_from = {start: None}
|
|
||||||
while q:
|
|
||||||
curr = q.popleft()
|
|
||||||
if curr == end:
|
|
||||||
break
|
|
||||||
|
|
||||||
r, c = curr
|
|
||||||
# Check neighbors (dist 1)
|
|
||||||
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
||||||
nr, nc = r + dr, c + dc
|
|
||||||
if 0 <= nr < self.height and 0 <= nc < self.width:
|
|
||||||
if current_grid[nr][nc] == 0 and (nr, nc) not in came_from:
|
|
||||||
came_from[(nr, nc)] = curr
|
|
||||||
q.append((nr, nc))
|
|
||||||
|
|
||||||
if end not in came_from:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Reconstruct path
|
|
||||||
path = []
|
|
||||||
curr = end
|
|
||||||
while curr:
|
|
||||||
path.append(curr)
|
|
||||||
curr = came_from[curr]
|
|
||||||
return path[::-1]
|
|
||||||
|
|
||||||
# 2. Place Endpoints
|
|
||||||
# We need len(self.options) endpoints.
|
|
||||||
endpoints = [None] * len(self.options)
|
|
||||||
|
|
||||||
# First, place the correct endpoint
|
|
||||||
attempts = 0
|
|
||||||
while endpoints[self.correct_index] is None and attempts < 1000:
|
|
||||||
r = random.randrange(1, self.height, 2)
|
|
||||||
c = random.randrange(1, self.width, 2)
|
|
||||||
if (r, c) != start_pos and grid[r][c] == 0:
|
|
||||||
endpoints[self.correct_index] = (r, c)
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
correct_endpoint = endpoints[self.correct_index]
|
|
||||||
if not correct_endpoint:
|
|
||||||
printer.text("Error: Could not place correct endpoint.\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Calculate true path to ensure we don't place fakes on it
|
|
||||||
true_path = find_path(start_pos, correct_endpoint, grid)
|
|
||||||
if not true_path:
|
|
||||||
printer.text("Error: No path to correct endpoint.\n")
|
|
||||||
return
|
|
||||||
true_path_set = set(true_path)
|
|
||||||
|
|
||||||
# Place fake endpoints
|
|
||||||
attempts = 0
|
|
||||||
while None in endpoints and attempts < 2000:
|
|
||||||
r = random.randrange(1, self.height, 2)
|
|
||||||
c = random.randrange(1, self.width, 2)
|
|
||||||
pt = (r, c)
|
|
||||||
if pt != start_pos and pt not in endpoints and grid[r][c] == 0:
|
|
||||||
if pt not in true_path_set:
|
|
||||||
# Fill first empty slot
|
|
||||||
for i in range(len(endpoints)):
|
|
||||||
if endpoints[i] is None:
|
|
||||||
endpoints[i] = pt
|
|
||||||
break
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
if None in endpoints:
|
|
||||||
printer.text("Error: Could not place enough endpoints.\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 4. Block Incorrect Paths
|
|
||||||
# Robust Multi-target isolation
|
|
||||||
|
|
||||||
fakes = [pt for i, pt in enumerate(endpoints) if i != self.correct_index]
|
|
||||||
|
|
||||||
def get_degree(r, c, current_grid):
|
|
||||||
deg = 0
|
|
||||||
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
||||||
nr, nc = r + dr, c + dc
|
|
||||||
if 0 <= nr < self.height and 0 <= nc < self.width:
|
|
||||||
if current_grid[nr][nc] == 0:
|
|
||||||
deg += 1
|
|
||||||
return deg
|
|
||||||
|
|
||||||
for fake in fakes:
|
|
||||||
# Repeat until fake is isolated from all other targets
|
|
||||||
while True:
|
|
||||||
connected_target = None
|
|
||||||
path_to_target = None
|
|
||||||
|
|
||||||
# Prioritize connection to Start/Correct (Main), then other fakes
|
|
||||||
check_order = [start_pos, correct_endpoint] + [f for f in fakes if f != fake]
|
|
||||||
|
|
||||||
for other in check_order:
|
|
||||||
path = find_path(other, fake, grid)
|
|
||||||
if path:
|
|
||||||
connected_target = other
|
|
||||||
path_to_target = path
|
|
||||||
break
|
|
||||||
|
|
||||||
if not connected_target:
|
|
||||||
break # Isolated from everyone
|
|
||||||
|
|
||||||
# Identify segment NOT on true_path
|
|
||||||
valid_segment_start = 0
|
|
||||||
for k in range(len(path_to_target)):
|
|
||||||
if path_to_target[k] not in true_path_set:
|
|
||||||
valid_segment_start = k
|
|
||||||
break
|
|
||||||
|
|
||||||
if valid_segment_start == len(path_to_target):
|
|
||||||
break # Should not happen unless fake is ON true path
|
|
||||||
|
|
||||||
# Find last junction on the path to maximize false path length
|
|
||||||
best_cut_u_index = -1
|
|
||||||
start_search = max(0, valid_segment_start - 1)
|
|
||||||
|
|
||||||
for k in range(start_search, len(path_to_target) - 1):
|
|
||||||
u = path_to_target[k]
|
|
||||||
if get_degree(u[0], u[1], grid) > 2:
|
|
||||||
best_cut_u_index = k
|
|
||||||
|
|
||||||
if best_cut_u_index != -1:
|
|
||||||
block_index = best_cut_u_index + 1
|
|
||||||
else:
|
|
||||||
block_index = len(path_to_target) - 2
|
|
||||||
|
|
||||||
# Ensure we block a valid node
|
|
||||||
block_index = max(block_index, valid_segment_start)
|
|
||||||
to_block = path_to_target[block_index]
|
|
||||||
grid[to_block[0]][to_block[1]] = 1
|
|
||||||
|
|
||||||
# 5. Print Maze
|
|
||||||
printer.text("Najdi spravny cil!\n\n")
|
|
||||||
|
|
||||||
# Map endpoints to letters
|
|
||||||
endpoint_map = {pt: chr(65 + i) for i, pt in enumerate(endpoints)}
|
|
||||||
|
|
||||||
for r in range(self.height):
|
|
||||||
line = ""
|
|
||||||
for c in range(self.width):
|
|
||||||
if (r, c) == start_pos:
|
|
||||||
line += "S"
|
|
||||||
elif (r, c) in endpoint_map:
|
|
||||||
line += endpoint_map[(r, c)]
|
|
||||||
elif grid[r][c] == 1:
|
|
||||||
line += "█" # Full block
|
|
||||||
else:
|
|
||||||
line += " "
|
|
||||||
printer.text(line + "\n")
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
printer.text("\nMoznosti:\n")
|
|
||||||
for i, opt in enumerate(self.options):
|
|
||||||
printer.text(f"{chr(65 + i)}: {opt}\n")
|
|
||||||
|
|
||||||
printer.text("\n\n")
|
|
||||||
printer.cut()
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import random
|
|
||||||
import time
|
|
||||||
import textwrap
|
|
||||||
from jobs.czech_words import CZECH_WORDS
|
|
||||||
|
|
||||||
class WordSearchJob:
|
|
||||||
def __init__(self):
|
|
||||||
self.width = 14
|
|
||||||
self.height = 14
|
|
||||||
self.hidden_phrase = "TAJENKA"
|
|
||||||
self.words_to_find = []
|
|
||||||
self.grid = []
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return "Word Search (Osmismerka)"
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
print("\n--- Configure Word Search ---")
|
|
||||||
phrase = input("Enter hidden phrase (default: Random): ").strip().upper()
|
|
||||||
if phrase:
|
|
||||||
self.hidden_phrase = "".join(c for c in phrase if c.isalnum())
|
|
||||||
else:
|
|
||||||
self.hidden_phrase = random.choice(CZECH_WORDS)
|
|
||||||
self.width = len(self.hidden_phrase) + 2
|
|
||||||
self.height = len(self.hidden_phrase) + 2
|
|
||||||
size_str = input(f"Enter size (default: {self.width}): ").strip()
|
|
||||||
if size_str:
|
|
||||||
try:
|
|
||||||
size = int(size_str)
|
|
||||||
self.width = size
|
|
||||||
self.height = size
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if self.width < 5:
|
|
||||||
self.width = 5
|
|
||||||
self.height = 5
|
|
||||||
if self.width > 40:
|
|
||||||
self.width = 40
|
|
||||||
self.height = 40
|
|
||||||
|
|
||||||
def run(self, printer):
|
|
||||||
# Directions: Right, Down, Diag-Down-Right
|
|
||||||
directions = [(0, 1), (1, 0), (1, 1)]
|
|
||||||
target_empty = len(self.hidden_phrase)
|
|
||||||
|
|
||||||
# Retry loop to ensure perfect fill (no leftover letters)
|
|
||||||
for attempt in range(20):
|
|
||||||
# Initialize grid
|
|
||||||
self.grid = [['' for _ in range(self.width)] for _ in range(self.height)]
|
|
||||||
used_mask = [[False for _ in range(self.width)] for _ in range(self.height)]
|
|
||||||
|
|
||||||
# Filter words that fit in the grid
|
|
||||||
available_words = [w for w in CZECH_WORDS if len(w) <= self.width and len(w) <= self.height]
|
|
||||||
random.shuffle(available_words)
|
|
||||||
|
|
||||||
self.words_to_find = []
|
|
||||||
|
|
||||||
total_cells = self.width * self.height
|
|
||||||
current_empty = total_cells
|
|
||||||
|
|
||||||
# Try to place words until we reach target_empty
|
|
||||||
for word in available_words:
|
|
||||||
if current_empty == target_empty:
|
|
||||||
break
|
|
||||||
|
|
||||||
placed = False
|
|
||||||
attempts = 0
|
|
||||||
while not placed and attempts < 50:
|
|
||||||
attempts += 1
|
|
||||||
direction = random.choice(directions)
|
|
||||||
dr, dc = direction
|
|
||||||
|
|
||||||
# Determine bounds
|
|
||||||
if dr == 0: # Horizontal
|
|
||||||
r = random.randint(0, self.height - 1)
|
|
||||||
c = random.randint(0, self.width - len(word))
|
|
||||||
elif dc == 0: # Vertical
|
|
||||||
r = random.randint(0, self.height - len(word))
|
|
||||||
c = random.randint(0, self.width - 1)
|
|
||||||
else: # Diagonal
|
|
||||||
r = random.randint(0, self.height - len(word))
|
|
||||||
c = random.randint(0, self.width - len(word))
|
|
||||||
|
|
||||||
# Check collision
|
|
||||||
fits = True
|
|
||||||
temp_new_cells = 0
|
|
||||||
for i, char in enumerate(word):
|
|
||||||
nr, nc = r + i*dr, c + i*dc
|
|
||||||
# Cell must be empty OR contain the same letter
|
|
||||||
if self.grid[nr][nc] != '' and self.grid[nr][nc] != char:
|
|
||||||
fits = False
|
|
||||||
break
|
|
||||||
if self.grid[nr][nc] == '':
|
|
||||||
temp_new_cells += 1
|
|
||||||
|
|
||||||
if fits:
|
|
||||||
# Check if this overfills
|
|
||||||
if current_empty - temp_new_cells < target_empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Place it
|
|
||||||
for i, char in enumerate(word):
|
|
||||||
nr, nc = r + i*dr, c + i*dc
|
|
||||||
if self.grid[nr][nc] == '':
|
|
||||||
current_empty -= 1
|
|
||||||
self.grid[nr][nc] = char
|
|
||||||
used_mask[nr][nc] = True
|
|
||||||
self.words_to_find.append(word)
|
|
||||||
placed = True
|
|
||||||
|
|
||||||
if current_empty == target_empty:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Fill remaining spots with hidden phrase
|
|
||||||
empty_slots = []
|
|
||||||
for r in range(self.height):
|
|
||||||
for c in range(self.width):
|
|
||||||
if not used_mask[r][c]:
|
|
||||||
empty_slots.append((r, c))
|
|
||||||
|
|
||||||
phrase_idx = 0
|
|
||||||
for r, c in empty_slots:
|
|
||||||
if phrase_idx < len(self.hidden_phrase):
|
|
||||||
self.grid[r][c] = self.hidden_phrase[phrase_idx]
|
|
||||||
phrase_idx += 1
|
|
||||||
else:
|
|
||||||
# Fill with random letters if phrase is done (extra filler)
|
|
||||||
self.grid[r][c] = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
||||||
|
|
||||||
# Print Header
|
|
||||||
printer.text("OSMISMĚRKA\n")
|
|
||||||
printer.text("Najdi slova a přečti tajenku!\n")
|
|
||||||
if current_empty != target_empty:
|
|
||||||
printer.text(f"Tajenka má {len(self.hidden_phrase)} písmen.\n")
|
|
||||||
printer.text("\n")
|
|
||||||
|
|
||||||
# Print Grid
|
|
||||||
for r in range(self.height):
|
|
||||||
line = " ".join(self.grid[r])
|
|
||||||
printer.text(line + "\n")
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
printer.text("\nSlova k hledání:\n")
|
|
||||||
words_str = ", ".join(sorted(self.words_to_find))
|
|
||||||
printer.text(textwrap.fill(words_str, width=42) + "\n")
|
|
||||||
printer.text("\n\n")
|
|
||||||
printer.cut()
|
|
||||||
@ -1,16 +1,9 @@
|
|||||||
import time
|
|
||||||
from escpos.printer import Usb, Dummy
|
from escpos.printer import Usb, Dummy
|
||||||
from escpos.exceptions import USBNotFoundError
|
from escpos.exceptions import USBNotFoundError
|
||||||
from jobs.math_homework import MathHomeworkJob
|
from jobs.math_homework import MathHomeworkJob
|
||||||
from jobs.unit_conversion import UnitConversionJob
|
from jobs.unit_conversion import UnitConversionJob
|
||||||
from jobs.chess_puzzle import ChessPuzzleJob
|
from jobs.chess_puzzle import ChessPuzzleJob
|
||||||
from jobs.maze import MazeJob
|
from jobs.maze import MazeJob
|
||||||
from jobs.division_cipher import DivisionCipherJob
|
|
||||||
from jobs.decimal_division import DecimalDivisionJob
|
|
||||||
from jobs.joke import JokeJob
|
|
||||||
from jobs.maze_multitarget import MazeMultitargetJob
|
|
||||||
from jobs.flush import FlushJob
|
|
||||||
from jobs.word_search import WordSearchJob
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
@ -19,13 +12,7 @@ from jobs.word_search import WordSearchJob
|
|||||||
# Example: 0x04b8 is Epson.
|
# Example: 0x04b8 is Epson.
|
||||||
USB_VENDOR_ID = 0x0525
|
USB_VENDOR_ID = 0x0525
|
||||||
USB_PRODUCT_ID = 0xa700
|
USB_PRODUCT_ID = 0xa700
|
||||||
# Input interface:
|
|
||||||
# `lsusb -vvv -d xxxx:xxxx | grep iInterface`
|
|
||||||
# iInterface 0
|
|
||||||
INPUT_ENDPOINT = 0x00
|
INPUT_ENDPOINT = 0x00
|
||||||
# Output interface:
|
|
||||||
# `lsusb -vvv -d xxxx:xxxx | grep bEndpointAddress | grep OUT`
|
|
||||||
# bEndpointAddress 0x01 EP 1 OUT
|
|
||||||
OUTPUT_ENDPOINT = 0x01
|
OUTPUT_ENDPOINT = 0x01
|
||||||
|
|
||||||
# Set to True to print to console instead of physical printer (for testing)
|
# Set to True to print to console instead of physical printer (for testing)
|
||||||
@ -41,12 +28,12 @@ def get_printer():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize USB printer
|
# Initialize USB printer
|
||||||
# Generic printer with custom settings:
|
# profile="TM-T88V" is a generic profile, works for many ESC/POS printers
|
||||||
|
|
||||||
p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="default")
|
p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="default")
|
||||||
p.profile.profile_data['media']['width']['mm'] = 80
|
p.profile.profile_data['media']['width']['mm'] = 80
|
||||||
p.profile.profile_data['media']['width']['pixels'] = 512
|
p.profile.profile_data['media']['width']['pixels'] = 512
|
||||||
|
|
||||||
# Specific printer based on a profile. See https://python-escpos.readthedocs.io/en/latest/printer_profiles/available-profiles.html
|
|
||||||
# p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="TM-T88V")
|
# p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="TM-T88V")
|
||||||
return p
|
return p
|
||||||
except USBNotFoundError:
|
except USBNotFoundError:
|
||||||
@ -60,14 +47,7 @@ JOBS = [
|
|||||||
MathHomeworkJob(),
|
MathHomeworkJob(),
|
||||||
UnitConversionJob(),
|
UnitConversionJob(),
|
||||||
ChessPuzzleJob(),
|
ChessPuzzleJob(),
|
||||||
MazeJob(),
|
MazeJob()
|
||||||
DivisionCipherJob(),
|
|
||||||
DecimalDivisionJob(),
|
|
||||||
JokeJob(),
|
|
||||||
MazeMultitargetJob(),
|
|
||||||
WordSearchJob(),
|
|
||||||
# keep this last:
|
|
||||||
FlushJob()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_tui():
|
def run_tui():
|
||||||
@ -99,33 +79,21 @@ def run_tui():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if job:
|
if job:
|
||||||
job.configure()
|
|
||||||
|
|
||||||
copies_str = input("\nNumber of copies [1]: ").strip()
|
|
||||||
try:
|
|
||||||
copies = max(1, int(copies_str)) if copies_str else 1
|
|
||||||
except ValueError:
|
|
||||||
copies = 1
|
|
||||||
|
|
||||||
p = get_printer()
|
p = get_printer()
|
||||||
if p:
|
if p:
|
||||||
print(f"Printing {job.get_name()} ({copies} copies)...")
|
print(f"Printing {job.get_name()}...")
|
||||||
try:
|
try:
|
||||||
for i in range(copies):
|
job.run(p)
|
||||||
if copies > 1:
|
|
||||||
print(f" Printing copy {i + 1}...")
|
|
||||||
job.run(p)
|
|
||||||
|
|
||||||
# If using Dummy, print the output to console to verify
|
# If using Dummy, print the output to console to verify
|
||||||
if isinstance(p, Dummy):
|
if isinstance(p, Dummy):
|
||||||
print(p.output.decode('utf-8', errors='ignore'))
|
print(p.output.decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
print("Success! Job sent to printer.")
|
print("Success! Job sent to printer.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Print Error: {e}")
|
print(f"Print Error: {e}")
|
||||||
finally:
|
finally:
|
||||||
if not isinstance(p, Dummy):
|
if not isinstance(p, Dummy):
|
||||||
time.sleep(0.5)
|
|
||||||
p.close()
|
p.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user