def __init__(self, p1: MapPoint, p2: MapPoint, s: int): """ :param p1: :type p1: MapPoint :param p2: :type p2: MapPoint :param s: :type s: int """ self.p1 = p1 self.p2 = p2 self.scale = min(s, 18) self.ta = [] self.xMin = None self.xMax = None self.yMin = None self.yMax = None self.tile_x_min = None self.tile_x_max = None self.tile_y_min = None self.tile_y_max = None self.width = None self.height = None self.genTileArray() if self.width > 5 or self.height > 5: LOGGER.warning(f"Large map ({self.width}x{self.height} tiles)")
def _run(self): LOGGER.debug( f"SIM connection started (device_address={self.device_address})") try: while True: id = self.stdout.read(1)[0] # Returns 0 if process was killed if id not in SimConnection.packetHandlers.keys(): LOGGER.error( f"SIM protocol violation!!! Shutting down. (device_address={self.device_address})" ) for b in self.stdout.getHistory(): LOGGER.error(hex(b[0])) LOGGER.error("^^^^ violation.") return # Call packet handler SimConnection.packetHandlers[id](self) except Exception as ex: with self._shutdown_lock: if not self._is_shutting_down: LOGGER.exception( f"Error in SIM connection. (device_address={self.device_address})" ) LOGGER.warning( f"SIM connection thread shut down (device_address={self.device_address})" )
def timer(self): """ """ LOGGER.debug("Auto-save thread started") while True: with self.as_cv: self.as_cv.wait_for(lambda: self._as_is_shutting_down, timeout=AUTOSAVE_INTERVAL_S) if self._as_is_shutting_down: break try: t1 = time.perf_counter() self.save(self.session_name) t2 = time.perf_counter() # LOGGER.debug("Auto-Save Successful.") LOGGER.debug(f"Successfully Auto-Saved in {t2 - t1} seconds.") except Exception as e: LOGGER.exception( "Exception in autosave thread" ) # Automatically grabs and prints exception info LOGGER.warning("Auto save thread shut down")
def _run(self) -> None: """ """ with self.cv: LOGGER.debug( f"Debug connection thread started (device_address={self.device_address})" ) while True: self.cv.wait_for(lambda: self._is_shutting_down, timeout=PACKET_INTERVAL_S) if self._is_shutting_down: break if not self.callback: continue full_arr: bytearray = bytearray() # full_arr.extend(self.config_mock_set_values()) # full_arr.extend(self.message_mock_set_values()) full_arr.extend(self.bulk_sensor_mock_random()) # full_arr.extend(self.bad_subpacket_id_mock()) # bad id, to see handling of itself and remaining data full_arr.extend(self.gps_mock_random()) full_arr.extend(self.orientation_mock_random()) self.receive(full_arr) LOGGER.warning( f"Debug connection thread shut down (device_address={self.device_address})" )
def _handleRadio(self): length = self._getLength() if length == 0: LOGGER.warning(f"Empty SIM radio packet received (device_address={self.device_address})") data = self.stdout.read(length) self._xbee.recieved_from_rocket(data)
def _parse_tx_request_frame(self, data, frame_len) -> None: """ :brief: Parses a TX Request frame, and passes a TX Status packet to the rocket. :param data: Iterable :param frame_len: length as defined in XBee protocol """ calculated_checksum = FrameType.TX_REQUEST.value # Checksum includes frame type frame_id = next(data) calculated_checksum += frame_id destination_address = bytearray() for _ in range(8): # 64 bit destination address b = next(data) destination_address.append(b) calculated_checksum += b # Reserved 2 bytes. But in one case it's labelled as network address? network_addr_msb = next(data) calculated_checksum += network_addr_msb network_addr_lsb = next(data) calculated_checksum += network_addr_lsb broadcast_radius = next(data) # Broadcast radius - not used calculated_checksum += broadcast_radius transmit_options = next(data) calculated_checksum += transmit_options payload = bytearray() for _ in range(frame_len - 14): b = next(data) payload.append(b) calculated_checksum += b received_checksum = next(data) calculated_checksum = 0xFF - (calculated_checksum & 0xFF ) # As per XBee's spec if received_checksum != calculated_checksum: raise ChecksumMismatchError() if destination_address == bytearray( self.gs_address) or destination_address == bytearray( XBEE_BROADCAST_ADDRESS): self.ground_callback(payload) else: LOGGER.warning( f"Discarding tx request frame with destination address other than GS ({destination_address.hex()})" ) # Send acknowledge status_payload = bytearray( (frame_id, network_addr_msb, network_addr_lsb, 0, 0, 0)) self.rocket_callback( self._create_frame(FrameType.TX_STATUS, status_payload))
def run(self): """ """ LOGGER.debug("Send thread started") # TODO : Once we have multiple connections, we will loop over and send a config request to each # Starting up, request hello/ha ndshake/identification for connection in self.connections.values(): try: connection.broadcast(self.command_parser.broadcast_data(CommandType.CONFIG)) except Exception as ex: LOGGER.exception("Exception in send thread while sending config requests") while True: try: message = self.commandQueue.get(block=True, timeout=None) # Block until something new self.commandQueue.task_done() if message is None: # Either received None or woken up for shutdown with self._shutdown_lock: if self._is_shutting_down: break else: continue try: (device, command, data) = self.command_parser.pase_command(message) except CommandParsingError as ex: LOGGER.error(f"Error parsing command: {str(ex)}") continue full_address = self.device_manager.get_full_address(device) if full_address is None: LOGGER.error(f"Device not yet connected: {device.name}") continue connection = self.connections[full_address.connection_name] LOGGER.info(f"Sending command {command.name} to device {device.name} ({full_address})") connection.send(full_address.device_address, data) LOGGER.info("Sent command!") COMMAND_SENT_EVENT.increment() except TimeoutException: # TODO: Connection should have converted this to a generic exception for decoupling LOGGER.error("Message timed-out!") except queue.Empty: pass except Exception as ex: LOGGER.exception("Unexpected error while sending!") # Automatically grabs and prints exception info LOGGER.warning("Send thread shut down")
def run(self) -> None: """ """ LOGGER.debug("Mapping thread started") last_latitude = None last_longitude = None last_desired_size = None last_update_time = 0 while True: with self.cv: self.cv.wait() # CV lock is released while waiting if self._is_shutting_down: break try: # Prevent update spam current_time = time.time() if current_time - last_update_time < 0.5: time.sleep(0.5) # copy location values to use, to keep the values consistent in synchronous but adjacent calls latitude = self.rocket_data.last_value_by_device( self.device, DataEntryIds.LATITUDE) longitude = self.rocket_data.last_value_by_device( self.device, DataEntryIds.LONGITUDE) desired_size = self.getDesiredMapSize() # Prevent unnecessary work while no location data is received if latitude is None or longitude is None: continue # Prevent unnecessary work while data hasnt changed if (latitude, longitude, desired_size) == (last_latitude, last_longitude, last_desired_size): continue if self.plotMap(latitude, longitude, DEFAULT_RADIUS, DEFAULT_ZOOM): # notify UI that new data is available to be displayed self.sig_received.emit() else: continue last_latitude = latitude last_longitude = longitude last_update_time = current_time last_desired_size = desired_size except Exception: LOGGER.exception( "Error in map thread loop" ) # Automatically grabs and prints exception info LOGGER.warning("Mapping thread shut down")
def register_device(self, device_type: DeviceType, device_version: str, full_address: FullAddress) -> None: with self._lock: if device_type not in self._device_type_to_device and full_address not in self._full_address_to_device: pass elif device_type in self._device_type_to_device and full_address != self._device_type_to_device[ device_type].full_address: raise InvalidRegistration( f"Cannot reassign device_type={device_type.name} (full_address={self._device_type_to_device[device_type].full_address}) to full_address={full_address}" ) elif full_address in self._full_address_to_device and device_type != self._full_address_to_device[ full_address].device_type: raise InvalidRegistration( f"Cannot reassign full_address={full_address} (device={self._full_address_to_device[full_address].device_type}) to device={device_type}" ) else: LOGGER.info( f"Already registered. Device={device_type.name}, full_address={full_address}" ) return if device_type in self.required_versions and device_version != self.required_versions[ device_type]: error_str = f"Version {device_version} does not match required version {self.required_versions[device_type]} for device {device_type.name}" if self.strict_versions: raise InvalidDeviceVersion(error_str) else: LOGGER.warning(error_str) self._device_type_to_device[device_type] = RegisteredDevice( device_type=device_type, full_address=full_address) self._full_address_to_device = { d.full_address: d for d in self._device_type_to_device.values() } # If two devices somehow have the same full address, mapping wont be one-to-one. assert len(self._device_type_to_device) == len( self._full_address_to_device) if device_type in self.expected_devices: LOGGER.info( f"Registered expected device={device_type.name}, full_address={full_address}, {self.num_expected_registered()}/{len(self.expected_devices)} expected devices" ) else: LOGGER.warning( f"Registered unexpected device={device_type.name}, full_address={full_address}, {self.num_expected_registered()}/{len(self.expected_devices)} expected devices" ) DEVICE_REGISTERED_EVENT.increment()
def setViewedDevice(self, devices) -> None: """ :param devices :type devices: list of rocket devices """ with self.cv: self.viewed_devices = devices for device in self.viewed_devices: if self.rocket_data.last_value_by_device( device, DataEntryIds.LATITUDE) is None: LOGGER.warning("Data unavailable for {}".format( device.name)) self.notify()
def _run_rocket_rx(self) -> None: """ :brief: Process the incoming rocket data queue. This is the top level function, and handles any unescaped start delimiters. """ LOGGER.debug(f"Xbee sim thread started") while True: try: start = next(self._rocket_rx_queue) assert start == START_DELIMITER self._parse_API_frame() except UnescapedDelimiterError: LOGGER.warning("Caught UnescapedDelimiterError exception") continue # drop it and try again except ShuttingDown: break LOGGER.warning("Xbee sim thread shut down")
def processMap(requestQueue, resultQueue): """To be run in a new process as the stitching and resizing is a CPU bound task :param requestQueue: :type requestQueue: Queue :param resultQueue: :type resultQueue: Queue """ # On Windows, process forking does not copy globals and thus all packeges are re-imported. Not for threads # though. # Note: This means that on Windows the logger will create one log file per process because the session ID # is based on the import time # https://docs.python.org/3/library/multiprocessing.html#logging # TODO: Fix by creating .session file which contains session ID and other # process-global constants. Look into file-locks to make this multiprocessing safe. This is an OS feature LOGGER.debug("Mapping process started") while True: try: request = requestQueue.get() if request is None: # Shutdown request break (p0, p1, p2, zoom, desiredSize) = request location = mapbox_utils.TileGrid(p1, p2, zoom) location.downloadArrayImages() largeMapImage = location.genStitchedMap() x_min, x_max, y_min, y_max = location.xMin, location.xMax, location.yMin, location.yMax if desiredSize is None: resizedMapImage = largeMapImage else: if desiredSize[0] / desiredSize[1] > abs(p1.x - p2.x) / abs( p1.y - p2.y): # Wider aspect ratio x_crop_size = (abs(p1.x - p2.x) * largeMapImage.shape[1] ) / (location.xMax - location.xMin) y_crop_size = (x_crop_size * desiredSize[1]) / desiredSize[0] else: # Taller aspect ratio y_crop_size = (abs(p1.y - p2.y) * largeMapImage.shape[0] ) / (location.yMax - location.yMin) x_crop_size = (y_crop_size * desiredSize[0]) / desiredSize[1] center_x = ( (p0.x - location.xMin) * largeMapImage.shape[1]) / (location.xMax - location.xMin) center_y = ( (p0.y - location.yMin) * largeMapImage.shape[0]) / (location.yMax - location.yMin) # Crop image centered around p0 (point of interest) and at the desired aspect ratio. # Crop is largest possible within rectangle defined by p1 & p2 x_crop_start = round(center_x - x_crop_size / 2) x_crop_end = round(x_crop_start + x_crop_size) y_crop_start = round(center_y - y_crop_size / 2) y_crop_end = round(y_crop_start + y_crop_size) croppedMapImage = largeMapImage[y_crop_start:y_crop_end, x_crop_start:x_crop_end] # Check obtained desired aspect ratio (within one pixel) assert abs(x_crop_size / y_crop_size - desiredSize[0] / desiredSize[1]) < 1 / max( croppedMapImage.shape[0:2]) assert croppedMapImage.shape[1] == round(x_crop_size) assert croppedMapImage.shape[0] == round(y_crop_size) x_min, x_max, y_min, y_max = min(p1.x, p2.x), max( p1.x, p2.x), min(p1.y, p2.y), max(p1.y, p2.y) if croppedMapImage.shape[1] < desiredSize[0]: # Dont scale up the image. Waste of memory. resizedMapImage = croppedMapImage else: # Downsizing the map here to the ideal size for the plot reduces the amount of work required in the # main thread and thus reduces stuttering resizedMapImage = np.array( Image.fromarray(croppedMapImage).resize( (desiredSize[0], desiredSize[1] ))) # x,y order is opposite for resize resultQueue.put((resizedMapImage, x_min, x_max, y_min, y_max)) except Exception as ex: LOGGER.exception("Exception in processMap process" ) # Automatically grabs and prints exception info resultQueue.put(None) resultQueue.cancel_join_thread() requestQueue.cancel_join_thread() resultQueue.close() requestQueue.close() LOGGER.warning("Mapping process shut down")
def run(self): """ """ LOGGER.debug("Send thread started") # TODO : Once we have multiple connections, we will loop over and send a config request to each # Starting up, request hello/ha ndshake/identification for connection in self.connections.values(): try: connection.broadcast(bytes([CommandType.CONFIG.value])) except Exception as ex: LOGGER.exception( "Exception in send thread while sending config requests") while True: try: message = self.commandQueue.get( block=True, timeout=None) # Block until something new self.commandQueue.task_done() if message is None: # Either received None or woken up for shutdown with self._shutdown_lock: if self._is_shutting_down: break else: continue message_parts = message.split('.') if len(message_parts) != 2: LOGGER.error("Bad command format") continue (device_str, command_str) = message_parts try: device = DeviceType[device_str.upper()] except KeyError: LOGGER.error(f"Unknown device: {device_str}") continue full_address = self.device_manager.get_full_address(device) if full_address is None: LOGGER.error(f"Device not yet connected: {device.name}") continue connection = self.connections[full_address.connection_name] try: command = CommandType[command_str.upper()] except KeyError: LOGGER.error(f"Unknown command {command_str}") continue LOGGER.info( f"Sending command {command} to device {device.name} ({full_address})" ) data = bytes([command.value]) connection.send(full_address.device_address, data) LOGGER.info("Sent command!") COMMAND_SENT_EVENT.increment() except TimeoutException: # TODO: Connection should have converted this to a generic exception for decoupling LOGGER.error("Message timed-out!") except queue.Empty: pass except Exception as ex: LOGGER.exception( "Unexpected error while sending!" ) # Automatically grabs and prints exception info LOGGER.warning("Send thread shut down")
def run(self): """This thread loop waits for new data and processes it when available""" LOGGER.debug("Read thread started") while True: connection_message = self.dataQueue.get( block=True, timeout=None) # Block until something new self.dataQueue.task_done() if connection_message is None: # Either received None or woken up for shutdown with self._shutdown_lock: if self._is_shutting_down: break else: continue connection = connection_message.connection full_address = FullAddress( connection_name=self.connection_to_name[connection], device_address=connection_message.device_address) data = connection_message.data byte_stream: BytesIO = BytesIO(data) # Get length of bytes (without using len(data) for decoupling) byte_stream.seek(0, SEEK_END) end = byte_stream.tell() byte_stream.seek(0) # Iterate over stream to extract subpackets where possible while byte_stream.tell() < end: try: self.packet_parser.set_endianness( connection.isIntBigEndian(), connection.isFloatBigEndian()) parsed_data: Dict[DataEntryIds, any] = self.packet_parser.extract( byte_stream) if DataEntryIds.DEVICE_TYPE in parsed_data and DataEntryIds.VERSION_ID in parsed_data: self.device_manager.register_device( parsed_data[DataEntryIds.DEVICE_TYPE], parsed_data[DataEntryIds.VERSION_ID], full_address) elif DataEntryIds.DEVICE_TYPE in parsed_data: LOGGER.warning( 'Received DEVICE_TYPE but not VERSION_ID') elif DataEntryIds.VERSION_ID in parsed_data: LOGGER.warning( 'Received VERSION_ID but not DEVICE_TYPE') self.rocket_data.add_bundle(full_address, parsed_data) # notify UI that new data is available to be displayed self.sig_received.emit() except Exception as e: LOGGER.exception("Error decoding new packet! %s", e) # Just discard rest of data TODO Review policy on handling remaining data or problem packets. Consider data errors too byte_stream.seek(0, SEEK_END) CONNECTION_MESSAGE_READ_EVENT.increment() LOGGER.warning("Read thread shut down")