forked from Mapan/odoo17e
849 lines
43 KiB
Python
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())
|