diff --git a/comIF/obsw_dummy_com_if.py b/comIF/obsw_dummy_com_if.py index 995e6f40f8942a835113f1bb45dc5c8fa95ba520..ddc36f8e89fe443f59371a75715e1b88436d8290 100644 --- a/comIF/obsw_dummy_com_if.py +++ b/comIF/obsw_dummy_com_if.py @@ -9,25 +9,39 @@ from typing import Tuple from comIF.obsw_com_interface import CommunicationInterface from tc.obsw_pus_tc_base import PusTelecommand, PusTcInfoT, TcDictionaryKeys - +from tm.obsw_pus_tm_factory import PusTelemetryFactory +from tm.obsw_pus_tm_creator import PusTelemetryCreator +from tm.obsw_tm_service_1 import Service1TmPacked class DummyComIF(CommunicationInterface): def __init__(self, tmtc_printer): super().__init__(tmtc_printer) self.service_sent = 0 + self.reply_pending = False def close(self) -> None: pass def data_available(self, parameters): + if self.reply_pending: + return True return False def poll_interface(self, parameters: any = 0) -> Tuple[bool, list]: pass def receive_telemetry(self, parameters: any = 0): - pass + if self.service_sent == 17 and self.reply_pending: + tm_packer = Service1TmPacked(subservice=1, ssc=0) + tm_packet_raw = tm_packer.pack() + tm_packet = PusTelemetryFactory.create(tm_packet_raw) + self.reply_pending = False + return [tm_packet] + return [] + + def send_telecommand(self, tc_packet: PusTelecommand, tc_packet_info: PusTcInfoT = None) -> None: if isinstance(tc_packet_info, dict) and tc_packet_info.__len__() > 0: self.service_sent = tc_packet_info[TcDictionaryKeys.SERVICE] + self.reply_pending = True diff --git a/comIF/obsw_serial_com_if.py b/comIF/obsw_serial_com_if.py index 892a3431f25a05326a48394e2f3053df61feceb1..c718b5da00ca893c7c6711552be554e0fb383f24 100644 --- a/comIF/obsw_serial_com_if.py +++ b/comIF/obsw_serial_com_if.py @@ -56,13 +56,14 @@ class SerialComIF(CommunicationInterface): return packet_list return [] - # TODO: DO NOT PRINT HERE! this will block the listener thread!!! + # TODO: Serialization is performed here, but I suspect this might slow down the + # listener thread.. but at least no printing is done here. def poll_interface(self, parameters: any = 0) -> Tuple[bool, PusTmListT]: if self.data_available(): pus_data_list, number_of_packets = self.__poll_pus_packets() packet_list = [] for counter in range(0, number_of_packets): - packet = PusTelemetryFactory.create(pus_data_list[counter]) + packet = PusTelemetryFactory.create(bytearray(pus_data_list[counter])) packet_list.append(packet) return True, packet_list return False, [] diff --git a/sendreceive/obsw_command_sender_receiver.py b/sendreceive/obsw_command_sender_receiver.py index 07cdb1ed2738e40266099dbeaae19737377cfa35..d800fd78212a20682317c07dd1103cc78ca47c2d 100644 --- a/sendreceive/obsw_command_sender_receiver.py +++ b/sendreceive/obsw_command_sender_receiver.py @@ -21,6 +21,7 @@ from sendreceive.obsw_tm_listener import TmListener from tc.obsw_pus_tc_base import TcQueueEntryT from tm.obsw_pus_tm_factory import PusTmQueueT from utility.obsw_logger import get_logger +from tm.obsw_pus_tm_factory import PusTelemetryFactory logger = get_logger() @@ -150,4 +151,6 @@ class CommandSenderReceiver: def print_tm_queue(self, tm_queue: PusTmQueueT): for tm_packet in tm_queue: - self._tmtc_printer.print_telemetry(tm_packet) + logger.debug(str(tm_packet)) + tm_info = tm_packet.pack_tm_information() + self._tmtc_printer.print_telemetry(tm_packet, tm_info) diff --git a/sendreceive/obsw_tm_listener.py b/sendreceive/obsw_tm_listener.py index 1be1a7125c815e3bfa00f80231f53e49768618f1..f0177702d66f5c7dc9eaa6be13b8fd3ca5f273a0 100644 --- a/sendreceive/obsw_tm_listener.py +++ b/sendreceive/obsw_tm_listener.py @@ -14,12 +14,12 @@ import time import threading from collections import deque from typing import TypeVar - -from comIF.obsw_com_interface import ComIfT +from utility.obsw_logger import get_logger +from comIF.obsw_com_interface import CommunicationInterface from tm.obsw_pus_tm_factory import PusTmQueueT, PusTmInfoQueueT import config.obsw_config as g - +logger = get_logger() TmListenerT = TypeVar('TmListenerT', bound='TmListener') @@ -30,7 +30,8 @@ class TmListener: and changing the mode to do special mode operations. The mode operation ends as soon the modeOpFinished Event is set() ! """ - def __init__(self, com_interface: ComIfT, tm_timeout: float, tc_timeout_factor: float): + def __init__(self, com_interface: CommunicationInterface, tm_timeout: float, + tc_timeout_factor: float): self.tm_timeout = tm_timeout self.tc_timeout_factor = tc_timeout_factor self.com_interface = com_interface @@ -79,7 +80,7 @@ class TmListener: while not self.mode_op_finished.is_set(): self.perform_mode_operation() self.mode_op_finished.clear() - print("Transitioning to listener mode.") + logger.info("Transitioning to listener mode.") self.mode_id = g.ModeList.ListenerMode def perform_mode_operation(self): @@ -102,10 +103,11 @@ class TmListener: elif self.mode_id == g.ModeList.ServiceTestMode or \ self.mode_id == g.ModeList.SoftwareTestMode: if self.check_for_one_telemetry_sequence(): - print("TM Listener: Reply sequence received!") + logger.info("TmListener: Reply sequence received!") self.replyEvent.set() elif self.mode_id == g.ModeList.UnitTest: - self.__tm_info_queue = self.com_interface.receive_telemetry_and_store_info(self.__tm_info_queue) + # TODO: needs to be reworked. + # self.__tm_info_queue = self.com_interface.receive_telemetry_and_store_info(self.__tm_info_queue) self.replyEvent.set() def check_for_one_telemetry_sequence(self) -> bool: @@ -132,7 +134,7 @@ class TmListener: self.tm_timeout = g.G_TM_TIMEOUT return True else: - print("TM Listener: Configuration error in communication interface!") + logger.error("TmListener: Configuration error in communication interface!") sys.exit() def retrieve_tm_packet_queue(self): diff --git a/tc/obsw_pus_tc_base.py b/tc/obsw_pus_tc_base.py index d8186cf433bfadb98b4fc2364e1634f6a5c2320d..b4678e52c41a3dd1cc23ab31d80de4e9df17c087 100644 --- a/tc/obsw_pus_tc_base.py +++ b/tc/obsw_pus_tc_base.py @@ -4,6 +4,8 @@ import logging from enum import Enum from typing import Dict, Union, Tuple, Deque +from utility.obsw_logger import get_logger +logger = get_logger() class TcDictionaryKeys(Enum): SERVICE = 1 @@ -21,7 +23,6 @@ TcQueueT = Deque[TcQueueEntryT] PusTcInfoQueueT = Deque[PusTcInfoT] - class PusTelecommand: HEADER_SIZE = 6 """ @@ -29,7 +30,7 @@ class PusTelecommand: input parameters. The structure of a PUS telecommand is specified in ECSS-E-70-41A on p.42 and is also shown below (bottom) """ - def __init__(self, service: int, subservice: int, ssc=0, app_data=bytearray([]), + def __init__(self, service: int, subservice: int, ssc=0, app_data: bytearray=bytearray([]), source_id: int = 0, version: int = 0, apid: int = 0x73): """ Initiates a telecommand with the given parameters. @@ -73,9 +74,8 @@ class PusTelecommand: data_length = 4 + len(self.app_data) + 1 return data_length except TypeError: - print("OBSW_TcPacket: Invalid service_type of data") - logging.exception("Error") - sys.exit() + logger.error("PusTelecommand: Invalid type of application data!") + return 0 def get_total_length(self): """ diff --git a/tm/obsw_pus_tm_base.py b/tm/obsw_pus_tm_base.py index a35eb6877082f73968ba0aa491f0e8379fb0c6df..691675804acb42af16024f2303328da409500672 100644 --- a/tm/obsw_pus_tm_base.py +++ b/tm/obsw_pus_tm_base.py @@ -1,8 +1,14 @@ +import math +import time + +from utility.obsw_logger import get_logger + import datetime from enum import Enum, auto from typing import Final, Dict from crcmod import crcmod +logger = get_logger() class TmDictionaryKeys(Enum): SERVICE = auto() @@ -34,6 +40,7 @@ class PusTelemetry: It automatically deserializes the packet, exposing various packet fields via getter functions. PUS Telemetry structure according to ECSS-E-70-41A p.46. Also see structure below (bottom). """ + PUS_HEADER_SIZE = 6 def __init__(self, byte_array: bytearray = bytearray()): if byte_array == bytearray(): return @@ -130,7 +137,7 @@ class PusTelemetry: :return: Size of the TM wiretapping_packet """ # PusHeader Size + _tm_data size - size = PusPacketHeader.PUS_HEADER_SIZE + self._pus_header.length + 1 + size = PusTelemetry.PUS_HEADER_SIZE + self._pus_header.length + 1 return size def get_ssc(self) -> int: @@ -161,46 +168,15 @@ class PusTelemetry: print(self.return_data_string()) -class PusTelemetryPacked(PusTelemetry): - """ - Alternative way to create a PUS Telemetry packet by specifying telemetry parameters, - similarly to the way telecommands are created. This can be used to create telemetry - directly in the software. - """ - def __init__(self, service: int, subservice: int, ssc: int = 0, apid: int = 0x73, version: int = 0, - packet_type: int = 0, data_field_header_flag: int = 1): - super().__init__() - self.packet_id = [0x0, 0x0] - self.packet_id[0] = ((version << 5) & 0xE0) | ((packet_type & 0x01) << 4) | \ - ((data_field_header_flag & 0x01) << 3) | ((apid & 0x700) >> 8) - self.ssc = ssc - self.psc = (ssc & 0x3FFF) | (0b11 << 16) - self.pus_version_and_ack_byte = 0b00011111 - - # NOTE: In PUS-C, the PUS Version is 2 and specified for the first 4 bits. - # The other 4 bits of the first byte are the spacecraft time reference status - # To change to PUS-C, set 0b00100000 - self.data_field_version = 0b00010000 - self.service = service - self.subservice = subservice - self.pack_subcounter = 0 - self.time = 0 - - def pack(self) -> bytearray: - - - - # pylint: disable=too-many-instance-attributes class PusPacketHeader: """ This class unnpacks the PUS packet header, also see PUS structure below or PUS documentation. """ - PUS_HEADER_SIZE = 6 def __init__(self, pus_packet_raw: bytes): data_to_check = pus_packet_raw - if len(pus_packet_raw) < PusPacketHeader.PUS_HEADER_SIZE: + if len(pus_packet_raw) < PusTelemetry.PUS_HEADER_SIZE: self.version = 0 self.type = 0 self.data_field_header_flag = 0 @@ -219,15 +195,15 @@ class PusPacketHeader: self.length = pus_packet_raw[4] << 8 | pus_packet_raw[5] self.valid = False crc_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - if len(data_to_check) < ((self.length + 1) + PusPacketHeader.PUS_HEADER_SIZE): - print("Invalid wiretapping_packet length") + if len(data_to_check) < ((self.length + 1) + PusTelemetry.PUS_HEADER_SIZE): + logger.warning("PusPacketHeader: Invalid packet length") return - data_to_check = data_to_check[0:(self.length + 1) + PusPacketHeader.PUS_HEADER_SIZE] + data_to_check = data_to_check[0:(self.length + 1) + PusTelemetry.PUS_HEADER_SIZE] crc = crc_func(data_to_check) if crc == 0: self.valid = True else: - print("Invalid CRC detected !") + logger.warning("PusPacketHeader: Invalid CRC detected !") def append_pus_packet_header(self, array): array.append(str(chr(self.apid))) @@ -280,20 +256,53 @@ class PusPacketDataFieldHeader: class PusTelemetryTimestamp: """ - Unpacks the time datafield of the TM packet. + Unpacks the time datafield of the TM packet. Right now, CDS Short timeformat is used, + and the size of the time stamp is expected to be seven bytes. """ - def __init__(self, byte_array): - # pField = byte_array[0] - byte_array = byte_array[1:] - self.days = ((byte_array[0] << 8) | (byte_array[1])) - 4383 - self.seconds = self.days * (24 * 60 * 60) - s_day = ((byte_array[2] << 24) | (byte_array[3] << 16) | - (byte_array[4]) << 8 | byte_array[5]) / 1000 - self.seconds += s_day - self.time = self.seconds - self.datetime = str(datetime.datetime. + CDS_ID = 4 + SECONDS_PER_DAY = 86400 + EPOCH = datetime.datetime.utcfromtimestamp(0) + DAYS_CCSDS_TO_UNIX = 4383 + def __init__(self, byte_array: bytearray=bytearray([])): + if len(byte_array) > 0: + # pField = byte_array[0] + self.days = ((byte_array[1] << 8) | (byte_array[2])) - \ + PusTelemetryTimestamp.DAYS_CCSDS_TO_UNIX + self.seconds = self.days * (24 * 60 * 60) + s_day = ((byte_array[3] << 24) | (byte_array[4] << 16) | + (byte_array[5]) << 8 | byte_array[6]) / 1000 + self.seconds += s_day + self.time = self.seconds + self.datetime = str(datetime.datetime. utcfromtimestamp(self.time).strftime("%Y-%m-%d %H:%M:%S.%f")) + def pack_current_time(self) -> bytearray: + """ + Returns a seven byte CDS short timestamp + """ + timestamp = bytearray() + p_field = (PusTelemetryTimestamp.CDS_ID << 4) + 0 + days = (datetime.datetime.utcnow() - PusTelemetryTimestamp.EPOCH).days + \ + PusTelemetryTimestamp.DAYS_CCSDS_TO_UNIX + days_h = (days & 0xFF00) >> 8 + days_l = days & 0xFF + seconds = time.time() + fraction_ms = seconds - math.floor(seconds) + days_ms = int((seconds % PusTelemetryTimestamp.SECONDS_PER_DAY) * 1000 + fraction_ms) + days_ms_hh = (days_ms & 0xFF000000) >> 24 + days_ms_h = (days_ms & 0xFF0000) >> 16 + days_ms_l = (days_ms & 0xFF00) >> 8 + days_ms_ll = (days_ms & 0xFF) + timestamp.append(p_field) + timestamp.append(days_h) + timestamp.append(days_l) + timestamp.append(days_ms_hh) + timestamp.append(days_ms_h) + timestamp.append(days_ms_l) + timestamp.append(days_ms_ll) + return timestamp + + def print_time(self, array): array.append(self.time) array.append(self.datetime) diff --git a/tm/obsw_pus_tm_creator.py b/tm/obsw_pus_tm_creator.py new file mode 100644 index 0000000000000000000000000000000000000000..fad430151e60b8ff05159e5d22a4900449b748fe --- /dev/null +++ b/tm/obsw_pus_tm_creator.py @@ -0,0 +1,85 @@ +from tm.obsw_pus_tm_base import PusTelemetry, PusTelemetryTimestamp +from utility.obsw_logger import get_logger +import crcmod + + +logger = get_logger() + + +class PusTelemetryCreator(PusTelemetry): + """ + Alternative way to create a PUS Telemetry packet by specifying telemetry parameters, + similarly to the way telecommands are created. This can be used to create telemetry + directly in the software. + """ + def __init__(self, service: int, subservice: int, ssc: int = 0, + source_data: bytearray=bytearray([]), apid: int = 0x73, version: int = 0): + super().__init__() + # packet type for telemetry is 0 as specified in standard + packet_type = 0 + # specified in standard + data_field_header_flag = 1 + self.packet_id = [0x0, 0x0] + self.packet_id[0] = ((version << 5) & 0xE0) | ((packet_type & 0x01) << 4) | \ + ((data_field_header_flag & 0x01) << 3) | ((apid & 0x700) >> 8) + self.packet_id[1] = apid & 0xFF + self.ssc = ssc + self.psc = (ssc & 0x3FFF) | (0b11 << 16) + self.pus_version_and_ack_byte = 0b00011111 + + # NOTE: In PUS-C, the PUS Version is 2 and specified for the first 4 bits. + # The other 4 bits of the first byte are the spacecraft time reference status + # To change to PUS-C, set 0b00100000 + self.data_field_version = 0b00010000 + self.service = service + self.subservice = subservice + self.pack_subcounter = 0 + # it is assumed the time field consts of 8 bytes. + + self.source_data = source_data + + def pack(self) -> bytearray: + tm_packet_raw = bytearray() + # PUS Header + tm_packet_raw.extend(self.packet_id) + tm_packet_raw.append((self.psc & 0xFF00) >> 8) + tm_packet_raw.append(self.psc & 0xFF) + source_length = self.get_source_data_length() + tm_packet_raw.append((source_length & 0xFF00) >> 8) + tm_packet_raw.append(source_length & 0xFF) + logger.debug(len(tm_packet_raw)) + # PUS Source Data Field + tm_packet_raw.append(self.data_field_version) + tm_packet_raw.append(self.service) + tm_packet_raw.append(self.subservice) + tm_packet_raw.append(self.pack_subcounter) + logger.debug(len(tm_packet_raw)) + timestamper = PusTelemetryTimestamp() + tm_packet_raw.extend(timestamper.pack_current_time()) + logger.debug(len(tm_packet_raw)) + + # Source Data + tm_packet_raw.extend(self.source_data) + logger.debug(len(tm_packet_raw)) + + # CRC16 checksum + crc_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) + crc16 = crc_func(tm_packet_raw) + tm_packet_raw.append((crc16 & 0xFF00) >> 8) + tm_packet_raw.append(crc16 & 0xFF) + logger.debug(len(tm_packet_raw)) + return tm_packet_raw + + def get_source_data_length(self) -> int: + """ + Retrieve size of TC packet in bytes. + Formula according to PUS Standard: C = (Number of octets in packet data field) - 1. + The size of the TC packet is the size of the packet secondary header with + source ID + the length of the application data + length of the CRC16 checksum - 1 + """ + try: + data_length = 4 + len(self.source_data) + 1 + return data_length + except TypeError: + logger.error("PusTelecommand: Invalid type of application data!") + return 0 diff --git a/tm/obsw_pus_tm_factory.py b/tm/obsw_pus_tm_factory.py index 547b2a42aef206d5df522f6567a9e576c7408013..5e99f6a38ef35b86db0ed70d8ce8350e72d566a1 100644 --- a/tm/obsw_pus_tm_factory.py +++ b/tm/obsw_pus_tm_factory.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- -from typing import Deque, List, Dict, Tuple +from typing import Deque, List, Tuple from tm.obsw_pus_tm_base import PusTelemetry, PusTmInfoT from tm.obsw_tm_service_1 import Service1TM from tm.obsw_tm_service_3 import Service3TM from tm.obsw_tm_service_5 import Service5TM +from utility.obsw_logger import get_logger import struct - +logger = get_logger() +PusRawTmList = List[bytearray] +PusRawTmQueue = Deque[bytearray] PusTmQueueT = Deque[PusTelemetry] PusTmListT = List[PusTelemetry] PusTmTupleT = Tuple[PusTmInfoT, PusTelemetry] @@ -39,6 +42,7 @@ class PusTelemetryFactory(object): return PusTelemetry(raw_tm_packet) + class Service2TM(PusTelemetry): def __init__(self, byte_array: bytearray): super().__init__(byte_array) diff --git a/tm/obsw_tm_service_1.py b/tm/obsw_tm_service_1.py index 248c4dd19516db29e7586e38b8994770c7963f82..c7908285e49d5b8ee01239304999b1a14cbc81e3 100644 --- a/tm/obsw_tm_service_1.py +++ b/tm/obsw_tm_service_1.py @@ -9,13 +9,13 @@ import struct from typing import Dict from tm.obsw_pus_tm_base import PusTelemetry, TmDictionaryKeys - +from tm.obsw_pus_tm_creator import PusTelemetryCreator pusPacketInfoService1T = Dict[TmDictionaryKeys, any] class Service1TM(PusTelemetry): - def __init__(self, byte_array: bytes): + def __init__(self, byte_array: bytearray): super().__init__(byte_array) self.has_tc_error_code = False self.is_step_reply = False @@ -84,3 +84,17 @@ class Service1TM(PusTelemetry): if self.is_step_reply: tm_information.update({TmDictionaryKeys.STEP_NUMBER: self.step_number}) return tm_information + + +class Service1TmPacked(PusTelemetryCreator): + def __init__(self, subservice: int, ssc: int=0, tc_packed_id: int=0, tc_ssc: int=0): + source_data = bytearray() + source_data.append((tc_packed_id & 0xFF00) >> 8) + source_data.append(tc_packed_id & 0xFF) + tc_psc = (tc_ssc & 0x3FFF) | (0b11 << 16) + source_data.append((tc_psc & 0xFF00) >> 8) + source_data.append(tc_psc & 0xFF) + super().__init__(service=1, subservice=subservice, ssc=ssc, source_data=source_data) + + def pack(self) -> bytearray: + return super().pack() diff --git a/utility/obsw_logger.py b/utility/obsw_logger.py index ab343805763b9d830eabdf316c915b12cef242b4..d15de57106b85a4ef71411df3a239d149d161eaa 100644 --- a/utility/obsw_logger.py +++ b/utility/obsw_logger.py @@ -9,7 +9,16 @@ class InfoFilter(logging.Filter): Filter object, which is used so that only INFO and DEBUG messages are printed to stdout. """ def filter(self, rec): - if rec.levelno == logging.INFO or rec.levelno == logging.DEBUG: + if rec.levelno == logging.INFO: + return rec.levelno + + +class DebugFilter(logging.Filter): + """ + Filter object, which is used so that only INFO and DEBUG messages are printed to stdout. + """ + def filter(self, rec): + if rec.levelno == logging.DEBUG: return rec.levelno @@ -27,9 +36,13 @@ def set_tmtc_logger() -> logging.Logger: datefmt='%Y-%m-%d %H:%M:%S') console_info_handler = logging.StreamHandler(stream=sys.stdout) - console_info_handler.setLevel(logging.DEBUG) + console_info_handler.setLevel(logging.INFO) console_info_handler.addFilter(InfoFilter()) + console_debug_handler = logging.StreamHandler(stream=sys.stdout) + console_debug_handler.setLevel(logging.DEBUG) + console_debug_handler.addFilter(DebugFilter()) + console_error_handler = logging.StreamHandler(stream=sys.stderr) console_error_handler.setLevel(logging.WARNING) try: @@ -44,9 +57,11 @@ def set_tmtc_logger() -> logging.Logger: file_handler.setFormatter(generic_format) console_info_handler.setFormatter(generic_format) + console_debug_handler.setFormatter(fault_format) console_error_handler.setFormatter(fault_format) logger.addHandler(file_handler) logger.addHandler(console_info_handler) + logger.addHandler(console_debug_handler) logger.addHandler(console_error_handler) return logger