Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Network backend updated functionality #137

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions brother_ql/backends/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
118 changes: 88 additions & 30 deletions brother_ql/backends/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
"""
Expand All @@ -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://<ipaddress>:<port> or os.open() raw device handle.
"""

self.read_timeout = 0.01
Expand All @@ -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)
Expand All @@ -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
117 changes: 117 additions & 0 deletions brother_ql/backends/quicksnmp.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"pillow>=3.3.0",
"pyusb",
'attrs',
'pysnmp'
'typing;python_version<"3.5"',
'enum34;python_version<"3.4"',
],
Expand Down