Better logging

This commit is contained in:
Eden Kirin
2025-10-14 08:20:19 +02:00
parent daa7e53d6d
commit 9d0557f96d
5 changed files with 143 additions and 141 deletions

View File

@ -5,13 +5,12 @@ Simulates a Glory MACH6 coin/currency counter for development/testing purposes.
Implements the Enhanced Serial Port Mode with full command support.
"""
import argparse
import logging
import random
import time
from typing import Optional
import serial
from loguru import logger
# Protocol constants
STX = 0x02
@ -28,7 +27,7 @@ STATE_MESSAGE_PENDING = "PRN"
class GlorySimulator:
def __init__(self, logger: logging.Logger, port: str, baudrate: int = 9600):
def __init__(self, port: str, baudrate: int = 9600):
self.port = port
self.baudrate = baudrate
self.serial_conn: Optional[serial.Serial] = None
@ -76,8 +75,6 @@ class GlorySimulator:
# Message buffer
self.pending_message = None
self.logger = logger
def get_status(self) -> str:
"""Get current machine status"""
if self.pending_message:
@ -107,7 +104,7 @@ class GlorySimulator:
"""Handle SS - Status command"""
status = self.get_status()
response = f"ST{self.count_mode}{status}"
self.logger.info(f"Status request - Response: {response}")
logger.info(f"Status request - Response: {response}")
return self.create_message(response)
def handle_clear_command(self, data: bytes) -> bytes:
@ -118,23 +115,23 @@ class GlorySimulator:
# Clear batch
self.batch_counts = {}
self.batch_id = ""
self.logger.info("Batch counts cleared")
logger.info("Batch counts cleared")
elif cmd == "CS":
# Clear subtotal
self.sub_counts = {}
self.sub_id = ""
self.logger.info("Subtotal counts cleared")
logger.info("Subtotal counts cleared")
elif cmd == "CG":
# Clear grand total
self.grand_counts = {}
self.grand_id = ""
self.logger.info("Grand total counts cleared")
logger.info("Grand total counts cleared")
elif cmd.startswith("CC"):
# Clear partial count
denom_str = cmd[2:]
if denom_str in self.partial_counts:
del self.partial_counts[denom_str]
self.logger.info(f"Partial count for {denom_str} cleared")
logger.info(f"Partial count for {denom_str} cleared")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
@ -153,21 +150,21 @@ class GlorySimulator:
total = sum(self.batch_counts.values())
response = f"BT{total:08d}"
self.logger.info(f"Get batch total: {total} coins")
logger.info(f"Get batch total: {total} coins")
return self.create_message(response)
elif cmd == "GS":
# Get subtotal
total = sum(self.sub_counts.values())
response = f"BS{total:08d}"
self.logger.info(f"Get subtotal: {total} coins")
logger.info(f"Get subtotal: {total} coins")
return self.create_message(response)
elif cmd == "GG":
# Get grand total
total = sum(self.grand_counts.values())
response = f"BG{total:08d}"
self.logger.info(f"Get grand total: {total} coins")
logger.info(f"Get grand total: {total} coins")
return self.create_message(response)
elif cmd.startswith("GD"):
@ -175,7 +172,7 @@ class GlorySimulator:
denom_str = cmd[2:]
count = self.partial_counts.get(denom_str, 0)
response = f"BD{count:08d}"
self.logger.info(f"Get partial count for {denom_str}: {count} coins")
logger.info(f"Get partial count for {denom_str}: {count} coins")
return self.create_message(response)
elif cmd.startswith("GT") and len(cmd) > 2:
@ -183,14 +180,14 @@ class GlorySimulator:
denom_str = cmd[2:]
count = self.batch_counts.get(denom_str, 0)
response = f"BT{count:08d}"
self.logger.info(f"Get batch count for {denom_str}: {count} coins")
logger.info(f"Get batch count for {denom_str}: {count} coins")
return self.create_message(response)
elif cmd == "GI":
# Get product totals (ID totals)
response = ""
# Return empty if no totals
self.logger.info("Get product totals (empty)")
logger.info("Get product totals (empty)")
return self.create_message(response)
elif cmd == "GV":
@ -199,7 +196,7 @@ class GlorySimulator:
for station, value in sorted(self.station_values.items()):
active = "i" if value == 0 else str(station)
response += f"{active} {value:.2f} \r"
self.logger.info("Get station values")
logger.info("Get station values")
return self.create_message(response)
return self.create_message("")
@ -215,9 +212,9 @@ class GlorySimulator:
try:
value = int(value_str)
self.bag_stops[denom_str] = value
self.logger.info(f"Bag stop set for {denom_str}: {value}")
logger.info(f"Bag stop set for {denom_str}: {value}")
except ValueError:
self.logger.error(f"Invalid bag stop value: {value_str}")
logger.error(f"Invalid bag stop value: {value_str}")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
@ -231,13 +228,13 @@ class GlorySimulator:
# Start motor
self.motor_running = True
self.motor_has_run = True
self.logger.info("Motor started")
logger.info("Motor started")
# Simulate coin counting
self._simulate_counting()
elif cmd == "MS":
# Stop motor
self.motor_running = False
self.logger.info("Motor stopped")
logger.info("Motor stopped")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
@ -245,7 +242,7 @@ class GlorySimulator:
def handle_beep_command(self, data: bytes) -> bytes:
"""Handle BB - Beep command"""
self.logger.info("Beep!")
logger.info("Beep!")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
return self.create_message(response)
@ -258,7 +255,7 @@ class GlorySimulator:
# Accept batch
total = sum(self.batch_counts.values())
response = f"BT{total:08d}"
self.logger.info(f"Accept batch: {total}")
logger.info(f"Accept batch: {total}")
self.pending_message = response
return self.create_message(response)
@ -266,7 +263,7 @@ class GlorySimulator:
# Accept subtotal
total = sum(self.sub_counts.values())
response = f"BS{total:08d}"
self.logger.info(f"Accept subtotal: {total}")
logger.info(f"Accept subtotal: {total}")
self.pending_message = response
return self.create_message(response)
@ -274,7 +271,7 @@ class GlorySimulator:
# Accept grand total
total = sum(self.grand_counts.values())
response = f"BG{total:08d}"
self.logger.info(f"Accept grand total: {total}")
logger.info(f"Accept grand total: {total}")
self.pending_message = response
return self.create_message(response)
@ -291,9 +288,9 @@ class GlorySimulator:
try:
value = int(value_str)
self.partial_counts[denom_str] = value
self.logger.info(f"Partial count set for {denom_str}: {value}")
logger.info(f"Partial count set for {denom_str}: {value}")
except ValueError:
self.logger.error(f"Invalid partial count value: {value_str}")
logger.error(f"Invalid partial count value: {value_str}")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
@ -305,13 +302,13 @@ class GlorySimulator:
if cmd.startswith("ID"):
self.batch_id = cmd[2:14] if len(cmd) >= 14 else cmd[2:]
self.logger.info(f"Batch ID set: {self.batch_id}")
logger.info(f"Batch ID set: {self.batch_id}")
elif cmd.startswith("IS"):
self.sub_id = cmd[2:14] if len(cmd) >= 14 else cmd[2:]
self.logger.info(f"Sub ID set: {self.sub_id}")
logger.info(f"Sub ID set: {self.sub_id}")
elif cmd.startswith("IG"):
self.grand_id = cmd[2:14] if len(cmd) >= 14 else cmd[2:]
self.logger.info(f"Grand ID set: {self.grand_id}")
logger.info(f"Grand ID set: {self.grand_id}")
status = self.get_status()
response = f"ST{self.count_mode}{status}"
@ -342,14 +339,14 @@ class GlorySimulator:
# Update grand total
self.grand_counts[denom] = self.grand_counts.get(denom, 0) + count
self.logger.info(f"Counted {count} coins of denomination {denom}")
logger.info(f"Counted {count} coins of denomination {denom}")
# Log total value counted
total_value = sum(
self.batch_counts.get(denom, 0) * int(denom)
for denom in denominations.keys()
)
self.logger.info(f"Total value counted in this batch: ${total_value/100:.2f}")
logger.info(f"Total value counted in this batch: ${total_value/100:.2f}")
def create_message(self, data: str) -> bytes:
"""Create a message with STX and ETX"""
@ -358,15 +355,15 @@ class GlorySimulator:
def parse_message(self, message: bytes) -> Optional[bytes]:
"""Parse and validate a received message"""
if len(message) < 3:
self.logger.error("Message too short")
logger.error("Message too short")
return None
if message[0] != STX:
self.logger.error("Invalid STX")
logger.error("Invalid STX")
return None
if message[-1] != ETX:
self.logger.error("Invalid ETX")
logger.error("Invalid ETX")
return None
data = message[1:-1]
@ -381,7 +378,7 @@ class GlorySimulator:
cmd_str = data.decode("ascii", errors="ignore").strip()
cmd = cmd_str[:2]
self.logger.info(f"Received command: {cmd_str}")
logger.info(f"Received command: {cmd_str}")
if cmd == "SS":
return self.handle_status_command(data)
@ -402,19 +399,19 @@ class GlorySimulator:
elif cmd in ["ID", "IS", "IG"]:
return self.handle_id_command(data)
else:
self.logger.warning(f"Unknown command: {cmd}")
logger.warning(f"Unknown command: {cmd}")
return None
except Exception as e:
self.logger.error(f"Error handling command: {e}", exc_info=True)
logger.error(f"Error handling command: {e}", exc_info=True)
return None
def run(self):
"""Main simulator loop"""
self.logger.info(
logger.info(
f"Starting Glory MACH6 simulator on {self.port} at {self.baudrate} baud"
)
self.logger.info("Enhanced Serial Port Mode enabled")
logger.info("Enhanced Serial Port Mode enabled")
try:
self.serial_conn = serial.Serial(
@ -426,7 +423,7 @@ class GlorySimulator:
timeout=1,
)
self.logger.info("Serial port opened successfully")
logger.info("Serial port opened successfully")
buffer = bytearray()
@ -437,13 +434,13 @@ class GlorySimulator:
if self.serial_conn.in_waiting > 0:
byte_data = self.serial_conn.read(1)
if byte_data[0] == ACK:
self.logger.info("Received ACK - clearing pending message")
logger.info("Received ACK - clearing pending message")
# Clear batch after ACK
if "BT" in self.pending_message:
self.batch_counts = {}
self.pending_message = None
elif byte_data[0] == NAK:
self.logger.info("Received NAK - retransmitting")
logger.info("Received NAK - retransmitting")
response = self.create_message(self.pending_message)
self.serial_conn.write(response)
time.sleep(0.01)
@ -461,14 +458,18 @@ class GlorySimulator:
message = bytes(buffer[stx_idx : etx_idx + 1])
buffer = buffer[etx_idx + 1 :]
self.logger.debug(f"RX: {' '.join(f'{b:02X}' for b in message)}")
logger.debug(
f"RX: {' '.join(f'{b:02X}' for b in message)}"
)
# Parse and handle message
parsed_data = self.parse_message(message)
if parsed_data:
response = self.handle_command(parsed_data)
if response:
self.logger.debug(f"TX: {' '.join(f'{b:02X}' for b in response)}")
logger.debug(
f"TX: {' '.join(f'{b:02X}' for b in response)}"
)
self.serial_conn.write(response)
except ValueError:
pass # ETX not found yet
@ -476,32 +477,10 @@ class GlorySimulator:
time.sleep(0.01)
except KeyboardInterrupt:
self.logger.info("Simulator stopped by user")
logger.info("Simulator stopped by user")
except Exception as e:
self.logger.error(f"Error: {e}", exc_info=True)
logger.error(f"Error: {e}", exc_info=True)
finally:
if self.serial_conn and self.serial_conn.is_open:
self.serial_conn.close()
self.logger.info("Serial port closed")
def main():
parser = argparse.ArgumentParser(description="Glory MACH6 Device Simulator")
parser.add_argument(
"--port",
"-p",
default="/dev/ttyUSB0",
help="Serial port (default: /dev/ttyUSB0)",
)
parser.add_argument(
"--baudrate", "-b", type=int, default=9600, help="Baud rate (default: 9600)"
)
args = parser.parse_args()
simulator = GlorySimulator(args.port, args.baudrate)
simulator.run()
if __name__ == "__main__":
main()
logger.info("Serial port closed")