From cca0c4c7d7e7db04d77dce61980eeef9936ddede Mon Sep 17 00:00:00 2001 From: Scott Baker Date: Fri, 4 Dec 2015 11:13:23 -0500 Subject: [PATCH 1/4] Testing mdns sd --- BrewPiUtil.py | 16 +++++++ tcpSerial.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ testTCP.py | 17 +++++++ 3 files changed, 157 insertions(+) create mode 100644 tcpSerial.py create mode 100644 testTCP.py diff --git a/BrewPiUtil.py b/BrewPiUtil.py index bc2f3fa..f8fb285 100644 --- a/BrewPiUtil.py +++ b/BrewPiUtil.py @@ -137,6 +137,22 @@ def setupSerial(config, baud_rate=57600, time_out=0.1): tries += 1 time.sleep(1) + if not(ser): + logMessage("No serial attached BrewPi found. Trying TCP serial (WiFi)") + while tries < 10: + error = "" + + if config['wifiHost'] is not None + if config['wifiPort'] is None + config['wifiPort']=8080 + ser = tcpSerial.TCPSerial(port, baudrate=baud_rate, timeout=time_out) + + if ser: + break + tries += 1 + time.sleep(1) + + if ser: # discard everything in serial buffers ser.flushInput() diff --git a/tcpSerial.py b/tcpSerial.py new file mode 100644 index 0000000..672ac2e --- /dev/null +++ b/tcpSerial.py @@ -0,0 +1,124 @@ +# wraps a tcp socket stream in a object that looks like a serial port +# this allows seemless integration with exsiting brewpi-script code + +import socket +import serial +from zeroconf import ServiceBrowser, Zeroconf, ServiceStateChange + + +class TCPSerial(object): + def __init__(self, host=None, port=None): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # find BrewPi's via mdns lookup + + + self.sock.connect((host, port)) + return + + def flushInput(self): + # this has no meaning to tcp + return + + def flushOutput(self): + #Clear output buffer, aborting the current output and discarding all that is in the buffer. + # this has no meaning to tcp + return + + + def read(self, size=1): + #Returns: Bytes read from the port. + #Read size bytes from the serial port. If a timeout is set it may return less characters as requested. With no timeout it will block until the requested number of bytes is read. + return self.sock.recv(size) + + + def readline(self,size=None, eol='\n'): + #Parameters: + #Read a line which is terminated with end-of-line (eol) character (\n by default) or until timeout. + buf=self.sock.recv(1) + line=buf + while buf!='\n': + buf=self.sock.recv(1) + if buf!='\n': + line+=buf + + return line + + + def write(self, data): + #Returns: Number of bytes written. + #Raises SerialTimeoutException: + # In case a write timeout is configured for the port and the time is exceeded. + #Write the string data to the port. + return self.sock.sendall(data) + + def inWaiting(self): + #Return the number of chars in the receive buffer. + return 1 #tcp socket doesnt give us a way to know how much is in the buffer, so we assume there is always something + + def name(self): + #Device name. This is always the device name even if the port was opened by a number. (Read Only). + return sock.getpeername() + + def timeout(self, value=0.1): + #Read or write current read timeout setting. + return setTimeout(value) + + def setTimeout(self, value=0.1): + if value: + self.sock.settimeout(value) + return self.sock.gettimeout() + + def flush(self): + #Flush of file like objects. In this case, wait until all data is written. + # this has no meaning to tcp + return + + + def close(self): + #close port immediately + return self.sock.close() + + + + + + +class MDNSListener(object): + + def remove_service(self, zeroconf, type, name): + print("Service %s removed" % (name,)) + + def add_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + print("Service %s added, service info: %s" % (name, info)) + +def on_service_state_change(zeroconf, service_type, name, state_change): + print("Service %s of type %s state changed: %s" % (name, service_type, state_change)) + + if state_change is ServiceStateChange.Added: + info = zeroconf.get_service_info(service_type, name) + if info: + print(" Address: %s:%d" % (socket.inet_ntoa(info.address), info.port)) + print(" Weight: %d, priority: %d" % (info.weight, info.priority)) + print(" Server: %s" % (info.server,)) + if info.properties: + print(" Properties are:") + for key, value in info.properties.items(): + print(" %s: %s" % (key, value)) + else: + print(" No properties") + else: + print(" No info") + print('\n') + +def discoverBrewpis(): + print "Discovering WiFi connected BrewPis..." + zeroconf = Zeroconf() + listener = MDNSListener() + browser = ServiceBrowser(zeroconf, "_brewpi._tcp.local", None,listener) + print "Listener established" + try: + input("Press enter to exit...\n\n") + finally: + zeroconf.close() + diff --git a/testTCP.py b/testTCP.py new file mode 100644 index 0000000..984e91d --- /dev/null +++ b/testTCP.py @@ -0,0 +1,17 @@ +import sys +import logging +import tcpSerial + + +#log = logging.getLogger(__name__) +#logging.setLevel(logging.DEBUG) + +tcpSerial.discoverBrewpis() + +#ser=tcpSerial.TCPSerial() + + +#ser.write("l\n") +#reply=ser.readline() + +#print reply \ No newline at end of file From 392d0e71485dca953a3a75a188752cbd0971928b Mon Sep 17 00:00:00 2001 From: sjbaker Date: Sat, 5 Dec 2015 14:14:34 -0500 Subject: [PATCH 2/4] Created tcpSerial to wrap a tcp socket in a serial-like interface Added MDNS code to auto discover brewpis Altered setup serial in BrewPiUtil to use tcp/mdns if no serial attached brewpi exists --- BrewPiUtil.py | 15 +++-- tcpSerial.py | 169 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 117 insertions(+), 67 deletions(-) diff --git a/BrewPiUtil.py b/BrewPiUtil.py index f8fb285..931ecf5 100644 --- a/BrewPiUtil.py +++ b/BrewPiUtil.py @@ -19,6 +19,7 @@ import os import serial import autoSerial +import tcpSerial try: import configobj @@ -113,6 +114,7 @@ def setupSerial(config, baud_rate=57600, time_out=0.1): # open serial port tries = 0 logMessage("Opening serial port") + tries=11 # skip serial for testing wifi while tries < 10: error = "" for portSetting in [config['port'], config['altport']]: @@ -138,15 +140,18 @@ def setupSerial(config, baud_rate=57600, time_out=0.1): time.sleep(1) if not(ser): + tries=0 logMessage("No serial attached BrewPi found. Trying TCP serial (WiFi)") while tries < 10: error = "" - if config['wifiHost'] is not None - if config['wifiPort'] is None - config['wifiPort']=8080 - ser = tcpSerial.TCPSerial(port, baudrate=baud_rate, timeout=time_out) - + if config['wifiHost'] == 'auto': + mdns=tcpSerial.MDNSBrowser() + (tcpHost, tcpPort)=mdns.discoverBrewpis() + ser = tcpSerial.TCPSerial(tcpHost,tcpPort) + else: + if not(config['wifiHost'] == None or config['wifiPort'] == None or config['wifiHost'] == 'None' or config['wifiPort'] == 'None' or config['wifiHost'] == 'none' or config['wifiPort'] == 'none'): + ser = tcpSerial.TCPSerial(config['wifiHost'],int(config['wifiPort'])) if ser: break tries += 1 diff --git a/tcpSerial.py b/tcpSerial.py index 672ac2e..37ec8a7 100644 --- a/tcpSerial.py +++ b/tcpSerial.py @@ -1,19 +1,25 @@ # wraps a tcp socket stream in a object that looks like a serial port # this allows seemless integration with exsiting brewpi-script code - +import BrewPiUtil import socket -import serial -from zeroconf import ServiceBrowser, Zeroconf, ServiceStateChange +import dbus, gobject, avahi +from dbus import DBusException +from dbus.mainloop.glib import DBusGMainLoop class TCPSerial(object): def __init__(self, host=None, port=None): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # find BrewPi's via mdns lookup - - + self.host=host + self.port=port + self.retries=10 # max reconnect attempts to try when doing a read or write operation + self.retryCount=0 # count of reconnect attempts performed + BrewPiUtil.logMessage("Connecting to BrewPi " + host + " on port " + str(port)) self.sock.connect((host, port)) - return + self.timeout=self.sock.gettimeout() + self.name=host + ':' + str(port) + return def flushInput(self): # this has no meaning to tcp @@ -28,19 +34,34 @@ def flushOutput(self): def read(self, size=1): #Returns: Bytes read from the port. #Read size bytes from the serial port. If a timeout is set it may return less characters as requested. With no timeout it will block until the requested number of bytes is read. - return self.sock.recv(size) + bytes=None + try: + bytes=self.sock.recv(size) + except socket.timeout: # timeout on receive just means there is nothing in the buffer. This is not an error + return None + except socket.error: # other socket errors probably mean we lost our connection. try to recover it. + if self.retryCount < self.retries: + self.retryCount=self.retryCount+1 + self.sock.close() + self.sock.connect((self.host, self.port)) + bytes=self.read(size) + else: + self.sock.close() + return None + if bytes is not None: + self.retryCount=0 + return bytes def readline(self,size=None, eol='\n'): #Parameters: #Read a line which is terminated with end-of-line (eol) character (\n by default) or until timeout. - buf=self.sock.recv(1) + buf=self.read(1) line=buf while buf!='\n': - buf=self.sock.recv(1) - if buf!='\n': + buf=self.read(1) + if buf is not None and buf!='\n': line+=buf - return line @@ -49,23 +70,41 @@ def write(self, data): #Raises SerialTimeoutException: # In case a write timeout is configured for the port and the time is exceeded. #Write the string data to the port. - return self.sock.sendall(data) + try: + bytes=self.sock.sendall(data) + except socket.timeout: # A write timeout is probably a connection issue + if self.retryCount < self.retries: + self.retryCount=self.retryCount+1 + self.sock.close() + self.sock.connect((self.host, self.port)) + bytes=self.write(data) + else: + self.sock.close() + return -1 + except socket.error: # general errors are most likely to be a timeout disconnect from BrewPi, so try to recover. + if self.retryCount < self.retries: + self.retryCount=self.retryCount+1 + self.sock.close() + self.sock.connect((self.host, self.port)) + bytes=self.write(data) + else: + self.sock.close() + return -1 + if bytes>=0: + self.retryCount=0 + return bytes def inWaiting(self): #Return the number of chars in the receive buffer. - return 1 #tcp socket doesnt give us a way to know how much is in the buffer, so we assume there is always something - - def name(self): - #Device name. This is always the device name even if the port was opened by a number. (Read Only). - return sock.getpeername() - - def timeout(self, value=0.1): - #Read or write current read timeout setting. - return setTimeout(value) + # Note: the value returned by inWaiting should be greater than the send buffer size on BrewPi firmware + # If not, brewpi.py may not grab the next whole buffered message. + return 4096 #tcp socket doesnt give us a way to know how much is in the buffer, so we assume there is always something + def setTimeout(self, value=0.1): if value: self.sock.settimeout(value) + self.timeout=self.sock.gettimeout() return self.sock.gettimeout() def flush(self): @@ -79,46 +118,52 @@ def close(self): return self.sock.close() +class MDNSBrowser(object): + + def __init__(self): + # Avahi global configs + self.Avahi_loop = DBusGMainLoop(set_as_default=True) + self.Avahi_busloop = gobject.MainLoop() + gobject.threads_init() + self.Avahi_bus = dbus.SystemBus(mainloop=self.Avahi_loop) + self.Avahi_server = dbus.Interface( self.Avahi_bus.get_object(avahi.DBUS_NAME, '/'), 'org.freedesktop.Avahi.Server') + self.Avahi_TYPE = '_brewpi._tcp' + self.tcpHost=None + self.tcpPort=None - - - -class MDNSListener(object): - - def remove_service(self, zeroconf, type, name): - print("Service %s removed" % (name,)) - - def add_service(self, zeroconf, type, name): - info = zeroconf.get_service_info(type, name) - print("Service %s added, service info: %s" % (name, info)) - -def on_service_state_change(zeroconf, service_type, name, state_change): - print("Service %s of type %s state changed: %s" % (name, service_type, state_change)) - - if state_change is ServiceStateChange.Added: - info = zeroconf.get_service_info(service_type, name) - if info: - print(" Address: %s:%d" % (socket.inet_ntoa(info.address), info.port)) - print(" Weight: %d, priority: %d" % (info.weight, info.priority)) - print(" Server: %s" % (info.server,)) - if info.properties: - print(" Properties are:") - for key, value in info.properties.items(): - print(" %s: %s" % (key, value)) - else: - print(" No properties") - else: - print(" No info") - print('\n') - -def discoverBrewpis(): - print "Discovering WiFi connected BrewPis..." - zeroconf = Zeroconf() - listener = MDNSListener() - browser = ServiceBrowser(zeroconf, "_brewpi._tcp.local", None,listener) - print "Listener established" - try: - input("Press enter to exit...\n\n") - finally: - zeroconf.close() + def _service_resolved(self,*args): + #global tcpHost + #global tcpPort + BrewPiUtil.logMessage('\taddress:' + args[7]) + BrewPiUtil.logMessage( '\tport:' + str(args[8])) + self.tcpHost=args[7] + self.tcpPort=args[8] + self.Avahi_busloop.quit() + def _print_error(self,*args): + BrewPiUtil.logMessage('error_handler:' + args[0]) + self.Avahi_busloop.quit() + + def _myhandler(self,interface, protocol, name, stype, domain, flags): + BrewPiUtil.logMessage("Found BrewPi service '" + name +"' type '" +stype+"' domain '"+domain+"' ") #% (name, stype, domain) + + if flags & avahi.LOOKUP_RESULT_LOCAL: + # local service, skip + pass + + self.Avahi_server.ResolveService(interface, protocol, name, stype, + domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), + reply_handler=self._service_resolved, error_handler=self._print_error) + + + def discoverBrewpis(self): + BrewPiUtil.logMessage("Running discovery...") + sbrowser = dbus.Interface(self.Avahi_bus.get_object(avahi.DBUS_NAME, + self.Avahi_server.ServiceBrowserNew(avahi.IF_UNSPEC, + avahi.PROTO_UNSPEC, self.Avahi_TYPE, 'local', dbus.UInt32(0))), + avahi.DBUS_INTERFACE_SERVICE_BROWSER) + + sbrowser.connect_to_signal("ItemNew", self._myhandler) + self.Avahi_busloop.run() + return (self.tcpHost,self.tcpPort) + From 6838fcb4aaf8e85433c575278755695f477657aa Mon Sep 17 00:00:00 2001 From: sjbaker Date: Sat, 5 Dec 2015 14:19:15 -0500 Subject: [PATCH 3/4] cleanup files --- testTCP.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 testTCP.py diff --git a/testTCP.py b/testTCP.py deleted file mode 100644 index 984e91d..0000000 --- a/testTCP.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys -import logging -import tcpSerial - - -#log = logging.getLogger(__name__) -#logging.setLevel(logging.DEBUG) - -tcpSerial.discoverBrewpis() - -#ser=tcpSerial.TCPSerial() - - -#ser.write("l\n") -#reply=ser.readline() - -#print reply \ No newline at end of file From 52d16f7bf842f3128c59ae354e1395a315d8209d Mon Sep 17 00:00:00 2001 From: sjbaker Date: Sat, 5 Dec 2015 14:21:19 -0500 Subject: [PATCH 4/4] removed debug switch --- BrewPiUtil.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BrewPiUtil.py b/BrewPiUtil.py index 931ecf5..385be1b 100644 --- a/BrewPiUtil.py +++ b/BrewPiUtil.py @@ -114,7 +114,6 @@ def setupSerial(config, baud_rate=57600, time_out=0.1): # open serial port tries = 0 logMessage("Opening serial port") - tries=11 # skip serial for testing wifi while tries < 10: error = "" for portSetting in [config['port'], config['altport']]: