bitvend/mdb/device.py

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]