class WebsocketClientModule(ModuleProcess): def __init__(self, baseConfig, pInBoundEventQueue, pOutBoundEventQueue, loggingQueue): super(WebsocketClientModule, self).__init__() self.alive = True self.config = baseConfig self.inQueue = pInBoundEventQueue # inQueue are messages from the main process to websocket clients self.outQueue = pOutBoundEventQueue # outQueue are messages from clients to main process self.websocketClient = None self.loggingQueue = loggingQueue self.threadProcessQueue = None # Configs self.moduleConfig = configLoader.load(self.loggingQueue, __name__) # Constants self._port = self.moduleConfig['WebsocketPort'] self._host = self.moduleConfig['WebsocketHost'] # logging setup self.logger = ThreadsafeLogger(loggingQueue, __name__) def run(self): """ Main thread entry point. Sets up websocket server and event callbacks. Starts thread to monitor inbound message queue. """ self.logger.info("Starting %s" % __name__) self.connect() def listen(self): self.threadProcessQueue = Thread(target=self.processQueue) self.threadProcessQueue.setDaemon(True) self.threadProcessQueue.start() def connect(self): #websocket.enableTrace(True) ws = websocket.WebSocketApp("ws://%s:%s"%(self._host, self._port), on_message = self.onMessage, on_error = self.onError, on_close = self.onClose) ws.on_open = self.onOpen ws.run_forever() def onError(self, ws, message): self.logger.error("Error callback fired, message: %s"%message) def onClose(self, ws): if self.alive: self.logger.warn("Closed") self.alive = False # TODO: reconnect timer else: self.logger.info("Closed") def onMessage(self, ws, message): self.logger.info("Message from websocket server: %s"%message) # Could put message on the out queue here to handle incoming coms def onOpen(self, ws): self.alive = True self.websocketClient = ws self.listen() def shutdown(self): """ Handle shutdown message. Close and shutdown websocket server. Join queue processing thread. """ self.logger.info("Shutting down %s"%__name__) try: self.logger.info("Closing websocket") self.websocketClient.close() except Exception as e: self.logger.error("Websocket close error : %s " %e) self.alive = False self.threadProcessQueue.join() time.sleep(1) self.exit = True def sendOutMessage(self, message): """ Send message to server """ self.websocketClient.send(json.dumps(message.__dict__)) def processQueue(self): """ Monitor queue of messages from main process to this thread. """ while self.alive: if (self.inQueue.empty() == False): try: message = self.inQueue.get(block=False,timeout=1) if message is not None: if (message.topic.upper()=="SHUTDOWN" and message.sender_id.lower()=="main"): self.logger.debug("SHUTDOWN handled") self.shutdown() else: self.sendOutMessage(message) except Exception as e: self.logger.error("Websocket unable to read queue : %s " %e) else: time.sleep(.25)
class CollectionModule(ModuleProcess): # You can keep these parameters the same, all modules receive the same params # self - reference to self # baseConfig - configuration settings defined in /simplesensor/config/base.conf # (https://github.com/AdobeAtAdobe/SimpleSensor/blob/master/config/base.conf) # pInBoundQueue - messages from handler to this module # pOutBoundQueue - messages from this module to other modules # loggingQueue - logging messages for threadsafe logger def __init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue): """ Initialize new CollectionModule instance. """ super(CollectionModule, self).__init__(baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue) # Most collection modules will follow a similar pattern... # 1. Set up some variables on the self object # Queues self.outQueue = pOutBoundQueue self.inQueue= pInBoundQueue self.loggingQueue = loggingQueue self.threadProcessQueue = None self.alive = False self.context = None self.reader = None # 2. Load the module's configuration file # Configs self.moduleConfig = configLoader.load(self.loggingQueue, __name__) self.config = baseConfig # 3. Set some constants to the self object from config parameters (if you want) self._id = self.moduleConfig['CollectionPointId'] self._type = self.moduleConfig['CollectionPointType'] self._port = self.moduleConfig['ReaderPortNumber'] # 4. Create a threadsafe logger object self.logger = ThreadsafeLogger(loggingQueue, __name__) def run(self): """ Main process method, run when the thread's start() function is called. Starts monitoring inbound messages to this module, and collection logic goes here. For example, you could put a loop with a small delay to keep polling the sensor, etc. When something is detected that's relevant, put a message on the outbound queue. """ # Monitor inbound queue on own thread self.listen() self.alive = True while self.context == None: self.establish_context() while self.alive: while self.reader == None: self.reader = self.get_reader() if self.reader is None: self.logger.info('Waiting for 5 seconds before ' + 'trying to find readers again. Is it plugged in?') time.sleep(5) # connect to card card = self.get_card() if card is None: continue # get block #10 and 11 of card, # contains the attendee ID msg = [0xFF, 0xB0, 0x00, bytes([10])[0], 0x04] chunk_one = self.send_transmission(card, msg) if chunk_one is None: self.reader = None continue msg = [0xFF, 0xB0, 0x00, bytes([11])[0], 0x04] chunk_two = self.send_transmission(card, msg) if chunk_two is None: self.reader = None continue # the id is in B1-3 of block 10 # and B0-2 of block 11 attendee_id_bytes = bytearray(chunk_one[1:4]+chunk_two[0:3]) attendee_id = attendee_id_bytes.decode('UTF-8') xdata = { 'attendee_id': attendee_id } msg = self.build_message(topic='scan_in', extendedData=xdata) self.logger.info('Sending message: {}'.format(msg)) self.put_message(msg) self.reader = None # sleep for a bit to avoid double scanning time.sleep(5) def establish_context(self): hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) if hresult != SCARD_S_SUCCESS: self.logger.error( 'Unable to establish context: {}'.format( SCardGetErrorMessage(hresult))) return self.context = hcontext def get_reader(self): hresult, readers = SCardListReaders(self.context, []) if hresult != SCARD_S_SUCCESS: self.logger.error( 'Failed to list readers: {}'.format( SCardGetErrorMessage(hresult))) return if len(readers)<1 or len(readers)-1<self._port: self.logger.error( 'Not enough readers attached. {} needed, {} attached'.format( (self._port+1), (len(readers)))) return else: return readers[self._port] def get_card(self, mode=None, protocol=None): hresult, hcard, dwActiveProtocol = SCardConnect( self.context, self.reader, mode or SCARD_SHARE_SHARED, protocol or (SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1)) if hresult != SCARD_S_SUCCESS: return else: return hcard def send_transmission(self, card, msg, protocol=None): hresult, response = SCardTransmit( card, protocol or SCARD_PCI_T1, msg) if hresult != SCARD_S_SUCCESS: self.logger.error( 'Failed to send transmission: {}'.format( SCardGetErrorMessage(hresult))) return else: return response[:-2] def listen(self): """ Start thread to monitor inbound messages, declare module alive. """ self.threadProcessQueue = Thread(target=self.process_queue) self.threadProcessQueue.setDaemon(True) self.threadProcessQueue.start() def build_message(self, topic, extendedData={}, recipients=['communication_modules']): """ Create a Message instance. topic (required): message type sender_id (required): id property of original sender sender_type (optional): type of sender, ie. collection point type, module name, hostname, etc extended_data (optional): payload to deliver to recipient(s) recipients (optional): module name, which module(s) the message will be delivered to, ie. `websocket_server`. use an array of strings to define multiple modules to send to. use 'all' to send to all available modules. use 'local_only' to send only to modules with `low_cost` prop set to True. [DEFAULT] use 'communication_modules' to send only to communication modules. use 'collection_modules' to send only to collection modules. """ msg = Message( topic=topic, sender_id=self._id, sender_type=self._type, extended_data=extendedData, recipients=recipients, timestamp=datetime.datetime.utcnow()) return msg def put_message(self, msg): """ Put message onto outgoing queue. """ self.outQueue.put(msg) def process_queue(self): """ Process inbound messages on separate thread. When a message is encountered, trigger an event to handle it. Sleep for some small amount of time to avoid overloading. Also receives a SHUTDOWN message from the main process when the user presses the esc key. """ self.logger.info("Starting to watch collection point inbound message queue") while self.alive: if (self.inQueue.empty() == False): self.logger.info("Queue size is %s" % self.inQueue.qsize()) try: message = self.inQueue.get(block=False,timeout=1) if message is not None: self.handle_message(message) except Exception as e: self.logger.error("Error, unable to read queue: %s " %e) self.shutdown() self.logger.info("Queue size is %s after" % self.inQueue.qsize()) else: time.sleep(.25) def handle_message(self, message): """ Handle messages from other modules to this one. Switch on the message topic, do something with the data fields. """ if message.topic.upper()=='SHUTDOWN' and message.sender_id=='main': self.shutdown() def shutdown(self): """ Shutdown the collection module. Set alive flag to false so it stops looping. Wait for things to die, then exit. """ self.alive = False print("Shutting down nfc_bcard_reader") time.sleep(1) self.exit = True
class BtleCollectionPoint(Thread): def __init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue): """ Initialize new CamCollectionPoint instance. Setup queues, variables, configs, constants and loggers. """ super().__init__() # super().__init__(baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue) self.loggingQueue = loggingQueue self.logger = ThreadsafeLogger(loggingQueue, __name__) # Queues self.outQueue = pOutBoundQueue # Messages from this thread to the main process self.inQueue = pInBoundQueue # Configs self.moduleConfig = configLoader.load(self.loggingQueue, __name__) self.config = baseConfig # Variables and objects self.alive = True self.callbacks = {_ON_SCAN: self.handleBtleClientEvent} self.clientRegistry = ClientRegistry(self.moduleConfig, self.loggingQueue) self.eventManager = EventManager(self.moduleConfig, pOutBoundQueue, self.clientRegistry, self.loggingQueue) # Threads self.btleThread = None self.repeatTimerSweepClients = None self.lastUpdate = datetime.now() # Constants self._cleanupInterval = self.moduleConfig[ 'AbandonedClientCleanupInterval'] def run(self): """ Main thread entrypoint. Sets up and starts the DeviceThread. Loops repeatedly reading incoming messages. """ # Pause for a bit to let things bootup on host machine self.logger.info("Pausing execution 15 seconds" + " waiting for other system services to start") time.sleep(15) self.logger.info("Done with our nap. " + "Time to start looking for clients") # Start device thread, handles IO self.deviceThread = BlueGigaDeviceThread(self.callbacks, self.moduleConfig, self.loggingQueue) self.deviceThread.start() # Setup repeat task to run the sweep every X interval self.repeatTimerSweepClients = RepeatedTimer( (self._cleanupInterval / 1000), self.clientRegistry.sweepOldClients) self.logger.info( "Starting to watch collection point inbound message queue") while self.alive: if not self.inQueue.empty(): self.logger.info("Queue size is %s" % self.inQueue.qsize()) try: message = self.inQueue.get(block=False, timeout=1) if message is not None: if (message.topic == "SHUTDOWN" and message.sender_id == 'main'): self.logger.info("SHUTDOWN command handled on %s" % __name__) self.shutdown() else: self.handleMessage(message) except Exception as e: self.logger.error("Unable to read queue, error: %s " % e) self.shutdown() self.logger.info("Queue size is %s after" % self.inQueue.qsize()) else: time.sleep(.45) def handleBtleClientEvent(self, detectedClient): self.eventManager.registerDetectedClient(detectedClient) def handleMessage(self, msg): # Handle incoming messages, eg. from other collection points pass def killProcess(self, proc, timeout=1): """ Kill a process, given a timeout to join. """ self.logger.info('Joining process: %s' % proc) proc.join() p_sec = 0 for second in range(timeout): if proc.is_alive(): time.sleep(1) p_sec += 1 if p_sec >= timeout: self.logger.info('Terminating process: %s' % proc) proc.terminate() def shutdown(self): self.logger.info("Shutting down") self.repeatTimerSweepClients.stop() self.eventManager.stop() self.deviceThread.stop() # self.killProcess(self.deviceThread) self.alive = False time.sleep(1) self.exit = True
class BtleThreadCollectionPoint(object): def __init__(self, clientEventHandler, btleConfig, loggingQueue, debugMode=False): # Logger self.loggingQueue = loggingQueue self.logger = ThreadsafeLogger(loggingQueue, __name__) self.btleConfig = btleConfig self.clientEventHandler = clientEventHandler self.debug = debugMode # define basic BGAPI parser self.bgapi_rx_buffer = [] self.bgapi_rx_expected_length = 0 def start(self): packet_mode = False # create BGLib object self.ble = BGLib() self.ble.packet_mode = packet_mode self.ble.debug = self.debug # add handler for BGAPI timeout condition (hopefully won't happen) self.ble.on_timeout += self.my_timeout # on busy hander self.ble.on_busy = self.on_busy # add handler for the gap_scan_response event self.ble.ble_evt_gap_scan_response += self.clientEventHandler # create serial port object and flush buffers self.logger.info( "Establishing serial connection to BLED112 on com port %s at baud rate %s" % (self.btleConfig['BtleDeviceId'], self.btleConfig['BtleDeviceBaudRate'])) self.serial = Serial(port=self.btleConfig['BtleDeviceId'], baudrate=self.btleConfig['BtleDeviceBaudRate'], timeout=1) self.serial.flushInput() self.serial.flushOutput() # disconnect if we are connected already self.ble.send_command(self.serial, self.ble.ble_cmd_connection_disconnect(0)) self.ble.check_activity(self.serial, 1) # stop advertising if we are advertising already self.ble.send_command(self.serial, self.ble.ble_cmd_gap_set_mode(0, 0)) self.ble.check_activity(self.serial, 1) # stop scanning if we are scanning already self.ble.send_command(self.serial, self.ble.ble_cmd_gap_end_procedure()) self.ble.check_activity(self.serial, 1) # set the TX # range 0 to 15 (real TX power from -23 to +3dBm) #self.ble.send_command(self.serial, self.ble.ble_cmd_hardware_set_txpower(self.btleConfig['btleDeviceTxPower'])) #self.ble.check_activity(self.serial,1) #ble_cmd_connection_update connection: 0 (0x00) interval_min: 30 (0x001e) interval_max: 46 (0x002e) latency: 0 (0x0000) timeout: 100 (0x0064) #interval_min 6-3200 #interval_man 6-3200 #latency 0-500 #timeout 10-3200 self.ble.send_command( self.serial, self.ble.ble_cmd_connection_update(0x00, 0x001e, 0x002e, 0x0000, 0x0064)) self.ble.check_activity(self.serial, 1) # set scan parameters #scan_interval 0x4 - 0x4000 #Scan interval defines the interval when scanning is re-started in units of 625us # Range: 0x4 - 0x4000 # Default: 0x4B (75ms) # After every scan interval the scanner will change the frequency it operates at # at it will cycle through all the three advertisements channels in a round robin # fashion. According to the Bluetooth specification all three channels must be # used by a scanner. # #scan_window 0x4 - 0x4000 # Scan Window defines how long time the scanner will listen on a certain # frequency and try to pick up advertisement packets. Scan window is defined # as units of 625us # Range: 0x4 - 0x4000 # Default: 0x32 (50 ms) # Scan windows must be equal or smaller than scan interval # If scan window is equal to the scan interval value, then the Bluetooth module # will be scanning at a 100% duty cycle. # If scan window is half of the scan interval value, then the Bluetooth module # will be scanning at a 50% duty cycle. # #active 1=active 0=passive # 1: Active scanning is used. When an advertisement packet is received the # Bluetooth stack will send a scan request packet to the advertiser to try and # read the scan response data. # 0: Passive scanning is used. No scan request is made. #self.ble.send_command(self.serial, self.ble.ble_cmd_gap_set_scan_parameters(0x4B,0x32,1)) self.ble.send_command( self.serial, self.ble.ble_cmd_gap_set_scan_parameters(0xC8, 0xC8, 0)) self.ble.check_activity(self.serial, 1) # start scanning now self.ble.send_command(self.serial, self.ble.ble_cmd_gap_discover(1)) self.ble.check_activity(self.serial, 1) # handler to notify of an API parser timeout condition def my_timeout(self, sender, args): self.logger.error( "BGAPI timed out. Make sure the BLE device is in a known/idle state." ) # might want to try the following lines to reset, though it probably # wouldn't work at this point if it's already timed out: self.ble.send_command(self.serial, self.ble.ble_cmd_system_reset(0)) self.ble.check_activity(self.serial, 1) self.ble.send_command(self.serial, self.ble.ble_cmd_gap_discover(1)) self.ble.check_activity(self.serial, 1) def on_busy(self, sender, args): self.logger.warn("BGAPI device is busy.") def scan(self): # check for all incoming data (no timeout, non-blocking) self.ble.check_activity(self.serial)
class ClientRegistry(object): onClientRemoved = RegistryEvent() onClientAdded = RegistryEvent() onClientUpdated = RegistryEvent() onSweepComplete = RegistryEvent() def __init__(self,collectionPointConfig,loggingQueue): # Logger self.loggingQueue = loggingQueue self.logger = ThreadsafeLogger(loggingQueue, __name__) self.rClients = {} #registered clients self.collectionPointConfig = collectionPointConfig def getAll(self, thresh=-68): """ Get registered clients as dict Only return clients above thresh RSSI """ toret = {} for k, v in self.rClients.items(): xdata = v.getExtendedDataForEvent() if xdata['rssi']>thresh: toret[k] = xdata return toret def getUpdateData(self, thresh=-68): return {'nearby': self.getAll(thresh)} def getClient(self,mac): """ Get an existing registered client by mac and if its found return it. If no existing registered client is found return None. """ try: eClient = self.rClients[mac] except KeyError: eClient = None return eClient def sweepOldClients(self): """ Look at the registry and look for expired and inactive clients. Returns a list of removed clients. """ self.logger.debug("*** Sweeping clients existing count" + " %s***"%len(self.rClients)) clientsToBeRemoved=[] #list of clients to be cleaned up clientTimeout = self.collectionPointConfig['AbandonedClientTimeout'] now = datetime.now() for mac in self.rClients: regClient = self.rClients[mac] # self.logger.debug('now-regClient.lastRegisteredTime ---- clientTimeout >0 : %s ---- %s'%(((now-regClient.lastRegisteredTime).total_seconds()*1000), clientTimeout)) # if regClient.sweepShouldSendClientOutEvent(): if (now-regClient.lastRegisteredTime).total_seconds()*1000-clientTimeout>0: clientsToBeRemoved.append(regClient) for client in clientsToBeRemoved: # self.logger.debug("Client sweep removing mac %s"%client.getMac()) self.removeClient(client) self.logger.debug("*** End of sweeping tags existing count "+ "%s***"%len(self.rClients)) self.onSweepComplete(clientsToBeRemoved) return clientsToBeRemoved def addClient(self,client): #self.logger.debug("in addNewRegisteredClient with %s"%client.getUdid()) self.rClients[client.getMac()] = client self.onClientAdded(client) def updateClient(self,client): #self.logger.debug("in updateRegisteredClient with %s"%client.getUdid()) self.rClients[client.getMac()] = client self.onClientUpdated(client) def removeClient(self,client): #self.logger.debug("in removeRegisteredClient with %s"%client.getUdid()) self.logger.info('length before remove: %s'%len(self.rClients)) self.rClients.pop(client.getMac()) self.logger.info('length after remove: %s'%len(self.rClients)) self.onClientRemoved(client)
class BtleCollectionPoint(ModuleProcess): def __init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue): """ Initialize new CamCollectionPoint instance. Setup queues, variables, configs, constants and loggers. """ super(BtleCollectionPoint, self).__init__() self.loggingQueue = loggingQueue self.logger = ThreadsafeLogger(loggingQueue, __name__) # Queues self.outQueue = pOutBoundQueue # Messages from this thread to the main process self.inQueue = pInBoundQueue self.queueBLE = mp.Queue() # Configs self.moduleConfig = configLoader.load(self.loggingQueue) self.config = baseConfig # Variables and objects self.registeredClientRegistry = RegisteredClientRegistry( self.moduleConfig, self.loggingQueue) self.eventManager = EventManager(self.moduleConfig, pOutBoundQueue, self.registeredClientRegistry, self.loggingQueue) self.alive = True self.btleThread = None self.BLEThread = None self.repeatTimerSweepClients = None # Constants self._cleanupInterval = self.moduleConfig[ 'AbandonedClientCleanupInterval'] def run(self): ###Pausing Startup to wait for things to start after a system restart self.logger.info( "Pausing execution 15 seconds waiting for other system services to start" ) time.sleep(15) self.logger.info( "Done with our nap. Time to start looking for clients") self.btleThread = BlueGigaBtleCollectionPointThread( self.queueBLE, self.moduleConfig, self.loggingQueue) self.BLEThread = Thread(target=self.btleThread.bleDetect, args=(__name__, 10)) self.BLEThread.daemon = True self.BLEThread.start() # Setup repeat task to run the sweep every X interval self.repeatTimerSweepClients = RepeatedTimer( (self._cleanupInterval / 1000), self.registeredClientRegistry.sweepOldClients) # Process queue from main thread for shutdown messages self.threadProcessQueue = Thread(target=self.processQueue) self.threadProcessQueue.setDaemon(True) self.threadProcessQueue.start() #read the queue while self.alive: if not self.queueBLE.empty(): result = self.queueBLE.get(block=False, timeout=1) self.__handleBtleClientEvents(result) def processQueue(self): self.logger.info( "Starting to watch collection point inbound message queue") while self.alive: if not self.inQueue.empty(): self.logger.info("Queue size is %s" % self.inQueue.qsize()) try: message = self.inQueue.get(block=False, timeout=1) if message is not None: if (message.topic == "SHUTDOWN" and message.sender_id == 'main'): self.logger.info("SHUTDOWN command handled on %s" % __name__) self.shutdown() else: self.handleMessage(message) except Exception as e: self.logger.error("Unable to read queue, error: %s " % e) self.shutdown() self.logger.info("Queue size is %s after" % self.inQueue.qsize()) else: time.sleep(.25) def __handleBtleClientEvents(self, detectedClients): for client in detectedClients: self.logger.debug("--- Found client ---") self.logger.debug(vars(client)) self.logger.debug("--- Found client end ---") self.eventManager.registerDetectedClient(client) def handleMessage(self, msg): # Handle incoming messages, eg. from other collection points pass def shutdown(self): self.logger.info("Shutting down") self.repeatTimerSweepClients.stop() self.btleThread.stop() self.alive = False time.sleep(1) self.exit = True
class WebsocketServerModule(ModuleProcess): def __init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue): # super(WebsocketServerModule, self).__init__() ModuleProcess.__init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue) self.alive = False self.config = baseConfig self.inQueue = pInBoundQueue # inQueue are messages from the main process to websocket clients self.outQueue = pOutBoundQueue # outQueue are messages from clients to main process self.websocketServer = None self.loggingQueue = loggingQueue self.threadProcessQueue = None # Configs self.moduleConfig = configLoader.load(self.loggingQueue, __name__) # Constants self._port = self.moduleConfig['WebsocketPort'] self._host = self.moduleConfig['WebsocketHost'] # logging setup self.logger = ThreadsafeLogger(loggingQueue, __name__) def run(self): """ Main thread entry point. Sets up websocket server and event callbacks. Starts thread to monitor inbound message queue. """ self.logger.info("Starting websocket server") self.alive = True self.listen() self.websocketServer = WebsocketServer(self._port, host=self._host) self.websocketServer.set_fn_new_client(self.new_websocket_client) self.websocketServer.set_fn_message_received( self.websocket_message_received) self.websocketServer.run_forever() def new_websocket_client(self, client, server): """ Client joined callback - called whenever a new client joins. """ self.logger.debug("Client joined") def websocket_message_received(self, client, server, message): """ Message received callback - called whenever a new message is received. """ self.logger.debug('Message received: %s' % message) message = json.loads(message) self.logger.info("message jsond: %s" % message) _msg = Message(topic=message['topic'], sender_id=message['sender_id']) if 'sender_type' in message: _msg.sender_type = message['sender_type'] if 'recipients' in message: _msg.recipients = message['recipients'] if 'extended_data' in message: _msg.extended_data = message['extended_data'] self.put_message(_msg) def listen(self): self.threadProcessQueue = Thread(target=self.process_queue) self.threadProcessQueue.setDaemon(True) self.threadProcessQueue.start() def shutdown(self): """ Handle shutdown message. Close and shutdown websocket server. Join queue processing thread. """ self.logger.info("Shutting down websocket server") try: self.logger.info("Closing websocket") self.websocketServer.server_close() except Exception as e: self.logger.error("Websocket close error : %s " % e) try: self.logger.info("Shutdown websocket") self.websocketServer.shutdown() except Exception as e: self.logger.error("Websocket shutdown error : %s " % e) self.alive = False self.threadProcessQueue.join() time.sleep(1) self.exit = True def handle_message(self, message): """ Send message to listening clients. """ self.websocketServer.send_message_to_all(json.dumps(message.__dict__)) def process_queue(self): """ Monitor queue of messages from main process to this thread. """ while self.alive: if (self.inQueue.empty() == False): try: message = self.inQueue.get(block=False, timeout=1) if message is not None: if (message.topic.upper() == "SHUTDOWN" and message.sender_id.lower() == 'main'): self.logger.debug("SHUTDOWN handled") self.shutdown() else: self.handle_message(message) except Exception as e: self.logger.error("Websocket unable to read queue : %s " % e) else: time.sleep(.25)
class MQTTClientModule(ModuleProcess): """ Threaded MQTT client for processing and publishing outbound messages""" def __init__(self, baseConfig, pInBoundEventQueue, pOutBoundEventQueue, loggingQueue): super(MQTTClientModule, self).__init__() self.config = baseConfig self.alive = True self.inQueue = pInBoundEventQueue # Module config self.moduleConfig = configLoader.load(self.loggingQueue, __name__) # Constants self._keepAlive = self.moduleConfig['MqttKeepAlive'] self._feedName = self.moduleConfig['MqttFeedName'] self._username = self.moduleConfig['MqttUsername'] self._key = self.moduleConfig['MqttKey'] self._host = self.moduleConfig['MqttHost'] self._port = self.moduleConfig['MqttPort'] self._publishJson = self.moduleConfig['MqttPublishJson'] # MQTT setup self._client = mqtt.Client() self._client.username_pw_set(self._username, self._key) self._client.on_connect = self.onConnect self._client.on_disconnect = self.onDisconnect self._client.on_message = self.onMessage self.mqttConnected = False # Logging setup self.logger = ThreadsafeLogger(loggingQueue, "MQTT") def onConnect(self, client, userdata, flags, rc): self.logger.debug('MQTT onConnect called') # Result code 0 is success if rc == 0: self.mqttConnected = True # Subscribe to feed here else: self.logger.error('MQTT failed to connect: %s' % rc) raise RuntimeError('MQTT failed to connect: %s' % rc) def onDisconnect(self, client, userdata, rc): self.logger.debug('MQTT onDisconnect called') self.mqttConnected = False if rc != 0: self.logger.debug('MQTT disconnected unexpectedly: %s' % rc) self.handleReconnect(rc) def onMessage(self, client, userdata, msg): self.logger.debug('MQTT onMessage called for client: %s' % client) def connect(self): """ Connect to MQTT broker Skip calling connect if already connected. """ if self.mqttConnected: return self._client.connect(self._host, port=self._port, keepalive=self._keepAlive) def disconnect(self): """ Check if connected""" if self.mqttConnected: self._client.disconnect() def subscribe(self, feed=False): """Subscribe to feed, defaults to feed specified in config""" if not feed: feed = self._feedName self._client.subscribe('{0}/feeds/{1}'.format(self._username, feed)) def publish(self, value, feed=False): """Publish a value to a feed""" if not feed: feed = self._feedName self._client.publish('{0}/feeds/{1}'.format(self._username, feed), payload=value) def flattenDict(self, aDict): """ Get average of simple dictionary of numerical values """ try: val = float(sum(aDict[key] for key in aDict)) / len(aDict) except Exception as e: self.logger.error('Error flattening dict, returning 0: %s' % e) return 0 return val def publishJsonMessage(self, message): msg_str = self.stringifyMessage(message) self.publish(msg_str) def stringifyMessage(self, message): """ Dump into JSON string """ return json.dumps(message.__dict__).encode('utf8') def processQueue(self): """ Process incoming messages. """ while self.alive: # Pump the loop self._client.loop(timeout=1) if (self.inQueue.empty() == False): try: message = self.inQueue.get(block=False, timeout=1) if message is not None and self.mqttConnected: if (message.topic.upper() == "SHUTDOWN" and message.sender_id.lower() == 'main'): self.logger.debug("SHUTDOWN command handled") self.shutdown() else: # Send message as string or split into channels if self._publishJson: self.publishJsonMessage(message) else: self.publishValues(message) except Exception as e: self.logger.error("MQTT unable to read queue : %s " % e) else: time.sleep(.25) def shutdown(self): self.logger.info("Shutting down") self.alive = False time.sleep(1) self.exit = True def run(self): """ Thread start method""" self.logger.info("Running MQTT") self.connect() self.alive = True # Start queue loop self.processQueue()
class CollectionModule(ModuleProcess): # You can keep these parameters the same, all modules receive the same params # self - reference to self # baseConfig - configuration settings defined in /simplesensor/config/base.conf # (https://github.com/AdobeAtAdobe/SimpleSensor/blob/master/config/base.conf) # pInBoundQueue - messages from handler to this module # pOutBoundQueue - messages from this module to other modules # loggingQueue - logging messages for threadsafe logger def __init__(self, baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue): """ Initialize new CollectionModule instance. """ super(CollectionModule, self).__init__(baseConfig, pInBoundQueue, pOutBoundQueue, loggingQueue) # Most collection modules will follow a similar pattern... # 1. Set up some variables on the self object # Queues self.outQueue = pOutBoundQueue self.inQueue = pInBoundQueue self.loggingQueue = loggingQueue self.threadProcessQueue = None self.alive = False self.context = None self.reader = None # 2. Load the module's configuration file # Configs self.moduleConfig = configLoader.load(self.loggingQueue, __name__) self.config = baseConfig # 3. Set some constants to the self object from config parameters (if you want) self._id = self.moduleConfig['CollectionPointId'] self._type = self.moduleConfig['CollectionPointType'] self._port = self.moduleConfig['ReaderPortNumber'] # 4. Create a threadsafe logger object self.logger = ThreadsafeLogger(loggingQueue, __name__) def run(self): """ Main process method, run when the thread's start() function is called. Starts monitoring inbound messages to this module, and collection logic goes here. For example, you could put a loop with a small delay to keep polling the sensor, etc. When something is detected that's relevant, put a message on the outbound queue. """ # Monitor inbound queue on own thread self.listen() self.alive = True while self.context == None: self.establish_context() while self.alive: while self.reader is None: print('.') self.reader = self.get_reader() if self.reader is None: self.logger.info( 'Waiting for 5 seconds before ' + 'trying to find readers again. Is it plugged in?') time.sleep(5) try: # connect to card card = self.get_card() if card is None or self.reader is None: continue # try: cc_block_msg = [0xFF, 0xB0, 0x00, bytes([3])[0], 0x04] try: cc_block = self.send_transmission(card, cc_block_msg) except Exception as e: self.reader = None continue if (cc_block is None or cc_block[0] != 225): # magic number 0xE1 means data to be read self.reader = None continue data_size = cc_block[ 2] * 8 / 4 # CC[2]*8 is data area size in bytes (/4 for blocks) messages = [] data = [] m_ptr = 4 # pointer to actual card memory location terminate = False errd = False while (m_ptr <= data_size + 4 and not errd): msg = None if m_ptr > 255: byte_one = math.floor(m_ptr / 256) byte_two = m_ptr % (byte_one * 256) msg = [ 0xFF, 0xB0, bytes([byte_one])[0], bytes([byte_two])[0], 0x01 ] else: msg = [0xFF, 0xB0, 0x00, bytes([m_ptr])[0], 0x01] try: block = self.send_transmission(card, msg) except RuntimeError as e: self.logger.error("Error, empty block, reset reader 2") self.reader = None errd = True break # decode TLV header tag, length, f_rem = self.parse_TLV_header(block) if length != 255: m_ptr -= 1 if tag == 'TERMINATOR': terminate = True # now read the block of data into a record m_ptr += 1 data = [] for i in range(int(length / 4)): # working with blocks if errd: break msg = None if m_ptr > 255: byte_one = math.floor(m_ptr / 256) byte_two = m_ptr % (byte_one * 256) msg = [ 0xFF, 0xB0, bytes([byte_one])[0], bytes([byte_two])[0], 0x04 ] else: msg = [0xFF, 0xB0, 0x00, bytes([m_ptr])[0], 0x04] try: block = self.send_transmission(card, msg) except RuntimeError as e: self.logger.error( "Error, empty block, reset reader 3") self.reader = None errd = True break data += block m_ptr += 1 amsg = None if tag == 'NDEF': amsg = self.parse_NDEF_msg(data[f_rem:]) messages.append(amsg) for record in amsg: _TYPE = 0 if record[_TYPE]: terminate = True if terminate: break attendee_id = None event_id = None salutation = None first_name = None last_name = None middle_name = None _TYPE = 0 _ID = 1 _PAYLOAD = 2 for message in messages: for record in message: if record[_TYPE] == 'bcard.net:bcard' and len( record[_PAYLOAD] ) > 40: # to avoid bcard url payloads try: (attendee_id, event_id, salutation, first_name, last_name, middle_name) = self.decode_bcard_payload( record[_PAYLOAD]) except Exception as e: self.logger.error( "Error decoding BCARD payload: {}".format( e)) xdata = { 'attendee_id': attendee_id, 'event_id': event_id, 'salutation': salutation, 'first_name': first_name, 'last_name': last_name, 'middle_name': middle_name } msg = self.build_message(topic='scan_in', extendedData=xdata) self.logger.info('Sending message: {}'.format(msg)) self.put_message(msg) except Exception as e: self.logger.error('Loop error: {}'.format(e)) self.reader = None continue self.reader = None # sleep for a bit to avoid double scanning time.sleep(3) def parse_TLV_header(self, barr): # print('parsing TLV header bytes: ', barr) # return (tag, length (in bytes), [value] (if length != 0x00)) try: tag = None length = None if barr[0] == 0x00: tag = 'NULL' if barr[0] == 0x03: tag = 'NDEF' if barr[0] == 0xFD: tag = 'PROPRIETARY' if barr[0] == 0xFE: tag = 'TERMINATOR' f_rem = 0 if barr[1] != 0xFF: print('NOT USING 3 BYTE FORMAT') length = barr[1] f_rem = 2 else: length = struct.unpack('>h', bytes(barr[2:4]))[0] except Exception as e: self.logger.error('Error parsing TLV header: {}'.format(e)) return 0, 0, 0 return tag, length, f_rem def parse_NDEF_header(self, barr): # parse ndef header # return TNF(type name format), ID_LEN (ID length), # SR (short record bit), CF (chunk flag), # ME (message end), MB (message begin), # TYPE_LEN (type length), PAY_LEN (payload length), # ID (record type indicator) try: TNF = (0b00000111 & barr[0]) ID_LEN = (0b00001000 & barr[0]) >> 3 SR = (0b00010000 & barr[0]) >> 4 CF = (0b00100000 & barr[0]) >> 5 ME = (0b01000000 & barr[0]) >> 6 MB = (0b10000000 & barr[0]) >> 7 TYPE_LEN = barr[1] if SR: PAY_LEN = barr[2] PAY_TYPE = None REC_ID = None if TYPE_LEN > 0: PAY_TYPE = bytearray(barr[3:3 + TYPE_LEN]).decode('UTF-8') if ID_LEN > 0: REC_ID = barr[3 + TYPE_LEN:3 + TYPE_LEN + ID_LEN] else: PAY_LEN = struct.unpack('>I', bytes(barr[2:6]))[0] PAY_TYPE = None if TYPE_LEN > 0: PAY_TYPE = bytearray(barr[6:6 + TYPE_LEN]).decode('UTF-8') REC_ID = None if ID_LEN > 0: REC_ID = barr[6 + TYPE_LEN:6 + TYPE_LEN + ID_LEN] return (TNF, ID_LEN, SR, CF, ME, MB, TYPE_LEN, PAY_LEN, PAY_TYPE, REC_ID) except Exception as e: self.logger.error('Error parsing NDEF header: {}'.format(e)) return (None, None, None, None, None, None, None, None, None, None) def parse_NDEF_msg(self, data): # iterate through the message bytes, reading ndef messages itr = 0 records = [] while itr < len(data): # get header first try: (TNF, ID_LEN, SR, CF, ME, MB, TYPE_LEN, PAY_LEN, PAY_TYPE, REC_ID) = self.parse_NDEF_header(data) if (TNF is None and ID_LEN is None and SR is None and CF is None and ME is None and MB is None and TYPE_LEN is None and PAY_LEN is None and PAY_TYPE is None and REC_ID is None): return [] if SR: itr += 3 + ID_LEN + TYPE_LEN else: itr += 6 + ID_LEN + TYPE_LEN rec_payload = data[itr:itr + PAY_LEN] itr += PAY_LEN record = [PAY_TYPE, REC_ID, rec_payload] # type, id, payload records.append(record) except Exception as e: self.logger.error('Error parsing NDEF message: {}'.format(e)) return records return records def decode_bcard_payload(self, payload): # loop through decoding segments # each field separated by 0x1E # 6 fields: fields = { 'attendee_id': '', 'event_id': '', 'salutation': '', 'first_name': '', 'last_name': '', 'middle_name': '' } field = 0 for b in payload: try: if field == 0 and b == 30: field += 1 continue elif b == 31: field += 1 if field >= len(list(fields.keys())): break continue fields[list(fields.keys())[field]] += bytearray( [b]).decode('UTF8') except: pass return tuple(fields.values()) def establish_context(self): hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) if hresult != SCARD_S_SUCCESS: self.logger.error('Unable to establish context: {}'.format( SCardGetErrorMessage(hresult))) return self.context = hcontext def get_reader(self): hresult, readers = SCardListReaders(self.context, []) if hresult != SCARD_S_SUCCESS: self.logger.error('Failed to list readers: {}'.format( SCardGetErrorMessage(hresult))) return if len(readers) < 1 or len(readers) - 1 < self._port: self.logger.error( 'Not enough readers attached. {} needed, {} attached'.format( (self._port + 1), (len(readers)))) return else: return readers[self._port] def get_card(self, mode=None, protocol=None): hresult, hcard, dwActiveProtocol = SCardConnect( self.context, self.reader, mode or SCARD_SHARE_SHARED, protocol or (SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1)) if hresult != SCARD_S_SUCCESS: return else: return hcard def send_transmission(self, card, msg, protocol=None): hresult, response = SCardTransmit(card, protocol or SCARD_PCI_T1, msg) if hresult != SCARD_S_SUCCESS: self.logger.error('Failed to send transmission: {}'.format( SCardGetErrorMessage(hresult))) raise RuntimeError("Error sending transmission: {}".format( SCardGetErrorMessage(hresult))) else: return response[:-2] def listen(self): """ Start thread to monitor inbound messages, declare module alive. """ self.threadProcessQueue = Thread(target=self.process_queue) self.threadProcessQueue.setDaemon(True) self.threadProcessQueue.start() def build_message(self, topic, extendedData={}, recipients=['communication_modules']): """ Create a Message instance. topic (required): message type sender_id (required): id property of original sender sender_type (optional): type of sender, ie. collection point type, module name, hostname, etc extended_data (optional): payload to deliver to recipient(s) recipients (optional): module name, which module(s) the message will be delivered to, ie. `websocket_server`. use an array of strings to define multiple modules to send to. use 'all' to send to all available modules. use 'local_only' to send only to modules with `low_cost` prop set to True. [DEFAULT] use 'communication_modules' to send only to communication modules. use 'collection_modules' to send only to collection modules. """ msg = Message(topic=topic, sender_id=self._id, sender_type=self._type, extended_data=extendedData, recipients=recipients, timestamp=datetime.datetime.utcnow()) return msg def put_message(self, msg): """ Put message onto outgoing queue. """ print('putting message: ', msg) self.outQueue.put(msg) def process_queue(self): """ Process inbound messages on separate thread. When a message is encountered, trigger an event to handle it. Sleep for some small amount of time to avoid overloading. Also receives a SHUTDOWN message from the main process when the user presses the esc key. """ self.logger.info( "Starting to watch collection point inbound message queue") while self.alive: if (self.inQueue.empty() == False): self.logger.info("Queue size is %s" % self.inQueue.qsize()) try: message = self.inQueue.get(block=False, timeout=1) if message is not None: self.handle_message(message) except Exception as e: self.logger.error("Error, unable to read queue: %s " % e) self.shutdown() self.logger.info("Queue size is %s after" % self.inQueue.qsize()) else: time.sleep(.25) def handle_message(self, message): """ Handle messages from other modules to this one. Switch on the message topic, do something with the data fields. """ if message.topic.upper() == 'SHUTDOWN' and message.sender_id == 'main': self.shutdown() def shutdown(self): """ Shutdown the collection module. Set alive flag to false so it stops looping. Wait for things to die, then exit. """ self.alive = False print("Shutting down nfc_bcard_reader") time.sleep(1) self.exit = True