From 9b0996cf07db5db0294347d34fc72dd53bafebe8 Mon Sep 17 00:00:00 2001 From: bloemp Date: Wed, 4 Jan 2023 19:10:41 +0100 Subject: [PATCH] Network backen updated functionality This commit adds SNMP calls to the network backend to allow: - Discover functionality through SNMP broadcast - Read status from the networked printer --- brother_ql/backends/helpers.py | 10 ++- brother_ql/backends/network.py | 118 +++++++++++++++++++++++-------- brother_ql/backends/quicksnmp.py | 117 ++++++++++++++++++++++++++++++ setup.py | 1 + 4 files changed, 210 insertions(+), 36 deletions(-) create mode 100644 brother_ql/backends/quicksnmp.py diff --git a/brother_ql/backends/helpers.py b/brother_ql/backends/helpers.py index 279221c..bb09601 100755 --- a/brother_ql/backends/helpers.py +++ b/brother_ql/backends/helpers.py @@ -7,7 +7,8 @@ * printing """ -import logging, time +import logging +import time from brother_ql.backends import backend_factory, guess_backend from brother_ql.reader import interpret_response @@ -63,9 +64,6 @@ def send(instructions, printer_identifier=None, backend_identifier=None, blockin if not blocking: return status - if selected_backend == 'network': - """ No need to wait for completion. The network backend doesn't support readback. """ - return status while time.time() - start < 10: data = printer.read() @@ -83,10 +81,10 @@ def send(instructions, printer_identifier=None, backend_identifier=None, blockin logger.error('Errors occured: %s', result['errors']) status['outcome'] = 'error' break - if result['status_type'] == 'Printing completed': + if result['status_type'] in ('Printing completed', 'Reply to status request'): status['did_print'] = True status['outcome'] = 'printed' - if result['status_type'] == 'Phase change' and result['phase_type'] == 'Waiting to receive': + if result['status_type'] in ('Phase change', 'Reply to status request') and result['phase_type'] == 'Waiting to receive': status['ready_for_next_job'] = True if status['did_print'] and status['ready_for_next_job']: break diff --git a/brother_ql/backends/network.py b/brother_ql/backends/network.py index b32448c..87f85d3 100755 --- a/brother_ql/backends/network.py +++ b/brother_ql/backends/network.py @@ -7,11 +7,39 @@ from __future__ import unicode_literals from builtins import str +import enum -import socket, os, time, select +from pysnmp.proto.rfc1157 import VarBind +from pysnmp.proto import api +from pysnmp import hlapi +import socket +from time import time + +from enum import Enum + +from . import quicksnmp from .generic import BrotherQLBackendGeneric +# Some SNMP OID's that we can use to get printer information +class Brother_SNMP_OID(Enum): + '''SNMP OID's''' + GET_IP = "1.3.6.1.4.1.1240.2.3.4.5.2.3.0" + GET_NETMASK = "1.3.6.1.4.1.1240.2.3.4.5.2.4.0" + GET_MAC = "1.3.6.1.4.1.1240.2.3.4.5.2.4.0" + GET_LOCATION = "1.3.6.1.2.1.1.6.0" + GET_MODEL = "1.3.6.1.2.1.25.3.2.1.3.1" + GET_SERIAL = "1.3.6.1.2.1.43.5.1.1.17" + GET_STATUS = "1.3.6.1.4.1.2435.3.3.9.1.6.1.0" + +# SNMP Contants +SNMP_MAX_WAIT_FOR_RESPONSES = 5 +SNMP_MAX_NUMBER_OF_RESPONSES = 10 + +# Global variables +Broadcast_Started_At = 0 +foundPrinters = set() + def list_available_devices(): """ List all available devices for the network backend @@ -20,10 +48,36 @@ def list_available_devices(): [ {'identifier': 'tcp://hostname[:port]', 'instance': None}, ] \ Instance is set to None because we don't want to connect to the device here yet. """ + # Protocol version to use + pMod = api.protoModules[api.protoVersion1] + + # Build PDU + reqPDU = pMod.GetRequestPDU() + pMod.apiPDU.setDefaults(reqPDU) + pMod.apiPDU.setVarBinds( + reqPDU, [(Brother_SNMP_OID.GET_IP.value, pMod.Null(''))] + ) + + # Build message + reqMsg = pMod.Message() + pMod.apiMessage.setDefaults(reqMsg) + pMod.apiMessage.setCommunity(reqMsg, 'public') + pMod.apiMessage.setPDU(reqMsg, reqPDU) + + # Clear current list of found printers + foundPrinters.clear() + + # set start time for timeout timer + Broadcast_Started_At = time() # We need some snmp request sent to 255.255.255.255 here - raise NotImplementedError() - return [{'identifier': 'tcp://' + path, 'instance': None} for path in paths] + try: + quicksnmp.broadcastSNMPReq(reqMsg, cbRecvFun, cbTimerFun, SNMP_MAX_NUMBER_OF_RESPONSES) + except TimeoutTimerExpired: + pass + + #raise NotImplementedError() + return [{'identifier': 'tcp://' + printer, 'instance': None} for printer in foundPrinters] class BrotherQLBackendNetwork(BrotherQLBackendGeneric): """ @@ -33,7 +87,7 @@ class BrotherQLBackendNetwork(BrotherQLBackendGeneric): def __init__(self, device_specifier): """ device_specifier: string or os.open(): identifier in the \ - format file:///dev/usb/lp0 or os.open() raw device handle. + format tcp://: or os.open() raw device handle. """ self.read_timeout = 0.01 @@ -42,28 +96,24 @@ def __init__(self, device_specifier): if isinstance(device_specifier, str): if device_specifier.startswith('tcp://'): device_specifier = device_specifier[6:] - host, _, port = device_specifier.partition(':') + self.host, _, port = device_specifier.partition(':') if port: port = int(port) else: port = 9100 - #try: - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - self.s.connect((host, port)) - #except OSError as e: - # raise ValueError('Could not connect to the device.') - if self.strategy == 'socket_timeout': - self.s.settimeout(self.read_timeout) - elif self.strategy == 'try_twice': + try: + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.s.connect((self.host, port)) + except socket.error as e: + raise ValueError('Could not connect to the device.') from e + if self.strategy in ('socket_timeout', 'try_twice'): self.s.settimeout(self.read_timeout) else: self.s.settimeout(0) - - elif isinstance(device_specifier, int): - self.dev = device_specifier else: - raise NotImplementedError('Currently the printer can be specified either via an appropriate string or via an os.open() handle.') + raise NotImplementedError('Currently the printer can be \ + specified either via an appropriate string.') def _write(self, data): self.s.settimeout(10) @@ -78,24 +128,32 @@ def _read(self, length=32): tries = 2 for i in range(tries): try: - data = self.s.recv(length) - return data + # Using SNMP, we retrieve the status of the remote device + dataset = quicksnmp.get(self.host, + [Brother_SNMP_OID.GET_STATUS.value], + hlapi.CommunityData('public')) + return dataset[Brother_SNMP_OID.GET_STATUS.value] except socket.timeout: pass return b'' - elif self.strategy == 'select': - data = b'' - start = time.time() - while (not data) and (time.time() - start < self.read_timeout): - result, _, _ = select.select([self.s], [], [], 0) - if self.s in result: - data += self.s.recv(length) - if data: break - time.sleep(0.001) - return data else: raise NotImplementedError('Unknown strategy') def _dispose(self): self.s.shutdown(socket.SHUT_RDWR) self.s.close() + +class TimeoutTimerExpired(Exception): + '''Timeout timer expired exception''' + +def cbTimerFun(timeNow): + '''Countdown callback to check if the requested wait time has elapased''' + if timeNow - Broadcast_Started_At > SNMP_MAX_WAIT_FOR_RESPONSES: + raise TimeoutTimerExpired + + +def cbRecvFun(transportDispatcher, transportDomain, + transportAddress, wholeMsg): + '''Receive SNMP data callback''' + foundPrinters.add(f"{transportAddress[0]}") + return wholeMsg diff --git a/brother_ql/backends/quicksnmp.py b/brother_ql/backends/quicksnmp.py new file mode 100644 index 0000000..2468e28 --- /dev/null +++ b/brother_ql/backends/quicksnmp.py @@ -0,0 +1,117 @@ +from pysnmp import hlapi +from pysnmp.carrier.asyncore.dispatch import AsyncoreDispatcher +from pysnmp.carrier.asyncore.dgram import udp +from pyasn1.codec.ber import encoder, decoder +from pysnmp.proto import api +from time import time +from pyasn1.type.univ import OctetString + +def construct_object_types(list_of_oids): + object_types = [] + for oid in list_of_oids: + object_types.append(hlapi.ObjectType(hlapi.ObjectIdentity(oid))) + return object_types + + +def construct_value_pairs(list_of_pairs): + pairs = [] + for key, value in list_of_pairs.items(): + pairs.append(hlapi.ObjectType(hlapi.ObjectIdentity(key), value)) + return pairs + + +def set(target, value_pairs, credentials, port=161, engine=hlapi.SnmpEngine(), context=hlapi.ContextData()): + handler = hlapi.setCmd( + engine, + credentials, + hlapi.UdpTransportTarget((target, port)), + context, + *construct_value_pairs(value_pairs) + ) + return fetch(handler, 1)[0] + + +def get(target, oids, credentials, port=161, engine=hlapi.SnmpEngine(), context=hlapi.ContextData()): + handler = hlapi.getCmd( + engine, + credentials, + hlapi.UdpTransportTarget((target, port)), + context, + *construct_object_types(oids) + ) + return fetch(handler, 1)[0] + + +def get_bulk(target, oids, credentials, count, start_from=0, port=161, + engine=hlapi.SnmpEngine(), context=hlapi.ContextData()): + handler = hlapi.bulkCmd( + engine, + credentials, + hlapi.UdpTransportTarget((target, port)), + context, + start_from, count, + *construct_object_types(oids) + ) + return fetch(handler, count) + + +def get_bulk_auto(target, oids, credentials, count_oid, start_from=0, port=161, + engine=hlapi.SnmpEngine(), context=hlapi.ContextData()): + count = get(target, [count_oid], credentials, port, engine, context)[count_oid] + return get_bulk(target, oids, credentials, count, start_from, port, engine, context) + + +def cast(value): + try: + return int(value) + except (ValueError, TypeError): + try: + return float(value) + except (ValueError, TypeError): + try: + return str(value, 'UTF-8') + except (ValueError, TypeError): + pass + return value + + +def fetch(handler, count): + result = [] + for i in range(count): + try: + error_indication, error_status, error_index, var_binds = next(handler) + if not error_indication and not error_status: + items = {} + for var_bind in var_binds: + items[str(var_bind[0])] = cast(var_bind[1]) + result.append(items) + else: + raise RuntimeError('Got SNMP error: {0}'.format(error_indication)) + except StopIteration: + break + return result + + +def broadcastSNMPReq(reqMsg, cbRecvFun, cbTimerFun, max_number_of_responses=10): + '''TODO''' + transportDispatcher = AsyncoreDispatcher() + transportDispatcher.registerRecvCbFun(cbRecvFun) + transportDispatcher.registerTimerCbFun(cbTimerFun) + + # UDP/IPv4 + udpSocketTransport = udp.UdpSocketTransport().openClientMode().enableBroadcast() + transportDispatcher.registerTransport(udp.domainName, udpSocketTransport) + + # Pass message to dispatcher + transportDispatcher.sendMessage( + encoder.encode(reqMsg), udp.domainName, ('255.255.255.255', 161) + ) + + # wait for a maximum of responses or time out + transportDispatcher.jobStarted(1, max_number_of_responses) + + # Dispatcher will finish as all jobs counter reaches zero + try: + transportDispatcher.runDispatcher() + finally: + transportDispatcher.closeDispatcher() diff --git a/setup.py b/setup.py index 09be49e..b20a8e6 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "pillow>=3.3.0", "pyusb", 'attrs', + 'pysnmp' 'typing;python_version<"3.5"', 'enum34;python_version<"3.4"', ],