# -*- coding: utf-8 -*- import serial import unilog from . import misc, excepts from .compat import unicode, xrange, str_compat from .handlers import commands as hc STX = bytearray((0x02, )) # START OF TEXT - начало текста ENQ = bytearray((0x05, )) # ENQUIRY - запрос ACK = bytearray((0x06, )) # ACKNOWLEDGE - положительное подтверждение NAK = bytearray((0x15, )) # NEGATIVE ACKNOWLEDGE - отрицательное подтверждение class Protocol(object): MAX_ATTEMPTS = 10 CHECK_NUM = 3 def __init__(self, port, baudrate, timeout, fs=False): """ Класс описывающий протокол взаимодействия в устройством. :type port: str :param port: порт взаимодействия с устройством :type baudrate: int :param baudrate: скорость взаимодействия с устройством :type timeout: float :param timeout: время таймаута ответа устройства :type fs: bool :param fs: признак наличия ФН (фискальный накопитель) """ self.port = port self.serial = serial.Serial( baudrate=baudrate, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=timeout, writeTimeout=timeout ) self.fs = fs self.connected = False def connect(self): """ Метод подключения к устройству. """ if not self.connected: self.serial.port = self.port if not self.serial.isOpen(): try: self.serial.open() except serial.SerialException as exc: raise excepts.NoConnectionError( u'Не удалось открыть порт {} ({})'.format( self.port, exc ) ) for r in self.check(self.CHECK_NUM, True): if r: self.connected = True return else: self.serial.close() raise excepts.NoConnectionError() def disconnect(self): """ Метод отключения от устройства. """ if self.connected: self.serial.close() self.connected = False def init(self): """ Метод инициализации устройства перед отправкой команды. """ try: self.serial.write(ENQ) byte = self.serial.read() if not byte: raise excepts.NoConnectionError() if byte == NAK: pass elif byte == ACK: self.handle_response() else: while self.serial.read(): pass return False return True except serial.SerialTimeoutException: self.serial.flushOutput() raise excepts.ProtocolError(u'Не удалось записать байт в ККМ') except serial.SerialException as exc: self.serial.flushInput() raise excepts.ProtocolError(unicode(exc)) def handle_response(self): """ Метод обработки ответа ККМ. :rtype: dict :return: ответ ККМ в виде словаря """ for _ in xrange(self.MAX_ATTEMPTS): stx = self.serial.read() if stx != STX: raise excepts.NoConnectionError() length = self.serial.read() payload = self.serial.read(misc.UNCAST_SIZE['1'](length)) _lrc = misc.UNCAST_SIZE['1'](self.serial.read()) if misc.lrc(misc.bytearray_concat(length, payload)) == _lrc: self.serial.write(ACK) return self.handle_payload(payload) else: self.serial.write(NAK) self.serial.write(ENQ) byte = self.serial.read() if byte != ACK: raise excepts.UnexpectedResponseError(u'Получен байт 0x{:02X}, ожидался ACK'.format(ord(byte))) else: raise excepts.NoConnectionError() def handle_payload(self, payload): """ Метод обработки полезной нагрузки ответа ККМ. :type payload: str or bytearray :param payload: часть ответа ККМ, содержащая полезную нагрузку :rtype: dict :return: набор параметров в виде словаря """ payload = misc.bytearray_cast(payload) # предполагаем, что команда однобайтная cmd_len = 1 try: cmd = payload[0] # если байт полный, то скорее всего команда двубайтная, # т.к. в спецификации Штриха не предусмотрено команды с кодом 0xFF if cmd == 0xFF: cmd_len = 2 cmd = misc.bytes_to_int((payload[1], cmd)) except IndexError: raise excepts.UnexpectedResponseError(u'Не удалось получить байт(ы) команды из ответа') response = payload[slice(cmd_len, None)] handler = hc.HANDLERS.get(cmd) if handler: result = {} for _slice, func, name in handler: chunk = _slice(response) if isinstance(_slice, misc.mslice) else response[_slice] if chunk and name is None: result.update(func(chunk)) elif chunk: result[name] = func(chunk) if func else chunk else: result[name] = None error = result.get(hc.ERROR_CODE_STR, 0) if error != 0: raise excepts.Error(cmd, error, fs=self.fs) return Response(cmd, result) return response def command_nopass(self, cmd, params=bytearray()): """ Метод отправки команды без пароля оператора. :type cmd: int :param cmd: номер команды :type params: bytearray :param params: набор параметров команды :rtype: dict :return: набор параметров ответа в виде словаря """ if not isinstance(params, bytearray): raise TypeError(u'{} expected, got {} instead'.format(bytearray, type(params))) cmd_len = len(misc.int_to_bytes(cmd)) buff = misc.bytearray_concat( misc.CAST_SIZE['1'](cmd_len + len(params)), misc.CAST_CMD[cmd_len](cmd), params ) command = misc.bytearray_concat(STX, buff, misc.CAST_SIZE['1'](misc.lrc(buff))) for r in self.check(self.CHECK_NUM): if not r: continue for _ in xrange(self.MAX_ATTEMPTS): try: self.serial.write(command) byte = self.serial.read() if byte == ACK: return self.handle_response() except serial.SerialTimeoutException: self.serial.flushOutput() raise excepts.ProtocolError(u'Не удалось записать байт в ККМ') except serial.SerialException as exc: self.serial.flushInput() raise excepts.ProtocolError(unicode(exc)) else: raise excepts.NoConnectionError() else: raise excepts.NoConnectionError() def command(self, cmd, password, *params): """ Метод отправки команды с паролем оператора. :type cmd: int :param cmd: номер команды :type password: int :param password: пароль оператора :type params: bytearray :param params: набор параметров команды :rtype: dict :return: набор параметров ответа в виде словаря """ params = misc.bytearray_concat( misc.CAST_SIZE['4'](password), *params ) return self.command_nopass(cmd, params) def check(self, count=1, quiet=False): """ Проверка связи с ККМ. :type count: int :param count: количество отправляемых пакетов :type quiet: bool :param quiet: подавление исключений """ if self.serial is None: raise excepts.ProtocolError(u'Необходимо вначале выполнить метод connect()') if count < 1: raise ValueError('Параметр count должен быть >= 1') for _ in xrange(count): try: yield self.init() except excepts.NoConnectionError: if quiet: yield False else: raise @str_compat class Response(object): __slots__ = ( 'cmd', 'cmd_name', 'params' ) def __init__(self, cmd, params): """ Класс ответа ККМ. :type cmd: int :param cmd: номер команды :type params: dict :param params: словарь параметров ответа ККМ """ self.cmd = cmd self.cmd_name = hc.COMMANDS[cmd] self.params = params def __getitem__(self, item): return self.params[item] def __setitem__(self, key, value): self.params[key] = value def __str__(self): return u'0x{:02X} ({}) - {}'.format( self.cmd, self.cmd_name, unilog.as_unicode(self.params) ) __repr__ = __str__