diff --git a/Apps/FlightSoftware/PrimaryFlightController/FlightMCU/Include/Version.h b/Apps/FlightSoftware/PrimaryFlightController/FlightMCU/Include/Version.h index 80c6a7c6..ba011af5 100644 --- a/Apps/FlightSoftware/PrimaryFlightController/FlightMCU/Include/Version.h +++ b/Apps/FlightSoftware/PrimaryFlightController/FlightMCU/Include/Version.h @@ -10,7 +10,7 @@ *****************************************************************************/ static const unsigned VERSION_MAJOR = 9; // Version: ++ when drafting a new Release Candidate (for a new upload opportunity) -static const unsigned VERSION_MINOR = 1; // Subversion: ++ when a new major feature has been added / made to work -static const unsigned VERSION_REVISION = 1; // Patch: ++ when you make a change and want to reflect that. +static const unsigned VERSION_MINOR = 2; // Subversion: ++ when a new major feature has been added / made to work +static const unsigned VERSION_REVISION = 0; // Patch: ++ when you make a change and want to reflect that. #endif // _VERSION_H_ diff --git a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.cpp b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.cpp index 48531de5..8c13dff9 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.cpp +++ b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.cpp @@ -111,8 +111,26 @@ namespace CubeRover void GroundInterfaceComponentImpl::schedIn_handler( NATIVE_INT_TYPE portNum, /*!< The port number*/ NATIVE_UINT_TYPE context /*!< The call order*/ - ){ + ) + { + // Downlink a new Name or Message if allowed (and it's time): this->downlinkNameOrMessageIfAllowed(getTime().get_time_ms()); + + // Automatically switch network mode if allowed: + if (m_auto_switch_allowed) + { + Wf121::DirectMessage::RadioSwState radio_state = networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getRadioState(); + if (m_interface_port_num != WF121 && radio_state == Wf121::DirectMessage::RadioSwState::UDP_CONNECTED) + { + log_ACTIVITY_HI_InterfaceAutoSwitch(static_cast(m_interface_port_num), static_cast(WF121)); + Switch_Primary_Interface(WF121); + } + else if (m_interface_port_num != WATCHDOG && radio_state != Wf121::DirectMessage::RadioSwState::UDP_CONNECTED) + { + log_ACTIVITY_HI_InterfaceAutoSwitch(static_cast(m_interface_port_num), static_cast(WATCHDOG)); + Switch_Primary_Interface(WATCHDOG); + } + } } void GroundInterfaceComponentImpl :: @@ -204,7 +222,8 @@ namespace CubeRover // ! (do this to avoid maxing out the radio Tx queue): flushTlmDownlinkBuffer(); // FLUSH BUFFER TO GET PACKET OUT int startUdpTxCount = networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount(); - while(startUdpTxCount == networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount() && networkManager.m_pRadioDriver->m_networkInterface.udpTxQueueRoom() < 1){ + while (startUdpTxCount == networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount() && networkManager.m_pRadioDriver->m_networkInterface.udpTxQueueRoom() < 1) + { vTaskDelay(10 / portTICK_PERIOD_MS); // Check back in 10ms } } @@ -239,7 +258,8 @@ namespace CubeRover // !Forcibly halt the idle thread until Wf121TxTask sends the packet (tx count goes up): // ! (do this to avoid maxing out the radio Tx queue): int startUdpTxCount = networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount(); - while(startUdpTxCount == networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount() && networkManager.m_pRadioDriver->m_networkInterface.udpTxQueueRoom() < 1){ + while (startUdpTxCount == networkManager.m_pRadioDriver->m_networkInterface.m_protectedRadioStatus.getUdpTxPacketCount() && networkManager.m_pRadioDriver->m_networkInterface.udpTxQueueRoom() < 1) + { vTaskDelay(10 / portTICK_PERIOD_MS); // Check back in 10ms } } @@ -314,15 +334,8 @@ namespace CubeRover updateTelemetry(); } - // ---------------------------------------------------------------------- - // Command handler implementations - // ---------------------------------------------------------------------- - void GroundInterfaceComponentImpl :: - Set_Primary_Interface_cmdHandler( - const FwOpcodeType opCode, - const U32 cmdSeq, - PrimaryInterface primary_interface) + Switch_Primary_Interface(PrimaryInterface primary_interface) { flushTlmDownlinkBuffer(); // TODO: Should probably flush file downlink buffers too @@ -339,6 +352,42 @@ namespace CubeRover } m_tlmDownlinkBufferSpaceAvailable = m_downlink_objects_size; m_interface_port_num = primary_interface; + } + + void GroundInterfaceComponentImpl::Set_Interface_Auto_Switch(bool on) + { + m_auto_switch_allowed = on; + log_ACTIVITY_HI_InterfaceAutoSwitchChanged(on); + } + + // ---------------------------------------------------------------------- + // Command handler implementations + // ---------------------------------------------------------------------- + + void GroundInterfaceComponentImpl :: + Set_Primary_Interface_cmdHandler( + const FwOpcodeType opCode, + const U32 cmdSeq, + PrimaryInterface primary_interface) + { + // If the primary interface is being manually changed, turn off + // auto-switch (and do this before the interface change so the event + // goes out on the old interface). + Set_Interface_Auto_Switch(false); + // Change interface: + Switch_Primary_Interface(primary_interface); + this->cmdResponse_out(opCode, cmdSeq, Fw::COMMAND_OK); + } + + //! Handler for command SetInterfaceAutoSwitch + /* Turn ON/OFF whether Hercules is allowed to automatically switch its + primary network interface based on connection status. Default is ON. */ + void GroundInterfaceComponentImpl::SetInterfaceAutoSwitch_cmdHandler( + FwOpcodeType opCode, /*!< The opcode*/ + U32 cmdSeq, /*!< The command sequence number*/ + bool on) + { + Set_Interface_Auto_Switch(on); this->cmdResponse_out(opCode, cmdSeq, Fw::COMMAND_OK); } @@ -356,25 +405,25 @@ namespace CubeRover /* Turn ON/OFF whether names and messages should be downlinked. Default is ON. */ void GroundInterfaceComponentImpl::RollCredits_cmdHandler( FwOpcodeType opCode, /*!< The opcode*/ - U32 cmdSeq, /*!< The command sequence number*/ - bool on - ){ + U32 cmdSeq, /*!< The command sequence number*/ + bool on) + { // Set allowed state: m_namesAndMessagesAllowed = on; // Reset awaiting status: m_awaitingNameOrMessageDownlink = false; - this->cmdResponse_out(opCode,cmdSeq,Fw::COMMAND_OK); + this->cmdResponse_out(opCode, cmdSeq, Fw::COMMAND_OK); } //! Handler for command Set_NameAndMessage_Period /* Set how many seconds (minimum) should occur between each name/message downlink. Min is 1. */ void GroundInterfaceComponentImpl::Set_NameAndMessage_Period_cmdHandler( FwOpcodeType opCode, /*!< The opcode*/ - U32 cmdSeq, /*!< The command sequence number*/ - U16 seconds - ){ + U32 cmdSeq, /*!< The command sequence number*/ + U16 seconds) + { m_nameOrMessageDownlinkPeriod_ms = seconds * 1000; - this->cmdResponse_out(opCode,cmdSeq,Fw::COMMAND_OK); + this->cmdResponse_out(opCode, cmdSeq, Fw::COMMAND_OK); } /* @@ -532,17 +581,19 @@ namespace CubeRover downlinkBufferWrite(&metadata, static_cast(sizeof(metadata)), DownlinkFile); } - void GroundInterfaceComponentImpl::downlinkNameCoreImpl(const char * pHead){ - // Convert to FW string and emit the log: - Fw::LogStringArg name(pHead); - this->log_ACTIVITY_LO_BroughtToYouBy(name); - } - - void GroundInterfaceComponentImpl::downlinkMessageCoreImpl(const char * pMessagerString, const char * pMessageString){ - // Convert to FW strings and emit the log: - Fw::LogStringArg messager(pMessagerString); - Fw::LogStringArg message(pMessageString); - this->log_ACTIVITY_LO_SpecialMessage(messager, message); - } + void GroundInterfaceComponentImpl::downlinkNameCoreImpl(const char *pHead) + { + // Convert to FW string and emit the log: + Fw::LogStringArg name(pHead); + this->log_ACTIVITY_LO_BroughtToYouBy(name); + } + + void GroundInterfaceComponentImpl::downlinkMessageCoreImpl(const char *pMessagerString, const char *pMessageString) + { + // Convert to FW strings and emit the log: + Fw::LogStringArg messager(pMessagerString); + Fw::LogStringArg message(pMessageString); + this->log_ACTIVITY_LO_SpecialMessage(messager, message); + } } // end namespace CubeRover diff --git a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.hpp b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.hpp index 4ca821b6..7e6beeb8 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.hpp +++ b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterface.hpp @@ -18,164 +18,171 @@ #include #include "NamesAndMessagesSender.hpp" -namespace CubeRover { +namespace CubeRover +{ -extern int8_t m_persistent_state; + extern int8_t m_persistent_state; - class GroundInterfaceComponentImpl : - public GroundInterfaceComponentBase, - public ::IrisNames::NamesAndMessageSender { + class GroundInterfaceComponentImpl : public GroundInterfaceComponentBase, + public ::IrisNames::NamesAndMessageSender + { public: + // ---------------------------------------------------------------------- + // Construction, initialization, and destruction + // ---------------------------------------------------------------------- - // ---------------------------------------------------------------------- - // Construction, initialization, and destruction - // ---------------------------------------------------------------------- - - //! Construct object GroundInterface - //! - GroundInterfaceComponentImpl( + //! Construct object GroundInterface + //! + GroundInterfaceComponentImpl( #if FW_OBJECT_NAMES == 1 - const char *const compName /*!< The component name*/ + const char *const compName /*!< The component name*/ #else - void + void #endif - ); - - //! Initialize object GroundInterface - //! - void init( - const NATIVE_INT_TYPE instance = 0 /*!< The instance number*/ - ); - - //! Destroy object GroundInterface - //! - ~GroundInterfaceComponentImpl(void); - - PRIVATE: - - // ---------------------------------------------------------------------- - // Handler implementations for user-defined typed input ports - // ---------------------------------------------------------------------- - - //! Handler for input port schedIn - // - void schedIn_handler( - NATIVE_INT_TYPE portNum, /*!< The port number*/ - NATIVE_UINT_TYPE context /*!< The call order*/ - ); - - //! Handler implementation for tlmDownlink - //! - void tlmDownlink_handler( - const NATIVE_INT_TYPE portNum, /*!< The port number*/ - Fw::ComBuffer &data, /*!< Buffer containing packet data*/ - U32 context /*!< Call context value; meaning chosen by user*/ - ); - - //! Handler for input port logDirectDownlink - // - void logDirectDownlink_handler( - NATIVE_INT_TYPE portNum, /*!< The port number*/ - Fw::ComBuffer &data, /*!< Buffer containing packet data*/ - U32 context /*!< Call context value; meaning chosen by user*/ - ); - - //! Handler implementation for logDownlink - //! - void logDownlink_handler( - const NATIVE_INT_TYPE portNum, /*!< The port number*/ - FwEventIdType id, /*!< Log ID*/ - Fw::Time &timeTag, /*!< Time Tag*/ - Fw::LogSeverity severity, /*!< The severity argument*/ - Fw::LogBuffer &args /*!< Buffer containing serialized log entry*/ - ); - - //! Handler implementation for appDownlink - //! - void appDownlink_handler( - const NATIVE_INT_TYPE portNum, /*!< The port number*/ - U16 callbackId, /*!< Metadata Field: Unique Id to map this file to the command that generated it*/ - U32 createTime, /*!< Metadata Field: Time the file was created in ms epoch*/ - Fw::Buffer &fwBuffer /*!< Buffer containing the data*/ - ); - - //! Handler implementation for cmdUplink - //! - void cmdUplink_handler( - const NATIVE_INT_TYPE portNum, /*!< The port number*/ - Fw::Buffer &fwBuffer - ); - - PRIVATE: - - // ---------------------------------------------------------------------- - // Command handler implementations - // ---------------------------------------------------------------------- - - //! Implementation for Set_Primary_Interface command handler - //! Sets the primary interface. - void Set_Primary_Interface_cmdHandler( - const FwOpcodeType opCode, /*!< The opcode*/ - const U32 cmdSeq, /*!< The command sequence number*/ - PrimaryInterface primary_interface - ); - - //! Implementation for Set_GroundInterface_Telemetry_Level command handler - //! Sets the telemetry level to emit for this component. - void Set_GroundInterface_Telemetry_Level_cmdHandler( - const FwOpcodeType opCode, /*!< The opcode*/ - const U32 cmdSeq, /*!< The command sequence number*/ - TelemetryLevel telemetry_level - ); - - //! Handler for command RollCredits - /* Turn ON/OFF whether names and messages should be downlinked. Default is ON. */ - void RollCredits_cmdHandler( - FwOpcodeType opCode, /*!< The opcode*/ - U32 cmdSeq, /*!< The command sequence number*/ - bool on - ); - - //! Handler for command Set_NameAndMessage_Period - /* Set how many seconds (minimum) should occur between each name/message downlink. Min is 1. */ - void Set_NameAndMessage_Period_cmdHandler( - FwOpcodeType opCode, /*!< The opcode*/ - U32 cmdSeq, /*!< The command sequence number*/ - U16 seconds - ); + ); + + //! Initialize object GroundInterface + //! + void init( + const NATIVE_INT_TYPE instance = 0 /*!< The instance number*/ + ); + + //! Destroy object GroundInterface + //! + ~GroundInterfaceComponentImpl(void); + + PRIVATE : + + // ---------------------------------------------------------------------- + // Handler implementations for user-defined typed input ports + // ---------------------------------------------------------------------- + + //! Handler for input port schedIn + // + void + schedIn_handler( + NATIVE_INT_TYPE portNum, /*!< The port number*/ + NATIVE_UINT_TYPE context /*!< The call order*/ + ); + + //! Handler implementation for tlmDownlink + //! + void tlmDownlink_handler( + const NATIVE_INT_TYPE portNum, /*!< The port number*/ + Fw::ComBuffer &data, /*!< Buffer containing packet data*/ + U32 context /*!< Call context value; meaning chosen by user*/ + ); + + //! Handler for input port logDirectDownlink + // + void logDirectDownlink_handler( + NATIVE_INT_TYPE portNum, /*!< The port number*/ + Fw::ComBuffer &data, /*!< Buffer containing packet data*/ + U32 context /*!< Call context value; meaning chosen by user*/ + ); + + //! Handler implementation for logDownlink + //! + void logDownlink_handler( + const NATIVE_INT_TYPE portNum, /*!< The port number*/ + FwEventIdType id, /*!< Log ID*/ + Fw::Time &timeTag, /*!< Time Tag*/ + Fw::LogSeverity severity, /*!< The severity argument*/ + Fw::LogBuffer &args /*!< Buffer containing serialized log entry*/ + ); + + //! Handler implementation for appDownlink + //! + void appDownlink_handler( + const NATIVE_INT_TYPE portNum, /*!< The port number*/ + U16 callbackId, /*!< Metadata Field: Unique Id to map this file to the command that generated it*/ + U32 createTime, /*!< Metadata Field: Time the file was created in ms epoch*/ + Fw::Buffer &fwBuffer /*!< Buffer containing the data*/ + ); + + //! Handler implementation for cmdUplink + //! + void cmdUplink_handler( + const NATIVE_INT_TYPE portNum, /*!< The port number*/ + Fw::Buffer &fwBuffer); + + PRIVATE : + + // ---------------------------------------------------------------------- + // Command handler implementations + // ---------------------------------------------------------------------- + + //! Implementation for Set_Primary_Interface command handler + //! Sets the primary interface. + void + Set_Primary_Interface_cmdHandler( + const FwOpcodeType opCode, /*!< The opcode*/ + const U32 cmdSeq, /*!< The command sequence number*/ + PrimaryInterface primary_interface); + + //! Implementation for Set_GroundInterface_Telemetry_Level command handler + //! Sets the telemetry level to emit for this component. + void Set_GroundInterface_Telemetry_Level_cmdHandler( + const FwOpcodeType opCode, /*!< The opcode*/ + const U32 cmdSeq, /*!< The command sequence number*/ + TelemetryLevel telemetry_level); + + //! Handler for command SetInterfaceAutoSwitch + /* Turn ON/OFF whether Hercules is allowed to automatically switch its + primary network interface based on connection status. Default is ON. */ + void SetInterfaceAutoSwitch_cmdHandler( + FwOpcodeType opCode, /*!< The opcode*/ + U32 cmdSeq, /*!< The command sequence number*/ + bool on); + + //! Handler for command RollCredits + /* Turn ON/OFF whether names and messages should be downlinked. Default is ON. */ + void RollCredits_cmdHandler( + FwOpcodeType opCode, /*!< The opcode*/ + U32 cmdSeq, /*!< The command sequence number*/ + bool on); + + //! Handler for command Set_NameAndMessage_Period + /* Set how many seconds (minimum) should occur between each name/message downlink. Min is 1. */ + void Set_NameAndMessage_Period_cmdHandler( + FwOpcodeType opCode, /*!< The opcode*/ + U32 cmdSeq, /*!< The command sequence number*/ + U16 seconds); // User defined methods, members, and structs - + void downlinkFileMetadata(uint16_t hashedId, uint8_t totalBlocks, uint16_t callbackId, uint32_t timestamp_ms); - uint16_t hashTime(uint32_t time); // Used for files to get unique Id for parallel downlinks + uint16_t hashTime(uint32_t time); // Used for files to get unique Id for parallel downlinks void downlinkBufferWrite(void *_data, uint16_t size, downlinkPacketType from); void flushTlmDownlinkBuffer(); void downlink(void *_data, uint16_t size); void updateTelemetry(); - - FswPacket::Seq_t m_uplinkSeq, m_downlinkSeq; // TLM0, TLM1 - uint32_t m_packetsRx, m_packetsTx, // TLM2, TLM3 - m_tlmItemsReceived, m_tlmItemsDownlinked, // TLM4, TLM5 - m_logsReceived, m_logsDownlinked, // TLM6, TLM7 - m_cmdsUplinked, m_cmdsSent, m_cmdErrs, // TLM8, TLM9, TLM10 - m_appBytesReceived, m_appBytesDownlinked; // TLM11, TLM 12 - + + FswPacket::Seq_t m_uplinkSeq, m_downlinkSeq; // TLM0, TLM1 + uint32_t m_packetsRx, m_packetsTx, // TLM2, TLM3 + m_tlmItemsReceived, m_tlmItemsDownlinked, // TLM4, TLM5 + m_logsReceived, m_logsDownlinked, // TLM6, TLM7 + m_cmdsUplinked, m_cmdsSent, m_cmdErrs, // TLM8, TLM9, TLM10 + m_appBytesReceived, m_appBytesDownlinked; // TLM11, TLM 12 + uint8_t m_tlmDownlinkBuffer[WF121_UDP_MAX_PAYLOAD]; uint8_t m_fileDownlinkBuffer[NUM_APPS_USE_FILE_DOWNLINK][WF121_UDP_MAX_PAYLOAD]; uint8_t *m_tlmDownlinkBufferPos; - uint16_t m_downlink_objects_size; // Maximum usable buffer space for the current network interface + uint16_t m_downlink_objects_size; // Maximum usable buffer space for the current network interface uint16_t m_tlmDownlinkBufferSpaceAvailable; PrimaryInterface m_interface_port_num; PrimaryInterface m_temp_interface_port_num; TelemetryLevel m_telemetry_level; - PRIVATE: - void downlinkNameCoreImpl(const char * pHead); - void downlinkMessageCoreImpl(const char * pMessagerString, const char * pMessageString); - }; - + // Whether or not automatically switching downlink mode is allowed: + bool m_auto_switch_allowed = true; + PRIVATE : void Set_Interface_Auto_Switch(bool on); + void Switch_Primary_Interface(PrimaryInterface primary_interface); + void downlinkNameCoreImpl(const char *pHead); + void downlinkMessageCoreImpl(const char *pMessagerString, const char *pMessageString); + }; } // end namespace CubeRover diff --git a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterfaceComponentAi.xml b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterfaceComponentAi.xml index f10f9f39..a524ee97 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterfaceComponentAi.xml +++ b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/GroundInterfaceComponentAi.xml @@ -183,6 +183,37 @@ + + + Whether or not `GroundInterface` is allowed to automatically change + Hercules' downlink interface based on the WiFi connection state. + + + + + + + + `GroundInterface` has automatically changed its downlink interface + based on the WiFi connection state. + + + + + + + + + + + + + + + + + + Thank you to an Iris member / donor stored on the Rover. @@ -236,6 +267,16 @@ + + + Turn ON/OFF whether Hercules is allowed to automatically switch + its primary network interface based on connection status. + Default is ON. + + + + + Turn ON/OFF whether names and messages should be downlinked. Default is ON. diff --git a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/docs/GroundInterface.md b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/docs/GroundInterface.md index a039e722..69675bf8 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/docs/GroundInterface.md +++ b/Apps/FlightSoftware/fprime/CubeRover/GroundInterface/docs/GroundInterface.md @@ -10,6 +10,10 @@ | | | |primary_interface|PrimaryInterface|| |Set_GroundInterface_Telemetry_Level|1 (0x1)|Sets the telemetry level of this component| | | | | | |telemetry_level|TelemetryLevel|| +|SetInterfaceAutoSwitch|2 (0x2)|Turn ON/OFF whether Hercules is allowed to automatically switch + its primary network interface based on connection status. + Default is ON.| | | +| | | |on|bool|| |RollCredits|160 (0xa0)|Turn ON/OFF whether names and messages should be downlinked. Default is ON.| | | | | | |on|bool|| |Set_NameAndMessage_Period|161 (0xa1)|Set how many seconds (minimum) should occur between each name/message downlink. Min is 1.| | | @@ -53,6 +57,13 @@ |GI_DownlinkedItem|4 (0x4)|An item was added to the packet for downlink. This will be basically never used since using it would mean adding 1 downlink item for every item downlinked (which would run away).| | | | | | | | |seq|U8||| | | | |from|downlinkPacketType||| +|InterfaceAutoSwitchChanged|5 (0x5)|Whether or not `GroundInterface` is allowed to automatically change + Hercules' downlink interface based on the WiFi connection state.| | | | | +| | | |on|bool||| +|InterfaceAutoSwitch|6 (0x6)|`GroundInterface` has automatically changed its downlink interface + based on the WiFi connection state.| | | | | +| | | |from|FromInterface||| +| | | |to|ToInterface||| |BroughtToYouBy|160 (0xa0)|Thank you to an Iris member / donor stored on the Rover.| | | | | | | | |name|Fw::LogStringArg&|50|Name of an Iris member / donor stored on the Rover.| |SpecialMessage|161 (0xa1)|Message from an Iris member / donor.| | | | | diff --git a/Apps/FlightSoftware/fprime/CubeRover/Top/commands/groundInterface_commands.html b/Apps/FlightSoftware/fprime/CubeRover/Top/commands/groundInterface_commands.html index 246c569c..2a0de81e 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/Top/commands/groundInterface_commands.html +++ b/Apps/FlightSoftware/fprime/CubeRover/Top/commands/groundInterface_commands.html @@ -63,6 +63,18 @@

