forked from Mapan/odoo17e
238 lines
7.9 KiB
Python
238 lines
7.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import serial
|
|
|
|
from odoo.addons.hw_drivers.event_manager import event_manager
|
|
from odoo.addons.hw_drivers.iot_handlers.drivers.SerialBaseDriver import SerialDriver, SerialProtocol, serial_connection
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
""" Protocol used for Swedish blackbox to communicate with RS232"""
|
|
SwedishBlackboxProtocol = SerialProtocol(
|
|
name="SWEDISH Retail Innovation Cleancash",
|
|
baudrate=9600,
|
|
bytesize=serial.EIGHTBITS,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
parity=serial.PARITY_NONE,
|
|
timeout=1,
|
|
writeTimeout=0.3,
|
|
measureRegexp=None,
|
|
statusRegexp=None,
|
|
commandTerminator=b"",
|
|
commandDelay=0.2,
|
|
measureDelay=0.2,
|
|
newMeasureDelay=0.2,
|
|
measureCommand=b"",
|
|
emptyAnswerValid=False,
|
|
)
|
|
|
|
"""Base Json for Swedish black box Messages.
|
|
Most of these constants are provided by Retail Innovation Cleancash and should not be changed.
|
|
"""
|
|
MainStatus = {
|
|
"0": "OK",
|
|
"1": "Warning condition(s) exists",
|
|
"2": "Protocol error condition(s) exists",
|
|
"3": "Non fatal error condition(s) exists",
|
|
"4": "Fatal Error condition(s) exists",
|
|
"5": "Busy (Not used in CCSP v2 currently).",
|
|
}
|
|
|
|
ErrorCode = {
|
|
"001": "Invalid LRC",
|
|
"002": "Unknown message type",
|
|
"003": "Invalid data/parameter",
|
|
"004": "Invalid sequence",
|
|
"005": "Deprecated (Not used)",
|
|
"006": "CleanCash® Not operational",
|
|
"007": "Invalid POS ID",
|
|
"008": "Internal error",
|
|
"009": "License exceeded (CCSP v2)",
|
|
"010": "Internal storage full (CCSP v2)",
|
|
"011": "Invalid sequence number",
|
|
}
|
|
|
|
SeverityError = {
|
|
"0": "Informational",
|
|
"1": "Warning",
|
|
"2": "Protocol error",
|
|
"3": "Non fatal error",
|
|
"4": "Fatal error",
|
|
}
|
|
|
|
StorageStatus = {
|
|
"0": "OK",
|
|
"1": "High level warning",
|
|
"2": "Transaction memory full",
|
|
}
|
|
|
|
|
|
class SwedishBlackBoxDriver(SerialDriver):
|
|
"""Driver for the Swedish blackbox fiscal data module."""
|
|
|
|
_protocol = SwedishBlackboxProtocol
|
|
|
|
def __init__(self, identifier, device):
|
|
super().__init__(identifier, device)
|
|
|
|
self.device_type = "fiscal_data_module"
|
|
self.data["value"] = device["UnitId"]
|
|
self.serial_number = 0
|
|
self._set_actions()
|
|
|
|
@classmethod
|
|
def supported(cls, device):
|
|
"""Checks whether the device at path `device` is supported by the driver.
|
|
:param device: path to the device
|
|
:type device: str
|
|
:return: whether the device is supported by the driver
|
|
:rtype: bool
|
|
"""
|
|
|
|
try:
|
|
protocol = cls._protocol
|
|
packet = cls._wrap_message("SQX")
|
|
with serial_connection(device["identifier"], protocol) as connection:
|
|
response = cls._send_to_blackbox(packet, connection, 2)
|
|
if len(response) > 1 and response[3] == "SRX":
|
|
device["UnitId"] = cls._get_unit_id(connection)
|
|
if response[4] != "0":
|
|
_logger.warning(
|
|
("Received error: %s - Severity: %s"),
|
|
MainStatus.get(response[4]),
|
|
SeverityError.get(response[5][:2])
|
|
)
|
|
_logger.warning("Sent request: %s", packet)
|
|
return True
|
|
except serial.serialutil.SerialTimeoutException:
|
|
pass
|
|
except Exception:
|
|
_logger.exception(
|
|
"Error while probing %s with protocol %s", device, protocol.name
|
|
)
|
|
|
|
@classmethod
|
|
def _get_unit_id(cls, connection):
|
|
""" "Get UnitId of Swedish Black box
|
|
:return: UnitId
|
|
:rtype: string
|
|
"""
|
|
try:
|
|
response = cls._request_action("IQ", connection)
|
|
if response[3] == "IR":
|
|
return response[4]
|
|
else:
|
|
_logger.error("Sent IQ request error")
|
|
return False
|
|
except Exception:
|
|
_logger.error("Did not receive a response")
|
|
|
|
@staticmethod
|
|
def _lrc(msg):
|
|
""" "Compute a message's longitudinal redundancy check value.
|
|
:param msg: the message the LRC is computed for
|
|
:type msg: byte
|
|
:return: the message LRC
|
|
:rtype: int
|
|
"""
|
|
|
|
lrc = ord(msg[0])
|
|
|
|
for i in range(1, len(msg)):
|
|
lrc ^= ord(msg[i])
|
|
|
|
return "{:02x}".format(int(lrc)).upper()
|
|
|
|
def _register_receipt(self, data):
|
|
"""The register receipt message registers a receipt. CleanCash® responds with Register
|
|
Receipt Response if OK or Negative Acknowledge if error"""
|
|
|
|
self.serial_number += 1
|
|
message = {"message_type": "RR", "serial_number": str(self.serial_number)}
|
|
message.update(data.get("high_level_message"))
|
|
request = "#".join(list(message.values()))
|
|
_logger.error(request)
|
|
response = self._request_action(request, self._connection)
|
|
_logger.error(response)
|
|
if len(response) > 1:
|
|
if response[3] == "RRR":
|
|
self.data["signature_control"] = response[4]
|
|
self.data["unit_id"] = response[5]
|
|
self.data["storage_status"] = StorageStatus.get(response[7])
|
|
self.data["status"] = "ok"
|
|
elif response[3] == "NAK":
|
|
_logger.error("Received error: %s", ErrorCode.get(response[4]))
|
|
_logger.error("Sent request: %s received NACK.", request)
|
|
self.data["status"] = ErrorCode.get(response[4])
|
|
else:
|
|
_logger.error("Error request: %s", data)
|
|
_logger.error("Response received: %s", response)
|
|
self.data["status"] = ("Sent request: %s without receiving response.", data)
|
|
event_manager.device_changed(self)
|
|
|
|
@classmethod
|
|
def _request_action(cls, data, connection):
|
|
|
|
packet = cls._wrap_message(data)
|
|
return cls._send_to_blackbox(packet, connection)
|
|
|
|
@classmethod
|
|
def _send_to_blackbox(cls, packet, connection, retry=1):
|
|
"""Sends a message to and wait for a response from the blackbox.
|
|
:param packet: the message to be sent to the blackbox
|
|
:type packet: bytearray
|
|
:param response_size: number of bytes of the expected response
|
|
:type response_size: int
|
|
:param connection: serial connection to the blackbox
|
|
:type connection: serial.Serial
|
|
:return: the response to the sent message
|
|
:rtype: bytearray
|
|
"""
|
|
|
|
ACK = ""
|
|
retries = 0
|
|
while ACK != "ACK" and retries < retry:
|
|
connection.write(packet)
|
|
response = connection.readline().decode().split("#")
|
|
|
|
try:
|
|
if response[3] != "NAK":
|
|
ACK = "ACK"
|
|
else:
|
|
_logger.error("Received error: %s", ErrorCode.get(response[4]))
|
|
_logger.error("Sent request: %s received NACK.", packet)
|
|
except Exception:
|
|
_logger.error("sent request: %s without receiving response.", packet)
|
|
|
|
retries += 1
|
|
|
|
return response
|
|
|
|
def _set_actions(self):
|
|
"""Initializes `self._actions`, a map of action keys sent by the frontend to backend action methods."""
|
|
|
|
self._actions.update(
|
|
{
|
|
"registerReceipt": self._register_receipt,
|
|
}
|
|
)
|
|
|
|
@classmethod
|
|
def _wrap_message(cls, high_level_message):
|
|
"""Builds a low level message to be sent the blackbox.
|
|
:param high_level_message: The message to be transmitted to the blackbox
|
|
:type high_level_message: str
|
|
:return: The modified message as it is transmitted to the blackbox
|
|
:rtype: bytearray
|
|
"""
|
|
|
|
length = 11 + len(high_level_message)
|
|
length = "{:03x}".format(int(length)).upper()
|
|
message = "#!#" + length + "#" + high_level_message + "#"
|
|
lrc = cls._lrc(message)
|
|
message += lrc + "\r"
|
|
|
|
return message.encode("utf-8")
|