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 downloadArrayImages(self, attempts: int = 3, overwrite: bool = False) -> None: """ :param attempts: :type attempts: int :param overwrite: :type overwrite: bool """ LOGGER.debug(f"Beginning download of size {str(self.scale)} tiles.") t1 = time.perf_counter() def getRow(row): for i in row: a = attempts while (not i.imageExists()) and (a > 0): i.getImage(overwrite=overwrite) a = a - 1 with concurrent.futures.ThreadPoolExecutor() as executor: executor.map(getRow, self.ta) t2 = time.perf_counter() LOGGER.debug( f"Successfully downloaded size {str(self.scale)} tiles in {t2 - t1} seconds." )
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 get_pin_mode(self, pin): """ :param pin: Should be a test pin """ with self._lock: val = self._pin_modes[pin] LOGGER.debug(f"Pin mode read from pin={pin} returned value={val}") return val
def set_pin_mode(self, pin, mode): """ :param pin: Should be a test pin :param mode: The mode you want to set the pin to -> 0 for INPUT and 1 for OUTPUT """ with self._lock: self._pin_modes[pin] = mode LOGGER.debug(f"Pin mode of pin={pin} set to={mode}")
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 analog_read(self, pin): """ :param pin: Should be a read pin. Don't rely on behaviour if the pin isn't a readable pin. """ with self.lock: val = 0 if pin in self._ignitor_reads: val = self._ignitor_reads[pin].read() LOGGER.debug(f"Analog read from pin={pin} returned value={val}") return val
def digital_write(self, pin, val): """ :param pin: Should be a test pin. :param val: True to set high, False to set low """ with self.lock: LOGGER.debug(f"Digital write to pin={pin} with value value={val}") if pin in self._ignitor_tests: self._ignitor_tests[pin].write(val) elif pin in self._ignitor_fires and val: self._ignitor_fires[pin].fire()
def analog_read(self, pin): """ :param pin: Should be a read pin. Don't rely on behaviour if the pin isn't a readable pin. """ with self._lock: val = 0 if pin in self._ignitor_reads: val = self._ignitor_reads[pin].read() LOGGER.debug( f"Analog read from pin={pin} returned value={val}") else: voltage_sensor = self._sensors[SensorType.VOLTAGE] if voltage_sensor is not None and pin == voltage_sensor.pin: val = self._sensors[SensorType.VOLTAGE].read() return val
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 shutdown(self): """ This is called when the app is being requested to shut down :return: :rtype: """ LOGGER.debug(f"MainApp shutting down") self.ReadThread.shutdown() self.SendThread.shutdown() self.rocket_data.shutdown() for connection in self.connections.values(): connection.shutdown() LOGGER.debug( f"All threads shut down, remaining threads: {threading.enumerate()}" ) LOGGER.info("Saving...") self.rocket_data.save( os.path.join(LOGS_DIR, "finalsave_" + SESSION_ID + ".csv")) LOGGER.info("Saved!")
def __init__(self, connections: Dict[str, Connection], rocket_profile: RocketProfile) -> None: """ :param connection: :type connection: Connection :param rocket_profile: :type rocket_profile: RocketProfile """ # Prints constructor arguments, leave at top of constructor LOGGER.debug(f"Starting MainApp with {locals()}") if connections is None: raise Exception("Invalid connections provided") QtWidgets.QMainWindow.__init__(self) self.setupUi(self) self.connections = connections self.rocket_profile = rocket_profile self.device_manager = DeviceManager( self.rocket_profile.expected_devices, self.rocket_profile.required_device_versions, strict_versions=False) self.rocket_data = RocketData(self.device_manager) self.command_parser = CommandParser(self.device_manager) packet_parser = self.rocket_profile.construct_packet_parser() # Init and connection of ReadThread self.ReadThread = ReadThread(self.connections, self.rocket_data, packet_parser, self.device_manager) self.ReadThread.sig_received.connect(self.receive_data) self.ReadThread.start() # Init and connection of SendThread self.SendThread = SendThread(self.connections, self.device_manager, self.command_parser) self.sig_send.connect(self.SendThread.queueMessage) self.SendThread.start()
def doneButtonPressed(self) -> None: """ """ rocket = self.rocketBox.currentText() connection = self.typeBox.currentText() baud_rate = int(self.baudBox.currentText()) com_port = self.comBox.currentText() LOGGER.debug(f"User has selected rocket={rocket}, connection={connection}, com_port={com_port}, baud_rate={baud_rate}") self.chosen_rocket = self.RocketProfiles[rocket] if connection == 'Serial': self.chosen_connection = self.chosen_rocket.construct_serial_connection(com_port, int(baud_rate)) elif connection == 'Debug': self.chosen_connection = self.chosen_rocket.construct_debug_connection() elif connection == 'SIM': self.chosen_connection = self.chosen_rocket.construct_sim_connection() else: raise Exception("Unknown connection") self.close()
def genStitchedMap(self, overwrite: bool = False) -> np.ndarray: """ :param overwrite: :type overwrite: bool :return: :rtype: numpy.ndarray """ def appendv(A, B): if A is None: return B elif B is None: return A else: return np.vstack((A, B)) def appendh(A, B): if A is None: return B elif B is None: return A else: return np.column_stack((A, B)) out = os.path.join(MAPBOX_CACHE, "out") if not os.path.isdir(out): os.mkdir(out) outfile = os.path.join(out, f"output_{str(self)}.png") if (not os.path.isfile(outfile)) or overwrite: LOGGER.debug(f"Generating size {str(self.scale)} map!") t1 = time.perf_counter() is_img_not_blank = False img = None for i in self.ta: row = None for j in i: row = appendh(row, j.getImage()) if j.is_tile_not_blank is True: is_img_not_blank = True img = appendv(img, row) if is_img_not_blank is True: plt.imsave(outfile, img) t2 = time.perf_counter() LOGGER.debug( f"Successfully generated size {str(self.scale)} map in {t2 - t1} seconds." ) return img else: LOGGER.debug(f"Found size {str(self.scale)} map!") return plt.imread(outfile, "jpeg")
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")
# QApplication expects the first argument to be the program name. qt_args = sys.argv[:1] + unparsed_args app = QtWidgets.QApplication(qt_args) font = app.font() font.setPointSize(max(MIN_APP_FONT_POINT_SIZE, font.pointSize())) app.setFont(font) if IS_PYINSTALLER and '_PYIBoot_SPLASH' in os.environ: # Now that we are all loaded, close the splash screen try: # pyi_splash is not a real module, its only available if splash was successfully included in the build import pyi_splash pyi_splash.close() except: LOGGER.debug("pyi_splash module expected but not found") if not args.self_test: # Open com_window dialog to get startup details com_window = ComWindow() com_window.show() return_code = app.exec_() if return_code != 0 or com_window.chosen_rocket is None or com_window.chosen_connection is None: sys.exit(return_code) rocket = com_window.chosen_rocket connection = com_window.chosen_connection main_window = rocket.construct_app(connection) else: rocket = TantalusProfile()
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")