Command: groundInterface:GroundInterface

+ + SetInterfaceAutoSwitch + 2306 (0x902) + Turn ON/OFF whether Hercules is allowed to automatically switch + its primary network interface based on connection status. + Default is ON. + + on + bool + + + RollCredits 2464 (0x9a0) diff --git a/Apps/FlightSoftware/fprime/CubeRover/Top/events/groundInterface_events.html b/Apps/FlightSoftware/fprime/CubeRover/Top/events/groundInterface_events.html index 99e0c3a3..1214903b 100644 --- a/Apps/FlightSoftware/fprime/CubeRover/Top/events/groundInterface_events.html +++ b/Apps/FlightSoftware/fprime/CubeRover/Top/events/groundInterface_events.html @@ -132,6 +132,36 @@

Events: groundInterface:GroundInterface

+ + InterfaceAutoSwitchChanged + 2309 (0x905) + Whether or not `GroundInterface` is allowed to automatically change + Hercules' downlink interface based on the WiFi connection state. + + on + bool + + + + + + InterfaceAutoSwitch + 2310 (0x906) + `GroundInterface` has automatically changed its downlink interface + based on the WiFi connection state. + + from + FromInterface + + + + + to + ToInterface + + + + BroughtToYouBy 2464 (0x9a0) diff --git a/Apps/GroundSoftware/config/command_aliases/basic_command_aliases.py b/Apps/GroundSoftware/config/command_aliases/basic_command_aliases.py index 4cf2a118..e2204e86 100644 --- a/Apps/GroundSoftware/config/command_aliases/basic_command_aliases.py +++ b/Apps/GroundSoftware/config/command_aliases/basic_command_aliases.py @@ -42,6 +42,7 @@ def ALIASES(standards: DataStandards) -> CommandAliases: Magic.WATCHDOG_COMMAND, comment="Switch to Mission Mode." ), + 'wifi-mode': PreparedCommand( 'GroundInterface_SetPrimaryInterface', OrderedDict(primary_interface='WF_121'), @@ -63,6 +64,21 @@ def ALIASES(standards: DataStandards) -> CommandAliases: Magic.COMMAND, comment="Tell Herc to downlink over RS422." ), + 'auto-switch-on': PreparedCommand( + 'GroundInterface_SetInterfaceAutoSwitch', + OrderedDict(on=True), + DataPathway.WIRED, + Magic.COMMAND, + comment="Allow Hercules to auto-switch it's primary downlink interface." + ), + 'auto-switch-off': PreparedCommand( + 'GroundInterface_SetInterfaceAutoSwitch', + OrderedDict(on=False), + DataPathway.WIRED, + Magic.COMMAND, + comment="DON'T allow Hercules to auto-switch it's primary downlink interface." + ), + 'monitor-hercules-on': PreparedCommand( 'WatchDogInterface_ResetSpecific', OrderedDict(reset_value='HERCULES_WATCHDOG_ENABLE'), diff --git a/Apps/GroundSoftware/makefile b/Apps/GroundSoftware/makefile index d0dc3f2f..5347d01a 100644 --- a/Apps/GroundSoftware/makefile +++ b/Apps/GroundSoftware/makefile @@ -4,7 +4,7 @@ PYTHON ?= python PYTHONVERSION ?= 3.10.8 VENV_NAME ?= py_irisbackendv3 -.PHONY = help init update clean run run-tests standards build-docs run-docs build-cte commander console xconsole xterm-normal xterm-wide cloc +.PHONY = help init update clean run run-tests standards build-docs run-docs build-cte commander console xconsole console-debug xconsole-debug xterm-normal xterm-wide cloc .DEFAULT_GOAL = help all: help @@ -36,6 +36,10 @@ help: @echo " " @echo " make xconsole: Run the Iris Console CLI application in XTerm (for WSL users)" @echo " " + @echo " make console-debug: Run the Iris Console CLI application for the debug harness (J36_debugger)." + @echo " " + @echo " make xconsole-debug: Run the Iris Console CLI application for the debug harness (J36_debugger) in XTerm (for WSL users)" + @echo " " @echo " make xterm-normal: Launches the program in the PROG variable inside a new XTerm window with normal settings and the geometry given in GEOM. Run as: 'make xterm-normal GEOM=\"300x100+0+0\" PROG=\"./run-script.sh whatever.py\"'." @echo " " @echo " make xterm-wide: Launches the program in the PROG variable inside a new XTerm window with wide settings and the geometry given in GEOM. Run as: 'make xterm-wide GEOM=\"300x100+0+0\" PROG=\"./run-script.sh whatever.py\"'." @@ -148,7 +152,7 @@ build-cte: #### # Run the Commander IPC App: #### -console: +commander: ./run-script.sh ipc_apps/commander.py #### @@ -163,6 +167,18 @@ console: xconsole: make xterm-wide GEOM="300x100+0+0" PROG="make console" +#### +# Run the Iris Console CLI application for the debug harness (J36_debugger): +#### +console-debug: + ./run-script.sh __run_console.py -s J36_debugger + +#### +# Run the Iris Console CLI application for the debug harness (J36_debugger) in XTerm (for WSL users): +#### +xconsole-debug: + make xterm-wide GEOM="300x100+0+0" PROG="make console-debug" + #### # Launches the program in the PROG variable inside a new XTerm window with # normal settings and the geometry given in GEOM. diff --git a/Apps/GroundSoftware/requirements.txt b/Apps/GroundSoftware/requirements.txt index 30830195..948952f0 100644 --- a/Apps/GroundSoftware/requirements.txt +++ b/Apps/GroundSoftware/requirements.txt @@ -1,6 +1,6 @@ mypy typing_extensions>=4.0.0 -typeguard +typeguard~=2.13.3 ipykernel scapy python-pcapng diff --git a/Apps/GroundSoftware/scripts/tiny_apps/parse_pcap.py b/Apps/GroundSoftware/scripts/tiny_apps/parse_pcap.py index b945c45d..83b43c21 100644 --- a/Apps/GroundSoftware/scripts/tiny_apps/parse_pcap.py +++ b/Apps/GroundSoftware/scripts/tiny_apps/parse_pcap.py @@ -212,8 +212,11 @@ def parse_pcap(opts): ) if opts.cache_telem or opts.plot: - print("\t > Building telemetry streams from payloads . . .") - update_telemetry_streams_from_payloads(all_payloads, auto_cache=False) + raise DeprecationWarning( + "This functionality is (currently) no longer supported from parse_pcap." + ) + # print("\t > Building telemetry streams from payloads . . .") + # update_telemetry_streams_from_payloads(all_payloads, auto_cache=False) if opts.cache_telem: print("\t > Caching telemetry streams . . .") diff --git a/Apps/GroundSoftware/scripts/tiny_apps/run_console.py b/Apps/GroundSoftware/scripts/tiny_apps/run_console.py index 96cf139f..3f3a41db 100644 --- a/Apps/GroundSoftware/scripts/tiny_apps/run_console.py +++ b/Apps/GroundSoftware/scripts/tiny_apps/run_console.py @@ -60,7 +60,7 @@ def main(): serial_device_sn=SERIAL_DEVICE_SNS[opts.serial_device], baud=opts.baud ) - + if __name__ == '__main__': main() diff --git a/Apps/GroundSoftware/scripts/utils/__command_aliases.py b/Apps/GroundSoftware/scripts/utils/__command_aliases.py index ef4c8906..e233a745 100644 --- a/Apps/GroundSoftware/scripts/utils/__command_aliases.py +++ b/Apps/GroundSoftware/scripts/utils/__command_aliases.py @@ -220,6 +220,20 @@ class Parameter(Enum): OrderedDict(primary_interface='WATCHDOG'), DataPathway.WIRELESS ), + 'auto-switch-on': ( + DataPathway.WIRED, + Magic.COMMAND, + 'GroundInterface_SetInterfaceAutoSwitch', + OrderedDict(on=True), + DataPathway.WIRED + ), + 'auto-switch-off': ( + DataPathway.WIRED, + Magic.COMMAND, + 'GroundInterface_SetInterfaceAutoSwitch', + OrderedDict(on=False), + DataPathway.WIRED + ), 'deploy': ( DataPathway.WIRED, Magic.COMMAND, # "normal" command is for Hercules diff --git a/Apps/GroundSoftware/scripts/utils/trans_tools.py b/Apps/GroundSoftware/scripts/utils/trans_tools.py index 8a867037..cd11e068 100644 --- a/Apps/GroundSoftware/scripts/utils/trans_tools.py +++ b/Apps/GroundSoftware/scripts/utils/trans_tools.py @@ -39,6 +39,11 @@ from IrisBackendv3.utils.basic import print_bytearray_hex as printraw from IrisBackendv3.utils.basic import bytearray_to_spaced_hex as hexstr from IrisBackendv3.utils.nameiddict import NameIdDict +from IrisBackendv3.utils.console_display import ( + packet_print_string, + update_telemetry_payload_log_dataframe, + update_packet_log_dataframe +) from IrisBackendv3.data_standards import DataStandards from IrisBackendv3.data_standards.fsw_data_type import FswDataType @@ -62,7 +67,7 @@ from IrisBackendv3.codec.settings import ENDIANNESS_CODE, set_codec_standards # Load all console helper function: -from scripts.utils.trans_tools_console import * +from scripts.utils.trans_tools_console import IrisConsoleDisplayDriver import seaborn as sns # type: ignore sns.set() @@ -367,11 +372,15 @@ def degK_to_adc(tempK: float) -> int: return degC_to_adc(tempK - 273.15) -def update_telemetry_streams_from_payloads(payloads: EnhancedPayloadCollection, auto_cache=True, include_in_dataframe: bool = True): +def update_telemetry_streams_from_payloads( + telemetry_payload_log_dataframe: pd.DataFrame, + payloads: EnhancedPayloadCollection, + auto_cache=True, + include_in_dataframe: bool = True +): """ Updates the `telemetry_streams` from an EnhancedPayloadCollection. """ - global telemetry_payload_log_dataframe # before = telemetry_payload_log_dataframe.copy() for t in payloads[TelemetryPayload]: # If this payload's channel is new (previously un-logged), add it: @@ -395,13 +404,18 @@ def update_telemetry_streams_from_payloads(payloads: EnhancedPayloadCollection, cache() -def update_telemetry_streams(packet: Packet, auto_cache=True) -> None: +def update_telemetry_streams( + telemetry_payload_log_dataframe: pd.DataFrame, + packet: Packet, + auto_cache=True +) -> None: """ Add all extracted data values in the packet to their streams: """ # Don't include WatchdogDetailedStatusPackets in the Dataframe since they're *very* detailed # (contain way too much data to display) update_telemetry_streams_from_payloads( + telemetry_payload_log_dataframe, packet.payloads, auto_cache=auto_cache, include_in_dataframe=not isinstance( @@ -590,13 +604,18 @@ def handle_streamed_packet(packet: Optional[Packet], use_telem_dataview: bool = packet [Packet]: Newly received packet. use_telem_dataview [bool]: Whether to display the new data using the new Telemetry Dataview (True) or by just printing the packet (False). """ + driver = app_context['console_driver'] + if packet is not None: # Feed the streams: - update_telemetry_streams(packet) # telem dataframe updated in here + # telem dataframe updated in here + update_telemetry_streams( + driver.telemetry_payload_log_dataframe, packet + ) if USE_LOG_DATAFRAMES: # Normal packet. Load it: - update_packet_log_dataframe(packet_log_dataframe, packet) + update_packet_log_dataframe(driver.packet_log_dataframe, packet) # If the packet doesn't contain any telemetry or events (i.e. log, debug print, etc.), add it to the non-telem packet log in LiFo manner: # - Also do this for WatchdogDetailedStatusPacket since they're *very* detailed (contain way too much data to display so we're just # going to display it here instead). @@ -613,7 +632,9 @@ def handle_streamed_packet(packet: Optional[Packet], use_telem_dataview: bool = RadioUartBytePacket )) ): - nontelem_packet_prints.appendleft(packet_print_string(packet)) + driver.nontelem_packet_prints.appendleft( + packet_print_string(packet) + ) # Save the printout of the packet: save_packet_to_packet_prints(packet) @@ -621,7 +642,7 @@ def handle_streamed_packet(packet: Optional[Packet], use_telem_dataview: bool = # Display the data: if use_telem_dataview: # Update the display: - refresh_console_view(app_context) + driver.refresh_console_view(app_context) else: # Just log the data: log_print(packet) @@ -675,8 +696,13 @@ class SlipState(Enum): def stream_data_ip_udp_serial(use_console_view: bool = False) -> None: + driver = IrisConsoleDisplayDriver() + app_context = { + 'console_driver': driver + } + if use_console_view: - init_console_view() + driver.init_console_view() escape = False keep_running = True @@ -731,7 +757,9 @@ def append_byte(b): # Process it: packet = parse_ip_udp_packet(data_bytes) # Handle it: - handle_streamed_packet(packet, use_console_view) + handle_streamed_packet( + packet, use_console_view, app_context + ) # Move on: data_bytes = bytearray(b'') slip_state = SlipState.FIRST_BYTE_OR_STARTING_END @@ -759,7 +787,7 @@ def append_byte(b): err_print(msg) else: # In data view, just push the string to the packet print console: - nontelem_packet_prints.appendleft(msg) + driver.nontelem_packet_prints.appendleft(msg) except Exception as e: msg = f"An otherwise unresolved error occurred during packet streaming: {e}" if not use_console_view: @@ -767,7 +795,7 @@ def append_byte(b): err_print(msg) else: # In data view, just push the string to the packet print console: - nontelem_packet_prints.appendleft(msg) + driver.nontelem_packet_prints.appendleft(msg) data_bytes = bytearray(b'') slip_state = SlipState.FIRST_BYTE_OR_STARTING_END diff --git a/Apps/GroundSoftware/scripts/utils/trans_tools_console.py b/Apps/GroundSoftware/scripts/utils/trans_tools_console.py index ba7c1f92..0f002759 100644 --- a/Apps/GroundSoftware/scripts/utils/trans_tools_console.py +++ b/Apps/GroundSoftware/scripts/utils/trans_tools_console.py @@ -5,7 +5,7 @@ it has been used a lot and seems to work fine. @author: Connor W. Colombo (CMU) -@last-updated: 03/08/2023 +@last-updated: 04/08/2023 """ from __future__ import annotations # Support things like OrderedDict[A,B] from typing import Any, Final, List, Type, cast, Union, Dict, Tuple, Optional @@ -61,545 +61,552 @@ set_codec_standards(standards) -# Title of the window (helps with focus checking): -IRIS_CONSOLE_WINDOW_TITLE: Final[str] = "Iris Console" -# Actual title of the window (grabbed once at init, right after setting the window title, just in case setting it didn't work: -ACTUAL_REFERENCE_WINDOW_TITLE: Optional[str] = None -# Whether or not the console is currently paused: -console_paused: bool = False -# All dataframes for storing data: -telemetry_payload_log_dataframe: pd.DataFrame = pd.DataFrame() -packet_log_dataframe: pd.DataFrame = pd.DataFrame() -# LiFo Queue Log of the string prints of the most recent packets received that don't contain telemetry (i.e. logs, events, debug prints, etc.): -nontelem_packet_prints: deque = deque(maxlen=50) -# String of all keys pressed so far (the user's current input): -user_cmd_input_str: str = "" -# Any arguments the user has supplied -user_args: OrderedDict[str, Any] = OrderedDict() -# Name of argument currently being filled: -current_user_arg: str = "" -# What the user is being asked to input: -user_prompt: str = USER_PROMPT_COMMAND -# Name of the last sent command (for quick re-sending): -last_sent_command_name: Optional[str] = None - -# DataFrame of all prepared commands: -prepared_commands_dataframe: pd.DataFrame = pd.DataFrame() - - -def init_console_view() -> None: - global ACTUAL_REFERENCE_WINDOW_TITLE - global prepared_commands_dataframe - global telemetry_payload_log_dataframe, packet_log_dataframe - - # Initializes the console view. - # NOTE: Window needs to be in focus for this ... which it should be since the user *just* started the program. - - # Set the appropriate window title (helps with focus checking): - - # Make sure the console only captures input if this window is focused - # (only works in bash, but, the same goes for the rest of this program). - # Likely doesn't work on Windows, even if running bash (though *might* work in WSL - untested): - set_window_title(IRIS_CONSOLE_WINDOW_TITLE) - - # Immediately update the reference title (just in case setting the title didn't work on this platform): - ACTUAL_REFERENCE_WINDOW_TITLE = update_reference_window_title() - - # Make sure console starts unpaused: - unpause_console() - - # Initialize all dataframes: - packet_log_dataframe = init_packet_log_dataframe(packet_log_dataframe) - telemetry_payload_log_dataframe = init_telemetry_payload_log_dataframe( - telemetry_payload_log_dataframe) - prepared_commands_dataframe = init_command_dataframe( - prepared_commands_dataframe) - - # Build the first frame: - prepared_commands_dataframe = build_command_dataframe( - prepared_commands, - prepared_commands_dataframe - ) - - -def send_slip(dat: bytes, serial_writer) -> None: +class IrisConsoleDisplayDriver: """ - Wraps the given data in SLIP and sends it over RS422, - using the given serial interface `serial_writer` + Drives the display of Iris Console. + Basically just wraps a bunch of hacky functions from iris_console. + NOTE: `iris_console` is generally deprecated in favor of equivalent IPC + apps. This class was only made to allow for legacy support of iris_console + in the new IPC era. """ - buf = bytearray(b'') - buf.append(0xC0) - - for d in dat: - if d == 0xC0: - buf.append(0xDB) - buf.append(0xDC) - elif d == 0xDB: - buf.append(0xDB) - buf.append(0xDD) - else: - buf.append(d) + # Title of the window (helps with focus checking): + IRIS_CONSOLE_WINDOW_TITLE: Final[str] = "Iris Console" + # Actual title of the window (grabbed once at init, right after setting the window title, just in case setting it didn't work: + ACTUAL_REFERENCE_WINDOW_TITLE: Optional[str] + # Whether or not the console is currently paused: + console_paused: bool + # All dataframes for storing data: + telemetry_payload_log_dataframe: pd.DataFrame + packet_log_dataframe: pd.DataFrame + # LiFo Queue Log of the string prints of the most recent packets received that don't contain telemetry (i.e. logs, events, debug prints, etc.): + nontelem_packet_prints: deque + # String of all keys pressed so far (the user's current input): + user_cmd_input_str: str + # Any arguments the user has supplied + user_args: OrderedDict[str, Any] + # Name of argument currently being filled: + current_user_arg: str + # What the user is being asked to input: + user_prompt: str + # Name of the last sent command (for quick re-sending): + last_sent_command_name: Optional[str] + + # DataFrame of all prepared commands: + prepared_commands_dataframe: pd.DataFrame = pd.DataFrame() + + def __init__(self) -> None: + self.ACTUAL_REFERENCE_WINDOW_TITLE = None + self.console_paused = False + self.telemetry_payload_log_dataframe = pd.DataFrame() + self.packet_log_dataframe = pd.DataFrame() + self.nontelem_packet_prints = deque(maxlen=50) + self.user_cmd_input_str = "" + self.user_args = OrderedDict() + self.current_user_arg = "" + self.user_prompt = USER_PROMPT_COMMAND + self.last_sent_command_name = None + self.prepared_commands_dataframe = pd.DataFrame() + + def init_console_view(self) -> None: + # Initializes the console view. + # NOTE: Window needs to be in focus for this ... which it should be since the user *just* started the program. + + # Set the appropriate window title (helps with focus checking): + + # Make sure the console only captures input if this window is focused + # (only works in bash, but, the same goes for the rest of this program). + # Likely doesn't work on Windows, even if running bash (though *might* work in WSL - untested): + set_window_title(self.IRIS_CONSOLE_WINDOW_TITLE) + + # Immediately update the reference title (just in case setting the title didn't work on this platform): + self.ACTUAL_REFERENCE_WINDOW_TITLE = update_reference_window_title() + + # Make sure console starts unpaused: + self.unpause_console() + + # Initialize all dataframes: + self.packet_log_dataframe = init_packet_log_dataframe( + self.packet_log_dataframe) + self.telemetry_payload_log_dataframe = init_telemetry_payload_log_dataframe( + self.telemetry_payload_log_dataframe) + self.prepared_commands_dataframe = init_command_dataframe( + self.prepared_commands_dataframe) + + # Build the first frame: + self.prepared_commands_dataframe = build_command_dataframe( + prepared_commands, + self.prepared_commands_dataframe + ) - buf.append(0xC0) + @classmethod + def send_slip(cls, dat: bytes, serial_writer) -> None: + """ + Wraps the given data in SLIP and sends it over RS422, + using the given serial interface `serial_writer` + """ + buf = bytearray(b'') + buf.append(0xC0) + + for d in dat: + if d == 0xC0: + buf.append(0xDB) + buf.append(0xDC) + elif d == 0xDB: + buf.append(0xDB) + buf.append(0xDD) + else: + buf.append(d) - if serial_writer is not None: - serial_writer.write(bytes(buf)) - else: - raise Exception( - "Can't send data, serial connection not started. Try `connect_serial()`." - ) + buf.append(0xC0) + if serial_writer is not None: + serial_writer.write(bytes(buf)) + else: + raise Exception( + "Can't send data, serial connection not started. Try `connect_serial()`." + ) -def send_data_wd_serial( - raw_data: bytes, - serial_writer, - ip_dest: str = '127.0.0.1', # arbitrary (WD doesn't care) - ip_src: str = '222.173.190.239', # arbitrary (WD doesn't care) - port: int = 8080 # arbitrary (WD doesn't care) -) -> None: - # Send WD command over the given serial interface (serial_writer) - # Build packet - full_packet = scp.IP(dst=ip_dest, src=ip_src) / \ - scp.UDP(dport=port)/scp.Raw(load=raw_data) - # printraw(scp.raw(scp.IP(scp.raw(full_packet)))) - # printraw(scp.raw(full_packet)) - data = cast(bytes, scp.raw(full_packet)) - send_slip(data, serial_writer) - - -def send_wifi_adv(data: bytes, app_context, gateway_send_force: bool = False) -> None: - # Sends an UDP packet to the rover over wifi (INET really). - # Only works if sending from a machine (NIC) bound to the lander IP while a - # separate machine (NIC), bound to the gateway ip, is hosting the network - # which the rover is also connected to. - # - # If `gateway_send_force`, this forces it to go through (use this only if - # sending from the gateway but you need it to look like its coming from the - # lander). You likely will need to run as sudo for this feature to work. - - ip = app_context['wifi_settings']['rover_ip'] - port = app_context['wifi_settings']['rover_port'] - gwy_ip = app_context['wifi_settings']['gateway_ip'] - src_ip = app_context['wifi_settings']['lander_ip'] - src_port = app_context['wifi_settings']['lander_port'] - - if not gateway_send_force: - # Main way of doing this if connected to network on separate machine - # with lander address: - sock = cast(socket.socket, app_context['udp_server'].socket) - sock.sendto(data, (ip, port)) - else: - # Craft UDP packet (incl. using the correct src_ip, even if that's not our own): - full_packet = scp.IP(dst=ip, src=src_ip) / \ - scp.UDP(sport=src_port, dport=port)/scp.Raw(load=data) + @classmethod + def send_data_wd_serial( + cls, + raw_data: bytes, + serial_writer, + ip_dest: str = '127.0.0.1', # arbitrary (WD doesn't care) + ip_src: str = '222.173.190.239', # arbitrary (WD doesn't care) + port: int = 8080 # arbitrary (WD doesn't care) + ) -> None: + # Send WD command over the given serial interface (serial_writer) + # Build packet + full_packet = scp.IP(dst=ip_dest, src=ip_src) / \ + scp.UDP(dport=port)/scp.Raw(load=raw_data) # printraw(scp.raw(scp.IP(scp.raw(full_packet)))) # printraw(scp.raw(full_packet)) - full_packet_data = scp.raw(full_packet) - - # Open raw socket and send: - try: - sock = socket.socket( - socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) - # Tell kernel not to add headers (we took care of that above): - sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) - sock.sendto(full_packet_data, (ip, port)) - sock.close() - except PermissionError as pe: - print("Failed to open socket for sending WiFi packets due to PermissionError. You should be running this as `sudo` - are you?") - - -def send_console_command(pathway: DataPathway, magic: Magic, command_name: str, kwargs: OrderedDict[str, Any], app_context, message_queue, serial_writer) -> None: - # Attempt to send the given console command: - - global last_sent_command_name, user_cmd_input_str - global current_user_arg, user_prompt - - # No matter what, we're done with this command now, so reset: - source_user_cmd_input_str = user_cmd_input_str # store before resetting - user_cmd_input_str, current_user_arg, user_prompt = reset_console_command() - - command_payload_type = { - Magic.WATCHDOG_COMMAND: WatchdogCommandPayload, - Magic.RADIO_COMMAND: CommandPayload, - Magic.COMMAND: CommandPayload - }[magic] - - # Try to convert any args to int (since they all would have come in as str): - for arg_name, arg_val in kwargs.items(): + data = cast(bytes, scp.raw(full_packet)) + cls.send_slip(data, serial_writer) + + @classmethod + def send_wifi_adv(cls, data: bytes, app_context, gateway_send_force: bool = False) -> None: + # Sends an UDP packet to the rover over wifi (INET really). + # Only works if sending from a machine (NIC) bound to the lander IP while a + # separate machine (NIC), bound to the gateway ip, is hosting the network + # which the rover is also connected to. + # + # If `gateway_send_force`, this forces it to go through (use this only if + # sending from the gateway but you need it to look like its coming from the + # lander). You likely will need to run as sudo for this feature to work. + + ip = app_context['wifi_settings']['rover_ip'] + port = app_context['wifi_settings']['rover_port'] + gwy_ip = app_context['wifi_settings']['gateway_ip'] + src_ip = app_context['wifi_settings']['lander_ip'] + src_port = app_context['wifi_settings']['lander_port'] + + if not gateway_send_force: + # Main way of doing this if connected to network on separate machine + # with lander address: + sock = cast(socket.socket, app_context['udp_server'].socket) + sock.sendto(data, (ip, port)) + else: + # Craft UDP packet (incl. using the correct src_ip, even if that's not our own): + full_packet = scp.IP(dst=ip, src=src_ip) / \ + scp.UDP(sport=src_port, dport=port)/scp.Raw(load=data) + # printraw(scp.raw(scp.IP(scp.raw(full_packet)))) + # printraw(scp.raw(full_packet)) + full_packet_data = scp.raw(full_packet) + + # Open raw socket and send: + try: + sock = socket.socket( + socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) + # Tell kernel not to add headers (we took care of that above): + sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + sock.sendto(full_packet_data, (ip, port)) + sock.close() + except PermissionError as pe: + print("Failed to open socket for sending WiFi packets due to PermissionError. You should be running this as `sudo` - are you?") + + def send_console_command(self, pathway: DataPathway, magic: Magic, command_name: str, kwargs: OrderedDict[str, Any], app_context, message_queue, serial_writer) -> None: + # Attempt to send the given console command: + + # No matter what, we're done with this command now, so reset: + source_user_cmd_input_str = self.user_cmd_input_str # store before resetting + self.user_cmd_input_str, self.current_user_arg, self.user_prompt = reset_console_command() + + command_payload_type = { + Magic.WATCHDOG_COMMAND: WatchdogCommandPayload, + Magic.RADIO_COMMAND: CommandPayload, + Magic.COMMAND: CommandPayload + }[magic] + + # Try to convert any args to int (since they all would have come in as str): + for arg_name, arg_val in kwargs.items(): + try: + if isinstance(arg_val, int): + kwargs[arg_name] = arg_val + else: + kwargs[arg_name] = int(str(arg_val), base=0) + except ValueError: + pass # this one's not convertible, that's fine + + module, command = standards.global_command_lookup(command_name) + + # Pack command: + packet_bytes = b'' try: - if isinstance(arg_val, int): - kwargs[arg_name] = arg_val - else: - kwargs[arg_name] = int(str(arg_val), base=0) - except ValueError: - pass # this one's not convertible, that's fine - - module, command = standards.global_command_lookup(command_name) - - # Pack command: - packet_bytes = b'' - try: - command_payload = command_payload_type( - pathway=pathway, - source=DataSource.GENERATED, - magic=magic, - module_id=module.ID, - command_id=command.ID, - args=kwargs - ) - payloads = EnhancedPayloadCollection(CommandPayload=[command_payload]) - packet = IrisCommonPacket( - seq_num=0x00, - payloads=payloads - ) - packet_bytes = packet.encode() - if pathway == DataPathway.WIRED: - path = "RS422" - elif pathway == DataPathway.WIRELESS: - path = ( - "WIFI [" - f"{app_context['wifi_settings']['lander_ip']}:{app_context['wifi_settings']['lander_port']}" - f"->{app_context['wifi_settings']['rover_ip']}:{app_context['wifi_settings']['rover_port']}" - "]" + command_payload = command_payload_type( + pathway=pathway, + source=DataSource.GENERATED, + magic=magic, + module_id=module.ID, + command_id=command.ID, + args=kwargs ) - message_queue.put_nowait( - f"\033[1m\033[48;5;15m\033[38;5;34m({datetime.now().strftime('%m-%d %H:%M:%S')})\033[0m " - f"Sending over {path}: {command_payload} . . .\n{hexstr(packet_bytes)}" - ) - except Exception as e: - message_queue.put_nowait( - f"An exception occurred while trying to pack bytes for an outbound command: {e}") - - # Send command: - if len(packet_bytes) > 0: - # If we get here, we we're able to send the command. Store it: - last_sent_command_name = source_user_cmd_input_str - - try: + payloads = EnhancedPayloadCollection( + CommandPayload=[command_payload]) + packet = IrisCommonPacket( + seq_num=0x00, + payloads=payloads + ) + packet_bytes = packet.encode() if pathway == DataPathway.WIRED: - send_data_wd_serial(packet_bytes, serial_writer) + path = "RS422" elif pathway == DataPathway.WIRELESS: - # Application Context (settings, flags, etc. to be passed by reference to functions that need it): - send_wifi_adv(packet_bytes, app_context) + path = ( + "WIFI [" + f"{app_context['wifi_settings']['lander_ip']}:{app_context['wifi_settings']['lander_port']}" + f"->{app_context['wifi_settings']['rover_ip']}:{app_context['wifi_settings']['rover_port']}" + "]" + ) + message_queue.put_nowait( + f"\033[1m\033[48;5;15m\033[38;5;34m({datetime.now().strftime('%m-%d %H:%M:%S')})\033[0m " + f"Sending over {path}: {command_payload} . . .\n{hexstr(packet_bytes)}" + ) except Exception as e: message_queue.put_nowait( - f"An exception occurred while trying to transmit an outbound command: {e}") - - -def attempt_console_command_send(app_context, message_queue, serial_writer) -> None: - global user_args, current_user_arg, user_prompt - - # Only send if one command is locked in (only one result OR our input exactly matches the first result): - filtered_results = filter_command_dataframe( - user_cmd_input_str, - prepared_commands_dataframe - ) - if filtered_results.shape[0] == 1 or filtered_results.shape[0] > 1 and user_cmd_input_str == filtered_results.index[0]: - command_alias = filtered_results.index[0] - pathway, magic, command_name, kwargs, telem_pathway = prepared_commands[command_alias] - - if current_user_arg == "": - # we're just starting this command, so fill up the buffer: - user_args = cast('OrderedDict[str, Any]', kwargs) - - # reset user args pointer (look for any more paste values) - current_user_arg == "" - # If there are any params still set to PASTE, set the pointer there: - for arg_name, arg_val in user_args.items(): - if arg_val == Parameter.PASTE: - current_user_arg = arg_name - user_args[current_user_arg] = "" - user_prompt = USER_PROMPT_ARG - break - - # If the pointer is still empty, then all the arguments are accounted for, - # and we can proceed with the send: - if current_user_arg == "": - send_console_command(pathway, magic, command_name, - user_args, app_context, message_queue, serial_writer) - - -def handle_keypress(key: Any, message_queue, serial_writer, app_context) -> None: - key = cast(Union[pynput.keyboard.Key, pynput.keyboard.KeyCode], key) - # Handles new key input: - global user_cmd_input_str, last_sent_command_name - global user_prompt, current_user_arg - - something_changed: bool = False - - # If window is not focused, ignore input: - if not window_is_focused(ACTUAL_REFERENCE_WINDOW_TITLE): - return - - if key == pynput.keyboard.Key.f2: - # Toggle the console input/output pause state: - toggle_console_pause() - something_changed = True - - # If the console is paused, ignore input (besides the unpause command above): - if console_is_paused(): - return - - if key == pynput.keyboard.Key.f3: - # Toggle the packet and SLIP byte printing: - app_context['echo_all_packet_slip'] = not app_context['echo_all_packet_slip'] - app_context['echo_all_packet_bytes'] = not app_context['echo_all_packet_bytes'] - something_changed = True - - if key == pynput.keyboard.Key.f4: - # Toggle showing/hiding the command table: - app_context['collapse_command_table'] = not app_context['collapse_command_table'] - something_changed = True - - if key == pynput.keyboard.Key.esc: - # Reset the command: - user_cmd_input_str, current_user_arg, user_prompt = reset_console_command() - something_changed = True - - elif key == pynput.keyboard.Key.tab: - # Accept first auto-complete suggestion: - user_cmd_input_str = accept_top_suggestion( - user_cmd_input_str, user_prompt, prepared_commands_dataframe + f"An exception occurred while trying to pack bytes for an outbound command: {e}") + + # Send command: + if len(packet_bytes) > 0: + # If we get here, we we're able to send the command. Store it: + self.last_sent_command_name = source_user_cmd_input_str + + try: + if pathway == DataPathway.WIRED: + self.send_data_wd_serial(packet_bytes, serial_writer) + elif pathway == DataPathway.WIRELESS: + # Application Context (settings, flags, etc. to be passed by reference to functions that need it): + self.send_wifi_adv(packet_bytes, app_context) + except Exception as e: + message_queue.put_nowait( + f"An exception occurred while trying to transmit an outbound command: {e}") + + def attempt_console_command_send(self, app_context, message_queue, serial_writer) -> None: + # Only send if one command is locked in (only one result OR our input exactly matches the first result): + filtered_results = filter_command_dataframe( + self.user_cmd_input_str, + self.prepared_commands_dataframe ) - something_changed = True - - elif key == pynput.keyboard.Key.up: - # Set the input to the name of the last sent command - # (for quick resending): - if last_sent_command_name is not None: - user_prompt = USER_PROMPT_COMMAND - user_cmd_input_str = last_sent_command_name + if filtered_results.shape[0] == 1 or filtered_results.shape[0] > 1 and self.user_cmd_input_str == filtered_results.index[0]: + command_alias = filtered_results.index[0] + pathway, magic, command_name, kwargs, telem_pathway = prepared_commands[ + command_alias] + + if self.current_user_arg == "": + # we're just starting this command, so fill up the buffer: + self.user_args = cast('OrderedDict[str, Any]', kwargs) + + # reset user args pointer (look for any more paste values) + self.current_user_arg == "" + # If there are any params still set to PASTE, set the pointer there: + for arg_name, arg_val in self.user_args.items(): + if arg_val == Parameter.PASTE: + self.current_user_arg = arg_name + self.user_args[self.current_user_arg] = "" + self.user_prompt = USER_PROMPT_ARG + break + + # If the pointer is still empty, then all the arguments are accounted for, + # and we can proceed with the send: + if self.current_user_arg == "": + self.send_console_command(pathway, magic, command_name, + self.user_args, app_context, message_queue, serial_writer) + + def handle_keypress(self, key: Any, message_queue, serial_writer, app_context) -> None: + # Handles new key input: + key = cast(Union[pynput.keyboard.Key, pynput.keyboard.KeyCode], key) + something_changed: bool = False + + # If window is not focused, ignore input: + if not window_is_focused(self.ACTUAL_REFERENCE_WINDOW_TITLE): + return + + if key == pynput.keyboard.Key.f2: + # Toggle the console input/output pause state: + self.toggle_console_pause() something_changed = True - elif key == pynput.keyboard.Key.backspace: - # Remove the last character if backspace is pressed: - if user_prompt == USER_PROMPT_COMMAND: - user_cmd_input_str = user_cmd_input_str[:-1] - something_changed = True - elif user_prompt == USER_PROMPT_ARG and current_user_arg in user_args: - user_args[current_user_arg] = user_args[current_user_arg][:-1] - something_changed = True + # If the console is paused, ignore input (besides the unpause command above): + if self.console_is_paused(): + return - elif key == pynput.keyboard.Key.space: - if user_prompt == USER_PROMPT_COMMAND: - user_cmd_input_str += ' ' + if key == pynput.keyboard.Key.f3: + # Toggle the packet and SLIP byte printing: + app_context['echo_all_packet_slip'] = not app_context['echo_all_packet_slip'] + app_context['echo_all_packet_bytes'] = not app_context['echo_all_packet_bytes'] something_changed = True - elif user_prompt == USER_PROMPT_ARG and current_user_arg in user_args: - user_args[current_user_arg] += ' ' + + if key == pynput.keyboard.Key.f4: + # Toggle showing/hiding the command table: + app_context['collapse_command_table'] = not app_context['collapse_command_table'] something_changed = True - elif key == pynput.keyboard.Key.enter: - attempt_console_command_send(app_context, message_queue, serial_writer) - something_changed = True + if key == pynput.keyboard.Key.esc: + # Reset the command: + self.user_cmd_input_str, self.current_user_arg, self.user_prompt = reset_console_command() + something_changed = True - elif isinstance(key, pynput.keyboard.KeyCode) and key.char is not None: - # Add the given key to the user input: - if user_prompt == USER_PROMPT_COMMAND: - user_cmd_input_str += key.char + elif key == pynput.keyboard.Key.tab: + # Accept first auto-complete suggestion: + self.user_cmd_input_str = accept_top_suggestion( + self.user_cmd_input_str, self.user_prompt, self.prepared_commands_dataframe + ) something_changed = True - elif user_prompt == USER_PROMPT_ARG and current_user_arg in user_args: - user_args[current_user_arg] += key.char + + elif key == pynput.keyboard.Key.up: + # Set the input to the name of the last sent command + # (for quick resending): + if self.last_sent_command_name is not None: + self.user_prompt = USER_PROMPT_COMMAND + self.user_cmd_input_str = self.last_sent_command_name + something_changed = True + + elif key == pynput.keyboard.Key.backspace: + # Remove the last character if backspace is pressed: + if self.user_prompt == USER_PROMPT_COMMAND: + self.user_cmd_input_str = self.user_cmd_input_str[:-1] + something_changed = True + elif self.user_prompt == USER_PROMPT_ARG and self.current_user_arg in self.user_args: + self.user_args[self.current_user_arg] = self.user_args[self.current_user_arg][:-1] + something_changed = True + + elif key == pynput.keyboard.Key.space: + if self.user_prompt == USER_PROMPT_COMMAND: + self.user_cmd_input_str += ' ' + something_changed = True + elif self.user_prompt == USER_PROMPT_ARG and self.current_user_arg in self.user_args: + self.user_args[self.current_user_arg] += ' ' + something_changed = True + + elif key == pynput.keyboard.Key.enter: + self.attempt_console_command_send( + app_context, message_queue, serial_writer) something_changed = True - # Redraw the screen so the key input is reflected (only if something changed): - if something_changed: - refresh_console_view(app_context) - - -def create_console_view(app_context) -> str: - # Creates a new Console view by pretty-formatting all log DataFrames into one big string: - - # Settings: - horiz_padding = tabs2spaces("\t\t") - term_cols, term_lines = os.get_terminal_size() - # Adjust by subtracting bottom and right side buffers (from experimentation): - term_lines = term_lines - 2 # -1 for padding -1 for help message on bottom row - term_cols = term_cols - 2 - - # Build each section: - # Build the left side: - # Telemetry Table: - telem_view = ( - "\n\nTelemetry:\n" - + str_telemetry_payload_log_dataframe(telemetry_payload_log_dataframe) - ) - telem_view_lines = [tabs2spaces(x) for x in telem_view.split('\n')] - telem_view_width = max(len(l) for l in telem_view_lines) - telem_view_lines = [l.ljust(telem_view_width, ' ') - for l in telem_view_lines] - - # Build the left-hand side (just telem view): - left_side_lines = telem_view_lines - left_side_max_width = telem_view_width - - # Build the right side: - right_side_max_width = term_cols - left_side_max_width - len(horiz_padding) - # Packets Table: - packets_view = ( - "\n\nPackets:\n" - + str_packet_log_dataframe(packet_log_dataframe) - ) - packets_view_lines = [tabs2spaces(x) for x in packets_view.split('\n')] - - # Commands list: - usr_cmd = str_user_command( - user_prompt, user_cmd_input_str, prepared_commands_dataframe) - command_view = f"\n\n{user_prompt}: {usr_cmd}\n" - if not app_context['collapse_command_table']: - # Include the command table only if it's not collapsed: - command_view += str_command_dataframe( - filter_command_dataframe(user_cmd_input_str, - prepared_commands_dataframe) + elif isinstance(key, pynput.keyboard.KeyCode) and key.char is not None: + # Add the given key to the user input: + if self.user_prompt == USER_PROMPT_COMMAND: + self.user_cmd_input_str += key.char + something_changed = True + elif self.user_prompt == USER_PROMPT_ARG and self.current_user_arg in self.user_args: + self.user_args[self.current_user_arg] += key.char + something_changed = True + + # Redraw the screen so the key input is reflected (only if something changed): + if something_changed: + self.refresh_console_view(app_context) + + def create_console_view(self, app_context) -> str: + # Creates a new Console view by pretty-formatting all log DataFrames into one big string: + + # Settings: + horiz_padding = tabs2spaces("\t\t") + term_cols, term_lines = os.get_terminal_size() + # Adjust by subtracting bottom and right side buffers (from experimentation): + term_lines = term_lines - 2 # -1 for padding -1 for help message on bottom row + term_cols = term_cols - 2 + + # Build each section: + # Build the left side: + # Telemetry Table: + telem_view = ( + "\n\nTelemetry:\n" + + str_telemetry_payload_log_dataframe(self.telemetry_payload_log_dataframe) ) - else: - command_view += "[Table collapsed. F4 to uncollapse.]" - command_view_lines = [tabs2spaces(x) for x in command_view.split('\n')] - command_view_width = max(len_noCodes(l) for l in command_view_lines) - command_view_lines = [l.ljust(command_view_width, ' ') - for l in command_view_lines] - # Cap the size of the command view: - command_view_max_len = term_lines - len(packets_view_lines) - command_view_lines = command_view_lines[:command_view_max_len] - - # Non-Telemetry Packets log: - # Get the boundaries of the area we're going to squeeze this into: - plog_header_lines = [ - ' ', ' ', 'Non-Telemetry Packets:', '---------------------------'] - plog_footer_lines = [' '] - plog_width = right_side_max_width - len(horiz_padding) - command_view_width - plog_max_len = term_lines - \ - len(plog_header_lines) - len(packets_view_lines) - len(plog_footer_lines) - # Build the raw (ideal / un-squeezed) output: - # add escape close to ensure end of formatting for each line - plog_raw = tabs2spaces('\n\n'.join([ - tabs2spaces(x).strip() + "\033[0m" for x in nontelem_packet_prints - ])) - plog_lines = plog_raw.split('\n') - # Wrap each line to the width: - plog_line_wraps = [] - for i, line in enumerate(plog_lines): - if line.strip() == '': - # if the line is just whitespace and that's it, keep it - # (it's intentional but would get wiped out by textwrap.wrap) - # just tack on a clump of the line instead: - plog_line_wraps.append([line]) + telem_view_lines = [tabs2spaces(x) for x in telem_view.split('\n')] + telem_view_width = max(len(l) for l in telem_view_lines) + telem_view_lines = [l.ljust(telem_view_width, ' ') + for l in telem_view_lines] + + # Build the left-hand side (just telem view): + left_side_lines = telem_view_lines + left_side_max_width = telem_view_width + + # Build the right side: + right_side_max_width = term_cols - \ + left_side_max_width - len(horiz_padding) + # Packets Table: + packets_view = ( + "\n\nPackets:\n" + + str_packet_log_dataframe(self.packet_log_dataframe) + ) + packets_view_lines = [tabs2spaces(x) for x in packets_view.split('\n')] + + # Commands list: + usr_cmd = str_user_command( + self.user_prompt, self.user_cmd_input_str, self.prepared_commands_dataframe) + command_view = f"\n\n{self.user_prompt}: {usr_cmd}\n" + if not app_context['collapse_command_table']: + # Include the command table only if it's not collapsed: + command_view += str_command_dataframe( + filter_command_dataframe(self.user_cmd_input_str, + self.prepared_commands_dataframe) + ) + else: + command_view += "[Table collapsed. F4 to uncollapse.]" + command_view_lines = [tabs2spaces(x) for x in command_view.split('\n')] + command_view_width = max(len_noCodes(l) for l in command_view_lines) + command_view_lines = [l.ljust(command_view_width, ' ') + for l in command_view_lines] + # Cap the size of the command view: + command_view_max_len = term_lines - len(packets_view_lines) + command_view_lines = command_view_lines[:command_view_max_len] + + # Non-Telemetry Packets log: + # Get the boundaries of the area we're going to squeeze this into: + plog_header_lines = [ + ' ', ' ', 'Non-Telemetry Packets:', '---------------------------'] + plog_footer_lines = [' '] + plog_width = right_side_max_width - \ + len(horiz_padding) - command_view_width + plog_max_len = term_lines - \ + len(plog_header_lines) - \ + len(packets_view_lines) - len(plog_footer_lines) + # Build the raw (ideal / un-squeezed) output: + # add escape close to ensure end of formatting for each line + plog_raw = tabs2spaces('\n\n'.join([ + tabs2spaces(x).strip() + "\033[0m" for x in self.nontelem_packet_prints + ])) + plog_lines = plog_raw.split('\n') + # Wrap each line to the width: + plog_line_wraps = [] + for i, line in enumerate(plog_lines): + if line.strip() == '': + # if the line is just whitespace and that's it, keep it + # (it's intentional but would get wiped out by textwrap.wrap) + # just tack on a clump of the line instead: + plog_line_wraps.append([line]) + else: + # otherwise, it contains text, so we wrap it like normal: + plog_line_wraps.append(textwrap.wrap( + line, width=plog_width, replace_whitespace=False, break_on_hyphens=False, tabsize=4)) + plog_lines = [tabs2spaces( + line) for clump in plog_line_wraps for line in clump] # Reflatten + + # Make sure formatting ends at end of each line (this would be broken if a formatted line was word wrapped - + # that's why we're doing it here and not at the end; only need it after word wrap): + plog_lines = [line + "\033[0m" for line in plog_lines] + + # Grab all lines up to the limit and then tack on header and footer: + plog_lines = plog_header_lines + \ + plog_lines[:plog_max_len] + plog_footer_lines + # Make sure no line exceeds max width: + plog_lines = [l[:plog_width] for l in plog_lines] + # Make sure it's fixed width: + plog_lines = [l + ' '*(plog_width-len_noCodes(l)) for l in plog_lines] + + # Build the lower right section (plog on left, command view on right): + if len(command_view_lines) > len(plog_lines): + # add extra lines that match the max width: + plog_lines += [' ' * plog_width] * \ + (len(command_view_lines) - len(plog_lines)) + lower_right_lines = [a + horiz_padding + b for a, + b in itertools.zip_longest(plog_lines, command_view_lines, fillvalue="")] + + # Build the right-hand side (packets_view on top of lower right side): + right_side_lines = packets_view_lines + lower_right_lines + + # Make sure it's fixed width: + right_side_lines = [l.ljust(right_side_max_width, ' ') + for l in right_side_lines] + + # Add notice to the upper right: + notice_lines = [ + '\033[231;41;1m Iris Lunar Rover Console \033[0m', '(use in full-size terminal) '] + notice_width = max(len_noCodes(l) for l in notice_lines) + notice_lines = [l.rjust(notice_width, ' ') for l in notice_lines] + for i, line in enumerate(notice_lines): + right_side_lines[i] = right_side_lines[i][:- + len_noCodes(line)] + line + + # Make sure left side has at least as many lines as right side: + if len(right_side_lines) > len(left_side_lines): + # add extra lines that match the max width: + left_side_lines += [' ' * left_side_max_width] * \ + (len(right_side_lines) - len(left_side_lines)) + + # Join left and right sides: + all_lines = [a + horiz_padding + b for a, + b in itertools.zip_longest(left_side_lines, right_side_lines, fillvalue="")] + + # Make sure formatting resets before each line starts: + all_lines = ["\033[0m" + line for line in all_lines] + + # Pad out total number of lines to max height: + all_lines += [' '] * (term_lines - len(all_lines)) + + # Trim to match console height: + all_lines = all_lines[:term_lines] + + # Add help message to bottom line: + all_lines += [ljust_noCodes( + f"\033[37;40m Type commands." + f" Press \033[1mTab\033[22m to accept autocomplete." + f" Press \033[1mEnter\033[22m when \033[32mgreen\033[37m to send." + f" Press \033[1mEscape\033[22m to reset." + f" Press \033[1mF2\033[22m to un/pause console in/output (packets RX'd, just not displayed)." + f" Press \033[1mF3\033[22m to start/stop printing bytes for \033[1mall\033[22m packets." + f" Press \033[1mF4\033[22m to un/collapse command table." + f" Press \033[1mCtrl+'\\'\033[22m to end session.", term_cols) + "\033[0m"] + + # Build the full message: + # make sure formatting is reset at the end + full_str = '\n'.join(all_lines) + "\033[0m" + + return full_str + + def pause_console(self) -> None: + # "Pauses" the Console window: + self.console_paused = True + + def unpause_console(self) -> None: + # "Unpauses" the Console window: + self.console_paused = False + + def console_is_paused(self) -> bool: + # Returns whether the console is currently paused: + return self.console_paused + + def toggle_console_pause(self) -> None: + # Toggles console pause state: + if self.console_is_paused(): + self.unpause_console() else: - # otherwise, it contains text, so we wrap it like normal: - plog_line_wraps.append(textwrap.wrap( - line, width=plog_width, replace_whitespace=False, break_on_hyphens=False, tabsize=4)) - plog_lines = [tabs2spaces( - line) for clump in plog_line_wraps for line in clump] # Reflatten - - # Make sure formatting ends at end of each line (this would be broken if a formatted line was word wrapped - - # that's why we're doing it here and not at the end; only need it after word wrap): - plog_lines = [line + "\033[0m" for line in plog_lines] - - # Grab all lines up to the limit and then tack on header and footer: - plog_lines = plog_header_lines + \ - plog_lines[:plog_max_len] + plog_footer_lines - # Make sure no line exceeds max width: - plog_lines = [l[:plog_width] for l in plog_lines] - # Make sure it's fixed width: - plog_lines = [l + ' '*(plog_width-len_noCodes(l)) for l in plog_lines] - - # Build the lower right section (plog on left, command view on right): - if len(command_view_lines) > len(plog_lines): - # add extra lines that match the max width: - plog_lines += [' ' * plog_width] * \ - (len(command_view_lines) - len(plog_lines)) - lower_right_lines = [a + horiz_padding + b for a, - b in itertools.zip_longest(plog_lines, command_view_lines, fillvalue="")] - - # Build the right-hand side (packets_view on top of lower right side): - right_side_lines = packets_view_lines + lower_right_lines - - # Make sure it's fixed width: - right_side_lines = [l.ljust(right_side_max_width, ' ') - for l in right_side_lines] - - # Add notice to the upper right: - notice_lines = [ - '\033[231;41;1m Iris Lunar Rover Console \033[0m', '(use in full-size terminal) '] - notice_width = max(len_noCodes(l) for l in notice_lines) - notice_lines = [l.rjust(notice_width, ' ') for l in notice_lines] - for i, line in enumerate(notice_lines): - right_side_lines[i] = right_side_lines[i][:-len_noCodes(line)] + line - - # Make sure left side has at least as many lines as right side: - if len(right_side_lines) > len(left_side_lines): - # add extra lines that match the max width: - left_side_lines += [' ' * left_side_max_width] * \ - (len(right_side_lines) - len(left_side_lines)) - - # Join left and right sides: - all_lines = [a + horiz_padding + b for a, - b in itertools.zip_longest(left_side_lines, right_side_lines, fillvalue="")] - - # Make sure formatting resets before each line starts: - all_lines = ["\033[0m" + line for line in all_lines] - - # Pad out total number of lines to max height: - all_lines += [' '] * (term_lines - len(all_lines)) - - # Trim to match console height: - all_lines = all_lines[:term_lines] - - # Add help message to bottom line: - all_lines += [ljust_noCodes( - f"\033[37;40m Type commands." - f" Press \033[1mTab\033[22m to accept autocomplete." - f" Press \033[1mEnter\033[22m when \033[32mgreen\033[37m to send." - f" Press \033[1mEscape\033[22m to reset." - f" Press \033[1mF2\033[22m to un/pause console in/output (packets RX'd, just not displayed)." - f" Press \033[1mF3\033[22m to start/stop printing bytes for \033[1mall\033[22m packets." - f" Press \033[1mF4\033[22m to un/collapse command table." - f" Press \033[1mCtrl+'\\'\033[22m to end session.", term_cols) + "\033[0m"] - - # Build the full message: - # make sure formatting is reset at the end - full_str = '\n'.join(all_lines) + "\033[0m" - - return full_str - - -def pause_console() -> None: - # "Pauses" the Console window: - global console_paused - console_paused = True - - -def unpause_console() -> None: - # "Unpauses" the Console window: - global console_paused - console_paused = False - - -def console_is_paused() -> bool: - # Returns whether the console is currently paused: - return console_paused - - -def toggle_console_pause() -> None: - # Toggles console pause state: - if console_is_paused(): - unpause_console() - else: - pause_console() - - -def refresh_console_view(app_context) -> None: - # Refreshes (clears) the Console Window with High-level Dataview: - - # Build the string before clearing the screen to minimize off-time: - # (only update console view if not paused): - if not console_is_paused(): - # Always use this opportunity to update the dataframe timestamps: - now = datetime.now() - update_all_packet_log_times(packet_log_dataframe, now) - - # Build the screen string: - data = create_console_view(app_context) - - # Update the screen: - clear_console() - print(data) + self.pause_console() + + def refresh_console_view(self, app_context) -> None: + # Refreshes (clears) the Console Window with High-level Dataview: + + # Build the string before clearing the screen to minimize off-time: + # (only update console view if not paused): + # (also don't draw in debug mode) + if not app_context['debug-mode'] and not self.console_is_paused(): + # Always use this opportunity to update the dataframe timestamps: + now = datetime.now() + update_all_packet_log_times(self.packet_log_dataframe, now) + + # Build the screen string: + data = self.create_console_view(app_context) + + # Update the screen: + clear_console() + print(data) diff --git a/Apps/GroundSoftware/scripts/utils/trans_tools_console_process.py b/Apps/GroundSoftware/scripts/utils/trans_tools_console_process.py index 0a3bbe84..b4faa36d 100644 --- a/Apps/GroundSoftware/scripts/utils/trans_tools_console_process.py +++ b/Apps/GroundSoftware/scripts/utils/trans_tools_console_process.py @@ -15,6 +15,7 @@ from termcolor import colored from scripts.utils.trans_tools import * +from scripts.utils.trans_tools_console import IrisConsoleDisplayDriver from IrisBackendv3.codec.payload import EventPayload import scapy.all as scp # type: ignore @@ -37,7 +38,11 @@ 'gateway_ip': '192.168.150.254', 'lander_ip': '192.168.10.105', 'lander_port': 43531, - } + }, + # Whether this is being run inside a debugger (e.x. we should forgo drawing): + 'debug-mode': False, + # Console driver instance: + 'console_driver': IrisConsoleDisplayDriver() } # Special message types that wrap primitives (so we can check type in Queue receiver): @@ -333,6 +338,7 @@ async def run_forever(): def handle_async_packet(packet: Packet) -> None: """Handles a packet pushed asynchronously to a queue.""" + driver = app_context['console_driver'] # If there are any EventPayloads in the packet, # print them in the order received (NOTE: some # don't have timestamps - e.g. RadioGround - so @@ -341,7 +347,7 @@ def handle_async_packet(packet: Packet) -> None: for event in events: # Push directly to the queue: # ... the handle_streamed_packet will take care of the refreshing - nontelem_packet_prints.appendleft( + driver.nontelem_packet_prints.appendleft( f"\033[35;47;1m({datetime.now().strftime(DATETIME_FORMAT_STR)})\033[0m {event!s}" ) @@ -354,7 +360,7 @@ def handle_async_packet(packet: Packet) -> None: elif packet.pathway == DataPathway.WIRELESS: prefix = 'WIFI ' # To the console: - nontelem_packet_prints.appendleft( + driver.nontelem_packet_prints.appendleft( f"{prefix}Packet Bytes:\n-----------------\n" + scp.hexdump(echo_bytes, dump=True) ) @@ -366,9 +372,9 @@ def handle_async_packet(packet: Packet) -> None: def handle_async_message(msg: str) -> None: """Handles a message pushed asynchronously to a queue.""" # Add the message to the non-telemetry feed: - nontelem_packet_prints.appendleft(msg) + app_context['console_driver'].nontelem_packet_prints.appendleft(msg) # Refresh the display: - refresh_console_view(app_context) + app_context['console_driver'].refresh_console_view(app_context) async def console_main(serial_settings): @@ -377,7 +383,7 @@ async def console_main(serial_settings): # Uses the serial interface defined by the given settings. # Set up the console: - init_console_view() + app_context['console_driver'].init_console_view() # Setup IPC Queues: packet_queue = asyncio.Queue() @@ -448,15 +454,17 @@ def __exit__(self, *args, **kwargs): pass handle_async_packet(cast(Packet, r)) elif isinstance(r, (pynput.keyboard.Key, pynput.keyboard.KeyCode)): # NOTE: this refreshes the screen too - handle_keypress(r, message_queue, - serial_writer, app_context) + app_context['console_driver'].handle_keypress(r, message_queue, + serial_writer, app_context) elif isinstance(r, str): handle_async_message(cast(str, r)) elif isinstance(r, QueueMessage): handle_async_message(cast(QueueMessage, r).msg) elif isinstance(r, QueueTick): # Don't do anything special besides update screen: - refresh_console_view(app_context) + app_context['console_driver'].refresh_console_view( + app_context + ) # Clean up if the above closes for some reason: [await t for t in tasks]