Better logging
This commit is contained in:
119
source/glory.py
119
source/glory.py
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user