def config(self, byte_stream: BytesIO, header: Header): """ :param byte_stream: :type byte_stream: :param header: :type header: :return: :rtype: """ data = {} data[DataEntryIds.IS_SIM] = bool(byte_stream.read(1)[0]) data[DataEntryIds.DEVICE_TYPE] = ID_TO_DEVICE_TYPE[byte_stream.read(1) [0]] version_id = byte_stream.read(VERSION_ID_LEN) data[DataEntryIds.VERSION_ID] = version_id.decode('ascii') LOGGER.info("Config: SIM? %s, Rocket type = %s, Version ID = %s", str(data[DataEntryIds.IS_SIM]), str(data[DataEntryIds.DEVICE_TYPE]), str(data[DataEntryIds.VERSION_ID])) CONFIG_EVENT.increment() return data
def extract(self, byte_stream: BytesIO): """ Return dict of parsed subpacket data and length of subpacket :param byte_stream: :type byte_stream: :return: parsed_data :rtype: Dict[Any, Any] """ if self.big_endian_ints is None or self.big_endian_floats is None: raise Exception("Endianness not set before parsing") # header extraction header: Header = self.header(byte_stream) # data extraction parsed_data: Dict[Any, Any] = {} try: parsed_data = self.parse_data(byte_stream, header) except Exception as e: LOGGER.exception("Error parsing data" ) # Automatically grabs and prints exception info parsed_data[DataEntryIds.TIME] = header.timestamp self.big_endian_ints = None self.big_endian_floats = None return parsed_data
def _handleBuzzer(self): length = self._getLength() assert length == 1 data = self.stdout.read(length) songType = int(data[0]) LOGGER.info(f"SIM: Bell rang with song type {songType} (device_address={self.device_address})")
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) -> 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 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 _handlePinMode(self): length = self._getLength() assert length == 2 pin, mode = self.stdout.read(2) self._hw_sim.set_pin_mode(pin, mode) LOGGER.info(f"SIM: Mode for pin {pin} set to {mode} (device_address={self.device_address})")
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 launch(self) -> None: assert self.get_flight_state() == FlightState.STANDBY self._state = FlightState.FLIGHT self._launch_time = self.get_time() LOGGER.info( f"Rocket launched at time {self.get_time()} s, and altitude {self.get_data(FlightDataType.TYPE_ALTITUDE)} m")
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 _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 _handleDigitalPinWrite(self): length = self._getLength() assert length == 2 pin, value = self.stdout.read(2) self._hw_sim.set_pin_mode(pin, PinModes.INPUT) self._hw_sim.digital_write(pin, value) LOGGER.info(f"SIM: Pin {pin} set to {value} (device_address={self.device_address})")
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 __init__(self, connections: Dict[str, Connection], rocket_profile: RocketProfile) -> None: """ :param connections: :param rocket_profile: """ super().__init__(connections, rocket_profile) self.map_data = map_data.MapData() self.im = None # Plot im self.command_history = [] self.command_history_index = None # Attach functions for static buttons self.sendButton.clicked.connect(self.send_button_pressed) self.actionSave.triggered.connect(self.save_file) self.actionSave.setShortcut("Ctrl+S") self.actionReset.triggered.connect(self.reset_view) # Hook into some of commandEdit's events qtHook(self.commandEdit, 'focusNextPrevChild', lambda _: False, override_return=True) # Prevents tab from changing focus qtHook(self.commandEdit, 'keyPressEvent', self.command_edit_key_pressed) qtHook(self.commandEdit, 'showPopup', self.command_edit_show_popup) # Hook-up logger to UI text output # Note: Currently doesn't print logs from other processes (e.g. mapping process) log_format = logging.Formatter("[%(asctime)s] (%(levelname)s) %(message)s") log_handler = qtSignalLogHandler(exception_traces=False) log_handler.setLevel(logging.INFO) log_handler.setFormatter(log_format) log_handler.qt_signal.connect(self.print_to_ui) LOGGER.addHandler(log_handler) # Setup dynamic UI elements self.setup_buttons() self.setup_labels() self.setup_subwindow().showMaximized() self.setup_view_menu() self.setWindowIcon(QtGui.QIcon(mapbox_utils.MARKER_PATH)) # Setup user window preferences self.original_geometry = self.saveGeometry() self.original_state = self.saveState() self.settings = QtCore.QSettings('UBCRocket', 'UBCRGS') self.restore_view() # Init and connection of MappingThread self.MappingThread = MappingThread(self.connections, self.map_data, self.rocket_data, self.rocket_profile) self.MappingThread.sig_received.connect(self.receive_map) self.MappingThread.start() LOGGER.info(f"Successfully started app (version = {GIT_HASH})")
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}") return val
def broadcast(self, data) -> None: # must be thead safe LOGGER.info( f"{data} sent to address={self.device_address} on DebugConnection") with self.lock: if data == bytes([0x41]): ARMED_EVENT.increment() elif data == bytes([0x44]): DISARMED_EVENT.increment() elif data == bytes([0x50]): self.receive(self.status_ping_mock_set_values()) elif data == bytes([0x43]): self.receive(self.config_mock_set_values())
def _getEndianness(self): id = self.stdout.read(1)[0] assert id == SimRxId.CONFIG.value length = self._getLength() assert length == 8 data = self.stdout.read(length) self.bigEndianInts = data[0] == 0x04 self.bigEndianFloats = data[4] == 0xC0 LOGGER.info( f"SIM: Big Endian Ints - {self.bigEndianInts}, Big Endian Floats - {self.bigEndianFloats} (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 receive_data(self) -> None: """ This is called when new data is available to be displayed. :return: :rtype: """ for label in self.rocket_profile.labels: try: getattr(self, label.name + "Label").setText( label.update(self.rocket_data)) except: LOGGER.exception(f'Failed to update {label.name}Label:') LABLES_UPDATED_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 __init__(self, connections: Dict[str, Connection], rocket_profile: RocketProfile) -> None: """ :param connection: :type connection: Connection :param rocket_profile: :type rocket_profile: RocketProfile """ super().__init__(connections, rocket_profile) self.map = map_data.MapData() self.im = None # Plot im # Attach functions for static buttons self.sendButton.clicked.connect(self.send_button_pressed) self.commandEdit.returnPressed.connect(self.send_button_pressed) self.actionSave.triggered.connect(self.save_file) self.actionSave.setShortcut("Ctrl+S") self.actionReset.triggered.connect(self.reset_view) # Hook-up logger to UI text output # Note: Currently doesnt print logs from other processes (e.g. mapping process) log_format = logging.Formatter("[%(asctime)s] (%(levelname)s) %(message)s") log_handler = qtSignalLogHandler(exception_traces=False) log_handler.setLevel(logging.INFO) log_handler.setFormatter(log_format) log_handler.qt_signal.connect(self.print_to_ui) LOGGER.addHandler(log_handler) self.setup_buttons() self.setup_labels() self.setup_subwindow().showMaximized() self.setWindowIcon(QtGui.QIcon(mapbox_utils.MARKER_PATH)) self.original_geometry = self.saveGeometry() self.original_state = self.saveState() self.settings = QtCore.QSettings('UBCRocket', 'UBCRGS') self.restore_view() # Init and connection of MappingThread self.MappingThread = MappingThread(self.connections, self.map, self.rocket_data, self.rocket_profile) self.MappingThread.sig_received.connect(self.receive_map) self.MappingThread.start() LOGGER.info(f"Successfully started app (version = {GIT_HASH})")
def state(self, byte_stream: BytesIO, header: Header, print_state=True): """ :param byte_stream: :param header: :return: """ data = {} state_id = self.bytestoint(byte_stream.read(2)) data_entry_value = STATE_IDS[state_id] data[DataEntryIds.STATE] = data_entry_value if print_state: LOGGER.info("State: %s", str(data_entry_value.name)) STATE_EVENT.increment() return data
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 event(self, byte_stream: BytesIO, header: Header): """ :param byte_stream: :type byte_stream: :param header: :type header: :return: """ data: Dict = {} event_bytes = byte_stream.read(2) event_int = self.bytestoint(event_bytes) data_entry_value = EVENT_IDS[event_int] data[DataEntryIds.EVENT] = data_entry_value LOGGER.info("Event: %s", str(data_entry_value.name)) EVENT_EVENT.increment() return data
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 _deploy_recovery(self): if self.get_flight_state() == FlightState.DROGUE_DESCENT: # Drogue deploying self._drogue_deployment_time = self.get_time_since_launch() elif self.get_flight_state() == FlightState.MAIN_DESCENT: # Main deploying self._main_deployment_time = self.get_time_since_launch() else: assert False new_data, new_events = self._run_simulation() assert abs(new_events[FlightEvent.RECOVERY_DEVICE_DEPLOYMENT][-1] - self.get_time_since_launch()) < 0.1 merged_events = dict() # Retain original events if already passed for event, times in self._events.items(): for i in range(len(times)): time = times[i] if time <= self.get_time_since_launch(): if i in range(len(new_events[event])): assert abs(new_events[event][i] - time) < 0.5 # TODO: Figure out where slight discrepancies come from if event in merged_events: merged_events[event].append(time) else: merged_events[event] = [time] # Update with new events for event, times in new_events.items(): for time in times: if time >= new_events[FlightEvent.RECOVERY_DEVICE_DEPLOYMENT][-1]: if event in merged_events: merged_events[event].append(time) else: merged_events[event] = [time] self._data = new_data self._events = merged_events LOGGER.info( f"Recovery deployed at time {self.get_time()} s, and altitude {self.get_data(FlightDataType.TYPE_ALTITUDE)} m")
def message(self, byte_stream: BytesIO, header: Header): """ Save and log an extracted message. :param byte_stream: :type byte_stream: :param header: :type header: :return: :rtype: """ data: Dict = {} length: int = byte_stream.read(1)[0] # Two step: immutable int[] -> string byte_data = byte_stream.read(length) data[DataEntryIds.MESSAGE] = byte_data.decode('ascii') # Do something with data LOGGER.info("Incoming message: " + str(data[DataEntryIds.MESSAGE])) return data
def header(self, byte_stream: BytesIO) -> Header: """ Header extractor helper. :param byte_stream: :type byte_stream: :return: :rtype: """ # Get ID subpacket_id: int = byte_stream.read(1)[0] # check that id is valid: if subpacket_id not in self.packet_type_to_parser: LOGGER.error("Subpacket id %d not valid.", subpacket_id) raise ValueError("Subpacket id " + str(subpacket_id) + " not valid.") # Get timestamp timestamp: int = self.bytestoint(byte_stream.read(4)) return Header(subpacket_id, timestamp)
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()