diff --git a/service/Pipfile b/service/Pipfile index 99a4c02..f0b8c97 100644 --- a/service/Pipfile +++ b/service/Pipfile @@ -12,6 +12,7 @@ pyobjc-framework-cocoa = {version = "*", platform_system = "== 'Darwin'"} plyer = {version = "*", platform_system = "== 'Windows'"} jsonschema = "*" PyQt5 = "*" +zeroconf = "*" [dev-packages] pyinstaller = "*" diff --git a/service/service.py b/service/service.py index 1e4df8d..8c46fa2 100644 --- a/service/service.py +++ b/service/service.py @@ -7,6 +7,7 @@ import time import uuid from PyQt5 import QtCore, QtGui, QtWidgets +from zeroconf import Zeroconf, ServiceBrowser import common import driver_auto as driver @@ -41,7 +42,7 @@ def __safe_get_status(getter_fn, fallback_value, name, duration_limit=0.25): # Previous return value is passed as last_status. On first call, None is passed. # counter goes up to args.send_rate-1, then back to 0. It can be used to do stuff every X iterations. # Before quitting, the function is called one more time with counter==-1. It sends out an "everything off" message in this case. -def loopbody(args, counter, last_status): +def loopbody(args, counter, last_status, enabled_devices): if counter >= 0: # Normal call fallback_status = last_status if last_status is not None else (False, False) @@ -67,16 +68,27 @@ def loopbody(args, counter, last_status): # Send message if status changed, or every Xth round if status != last_status or (counter % args.send_rate)==0: common.log('Sending UDP message...') - for ip in args.ip: - sendudp(ip, args.port, msg) + for ip, port in enabled_devices: + sendudp(ip, port, msg) return status class App(QtWidgets.QApplication): + onDeviceAppeared = QtCore.pyqtSignal(str, list, int) + onDeviceDisappeared = QtCore.pyqtSignal(str) + def __init__(self, args): QtWidgets.QApplication.__init__(self, sys.argv) + self.setQuitOnLastWindowClosed(False) + + self.nearbyDevices = {} + self.onDeviceAppeared.connect(self.deviceAppeared) + self.onDeviceDisappeared.connect(self.deviceDisappeared) self.menu = QtWidgets.QMenu() + self.devicesMenu = self.menu.addMenu("Nearby devices") + self.devicesMenu.triggered.connect(self.toggleDevice) + self.menu.addAction("Add device manually", self.addDeviceManually) self.exitAction = self.menu.addAction("Exit", self.shutdown) style = self.style() @@ -92,32 +104,81 @@ def __init__(self, args): self.last_status = None self.icounter = itertools.cycle(itertools.islice(itertools.count(start=0), self.args.send_rate)) - def getIpAddress(self): - text, ok = QtWidgets.QInputDialog.getText(None, 'CheckMeet', 'Please provide IP address of device:') - if ok: - self.args.ip.append(text) - else: - driver.show_notification(APPNAME, 'Could not start service') - sys.exit(1) # can't use self.exit() here because the Qt event loop is not running yet - def start(self): - if len(self.args.ip)==0: - self.getIpAddress() - self.onTick() self.timer.start(self.args.query_interval*1000) self.trayIcon.show() driver.show_notification(APPNAME, 'Service started!✨') + if len(self.args.ip) > 0: + self.deviceAppeared(f"[Added on command line]", self.args.ip, self.args.port) + def onTick(self): counter = next(self.icounter) - self.last_status = loopbody(self.args, counter, self.last_status) + self.last_status = loopbody(self.args, counter, self.last_status, self.getEnabledDevices()) def shutdown(self): - loopbody(self.args, -1, self.last_status) + loopbody(self.args, -1, self.last_status, self.getEnabledDevices()) common.log('Shutting down') self.exit() + def deviceAppeared(self, name, addresses, port): + print(f"deviceAppeared {name} {addresses} {port}") + if name in self.nearbyDevices: + print(f"Duplicate device {name}") + return + + updatesEnabled = True + self.nearbyDevices[name] = { + 'addresses': addresses, + 'port': port, + 'updatesEnabled': updatesEnabled, + } + a = self.devicesMenu.addAction(f"{name} {addresses}:{port}") + a.setCheckable(True) + a.setChecked(updatesEnabled) + self.nearbyDevices[name]['action'] = a + + def deviceDisappeared(self, name): + print(f"deviceDisappeared {name}") + if name not in self.nearbyDevices: + print(f"Device not found {name}") + return + + self.devicesMenu.removeAction(self.nearbyDevices[name]['action']) + del self.nearbyDevices[name] + + def toggleDevice(self, action): + if action.isCheckable(): + self.nearbyDevices[action.text()]["updatesEnabled"] = action.isChecked() + + def getEnabledDevices(self): + result = [] + + for _, device in self.nearbyDevices.items(): + if device['updatesEnabled']: + for ip in device['addresses']: + result.append((ip, device['port'])) + + return result + + def addDeviceManually(self): + text, ok = QtWidgets.QInputDialog.getText(None, 'CheckMeet', 'Please provide IP address of device:') + if ok: + self.deviceAppeared(f"[Manually added]", [text], self.args.port) + + def add_service(self, zeroconf, serviceType, name): + info = zeroconf.get_service_info(serviceType, name) + self.onDeviceAppeared.emit(info.server, info.parsed_addresses(), info.port) + + def remove_service(self, zeroconf, serviceType, name): + info = zeroconf.get_service_info(serviceType, name) + self.onDeviceDisappeared.emit(info.server) + + def update_service(self, zeroconf, serviceType, name): + pass + + def main(): parser = argparse.ArgumentParser(description='Service') parser.add_argument('--port', default=26999, type=int, help='Use UDP port') @@ -127,9 +188,16 @@ def main(): parser.add_argument('ip', nargs='*', help='Send UDP packets to these IP adresses') args = parser.parse_args() + zconf = Zeroconf() app = App(args) - app.start() - sys.exit(app.exec_()) + ServiceBrowser(zconf, "_checkmeet._udp.local.", app) + try: + app.start() + exit_status = app.exec_() + finally: + zconf.close() + + sys.exit(exit_status) if __name__ == '__main__': main()