374 lines
11 KiB
Python
374 lines
11 KiB
Python
import time
|
|
import struct
|
|
import logging
|
|
|
|
try:
|
|
import queue
|
|
except ImportError:
|
|
import Queue as queue
|
|
|
|
try:
|
|
import cygpio
|
|
from cygpio import CythonRaspiBackend
|
|
except ImportError:
|
|
cygpio = None
|
|
|
|
from mdb.utils import compute_checksum, compute_chk, bcd_decode
|
|
from mdb.constants import *
|
|
from mdb.backend import RaspiBackend, DummyBackend, SerialBackend, pigpio
|
|
|
|
|
|
class MDBRequest(object):
|
|
timestamp = None
|
|
data = None
|
|
command = None
|
|
processed = False
|
|
|
|
def __init__(self, command):
|
|
self.reset(command)
|
|
|
|
def reset(self, command):
|
|
self.processed = False
|
|
self.timestamp = time.time()
|
|
self.command = command
|
|
self.data = bytearray()
|
|
|
|
def validate_checksum(self):
|
|
try:
|
|
if self.processed and self.ack:
|
|
if len(self.data) == 1:
|
|
return True # Only ACK
|
|
|
|
return len(self.data) >= 2 and self.data[-2] == compute_checksum(
|
|
self.command, self.data[:-2]
|
|
)
|
|
else:
|
|
return len(self.data) >= 1 and self.data[-1] == compute_checksum(
|
|
self.command, self.data[:-1]
|
|
)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
logging.exception(
|
|
"Checksum validation failed for: %r %r", self.command, self.data
|
|
)
|
|
return False
|
|
|
|
@property
|
|
def ack(self):
|
|
return len(self.data) and self.data[-1] == 0x00
|
|
|
|
def __repr__(self):
|
|
return "<MDBRequest 0x%02x [%s] chk:%r>" % (
|
|
self.command,
|
|
" ".join(["0x%02x" % b for b in self.data]),
|
|
self.validate_checksum(),
|
|
)
|
|
|
|
|
|
class MDBDevice(object):
|
|
current_request = None
|
|
send_buffer = None
|
|
|
|
poll_msg = []
|
|
|
|
def __init__(self, app=None):
|
|
self.logger = logging.getLogger(type(self).__name__)
|
|
self.poll_queue = queue.Queue()
|
|
if cygpio:
|
|
self.backend = cygpio.CythonRaspiBackend()
|
|
self.logger.warning("Running with FAST CYTHON BACKEND")
|
|
elif pigpio:
|
|
self.backend = RaspiBackend()
|
|
else:
|
|
self.logger.warning("Running with dummy backend device")
|
|
self.backend = SerialBackend()
|
|
|
|
def initialize(self):
|
|
self.logger.info("Initializing... %r backend", self.backend)
|
|
self.backend.open()
|
|
# here should IO / connection initizliation go
|
|
|
|
def run(self):
|
|
self.initialize()
|
|
|
|
self.current_request = MDBRequest(0)
|
|
self.current_request.processed = True
|
|
|
|
while True:
|
|
data = self.backend.read()
|
|
for b in range(0, len(data), 2):
|
|
if data[b + 1]:
|
|
if self.current_request: # and not self.current_request.processed:
|
|
if self.current_request.command not in [0xF2]:
|
|
self.logger.debug(self.current_request)
|
|
if self.current_request.processed and self.current_request.ack:
|
|
self.logger.info(
|
|
"Got response: %d", self.current_request.data[-1]
|
|
)
|
|
self.poll_msg = []
|
|
|
|
self.current_request.reset(data[b])
|
|
self.send_buffer = None
|
|
elif self.current_request:
|
|
if (
|
|
self.current_request.processed
|
|
and data[b] == 0xAA
|
|
and self.send_buffer
|
|
):
|
|
self.logger.warning(
|
|
"Received RETRANSMIT %r %r",
|
|
self.current_request,
|
|
self.send_buffer,
|
|
)
|
|
# self.send(self.send_buffer, checksum=False)
|
|
else:
|
|
self.current_request.data.append(data[b])
|
|
else:
|
|
self.logger.warning("Received unexpected data: 0x%02x", data[b])
|
|
|
|
if self.current_request and not self.current_request.processed:
|
|
try:
|
|
resp = self.process_request(self.current_request)
|
|
|
|
if resp is not None and not self.current_request.processed:
|
|
self.current_request.processed = True
|
|
self.send(resp)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
self.logger.exception("Request process failed!")
|
|
|
|
def send(self, data, checksum=True):
|
|
data = list(data)
|
|
if checksum:
|
|
data.append(0x100 | compute_chk(data))
|
|
|
|
msg = struct.pack("<%dh" % len(data), *data)
|
|
|
|
self.send_buffer = data
|
|
|
|
self.logger.debug(
|
|
">> [%d bytes] %s", len(data), " ".join(["0x%02x" % b for b in data])
|
|
)
|
|
|
|
self.backend.write(msg)
|
|
|
|
def process_request(self, req):
|
|
# Unimplemented...
|
|
return
|
|
|
|
|
|
#
|
|
# This is mostly working cashless device implementation
|
|
#
|
|
|
|
|
|
class CashlessMDBDevice(MDBDevice):
|
|
base_address = CASHLESS_RESET
|
|
state = "IDLE"
|
|
last_poll = 0
|
|
config_data = [
|
|
0x01, # Feature level
|
|
0x19,
|
|
0x85, # PLN x---DD
|
|
1, # scaling factor
|
|
0x02, # decimal places factor
|
|
10, # 10s response time
|
|
0x00, # misc options...
|
|
]
|
|
|
|
manufacturer = "GMD"
|
|
serial_number = "123456789012"
|
|
model_number = "123456789012"
|
|
software_version = (0x21, 0x37)
|
|
|
|
lockup_counter = 0
|
|
|
|
def process_request(self, req):
|
|
if (req.command & self.base_address) != self.base_address:
|
|
# Target mismatch
|
|
return
|
|
|
|
if not req.validate_checksum():
|
|
# Invalid checksum
|
|
return
|
|
|
|
self.lockup_counter = 0
|
|
|
|
if req.command == CASHLESS_RESET:
|
|
self.state = "RESET"
|
|
self.logger.info("RESET: Device reset")
|
|
self.poll_queue.put([0x00])
|
|
|
|
return []
|
|
|
|
elif req.command == CASHLESS_POLL:
|
|
self.last_poll = time.time()
|
|
|
|
if not self.poll_msg:
|
|
try:
|
|
self.poll_msg = self.poll_queue.get_nowait()
|
|
except queue.Empty:
|
|
pass
|
|
|
|
if self.poll_msg:
|
|
self.logger.info("Sending POLL response: %r", self.poll_msg)
|
|
|
|
return self.poll_msg
|
|
|
|
elif req.command == CASHLESS_VEND:
|
|
if req.data[0] == 0x00: # vend request
|
|
self.logger.info("VEND: request %r", req)
|
|
value, product = struct.unpack(">xhhx", req.data)
|
|
self.logger.info(
|
|
"VEND: requested %d (%04x) for %d", product, product, value
|
|
)
|
|
if self.vend_request(product, value):
|
|
# accept. two latter bytes are value subtracted from balance
|
|
# displayed after purchase FIXME
|
|
return [0x05, 0x00, 0xFF]
|
|
else:
|
|
# welp?
|
|
return [0x06]
|
|
|
|
elif req.data[0] == 0x01: # vend cancel
|
|
self.logger.info("VEND: cancel")
|
|
return [0x06] # deny == ok
|
|
|
|
elif req.data[0] == 0x02: # vend succ
|
|
self.logger.info("VEND: success %r", req)
|
|
self.vend_success()
|
|
return []
|
|
|
|
elif req.data[0] == 0x03:
|
|
self.logger.info("VEND: failure")
|
|
self.vend_failure()
|
|
return []
|
|
|
|
elif req.data[0] == 0x04:
|
|
self.logger.info("VEND: session complete %r", req)
|
|
self.state = "OK"
|
|
self.vend_complete()
|
|
return [0x07]
|
|
|
|
elif req.data[0] == 0x05:
|
|
self.logger.info("VEND: cash sale")
|
|
return []
|
|
|
|
elif req.data[0] == 0x06:
|
|
self.logger.info("VEND: negative vend request")
|
|
return [0x06] # deny
|
|
else:
|
|
self.logger.warning("VEND: unknown command %r", req)
|
|
|
|
elif req.command == CASHLESS_EXP and req.data[0] == CASHLESS_EXP_ID:
|
|
self.logger.info("EXP_ID request: %r", req)
|
|
return (
|
|
bytearray([0x09])
|
|
+ bytearray( # peripheral ID
|
|
self.manufacturer.rjust(3, " ").encode("ascii")
|
|
)
|
|
+ bytearray(self.serial_number.rjust(12, "0").encode("ascii"))
|
|
+ bytearray(self.model_number.rjust(12, "0").encode("ascii"))
|
|
+ bytearray(self.software_version)
|
|
)
|
|
|
|
elif (
|
|
req.command == CASHLESS_SETUP and req.data[0] == 0x00 and len(req.data) == 6
|
|
):
|
|
vmc_level, disp_cols, disp_rows, disp_info = req.data[1:-1]
|
|
|
|
self.logger.info("SETUP config")
|
|
self.logger.info(" -> VMC level: %d", vmc_level)
|
|
self.logger.info(" -> Disp cols: %d", disp_cols)
|
|
self.logger.info(" -> Disp rows: %d", disp_rows)
|
|
self.logger.info(" -> Disp info: %d", disp_info)
|
|
|
|
self.state = "OK"
|
|
|
|
return [0x01] + self.config_data
|
|
|
|
elif req.command == CASHLESS_SETUP and req.data[0] == 0x01:
|
|
self.logger.info("SETUP max/min price: %r", req)
|
|
return []
|
|
|
|
elif req.command == CASHLESS_READER:
|
|
self.logger.info("READER update: %r", req.data[0])
|
|
return []
|
|
|
|
def begin_session(self, amount):
|
|
# Begins new session with balance provided
|
|
|
|
self.logger.info("Beginning session for %d", amount)
|
|
if amount > 10000:
|
|
amount = 10000
|
|
|
|
self.poll_queue.put([0x03, (amount >> 8) & 0xFF, amount & 0xFF])
|
|
|
|
def cancel_session(self):
|
|
# Cancels current session
|
|
self.poll_queue.put([0x04])
|
|
|
|
def vend_request(self, product, value):
|
|
# Called when user selects a product - funds shall be locked here
|
|
return True
|
|
|
|
def vend_success(self):
|
|
# Called when product has been released to user - funds shall be
|
|
# deducted here
|
|
pass
|
|
|
|
def vend_failure(self):
|
|
# Called when product release has failed - funds shall be reverted here
|
|
pass
|
|
|
|
def vend_complete(self):
|
|
# Called after vend request/success/failure
|
|
pass
|
|
|
|
@property
|
|
def online(self):
|
|
return time.time() - self.last_poll < 5
|
|
|
|
|
|
#
|
|
# This is mostly unfinished Bill validator implementation
|
|
#
|
|
|
|
|
|
class BillMDBDevice(MDBDevice):
|
|
scaling_factor = 50
|
|
|
|
bills = [
|
|
50,
|
|
100,
|
|
200,
|
|
500,
|
|
1000,
|
|
2000,
|
|
5000,
|
|
10000,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
]
|
|
|
|
feed_bills = []
|
|
|
|
def feed_amount(self, amount):
|
|
if amount % self.scaling_factor:
|
|
raise Exception("Invalid amount")
|
|
|
|
while amount > 0:
|
|
bills_list = filter(lambda v: v <= amount, self.bills)
|
|
bills_list.sort()
|
|
|
|
self.feed_bills.append(self.bills.index(bills_list[-1]))
|
|
amount -= bills_list[-1]
|