1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/iot/iot_handlers/drivers/IngenicoDriver.py
2024-12-10 09:04:09 +07:00

849 lines
43 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from binascii import unhexlify
import logging
from time import sleep
from traceback import format_exc
from zlib import crc32
import socket
from odoo.addons.hw_drivers.driver import Driver
from odoo.addons.hw_drivers.event_manager import event_manager
from odoo.addons.hw_drivers.iot_handlers.interfaces.SocketInterface import socket_devices
_logger = logging.getLogger(__name__)
# Because drivers don't get loaded as normal Python modules but directly in
# load_iot_handlers called by Manager.run, the log levels that get applied to the odoo
# import hierarchy won't apply here. This means DEBUG level messages will not display
# even if specified and INFO messages will show even if the log level is configured to
# be ERROR at the odoo-bin level. In order to work around this, it's possible to
# uncomment this line and set the desired level directly for this module.
# _logger.setLevel(logging.DEBUG)
class IngenicoTagType():
"""Tag type Function.
This class is used to make working with the provided Ingenico tags easier.
Instances of this class should only be generated by the static list
provided by Ingenico.
"""
def __init__(self, name, tag, tagFormat, tagLen):
"""
Args:
name (str): Human readable tag name.
tag (b): Identification tag formated as a byteArray.
tagformat (str): Format of the tag content.
* b: boolean values. Each boolean is 1 bit.
* a: ASCII characters
* i: Binari Code Decimals
* x: Hexadecimal digits
tagLen (int): Length of the tag content. This value is always the numbers of bytes
(This is not always the case in the official documentation provided by Ingenico!!)
"""
self.name = name
self.tag = tag
self.format = tagFormat
self.len = tagLen
def getDict(self):
"""Get a dictionary with the tag
Returns {
name (str): tag name,
tag (b): Tag identifier,
tagLen (int): The length of the tag identifier,
format (str): format of the tag content,
len (int): Length of the tag content
}
"""
return {
'name': self.name,
'tag': self.tag,
'tagLen': len(self.tag)/2,
'format': self.format,
'len': self.len,
}
def hasTag(self, tag):
"""Check if tag is equal
Check if a tag is equal, regardless of the case of the characters. The case does not change anything
in hexadecimal, but comparing without upper/lower would still give false negatives.
Returns True if equal
"""
return tag.upper() == self.tag.upper()
class IngenicoMessage():
"""Base Class for Ingenico Messages.
Use OutgoingIngenicoMessage or IncommingIngenicoMessage instead to initialize messages.
_const: Most of these constants are provided by Ingenico and should not be changed.
"""
_const = type('',(),{
'keepAliveInterval': b'\x00\x05',
'magic' : b'P4Y-ECR!',
'messageType' : {
'HelloRequest' : b'\x00\x00\x00\x16', #!< Request# a# connection# with# the# ECR.
'HelloResponse' : b'\x00\x00\x00\x17', #!< Result# of# the# connection# request.
'KeepAliveRequest' : b'\x00\x00\x00\x18', #!< Notification# of# status# and# keep-alive.
'KeepAliveResponse' : b'\x00\x00\x00\x19', #!< Result# of# the# notification.
'ByeRequest' : b'\x00\x00\x00\x20', #!< Request# to# terminate# the# connection.
'ByeResponse' : b'\x00\x00\x00\x21', #!< Result# of# terminate# connection# request.
'AcquirerDownloadListRequest' : b'\x00\x00\x00\x30', # The# ECR# Requests# CTAP# to# give# a# list# with# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'AcquirerDownloadListResponse' : b'\x00\x00\x00\x31', # CTAP# sends# the# ECR# a# list# of# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'AcquirerDelMsgListRequest' : b'\x00\x00\x00\x32', # The# ECR# Requests# CTAP# to# give# a# list# with# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'AcquirerDelMsgListResponse' : b'\x00\x00\x00\x33', # CTAP# sends# the# ECR# a# list# of# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'AcquirerDelMsgRequest' : b'\x00\x00\x00\x34', # The# ECR# Requests# CTAP# to# give# a# list# with# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'AcquirerDelMsgResponse' : b'\x00\x00\x00\x35', # CTAP# sends# the# ECR# a# list# of# available# acquirers# (used# to# select# one# for# an# acquirer# download)
'PerformAcqDownLoadRequest' : b'\x00\x00\x00\x36',
'PerformAcqDownLoadResponse' : b'\x00\x00\x00\x37',
'SecuritySchemeListRequest' : b'\x00\x00\x00\x38', # The# ECR# Requests# CTAP# to# give# a# list# with# available# security-schemes# (used# to# select# one# for# an# security-scheme# download)
'SecuritySchemeListResponse' : b'\x00\x00\x00\x39', # CTAP# sends# the# ECR# a# list# of# available# security-schemes# (used# to# select# one# for# an# security-scheme# download)
'PerformKeyLoadRequest' : b'\x00\x00\x00\x40',
'PerformKeyLoadResponse' : b'\x00\x00\x00\x41',
'PrintInfoRequest' : b'\x00\x00\x00\x46',
'PrintInfoResponse' : b'\x00\x00\x00\x47',
'TransactionRequest' : b'\x00\x00\x00\x48', #!< Request# to# perform# a# transaction.
'TransactionResponse' : b'\x00\x00\x00\x49', #!< Result# of# the# transaction.
'TotalsRequest' : b'\x00\x00\x00\x50', #!< Request# an# overview# of# the# counters# (print# on# terminal# or# send# to# ECR).
'TotalsResponse' : b'\x00\x00\x00\x51', #!< Result# of# the# totals# request.
'LastTicketRequest' : b'\x00\x00\x00\x52', #!< Request# the# last# ticket# (print# on# terminal# or# send# to# ECR).# (Was# called# print# request# in# IDD).
'LastTicketResponse' : b'\x00\x00\x00\x53', #!< Result# of# the# last# ticket# request.# (Was# called# print# response# in# IDD).
'CancelRequest' : b'\x00\x00\x00\x54', #!< Request# the# cancellation# of# an# on-going# operation.
'CancelResponse' : b'\x00\x00\x00\x55', #!< Result# of# the# cancellation# request.
'LastTransactionRequest' : b'\x00\x00\x00\x56', #!< Request# the# result# of# the# last# transaction.
'LastTransactionResponse' : b'\x00\x00\x00\x57', #!< Result# of# the# last# transaction# request.
'PrintConfirmationRequest' : b'\x00\x00\x00\x64', #!< Print# confirmation# from# ECR# to# terminal# when# a# card# holder# ticket# must# be# printed# on# the# ECR.
'PrintConfirmationResponse' : b'\x00\x00\x00\x65', #!< Print# confirmation# from# ECR# to# terminal# when# a# card# holder# ticket# must# be# printed# on# the# ECR.
'PrintRequest' : b'\x00\x00\x00\x66', #!< Request# to# print# some# data# (e.g.# ECR# ticket)# on# the# terminal# printer.# (ECR# does# not# need# a# printer# then)# (this# message# is# not# in# IDD).
'PrintResponse' : b'\x00\x00\x00\x67', #!< Result# of# the# print# request# (this# message# is# not# in# IDD).
'IntermediateResultRequest' : b'\x00\x00\x00\x68', #!< Request# with# the# intermediate# result.
'IntermediateResultResponse' : b'\x00\x00\x00\x69', #!< Result# of# the# intermediate# result# request# (continue/abort# transaction).
'InformationReport' : b'\x00\x00\x00\x80', #!< Report# the# progress# of# a# transaction# and# other# information# like# merchant# messages.
'SettingsRequest' : b'\x00\x00\x00\x82', #!< Change# one# or# more# settings# in# the# terminal.
'SettingsResponse' : b'\x00\x00\x00\x83', #!< Result# of# the# change# settings# request.
'VersionInformationRequest' : b'\x00\x00\x00\x90', #!< Request# the# version# of# the# terminal# software
'VersionInformationResponse' : b'\x00\x00\x00\x91', #!< Result# of# the# version# request.
'PerformTmsSessionReuqest' : b'\x00\x00\x00\x92',
'PerformTmsSessionResponse' : b'\x00\x00\x00\x93',
'RebootAndClearCtapDataBaseRequest' : b'\x00\x00\x01\x00', # there# is# no# response# on# this# command:# the# terminal# will# reboot
# Transparent# mode# messages.
'TmTransparentModeRequest' : b'\x00\x00\x10\x00', #!< Request# to# start# or# stop# transparent# mode.
'TmTransparentModeResponse' : b'\x00\x00\x10\x01', #!< Result# of# the# request# to# start# or# stop# transparent# mode.
'TmUiControlRequest' : b'\x00\x00\x10\x02', #!< Request# to# update# the# user# interface# (buzzer,# display,# LEDs)# when# in# transparent# mode.
'TmUiControlResponse' : b'\x00\x00\x10\x03', #!< Result# of# the# UI# request.
'TmAuthenticateRequest' : b'\x00\x00\x10\x04', #!< Request# to# authenticate# to# the# card# when# in# transparent# mode.
'TmAuthenticateResponse' : b'\x00\x00\x10\x05', #!< Result# of# the# authenticate# request.
'TmReadCardDataRequest' : b'\x00\x00\x10\x06', #!< Request# to# read# data# from# the# card# when# in# transparent# mode.
'TmReadCardDataResponse' : b'\x00\x00\x10\x07', #!< Result# of# the# read# card# data# request.
'TmWriteCardDataRequest' : b'\x00\x00\x10\x08', #!< Request# to# write# data# to# the# card# when# in# transparent# mode.
'TmWriteCardDataResponse' : b'\x00\x00\x10\x09', #!< Result# of# the# write# card# data# request.
'TmStatusRequest' : b'\x00\x00\x10\x10', #!< Request# the# status# of# the# transparent# mode.
'TmStatusResponse' : b'\x00\x00\x10\x11', #!< Result# of# the# status# request.
},
'tagType' : [
IngenicoTagType( 'None' , '00' , '' ,False ),
# Header, body, footer primitive tags.
IngenicoTagType( 'TransactionStage' , '0E' , 'x' , 1 ),
IngenicoTagType( 'StageMessage' , '0F' , 'a' , False ),
IngenicoTagType( 'ProtocolId' , '10' , 'i' , 4 ),
IngenicoTagType( 'MessageType' , '11' , 'i' , 4 ),
IngenicoTagType( 'TerminalId' , '12' , 'a' , False ),
IngenicoTagType( 'EcrId' , '13' , 'a' , False ),
IngenicoTagType( 'SequenceNumber' , '14' , 'x' , 2 ),
IngenicoTagType( 'KeepAliveReason' , '15' , 'x' , 1 ),
IngenicoTagType( 'ResultCode' , '16' , 'x' , 2 ),
IngenicoTagType( 'ByeReason' , '17' , 'x' , 1 ),
IngenicoTagType( 'Language' , '18' , 'a' , False ),
IngenicoTagType( 'TerminalState' , '19' , 'b' , 8 ),
IngenicoTagType( 'TransactionId' , '1A' , 'x' , 8 ),
IngenicoTagType( 'PrintResult' , '1B' , '' , False ),
IngenicoTagType( 'Mdc' , '1C' , 'x' , False ),
IngenicoTagType( 'MerchantText' , '1D' , 'a' , False ),
IngenicoTagType( 'CancelReason' , '1E' , 'x' , 1 ),
# Communication parameter group tags.
IngenicoTagType( 'IpAddress' , '40' , '' , False ),
IngenicoTagType( 'PortNumber' , '41' , '' , False ),
# Connection parameters groups tags.
IngenicoTagType( 'ConnectionTimeout' , '47' , '' , False ),
IngenicoTagType( 'ConnectionRetries' , '48' , '' , False ),
IngenicoTagType( 'KeepAliveInterval' , '49' , 'i' , 2 ),
# Ticket data group tags.
IngenicoTagType( 'TicketType' , '4A' , 'x' , False ),
IngenicoTagType( 'TicketHeader' , '4B' , '' , False ),
IngenicoTagType( 'TicketBody' , '4C' , 'a' , False ),
IngenicoTagType( 'TicketFooter' , '4D' , '' , False ),
# Print data group.
IngenicoTagType( 'PrintOrigin' , '4E' , '' , False ),
IngenicoTagType( 'PaperWidth' , '4F' , '' , False ),
# Transaction (information) group tags.
IngenicoTagType( 'Amount' , '50' , 'i' , 4 ),
IngenicoTagType( 'CurrencyCode' , '51' , 'i' , 4 ),
IngenicoTagType( 'CurrencyExponent' , '52' , 'i' , 2 ), # Named Decimal in IDD.
IngenicoTagType( 'ProgressReportLanguage' , '53' , 'a' , 2 ),
IngenicoTagType( 'TransactionType' , '54' , '' , False ),
IngenicoTagType( 'MerchantTransactionReference' , '55' , '' , False ),
IngenicoTagType( 'TransactionResult' , '56' , '' , False ),
IngenicoTagType( 'TransactionDateTime' , '57' , '' , False ),
IngenicoTagType( 'IntermediateResultMode' , '58' , '' , False ),
IngenicoTagType( 'TransactionMode' , '59' , '' , False ),
IngenicoTagType( 'AuthorisationCode' , '1F70' , '' , False ),
IngenicoTagType( 'Token' , '1F71' , '' , False ),
# Settings# (result) group.
IngenicoTagType( 'SettingId' , '5A' , '' , False ),
IngenicoTagType( 'SettingType' , '5B' , '' , False ),
IngenicoTagType( 'SettingValue' , '5C' , '' , False ),
IngenicoTagType( 'SettingResult' , '5D' , '' , False ),
IngenicoTagType( 'TotalsType' , '5F54' , '' , False ),
IngenicoTagType( 'InfoType' , '5F55' , '' , False ),
# Version information tags.
IngenicoTagType( 'ApplicationId' , '80' , '' , False ),
IngenicoTagType( 'LogicalId' , '81' , 'a' , False ),
IngenicoTagType( 'SerialNumber' , '82' , 'a' , False ),
IngenicoTagType( 'VersionNumber' , '83' , '' , False ),
IngenicoTagType( 'VersionString' , '84' , '' , False ),
IngenicoTagType( 'ExtraInformationName' , '85' , '' , False ),
IngenicoTagType( 'ExtraInformationValue' , '86' , '' , False ),
IngenicoTagType( 'ExtraInformationUnit' , '87' , '' , False ),
# Group tags.
IngenicoTagType( 'Group_EncryptionParameters' , 'A0' , 'GRP' , False ),
IngenicoTagType( 'Group_CommunicationParameters', 'A1' , 'GRP' , False ),
IngenicoTagType( 'Group_SupportedLanguages' , 'A2' , 'TBL' , False ),
IngenicoTagType( 'Group_TransactionData' , 'A3' , 'GRP' , False ),
IngenicoTagType( 'Group_ConnectionParameters' , 'A4' , 'GRP' , False ),
IngenicoTagType( 'Group_PrintData' , 'A5' , 'GRP' , False ),
IngenicoTagType('Group_TicketData', 'A6', 'TBL', False),
IngenicoTagType( 'Group_ExtraInformation' , 'A7' , 'GRP' , False ),
IngenicoTagType( 'Group_TransactionInformation' , 'A8' , 'GRP' , False ),
IngenicoTagType( 'Group_TcpParameter' , 'A9' , 'GRP' , False ),
IngenicoTagType( 'Group_UsbParameters' , 'AA' , 'GRP' , False ),
IngenicoTagType( 'Group_SerialParameters' , 'AB' , 'GRP' , False ),
IngenicoTagType( 'Group_Settings' , 'AC' , 'GRP' , False ),
IngenicoTagType( 'Group_SettingsResult' , 'AD' , 'GRP' , False ),
# General group tags.
IngenicoTagType( 'Group_Header' , 'E1' , 'GRP' , False ),
IngenicoTagType( 'Group_Body' , 'E2' , 'GRP' , False ),
IngenicoTagType( 'Group_Footer' , 'E3' , 'GRP' , False ),
IngenicoTagType( 'Group_TableRecord' , 'EF' , 'REC' , False ), # Used for repeated fields. e.g. The ticket data tag contains for each ticket a table record tag.
IngenicoTagType( 'Group_Root' , 'F0' , 'GRP' , False ),
# Transparent mode tags.
IngenicoTagType( 'TmTransparentMode' , '1F01' , '' , False ),
IngenicoTagType( 'TmCardDetectionTimeout' , '1F02' , '' , False ),
IngenicoTagType( 'TmCardUid' , '1F10' , '' , False ),
IngenicoTagType( 'TmCardAtr' , '1F11' , '' , False ),
IngenicoTagType( 'TmCardType' , '1F12' , '' , False ),
# Transparent mode UI Control tags.
IngenicoTagType( 'TmDisplayText' , '1F20' , '' , False ),
IngenicoTagType( 'TmBeepType' , '1F21' , '' , False ),
IngenicoTagType( 'TmLedControl' , '1F22' , '' , False ),
# Transparent mode authentication/read data/writ,.
IngenicoTagType( 'TmKey' , '1F30' , '' , False ),
IngenicoTagType( 'TmAddress' , '1F31' , '' , False ),
IngenicoTagType( 'TmDataSize' , '1F32' , '' , False ),
IngenicoTagType( 'TmData' , '1F33' , '' , False ),
# Transparent mode groups.
IngenicoTagType( 'Group_TmTransparentMode' , '3F01' , '' , False ),
IngenicoTagType( 'Group_TmUiControl' , '3F02' , '' , False ),
IngenicoTagType( 'RebootAndClearType' , 'C1' , '' , False ),
IngenicoTagType( 'SendOrDelete' , 'C2' , '' , False ), # used# for# pending# messages# :# 1=send# 2=delete
IngenicoTagType( 'Group_AcquirerList' , 'E7' , '' , False ), # group: AcquirerIdentifier=0xDF68, AcquirerLabelName=0xDF69
IngenicoTagType( 'Group_SecuritySchemeList' , 'BF01' , '' , False ), # group: SecuritySchemeIdentifier=0xDF6A, SecuritySchemeLabelName=0xDF6B
IngenicoTagType( 'CardholderLanguage' , 'DF1A' , '' , False ),
IngenicoTagType( 'Card_Brand_Identifier' , 'DF5F' , 'i' , 2 ),
IngenicoTagType( 'SecuritySchemeIdentifier' , 'DF8204' , '' , False ),
IngenicoTagType( 'AcquirerIdentifier' , 'DF68' , '' , False ),
IngenicoTagType( 'AcquirerLabelName' , 'DF69' , '' , False ),
],
'transactionStage' : {
b'\x00' : 'None',
b'\x01' : 'WaitingForCard',
b'\x02' : 'WaitingForPin',
b'\x03' : 'WaitingForTransaction',
b'\x04' : 'Finished',
b'\x05' : 'WaitingForTipInput',
b'\x06' : 'WaitingForConfirmationService',
b'\x07' : 'WaitingForConfirmationAmount',
b'\x08' : 'WaitingForConfirmationServiceAndAmount',
b'\x09' : 'WaitingForCardRemoval',
b'\x0a' : 'WaitingForLastTransactionResult',
b'\x0b' : 'WaitingForApplicationSelection',
b'\x0c' : 'CardDetected',
b'\x0d' : 'WaitingForIntermediateResult',
b'\x0e' : 'CardRemoved',
},
'transactionResult' : {
b'\x00' : 'Approved',
b'\x01' : 'Error',
b'\x02' : 'Declined',
b'\x03' : 'Stopped',
b'\x04' : 'TechnicalProblem',
b'\x05' : 'TransparentMode',
},
'cancelReasons' : {
'manual' : b'\x00',
'system' : b'\x01',
},
'byeReasons' : {
'Deactivate' : b'\x01',
'Shutdown' : b'\x02',
'Reboot' : b'\x03',
'Reconnect' : b'\x04',
'BatteryEmpty' : b'\x05',
},
})()
@classmethod
def _getTagDetailsByCode(cls, tagCode):
"""Search for tag in _const using the hex identifier.
Returns InenicoTagType instance.
Args:
tagCode (b): hexadecimal identifier of tag.
"""
return next((tagType for tagType in cls._const.tagType if tagType.hasTag(tagCode) == True), None)
@classmethod
def _getTagDetailsByName(cls, tagName):
"""Search for tag in _const providing the Human readable name.
Returns InenicoTagType instance.
Args:
tagCode (b): hexadecimal identifier of tag.
"""
return next((tagType for tagType in cls._const.tagType if tagType.name == tagName), None)
def __init__(self, dev):
"""Base Initialisation of Ingenico Message.
Args:
dev (Obj): tcp socket (or other device with byte-based send and recv function)
"""
self.dev = dev
class OutgoingIngenicoMessage(IngenicoMessage):
@staticmethod
def _withLength(msg, length):
"""Return tag content with given length.
Some tags have to have a fixed length to be accepted by the payment terminal. This function will add null-bytes
to match the required length.
Args:
msg (b): the message to edit
length (int): wanted length
"""
try:
toAdd = length - len(msg)
except:
_logger.error(format_exc())
if toAdd > 0:
return b'\x00' * toAdd + msg
return msg
@staticmethod
def _getCRC32(msg):
"""Return the crc for the specified message as a bytestring.
The result will always be 4 bytes long.
Args:
msg (b): the message to calculate the CRC for
"""
return unhexlify('{:08x}'.format(crc32(msg)))
@classmethod
def _generateTag(cls, tagName, content):
"""Return formatted tag with tag identifier + length + content.
The content of a tag often includes other tags, these have to be already formatted.
Args:
tagName (str): Human readable tag name
content (b): formatted tag content
"""
tag = cls._getTagDetailsByName(tagName)
if tag.len:
return unhexlify(tag.tag) + chr(tag.len).encode() + cls._withLength(content, tag.len)
return unhexlify(tag.tag) + chr(len(content)).encode() + content
@classmethod
def _generateMsg(cls, header, body, footer):
"""Return The formatted outgoing message including MessageLength and Magic string.
This is the very last step of the message generation. All arguments have to be completely formatted.
Args:
header (b)
body (b)
footer (b)
"""
root = cls._generateTag("Group_Root", header + body + footer)
msgLength = (len(cls._const.magic + root)).to_bytes(3, byteorder='big')
while len(msgLength) < 4:
msgLength = b'\x00' + msgLength
return msgLength + cls._const.magic + root
def __init__(self, dev, terminalId, ecrId, protocolId, messageType, sequence, **kwargs):
"""Initialisation of Outgoing Ingenico messages.
After initialisation the message will be automatically generated. the send function can be called to send the
message to the device.
Args:
dev (Obj): tcp socket (or other device with byte-based send and recv function)
protocolId
messageType
Kwargs:
keepAliveInterval
keepAliveResult
resultCode
transactionId
amount
reason
"""
super().__init__(dev)
self.terminalId = terminalId
self.ecrId = ecrId
self.protocolId = protocolId
messageTypes = self._const.messageType
self.messageTypeId = messageTypes[messageType]
self.sequence = sequence
self.resultCode = b'\x00'
if messageType in ["CancelRequest", "ByeRequest", "KeepAliveResponse"]:
self.reason = kwargs["reason"]
elif messageType == "HelloResponse":
self.keepAliveInterval = self._const.keepAliveInterval
elif messageType == "LastTransactionStatusRequest":
self.transactionId = kwargs["transactionId"]
elif messageType == "TransactionRequest":
self.transactionId = kwargs["transactionId"]
self.amount = kwargs["amount"]
header = self._generateHeader()
body, mdc = self._generateBody(self.messageTypeId)
footer = self._generateFooter(mdc)
self.message = self._generateMsg(header, body, footer)
self.send()
def _generateHeader(self):
"""Return formatted header.
The header does not depend on the message type.
"""
return self._generateTag( "Group_Header",
self._generateTag( "ProtocolId", self.protocolId) +
self._generateTag( "MessageType", self.messageTypeId) +
self._generateTag( "TerminalId", self.terminalId) +
self._generateTag( "EcrId", self.ecrId.encode()) +
self._generateTag( "SequenceNumber", self.sequence)
)
def _generateFooter(self, mdc):
"""Return the formatted footer
The footer can only be created after the body has been generated.
Args:
mdc (b): The Modification Detection Code generated on the Body tag.
"""
return self._generateTag( "Group_Footer", mdc)
def _generateMDC(self, innerBody):
"""Return the Modification Detection Code needed to generate the footer.
This function gets called after generating the body and before generating the footer.
Args:
innerBody (b): formatted body excluding body-tag and length.
"""
return self._generateTag("Mdc", self._getCRC32(innerBody))
def _generateBody(self, messageTypeId):
"""Return formatted body and Modification Detection Code.
Args:
messageTypeId (b): Hexadecimal message type identifier.
"""
innerBody = b''
messageTypes = self._const.messageType
if messageTypeId == messageTypes["HelloResponse"]:
innerBody = self._generateTag( "ResultCode", self.resultCode) + \
self._generateTag( "Group_ConnectionParameters",
self._generateTag( "KeepAliveInterval", self.keepAliveInterval,))
elif messageTypeId == messageTypes["KeepAliveResponse"]:
innerBody = self._generateTag( "KeepAliveReason", self.reason) + \
self._generateTag( "ResultCode", self.resultCode)
elif messageTypeId == messageTypes["TransactionRequest"]:
innerBody = self._generateTag( "TransactionId",
unhexlify('{:016x}'.format(int(self.transactionId)))) +\
self._generateTag( "Group_TransactionData", self._generateTag( "Amount" ,
int(str(self.amount), 16).to_bytes(4, byteorder='big'))) +\
self._generateTag( "Group_PrintData", self._generateTag( "PrintOrigin",b'\x02'))
elif messageTypeId == messageTypes["CancelRequest"]:
innerBody = self._generateTag( "CancelReason", self._const.cancelReasons[self.reason])
return self._generateTag( "Group_Body", innerBody), self._generateMDC(innerBody)
def send(self):
"""Send the generated message to the device.
This is the only function that has to be called manually!
"""
self.dev.send(self.message)
class IncomingIngenicoMessage(IngenicoMessage):
@staticmethod
def _hexToInt(byteArray):
return int.from_bytes(byteArray, byteorder='big')
def _getMsg(self, length ):
"""Return a dictionary of the next tag in the buffer.
Returns the decoded content of an message tag. If the tag is an group of other tags, this function will get
called again to generate an dictionary of the entire message tree.
Returns length left in parent tag.
Args:
length (int): length left to be read in the parent tag.
"""
tag = self._getTag()
tag['len'], lengthBytes = self._getLength()
if tag['format'] in ['GRP', 'REC']:
xTags = {}
xMsgLength = tag['len']
while xMsgLength > 0:
xTag, xMsgLength = self._getMsg(xMsgLength)
xTags[xTag['name']] = xTag['msg']
tag["msg"] = xTags
elif tag['format'] == 'TBL':
xTags = []
xMsgLength = tag['len']
while xMsgLength > 0:
xTag, xMsgLength = self._getMsg(xMsgLength)
if xTag['format'] != 'REC':
_logger.warning("Expected REC field but got %s with tag %s", xTag['format'], xTag['tag'])
xTags.append(xTag['msg'])
tag["msg"] = xTags
else:
tag["msg"] = self.dev.recv(tag['len'])
return tag, length - (tag['len'] + lengthBytes +tag['tagLen'] )
def __init__(self, dev):
"""Initialisation of incomming Ingenico messages.
After initialisation there will be a check if there is an Ingenico message available. If so, the message will
be requested from the socket buffer and will be decoded. The data will be made available in the variable
_tagTree
All data is read directly from the device buffer. It is from upmost importance to call the read functions in the
correct sequence. The messages from Ingenico have the Tag Length Value format. Becouse the mixed content of the
messages the standard Python TLV library cannot be used to decode the messages.
Raises:
ValueError: If the `Magic String` is not found an error will be thrown indicating the received message is
no Ingenico message.
Args:
dev (Obj): tcp socket (or other device with byte-based send and recv function)
"""
super().__init__(dev)
# If we're being called in `supported`, `self.dev` will be a socket. If we're
# being called in `run`, `self.dev` will be an IngenicoDriver and `self.dev.dev`
# will be the socket.
if hasattr(self.dev, 'dev'):
_logger.debug("Listening on: %s", self.dev.dev)
# Receive message length and reduce it with length of magic string
_logger.debug("Waiting for message length")
length = self._hexToInt(self.dev.recv(4)) - 8
# Check if message is from Ingenico terminal by comparing magic string
_logger.debug("Waiting for magic string")
self.magic = self.dev.recv(8)
if self.magic and self.magic == self._const.magic:
# Receive and decode message
self._tagTree, leftLength = self._getMsg(length)
else:
_logger.warning('Out of magic!')
def _getLength(self):
"""Returns the message length of the tag as well as the length of the message length itself
The length is read directly from the device buffer. It is important to call this function only after receiving
the tag identifier.
"""
# The message length has a short form that fits in one byte (for lengths < 128)
# and a variable length long form where bit 8 is set on the first byte and
# the other bits specify the amount of bytes that follow. Those bytes then
# contain the length of the actual message. See section 2.1.2 in v1.0.9 of the
# TLV Cash Register Interface Specification. It's allowed to encode
# lengths < 128 using the long form.
length = int(self.dev.recv(1).hex(), 16)
if length // 128 == 1:
return int(self.dev.recv(length % 128).hex(), 16), 1 + length % 128
else:
return length, 1
def _getTag(self):
"""Return the tag identifier
The tag identifier is read directly from the device buffer.
"""
tagLength = 1
tag = self.dev.recv(1).hex()
if int(tag, 16) % 32 == 31:
getNext = True
while (getNext):
tagLength += 1
nextByte = self.dev.recv(1).hex()
if (int(nextByte, 16) < 128):
getNext = False
tag += nextByte
tagObject = self._getTagDetailsByCode(tag)
return tagObject.getDict()
def getProtocolId(self):
"""Return The Protocol Id from the tagtree.
"""
return self._tagTree['msg']['Group_Header']['ProtocolId']
def getTerminalId(self):
"""Return The Protocol Id from the tagtree.
"""
return self._tagTree['msg']['Group_Header']['TerminalId']
def getTransactionResult(self):
"""Return The Protocol Id from the tagtree.
"""
if 'TransactionResult' in self._tagTree['msg']['Group_Body'].keys():
return self._const.transactionResult[self._tagTree['msg']['Group_Body']['TransactionResult']]
return False
def getTransactionStage(self):
"""Return The Transaction Stage from the tagtree.
If the transaction stage is not found return False.
"""
if 'TransactionStage' in self._tagTree['msg']['Group_Body'].keys():
return self._const.transactionStage[self._tagTree['msg']['Group_Body']['TransactionStage']]
return False
def getTransactionTicket(self):
"""Return The Transaction ticket from the tagtree.
If there is no ticket data available return False.
"""
if 'Group_TicketData' in self._tagTree['msg']['Group_Body']:
# We currently don't do anything with different ticket types and ignore
# headers and footers. The TLV Cash Register Interface spec mentions header
# (4.5.7.2) and footer (4.5.7.4) currently being empty, but that they might
# be implemented in the future.
ticket_data = self._tagTree['msg']['Group_Body']['Group_TicketData']
ticket_bodies = [r['TicketBody'] for r in ticket_data if 'TicketBody' in r and r['TicketBody']]
return b'\n'.join(ticket_bodies)
return False
def getKeepAliveInterval(self):
"""Return the keep alive interval from the tagtree.
If there is connection data available return False.
"""
if 'Group_ConnectionParameters' in self._tagTree['msg'].keys():
return self._tagTree['msg']['Group_ConnectionParameters']['KeepAliveInterval']
return False
def getKeepAliveReasonId(self):
"""Return The keep alive reason from the tagtree.
If the message is no keep alive message return False.
"""
if 'KeepAliveReason' in self._tagTree['msg']['Group_Body'].keys():
return self._tagTree['msg']['Group_Body']['KeepAliveReason']
return False
def getMessageType(self):
"""Return The message type from the constants, as found in the tagtree.
"""
messageTypeId = self._tagTree['msg']['Group_Header']['MessageType']
return next((mt for mt, mtId in self._const.messageType.items() if mtId == messageTypeId and not mt == "HelloResponse" ), None)
class IngenicoDriver(Driver):
connection_type = 'socket'
_ecrId = 'odoo'
def __init__(self, identifier, device):
super(IngenicoDriver, self).__init__(identifier, device)
self.dev = device.dev
self._terminalId = device.terminalId
self._protocolId = device.protocolId
self._sequence = 0
self.device_type = 'payment'
self.device_connection = 'network'
self.device_name = 'Ingenico payment terminal'
self.device_manufacturer = 'Ingenico'
self.cid = None
self._actions.update({
'': self._action_default,
})
@classmethod
def supported(cls, device):
"""Try to initialize a connection with the payment terminal.
Override
"""
try:
# Setup socket connection
msg = IncomingIngenicoMessage(device.dev)
if msg and msg.magic == b'P4Y-ECR!' and msg.getMessageType() == "HelloRequest":
device.terminalId = msg.getTerminalId()
device.protocolId = msg.getProtocolId()
OutgoingIngenicoMessage( device.dev, device.terminalId, cls._ecrId, device.protocolId, "HelloResponse", b'\x00')
return True
elif msg and msg.magic == b'P4Y-ECR!' and msg.getMessageType() == "KeepAliveRequest":
device.terminalId = msg.getTerminalId()
device.protocolId = msg.getProtocolId()
OutgoingIngenicoMessage(device.dev, device.terminalId, cls._ecrId, device.protocolId, "KeepAliveResponse", b'\x00', reason=msg.getKeepAliveReasonId())
return True
return False
except Exception:
_logger.error(format_exc())
return False
def disconnect(self):
# Close the socket but leave the socket_devices entry. If we were to delete it,
# and the SocketInterface gets a new connection from the terminal, it would
# create a new socket_devices entry with the same key. Interface's
# update_iot_devices method would not detect that something changed and no new
# IngenicoDriver thread would be created, resulting in a deadlock. What will
# instead happen by leaving the socket_devices entry, is that
# replace_socket_device will get called instead. It will update
# _detected_devices in Interface, so update_iot_devices will create a new
# IngenicoDriver thread. None of this is ideal, but a cleaner fix would require
# changing the architecture of Interface and how interfaces and drivers can
# talk to each other.
sock = socket_devices[self.device_identifier].dev
try:
sock.shutdown(socket.SHUT_RD)
except OSError:
# A bad file descriptor OSError will be thrown if the socket was already
# closed
pass
sock.close()
super().disconnect()
def _getSequence(self):
"""Returns the sequence number for the next outgoing message.
The sequence of incomming and outgoing messages are unrelated. If the sequence of outgoing messages is wrong
the terminal will automatically close the connection.
"""
self._sequence += 1
return (self._sequence%(256**2)).to_bytes(2,byteorder='big')
def _outgoingMessage(self, messageType, **kwargs):
"""Base function to generate in instance of OutgoingIngenicoMessage.
"""
OutgoingIngenicoMessage( self, self._terminalId, self._ecrId,
self._protocolId, messageType, self._getSequence(), **kwargs)
def _action_default(self, data):
"""Action trigered on request from Odoo.
Override
"""
try:
self.data["Ticket"] = False
if data['messageType'] == 'Transaction':
self.cid = data['cid']
if data['amount'] < 0:
raise ValueError("The transaction amount value should be positive")
self._outgoingMessage( "TransactionRequest", transactionId=data['TransactionID'], amount=data['amount'])
elif data['messageType'] == 'Cancel':
self._outgoingMessage( "CancelRequest", reason=data['reason'])
except Exception as e:
error_message = "Error while performing transaction request to the Ingenico payment terminal"
_logger.exception(error_message)
self.data["Error"] = "{}\n{}: {}".format(error_message, type(e).__name__, e)
self.data["cid"] = self.cid
event_manager.device_changed(self)
def recv(self, length):
try:
return self.dev.recv(length)
except socket.error as e:
_logger.error("Socket error in recv: %s", e)
def send(self, request):
try:
return self.dev.send(request)
except socket.error as e:
_logger.error("Socket error in send: %s", e)
def run(self):
"""If an payment terminal is found, start listening for messages from the terminal.
Override
"""
try:
self.data = {'value': '', 'Stage': False, 'Response': False, 'Ticket': False, 'Error': False}
while not self._stopped.isSet():
sleep(1)
_logger.debug("Waiting for incoming message")
msg = IncomingIngenicoMessage(self)
_logger.debug("Incoming message received")
if msg and msg.magic == b'P4Y-ECR!':
self.data['value'] = 'Connected'
self.data["Response"] = False
self.data["Error"] = False
msgType = msg.getMessageType()
if msgType == "KeepAliveRequest":
self._outgoingMessage( "KeepAliveResponse", reason=msg.getKeepAliveReasonId())
elif msgType == "TransactionResponse":
self.data["Response"] = msg.getTransactionResult() if msg.getTransactionResult() else self.data["Response"]
if self.data["Response"] == 'Error':
self.data["Error"] = 'Canceled'
self.data["Ticket"] = msg.getTransactionTicket() if msg.getTransactionTicket() else self.data["Ticket"]
self.data['Stage'] = msg.getTransactionStage() if msg.getTransactionStage() else self.data['Stage']
self.data['cid'] = self.cid
else:
_logger.info("Terminating due to an invalid message")
self.disconnect()
break
event_manager.device_changed(self)
except Exception:
_logger.info("Terminating due to an exception")
self.disconnect()
_logger.error(format_exc())