From 996aa72fb4d3bcab1550779f6b7f7ffc2aa5f33e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 15 Apr 2026 13:25:48 +0200 Subject: [PATCH] squashed EIVE software --- .clang-format | 8 + .dockerignore | 6 + .gitignore | 28 + .gitmodules | 23 +- .idea/cmake.xml | 16 + .run/Q7S FM.run.xml | 10 + CHANGELOG.md | 2362 +++++++++++++ CMakeLists.txt | 581 ++++ Justfile | 12 + LICENSE | 202 ++ NOTICE | 13 + README.md | 1403 ++++++++ archive/PlocMpsocHandler.cpp | 1559 +++++++++ archive/PlocMpsocHandler.h | 322 ++ archive/PlocMpsocSpecialComHelperLegacy.cpp | 545 +++ archive/PlocMpsocSpecialComHelperLegacy.h | 200 ++ archive/PlocSupervisorHandler.cpp | 2027 +++++++++++ archive/PlocSupervisorHandler.h | 389 +++ archive/gpio/CMakeLists.txt | 12 + archive/gpio/GpioCookie.cpp | 32 + archive/gpio/GpioCookie.h | 39 + archive/gpio/GpioIF.h | 54 + archive/gpio/LinuxLibgpioIF.cpp | 295 ++ archive/gpio/LinuxLibgpioIF.h | 75 + archive/gpio/gpioDefinitions.h | 83 + archive/tmtc/CCSDSIPCoreBridge.cpp | 128 + archive/tmtc/CCSDSIPCoreBridge.h | 129 + arduino | 1 + automation/Dockerfile | 27 + automation/Jenkinsfile | 47 + bsp_egse/CMakeLists.txt | 3 + bsp_egse/InitMission.cpp | 192 ++ bsp_egse/InitMission.h | 21 + bsp_egse/ObjectFactory.cpp | 48 + bsp_egse/ObjectFactory.h | 8 + bsp_egse/boardconfig/CMakeLists.txt | 3 + bsp_egse/boardconfig/busConf.h | 8 + bsp_egse/boardconfig/etl_profile.h | 38 + bsp_egse/boardconfig/gcov.h | 15 + bsp_egse/boardconfig/print.c | 10 + bsp_egse/boardconfig/print.h | 8 + bsp_egse/boardconfig/rpiConfig.h.in | 6 + bsp_egse/main.cpp | 28 + bsp_hosted/CMakeLists.txt | 4 + bsp_hosted/Dockerfile | 21 + bsp_hosted/OBSWConfig.h.in | 127 + bsp_hosted/boardconfig/CMakeLists.txt | 3 + bsp_hosted/boardconfig/etl_profile.h | 38 + bsp_hosted/boardconfig/gcov.h | 15 + bsp_hosted/boardconfig/print.c | 11 + bsp_hosted/boardconfig/print.h | 8 + bsp_hosted/comIF/ArduinoComIF.cpp | 349 ++ bsp_hosted/comIF/ArduinoComIF.h | 66 + bsp_hosted/comIF/ArduinoCookie.cpp | 8 + bsp_hosted/comIF/ArduinoCookie.h | 22 + bsp_hosted/comIF/CMakeLists.txt | 1 + bsp_hosted/fsfwconfig/CMakeLists.txt | 17 + bsp_hosted/fsfwconfig/FSFWConfig.h.in | 77 + bsp_hosted/fsfwconfig/OBSWConfig.h.in | 46 + .../fsfwconfig/events/subsystemIdRanges.h | 16 + .../fsfwconfig/events/translateEvents.cpp | 990 ++++++ .../fsfwconfig/events/translateEvents.h | 8 + .../fsfwconfig/ipc/MissionMessageTypes.cpp | 10 + .../fsfwconfig/ipc/MissionMessageTypes.h | 22 + .../fsfwconfig/objects/systemObjectList.h | 31 + .../fsfwconfig/objects/translateObjects.cpp | 544 +++ .../fsfwconfig/objects/translateObjects.h | 8 + .../fsfwconfig/pollingsequence/CMakeLists.txt | 1 + .../fsfwconfig/pollingsequence/DummyPst.cpp | 139 + .../fsfwconfig/pollingsequence/DummyPst.h | 14 + bsp_hosted/fsfwconfig/returnvalues/classIds.h | 20 + bsp_hosted/main.cpp | 44 + bsp_hosted/objectFactory.cpp | 124 + bsp_hosted/objectFactory.h | 9 + bsp_hosted/scheduling.cpp | 280 ++ bsp_hosted/scheduling.h | 6 + bsp_linux_board/CMakeLists.txt | 6 + bsp_linux_board/Dockerfile | 37 + bsp_linux_board/InitMission.cpp | 267 ++ bsp_linux_board/InitMission.h | 23 + bsp_linux_board/OBSWConfig.h.in | 129 + bsp_linux_board/ObjectFactory.cpp | 248 ++ bsp_linux_board/ObjectFactory.h | 16 + bsp_linux_board/boardconfig/CMakeLists.txt | 3 + bsp_linux_board/boardconfig/etl_profile.h | 38 + bsp_linux_board/boardconfig/gcov.h | 15 + bsp_linux_board/boardconfig/print.c | 10 + bsp_linux_board/boardconfig/print.h | 8 + bsp_linux_board/boardconfig/rpiConfig.h.in | 20 + bsp_linux_board/boardtest/CMakeLists.txt | 1 + bsp_linux_board/definitions.h | 43 + bsp_linux_board/gpioInit.cpp | 56 + bsp_linux_board/gpioInit.h | 20 + bsp_linux_board/main.cpp | 34 + bsp_q7s/CMakeLists.txt | 28 + bsp_q7s/OBSWConfig.h.in | 147 + bsp_q7s/acs/CMakeLists.txt | 1 + bsp_q7s/acs/StrConfigPathGetter.h | 23 + bsp_q7s/boardconfig/CMakeLists.txt | 5 + bsp_q7s/boardconfig/busConf.h | 121 + bsp_q7s/boardconfig/etl_profile.h | 39 + bsp_q7s/boardconfig/gcov.h | 15 + bsp_q7s/boardconfig/print.c | 10 + bsp_q7s/boardconfig/print.h | 8 + bsp_q7s/boardconfig/q7sConfig.h.in | 34 + bsp_q7s/boardtest/CMakeLists.txt | 5 + bsp_q7s/boardtest/FileSystemTest.cpp | 23 + bsp_q7s/boardtest/FileSystemTest.h | 12 + bsp_q7s/boardtest/Q7STestTask.cpp | 458 +++ bsp_q7s/boardtest/Q7STestTask.h | 61 + bsp_q7s/callbacks/CMakeLists.txt | 2 + bsp_q7s/callbacks/gnssCallback.cpp | 29 + bsp_q7s/callbacks/gnssCallback.h | 18 + bsp_q7s/callbacks/pcduSwitchCb.cpp | 32 + bsp_q7s/callbacks/pcduSwitchCb.h | 14 + bsp_q7s/callbacks/q7sGpioCallbacks.cpp | 54 + bsp_q7s/callbacks/q7sGpioCallbacks.h | 15 + bsp_q7s/callbacks/rwSpiCallback.cpp | 283 ++ bsp_q7s/callbacks/rwSpiCallback.h | 37 + bsp_q7s/core/CMakeLists.txt | 2 + bsp_q7s/core/CoreController.cpp | 2633 +++++++++++++++ bsp_q7s/core/CoreController.h | 401 +++ bsp_q7s/core/WatchdogHandler.cpp | 87 + bsp_q7s/core/WatchdogHandler.h | 23 + bsp_q7s/core/XiphosWdtHandler.cpp | 122 + bsp_q7s/core/XiphosWdtHandler.h | 36 + bsp_q7s/core/defs.h | 45 + bsp_q7s/em/CMakeLists.txt | 1 + bsp_q7s/em/emObjectFactory.cpp | 184 + bsp_q7s/fmObjectFactory.cpp | 135 + bsp_q7s/fs/CMakeLists.txt | 2 + bsp_q7s/fs/FilesystemHelper.cpp | 38 + bsp_q7s/fs/FilesystemHelper.h | 49 + bsp_q7s/fs/SdCardManager.cpp | 584 ++++ bsp_q7s/fs/SdCardManager.h | 246 ++ bsp_q7s/fs/helpers.cpp | 11 + bsp_q7s/fs/helpers.h | 14 + bsp_q7s/main.cpp | 22 + bsp_q7s/memory/CMakeLists.txt | 1 + bsp_q7s/memory/LocalParameterHandler.cpp | 41 + bsp_q7s/memory/LocalParameterHandler.h | 106 + bsp_q7s/memory/scratchApi.cpp | 50 + bsp_q7s/memory/scratchApi.h | 146 + bsp_q7s/objectFactory.cpp | 1093 ++++++ bsp_q7s/objectFactory.h | 97 + bsp_q7s/obsw.cpp | 151 + bsp_q7s/obsw.h | 15 + bsp_q7s/scheduling.cpp | 702 ++++ bsp_q7s/scheduling.h | 26 + bsp_q7s/simple/CMakeLists.txt | 1 + bsp_q7s/simple/simple.cpp | 22 + bsp_q7s/simple/simple.h | 10 + bsp_q7s/spi/Q7sSpiComIF.cpp | 5 + bsp_q7s/spi/Q7sSpiComIF.h | 32 + bsp_q7s/xadc/CMakeLists.txt | 1 + bsp_q7s/xadc/Xadc.cpp | 149 + bsp_q7s/xadc/Xadc.h | 108 + bsp_te0720_1cfa/CMakeLists.txt | 7 + bsp_te0720_1cfa/InitMission.cpp | 227 ++ bsp_te0720_1cfa/InitMission.h | 21 + bsp_te0720_1cfa/OBSWConfig.h.in | 126 + bsp_te0720_1cfa/ObjectFactory.cpp | 159 + bsp_te0720_1cfa/ObjectFactory.h | 12 + bsp_te0720_1cfa/boardconfig/CMakeLists.txt | 7 + bsp_te0720_1cfa/boardconfig/busConf.h | 39 + bsp_te0720_1cfa/boardconfig/etl_profile.h | 38 + bsp_te0720_1cfa/boardconfig/gcov.h | 15 + bsp_te0720_1cfa/boardconfig/print.c | 10 + bsp_te0720_1cfa/boardconfig/print.h | 8 + bsp_te0720_1cfa/main.cpp | 29 + clone-submodules-no-privlibs.sh | 6 + cmake/BBBCrossCompileConfig.cmake | 103 + cmake/BuildType.cmake | 48 + cmake/EiveHelpers.cmake | 30 + cmake/GetGitRevisionDescription.cmake | 284 ++ cmake/GetGitRevisionDescription.cmake.in | 43 + cmake/HardwareOsPostConfig.cmake | 66 + cmake/PreProjectConfig.cmake | 144 + cmake/RPiCrossCompileConfig.cmake | 148 + cmake/Zynq7020CrossCompileConfig.cmake | 112 + .../crosscompile/bbb_path_helper.sh | 3 + .../crosscompile/make-debug-cfg.sh | 35 + .../crosscompile/make-release-cfg.sh | 35 + cmake/scripts/beagleboneb/make-debug-cfg.sh | 35 + cmake/scripts/cmake-build-cfg.py | 173 + cmake/scripts/egse/egse_path_helper_win.sh | 13 + cmake/scripts/egse/make-debug-cfg.sh | 34 + cmake/scripts/host/host-make-debug.sh | 41 + cmake/scripts/host/host-make-release.sh | 39 + cmake/scripts/host/host-ninja-debug.sh | 38 + cmake/scripts/linux/host-make-debug.sh | 37 + cmake/scripts/linux/host-make-release.sh | 37 + cmake/scripts/linux/host-ninja-debug.sh | 38 + cmake/scripts/q7s/q7s-make-debug.sh | 50 + cmake/scripts/q7s/q7s-make-release.sh | 50 + cmake/scripts/q7s/q7s-make-size.sh | 35 + cmake/scripts/q7s/q7s-ninja-debug.sh | 48 + cmake/scripts/q7s/q7s-ninja-release.sh | 48 + cmake/scripts/rpi/make-debug-cfg.sh | 34 + cmake/scripts/rpi/make-release-cfg.sh | 33 + cmake/scripts/rpi/ninja-debug-cfg.sh | 34 + cmake/scripts/rpi/rpi_path_helper.sh | 24 + cmake/scripts/rpi/rpi_path_helper_win.sh | 22 + cmake/scripts/te0720-1cfa/make-debug-cfg.sh | 34 + .../te0720-1cfa/win-env-te0720-1cfa.sh | 49 + common/CMakeLists.txt | 1 + common/config/CMakeLists.txt | 3 + common/config/ccsdsConfig.h | 8 + common/config/commonConfig.cpp | 11 + common/config/commonConfig.h.in | 43 + common/config/devConf.h | 78 + common/config/devices/addresses.h | 90 + common/config/devices/gpioIds.h | 128 + common/config/devices/powerSwitcherList.h | 6 + common/config/eive/definitions.h | 135 + common/config/eive/eventSubsystemIds.h | 50 + common/config/eive/objects.h | 188 ++ common/config/eive/resultClassIds.h | 50 + common/config/lwgps_opts.h | 48 + common/config/tmtc/pusIds.h | 25 + doc/XSC-1542-6025-i_Q7RevB_User_Manual.pdf | Bin 0 -> 1573503 bytes doc/deprecated-Q7S-user-manual14042020.pdf | Bin 0 -> 1572853 bytes doc/img/ProcessSettings.png | Bin 0 -> 18799 bytes doc/img/eive-logo.png | Bin 0 -> 106723 bytes doc/img/vivado-edition.png | Bin 0 -> 36951 bytes doc/img/vivado-hl-design.png | Bin 0 -> 50686 bytes doc/img/xilinx-install.PNG | Bin 0 -> 46795 bytes docker-compose.yml | 18 + dummies/AcuDummy.cpp | 88 + dummies/AcuDummy.h | 39 + dummies/BatteryDummy.cpp | 54 + dummies/BatteryDummy.h | 52 + dummies/BpxDummy.cpp | 55 + dummies/BpxDummy.h | 48 + dummies/CMakeLists.txt | 33 + dummies/ComCookieDummy.cpp | 5 + dummies/ComCookieDummy.h | 12 + dummies/ComIFDummy.cpp | 21 + dummies/ComIFDummy.h | 26 + dummies/CoreControllerDummy.cpp | 54 + dummies/CoreControllerDummy.h | 21 + dummies/ExecutableComIfDummy.cpp | 27 + dummies/ExecutableComIfDummy.h | 21 + dummies/GpsCtrlDummy.cpp | 36 + dummies/GpsCtrlDummy.h | 23 + dummies/GpsDhbDummy.cpp | 58 + dummies/GpsDhbDummy.h | 33 + dummies/GyroAdisDummy.cpp | 52 + dummies/GyroAdisDummy.h | 35 + dummies/GyroL3GD20Dummy.cpp | 48 + dummies/GyroL3GD20Dummy.h | 33 + dummies/ImtqDummy.cpp | 120 + dummies/ImtqDummy.h | 70 + dummies/Max31865Dummy.cpp | 58 + dummies/Max31865Dummy.h | 34 + dummies/MgmLIS3MDLDummy.cpp | 47 + dummies/MgmLIS3MDLDummy.h | 35 + dummies/MgmRm3100Dummy.cpp | 46 + dummies/MgmRm3100Dummy.h | 30 + dummies/P60DockDummy.cpp | 50 + dummies/P60DockDummy.h | 37 + dummies/PcduHandlerDummy.cpp | 82 + dummies/PcduHandlerDummy.h | 62 + dummies/PduDummy.cpp | 45 + dummies/PduDummy.h | 38 + dummies/PlPcduDummy.cpp | 58 + dummies/PlPcduDummy.h | 40 + dummies/PlocMpsocDummy.cpp | 55 + dummies/PlocMpsocDummy.h | 37 + dummies/PlocSupervisorDummy.cpp | 67 + dummies/PlocSupervisorDummy.h | 40 + dummies/RadSensorDummy.cpp | 55 + dummies/RadSensorDummy.h | 35 + dummies/RwDummy.cpp | 113 + dummies/RwDummy.h | 43 + dummies/SaDeploymentDummy.cpp | 7 + dummies/SaDeploymentDummy.h | 19 + dummies/ScexDummy.cpp | 40 + dummies/ScexDummy.h | 30 + dummies/StarTrackerDummy.cpp | 70 + dummies/StarTrackerDummy.h | 33 + dummies/SusDummy.cpp | 44 + dummies/SusDummy.h | 35 + dummies/SyrlinksDummy.cpp | 47 + dummies/SyrlinksDummy.h | 34 + dummies/TemperatureSensorInserter.cpp | 149 + dummies/TemperatureSensorInserter.h | 45 + dummies/Tmp1075Dummy.cpp | 64 + dummies/Tmp1075Dummy.h | 35 + dummies/helperFactory.cpp | 276 ++ dummies/helperFactory.h | 41 + fsfw | 2 +- generators/.gitignore | 4 + generators/.run/all.run.xml | 24 + generators/.run/events.run.xml | 24 + generators/.run/objects.run.xml | 24 + generators/.run/returnvalues.run.xml | 24 + generators/bsp_hosted_events.csv | 326 ++ generators/bsp_hosted_objects.csv | 176 + generators/bsp_hosted_returnvalues.csv | 533 +++ generators/bsp_hosted_subsystems.csv | 64 + generators/bsp_q7s_events.csv | 326 ++ generators/bsp_q7s_objects.csv | 180 + generators/bsp_q7s_returnvalues.csv | 633 ++++ generators/bsp_q7s_subsystems.csv | 64 + generators/definitions.py | 23 + generators/deps/.gitignore | 3 + generators/deps/install_fsfwgen.sh | 3 + generators/events/__init__.py | 0 generators/events/event_parser.py | 178 + generators/events/translateEvents.cpp | 990 ++++++ generators/events/translateEvents.h | 8 + generators/gen.py | 72 + generators/objects/__init__.py | 0 generators/objects/objects.py | 140 + generators/objects/translateObjects.cpp | 556 +++ generators/objects/translateObjects.h | 8 + generators/requirements.txt | 2 + generators/returnvalues/__init__.py | 0 .../returnvalues/returnvalues_parser.py | 140 + generators/system/__init__.py | 0 generators/system/eive-system.yml | 464 +++ hooks/post-checkout | 6 + linux/CMakeLists.txt | 19 + linux/ObjectFactory.cpp | 355 ++ linux/ObjectFactory.h | 38 + linux/acs/AcsBoardPolling.cpp | 837 +++++ linux/acs/AcsBoardPolling.h | 94 + linux/acs/CMakeLists.txt | 11 + linux/acs/GPSDefinitions.h | 119 + linux/acs/GpsHyperionLinuxController.cpp | 463 +++ linux/acs/GpsHyperionLinuxController.h | 99 + linux/acs/ImtqPollingTask.cpp | 520 +++ linux/acs/ImtqPollingTask.h | 74 + linux/acs/RwPollingTask.cpp | 533 +++ linux/acs/RwPollingTask.h | 90 + linux/acs/StrComHandler.cpp | 838 +++++ linux/acs/StrComHandler.h | 380 +++ linux/acs/SusPolling.cpp | 226 ++ linux/acs/SusPolling.h | 52 + linux/boardtest/CMakeLists.txt | 2 + linux/boardtest/I2cTestClass.cpp | 101 + linux/boardtest/I2cTestClass.h | 39 + linux/boardtest/LibgpiodTest.cpp | 127 + linux/boardtest/LibgpiodTest.h | 31 + linux/boardtest/SpiTestClass.cpp | 908 +++++ linux/boardtest/SpiTestClass.h | 127 + linux/boardtest/UartTestClass.cpp | 402 +++ linux/boardtest/UartTestClass.h | 74 + linux/callbacks/CMakeLists.txt | 1 + linux/callbacks/gpioCallbacks.cpp | 431 +++ linux/callbacks/gpioCallbacks.h | 66 + linux/com/CMakeLists.txt | 1 + linux/com/SyrlinksComHandler.cpp | 209 ++ linux/com/SyrlinksComHandler.h | 53 + linux/fsfwconfig/CMakeLists.txt | 15 + linux/fsfwconfig/FSFWConfig.h.in | 83 + linux/fsfwconfig/events/subsystemIdRanges.h | 19 + linux/fsfwconfig/events/translateEvents.cpp | 990 ++++++ linux/fsfwconfig/events/translateEvents.h | 8 + linux/fsfwconfig/ipc/MissionMessageTypes.cpp | 10 + linux/fsfwconfig/ipc/MissionMessageTypes.h | 22 + linux/fsfwconfig/objects/systemObjectList.h | 64 + linux/fsfwconfig/objects/translateObjects.cpp | 556 +++ linux/fsfwconfig/objects/translateObjects.h | 8 + linux/fsfwconfig/returnvalues/classIds.h | 21 + linux/ipcore/AxiPtmeConfig.cpp | 103 + linux/ipcore/AxiPtmeConfig.h | 122 + linux/ipcore/CMakeLists.txt | 3 + linux/ipcore/PapbVcInterface.cpp | 147 + linux/ipcore/PapbVcInterface.h | 137 + linux/ipcore/PdecConfig.cpp | 217 ++ linux/ipcore/PdecConfig.h | 166 + linux/ipcore/PdecHandler.cpp | 854 +++++ linux/ipcore/PdecHandler.h | 355 ++ linux/ipcore/Ptme.cpp | 56 + linux/ipcore/Ptme.h | 83 + linux/ipcore/PtmeConfig.cpp | 60 + linux/ipcore/PtmeConfig.h | 91 + linux/ipcore/PtmeIF.h | 23 + linux/ipcore/VirtualChannelIF.h | 24 + linux/ipcore/pdec.h | 150 + linux/ipcore/pdecconfigdefs.h | 20 + linux/payload/CMakeLists.txt | 13 + linux/payload/FreshMpsocHandler.cpp | 1288 +++++++ linux/payload/FreshMpsocHandler.h | 212 ++ linux/payload/FreshSupvHandler.cpp | 1607 +++++++++ linux/payload/FreshSupvHandler.h | 188 ++ linux/payload/MpsocCommunication.cpp | 75 + linux/payload/MpsocCommunication.h | 44 + linux/payload/PlocMemoryDumper.cpp | 195 ++ linux/payload/PlocMemoryDumper.h | 117 + linux/payload/PlocMpsocSpecialComHelper.cpp | 517 +++ linux/payload/PlocMpsocSpecialComHelper.h | 199 ++ linux/payload/PlocSupvUartMan.cpp | 1183 +++++++ linux/payload/PlocSupvUartMan.h | 381 +++ linux/payload/ScexDleParser.cpp | 7 + linux/payload/ScexDleParser.h | 13 + linux/payload/ScexHelper.cpp | 85 + linux/payload/ScexHelper.h | 47 + linux/payload/ScexUartReader.cpp | 228 ++ linux/payload/ScexUartReader.h | 61 + linux/payload/SerialCommunicationHelper.cpp | 126 + linux/payload/SerialCommunicationHelper.h | 69 + linux/payload/SerialConfig.h | 70 + linux/payload/plocMemDumpDefs.h | 28 + linux/payload/plocMpsocHelpers.cpp | 118 + linux/payload/plocMpsocHelpers.h | 1220 +++++++ linux/payload/plocSupvDefs.h | 2089 ++++++++++++ linux/power/CMakeLists.txt | 1 + linux/power/CspComIF.cpp | 396 +++ linux/power/CspComIF.h | 93 + linux/scheduling.cpp | 49 + linux/scheduling.h | 13 + linux/tcs/CMakeLists.txt | 1 + linux/tcs/Max31865RtdPolling.cpp | 473 +++ linux/tcs/Max31865RtdPolling.h | 93 + linux/utility/CMakeLists.txt | 1 + linux/utility/utility.cpp | 27 + linux/utility/utility.h | 16 + misc/archive/GPIORPi.cpp | 35 + misc/archive/GPIORPi.h | 26 + misc/archive/GyroL3GD20Handler.cpp | 260 ++ misc/archive/GyroL3GD20Handler.h | 80 + misc/archive/Makefile | 387 +++ misc/archive/Makefile-Hosted | 440 +++ misc/archive/RPiGPIO.cpp | 123 + misc/archive/RPiGPIO.h | 41 + misc/eclipse/.cproject | 1549 +++++++++ misc/eclipse/.project | 27 + .../host/eive-linux-host-debug-cmake.launch | 33 + .../host/eive-linux-host-release-cmake.launch | 33 + .../host/eive-mingw-debug-cmake.launch | 33 + .../host/eive-mingw-release-cmake.launch | 33 + misc/eclipse/host/eive-unittest.launch | 32 + mission/CMakeLists.txt | 17 + mission/SolarArrayDeploymentHandler.cpp | 519 +++ mission/SolarArrayDeploymentHandler.h | 257 ++ mission/acs/CMakeLists.txt | 18 + mission/acs/GyrAdis1650XHandler.cpp | 225 ++ mission/acs/GyrAdis1650XHandler.h | 70 + mission/acs/GyrL3gCustomHandler.cpp | 185 + mission/acs/GyrL3gCustomHandler.h | 85 + mission/acs/ImtqHandler.cpp | 2425 +++++++++++++ mission/acs/ImtqHandler.h | 209 ++ mission/acs/MgmLis3CustomHandler.cpp | 151 + mission/acs/MgmLis3CustomHandler.h | 103 + mission/acs/MgmRm3100CustomHandler.cpp | 140 + mission/acs/MgmRm3100CustomHandler.h | 97 + mission/acs/RwHandler.cpp | 598 ++++ mission/acs/RwHandler.h | 137 + mission/acs/SusHandler.cpp | 163 + mission/acs/SusHandler.h | 84 + mission/acs/acsBoardPolling.h | 93 + mission/acs/archive/GPSHyperionHandler.cpp | 204 ++ mission/acs/archive/GPSHyperionHandler.h | 62 + mission/acs/archive/LegacySusHandler.cpp | 233 ++ mission/acs/archive/LegacySusHandler.h | 92 + mission/acs/defs.cpp | 36 + mission/acs/defs.h | 102 + mission/acs/gyroAdisHelpers.cpp | 65 + mission/acs/gyroAdisHelpers.h | 186 + mission/acs/imtqHelpers.cpp | 70 + mission/acs/imtqHelpers.h | 1239 +++++++ mission/acs/rwHelpers.cpp | 54 + mission/acs/rwHelpers.h | 348 ++ mission/acs/str/ArcsecDatalinkLayer.cpp | 83 + mission/acs/str/ArcsecDatalinkLayer.h | 95 + mission/acs/str/ArcsecJsonParamBase.cpp | 106 + mission/acs/str/ArcsecJsonParamBase.h | 144 + mission/acs/str/CMakeLists.txt | 4 + mission/acs/str/StarTrackerHandler.cpp | 2996 +++++++++++++++++ mission/acs/str/StarTrackerHandler.h | 563 ++++ mission/acs/str/arcsecJsonKeys.h | 188 ++ mission/acs/str/strHelpers.cpp | 7 + mission/acs/str/strHelpers.h | 1759 ++++++++++ mission/acs/str/strJsonCommands.cpp | 960 ++++++ mission/acs/str/strJsonCommands.h | 257 ++ mission/acs/susMax1227Helpers.h | 85 + mission/cfdp/CMakeLists.txt | 1 + mission/cfdp/CfdpFaultHandler.h | 38 + mission/cfdp/CfdpHandler.cpp | 253 ++ mission/cfdp/CfdpHandler.h | 100 + mission/cfdp/CfdpUser.cpp | 51 + mission/cfdp/CfdpUser.h | 37 + mission/cfdp/defs.h | 16 + mission/com/CMakeLists.txt | 11 + mission/com/CcsdsIpCoreHandler.cpp | 391 +++ mission/com/CcsdsIpCoreHandler.h | 214 ++ mission/com/LiveTmTask.cpp | 248 ++ mission/com/LiveTmTask.h | 79 + mission/com/PersistentLogTmStoreTask.cpp | 72 + mission/com/PersistentLogTmStoreTask.h | 43 + mission/com/PersistentSingleTmStoreTask.cpp | 49 + mission/com/PersistentSingleTmStoreTask.h | 30 + mission/com/SyrlinksHandler.cpp | 835 +++++ mission/com/SyrlinksHandler.h | 268 ++ mission/com/TmStoreTaskBase.cpp | 233 ++ mission/com/TmStoreTaskBase.h | 108 + mission/com/VirtualChannel.cpp | 80 + mission/com/VirtualChannel.h | 50 + mission/com/VirtualChannelWithQueue.cpp | 61 + mission/com/VirtualChannelWithQueue.h | 42 + mission/com/defs.h | 33 + mission/com/syrlinksDefs.h | 127 + mission/config/CMakeLists.txt | 1 + mission/config/comCfg.cpp | 28 + mission/config/comCfg.h | 15 + mission/config/configfile.h | 9 + mission/config/torquer.cpp | 27 + mission/config/torquer.h | 25 + mission/controller/AcsController.cpp | 1216 +++++++ mission/controller/AcsController.h | 294 ++ mission/controller/CMakeLists.txt | 7 + mission/controller/PowerController.cpp | 372 ++ mission/controller/PowerController.h | 131 + mission/controller/ThermalController.cpp | 1938 +++++++++++ mission/controller/ThermalController.h | 338 ++ mission/controller/acs/AcsParameters.cpp | 805 +++++ mission/controller/acs/AcsParameters.h | 977 ++++++ mission/controller/acs/ActuatorCmd.cpp | 77 + mission/controller/acs/ActuatorCmd.h | 46 + mission/controller/acs/AttitudeEstimation.cpp | 123 + mission/controller/acs/AttitudeEstimation.h | 31 + mission/controller/acs/CMakeLists.txt | 15 + .../acs/FusedRotationEstimation.cpp | 330 ++ .../controller/acs/FusedRotationEstimation.h | 47 + mission/controller/acs/Guidance.cpp | 403 +++ mission/controller/acs/Guidance.h | 64 + mission/controller/acs/Igrf13Model.cpp | 137 + mission/controller/acs/Igrf13Model.h | 133 + .../acs/MultiplicativeKalmanFilter.cpp | 637 ++++ .../acs/MultiplicativeKalmanFilter.h | 146 + mission/controller/acs/Navigation.cpp | 73 + mission/controller/acs/Navigation.h | 35 + mission/controller/acs/SensorProcessing.cpp | 656 ++++ mission/controller/acs/SensorProcessing.h | 87 + mission/controller/acs/SensorValues.cpp | 140 + mission/controller/acs/SensorValues.h | 71 + mission/controller/acs/SusConverter.cpp | 64 + mission/controller/acs/SusConverter.h | 31 + mission/controller/acs/control/CMakeLists.txt | 2 + mission/controller/acs/control/Detumble.cpp | 47 + mission/controller/acs/control/Detumble.h | 26 + mission/controller/acs/control/PtgCtrl.cpp | 231 ++ mission/controller/acs/control/PtgCtrl.h | 62 + mission/controller/acs/control/SafeCtrl.cpp | 198 ++ mission/controller/acs/control/SafeCtrl.h | 64 + .../AcsCtrlDefinitions.h | 349 ++ .../PowerCtrlDefinitions.h | 51 + mission/controller/tcsDefs.h | 352 ++ mission/genericFactory.cpp | 409 +++ mission/genericFactory.h | 64 + mission/memory/CMakeLists.txt | 1 + mission/memory/NvmParameterBase.cpp | 60 + mission/memory/NvmParameterBase.h | 85 + mission/memory/SdCardMountedIF.h | 21 + mission/memory/definitions.h | 19 + mission/payload/CMakeLists.txt | 3 + mission/payload/PayloadPcduHandler.cpp | 904 +++++ mission/payload/PayloadPcduHandler.h | 193 ++ mission/payload/RadiationSensorHandler.cpp | 260 ++ mission/payload/RadiationSensorHandler.h | 60 + mission/payload/ScexDeviceHandler.cpp | 395 +++ mission/payload/ScexDeviceHandler.h | 76 + mission/payload/defs.cpp | 32 + mission/payload/defs.h | 24 + mission/payload/payloadPcduDefinitions.h | 244 ++ mission/payload/plocSpBase.h | 117 + mission/payload/radSensorDefinitions.h | 83 + mission/payload/scexHelpers.cpp | 34 + mission/payload/scexHelpers.h | 53 + mission/persistentTmStoreDefs.h | 52 + mission/pollingSeqTables.cpp | 645 ++++ mission/pollingSeqTables.h | 76 + mission/power/AcuHandler.cpp | 190 ++ mission/power/AcuHandler.h | 56 + mission/power/BpxBatteryHandler.cpp | 298 ++ mission/power/BpxBatteryHandler.h | 67 + mission/power/CMakeLists.txt | 10 + mission/power/CspCookie.cpp | 29 + mission/power/CspCookie.h | 38 + mission/power/GomSpacePackets.h | 384 +++ mission/power/GomspaceDeviceHandler.cpp | 652 ++++ mission/power/GomspaceDeviceHandler.h | 188 ++ mission/power/P60DockHandler.cpp | 273 ++ mission/power/P60DockHandler.h | 74 + mission/power/PcduHandler.cpp | 489 +++ mission/power/PcduHandler.h | 144 + mission/power/Pdu1Handler.cpp | 182 + mission/power/Pdu1Handler.h | 59 + mission/power/Pdu2Handler.cpp | 179 + mission/power/Pdu2Handler.h | 58 + mission/power/bpxBattDefs.h | 250 ++ mission/power/defs.h | 79 + mission/power/gsDefs.h | 755 +++++ mission/power/gsLibDefs.h | 33 + mission/scheduling.cpp | 50 + mission/scheduling.h | 11 + mission/sysDefs.h | 341 ++ mission/system/CMakeLists.txt | 11 + mission/system/DualLanePowerStateMachine.cpp | 111 + mission/system/DualLanePowerStateMachine.h | 25 + mission/system/EiveSystem.cpp | 498 +++ mission/system/EiveSystem.h | 97 + mission/system/SharedPowerAssemblyBase.cpp | 91 + mission/system/SharedPowerAssemblyBase.h | 27 + mission/system/acs/AcsBoardAssembly.cpp | 407 +++ mission/system/acs/AcsBoardAssembly.h | 143 + mission/system/acs/AcsBoardFdir.cpp | 6 + mission/system/acs/AcsBoardFdir.h | 11 + mission/system/acs/AcsSubsystem.cpp | 17 + mission/system/acs/AcsSubsystem.h | 14 + mission/system/acs/CMakeLists.txt | 13 + mission/system/acs/DualLaneAssemblyBase.cpp | 286 ++ mission/system/acs/DualLaneAssemblyBase.h | 111 + mission/system/acs/ImtqAssembly.cpp | 57 + mission/system/acs/ImtqAssembly.h | 18 + mission/system/acs/RwAssembly.cpp | 98 + mission/system/acs/RwAssembly.h | 41 + mission/system/acs/StrAssembly.cpp | 44 + mission/system/acs/StrAssembly.h | 18 + mission/system/acs/StrFdir.cpp | 26 + mission/system/acs/StrFdir.h | 13 + mission/system/acs/SusAssembly.cpp | 171 + mission/system/acs/SusAssembly.h | 65 + mission/system/acs/SusFdir.cpp | 7 + mission/system/acs/SusFdir.h | 11 + mission/system/acs/acsModeTree.cpp | 495 +++ mission/system/acs/acsModeTree.h | 10 + mission/system/com/CMakeLists.txt | 3 + mission/system/com/ComSubsystem.cpp | 254 ++ mission/system/com/ComSubsystem.h | 93 + mission/system/com/SyrlinksAssembly.cpp | 64 + mission/system/com/SyrlinksAssembly.h | 20 + mission/system/com/SyrlinksFdir.cpp | 122 + mission/system/com/SyrlinksFdir.h | 15 + mission/system/com/comModeTree.cpp | 307 ++ mission/system/com/comModeTree.h | 24 + mission/system/objects/CMakeLists.txt | 3 + mission/system/objects/CamSwitcher.cpp | 24 + mission/system/objects/CamSwitcher.h | 20 + mission/system/objects/PayloadSubsystem.cpp | 13 + mission/system/objects/PayloadSubsystem.h | 15 + .../system/objects/PowerStateMachineBase.cpp | 35 + .../system/objects/PowerStateMachineBase.h | 31 + mission/system/objects/Stack5VHandler.cpp | 81 + mission/system/objects/Stack5VHandler.h | 39 + mission/system/payload/CMakeLists.txt | 1 + mission/system/payload/payloadModeTree.cpp | 376 +++ mission/system/payload/payloadModeTree.h | 17 + mission/system/power/CMakeLists.txt | 2 + mission/system/power/EpsSubsystem.cpp | 27 + mission/system/power/EpsSubsystem.h | 13 + mission/system/power/GomspacePowerFdir.cpp | 129 + mission/system/power/GomspacePowerFdir.h | 15 + mission/system/power/epsModeTree.cpp | 104 + mission/system/power/epsModeTree.h | 15 + mission/system/systemTree.cpp | 408 +++ mission/system/systemTree.h | 14 + mission/system/tcs/CMakeLists.txt | 3 + mission/system/tcs/RtdFdir.cpp | 6 + mission/system/tcs/RtdFdir.h | 11 + mission/system/tcs/TcsBoardAssembly.cpp | 179 + mission/system/tcs/TcsBoardAssembly.h | 59 + mission/system/tcs/TcsSubsystem.cpp | 27 + mission/system/tcs/TcsSubsystem.h | 13 + mission/system/tcs/TmpDevFdir.cpp | 95 + mission/system/tcs/TmpDevFdir.h | 20 + mission/system/tcs/tcsModeTree.cpp | 130 + mission/system/tcs/tcsModeTree.h | 15 + mission/system/treeUtil.cpp | 19 + mission/system/treeUtil.h | 12 + mission/tcs/CMakeLists.txt | 4 + mission/tcs/HeaterHandler.cpp | 460 +++ mission/tcs/HeaterHandler.h | 214 ++ mission/tcs/HeaterHealthDev.cpp | 12 + mission/tcs/HeaterHealthDev.h | 12 + mission/tcs/Max31865Definitions.h | 137 + mission/tcs/Max31865EiveHandler.cpp | 212 ++ mission/tcs/Max31865EiveHandler.h | 46 + mission/tcs/Max31865PT1000Handler.cpp | 547 +++ mission/tcs/Max31865PT1000Handler.h | 124 + mission/tcs/Tmp1075Definitions.h | 42 + mission/tcs/Tmp1075Handler.cpp | 146 + mission/tcs/Tmp1075Handler.h | 61 + mission/tcs/defs.h | 29 + mission/tcs/max1227.cpp | 31 + mission/tcs/max1227.h | 84 + mission/tmtc/CMakeLists.txt | 13 + mission/tmtc/CfdpTmFunnel.cpp | 118 + mission/tmtc/CfdpTmFunnel.h | 31 + mission/tmtc/DirectTmSinkIF.h | 58 + mission/tmtc/PersistentTmStore.cpp | 500 +++ mission/tmtc/PersistentTmStore.h | 154 + mission/tmtc/PersistentTmStoreWithTmQueue.cpp | 33 + mission/tmtc/PersistentTmStoreWithTmQueue.h | 20 + mission/tmtc/PusLiveDemux.cpp | 79 + mission/tmtc/PusLiveDemux.h | 40 + mission/tmtc/PusPacketFilter.cpp | 64 + mission/tmtc/PusPacketFilter.h | 24 + mission/tmtc/PusTmFunnel.cpp | 121 + mission/tmtc/PusTmFunnel.h | 50 + mission/tmtc/PusTmRouteByFilterHelper.cpp | 19 + mission/tmtc/PusTmRouteByFilterHelper.h | 29 + mission/tmtc/Service15TmStorage.cpp | 107 + mission/tmtc/Service15TmStorage.h | 28 + mission/tmtc/TmFunnelBase.cpp | 78 + mission/tmtc/TmFunnelBase.h | 65 + mission/tmtc/TmFunnelHandler.cpp | 17 + mission/tmtc/TmFunnelHandler.h | 35 + mission/tmtc/tmFilters.cpp | 57 + mission/tmtc/tmFilters.h | 15 + mission/utility/CMakeLists.txt | 4 + mission/utility/DummySdCardManager.cpp | 13 + mission/utility/DummySdCardManager.h | 18 + mission/utility/Filenaming.cpp | 17 + mission/utility/Filenaming.h | 28 + mission/utility/GlobalConfigFileDefinitions.h | 16 + mission/utility/GlobalConfigHandler.cpp | 269 ++ mission/utility/GlobalConfigHandler.h | 79 + mission/utility/InitMission.h | 19 + mission/utility/ProgressPrinter.cpp | 25 + mission/utility/ProgressPrinter.h | 42 + mission/utility/Timestamp.cpp | 22 + mission/utility/Timestamp.h | 31 + mission/utility/compileTime.h | 90 + mission/utility/trace.cpp | 10 + mission/utility/trace.h | 14 + q7s-env-em.sh | 23 + q7s-env.sh | 23 + release-checklist.md | 16 + scripts/auto-formatter.sh | 44 + scripts/create-sw-update.sh | 34 + scripts/create-version-file.sh | 7 + scripts/egse-port.sh | 6 + scripts/install-obsw-yocto.sh | 100 + scripts/q7s-cp.py | 117 + scripts/q7s-em-udp-forwarding.sh | 7 + scripts/q7s-fm-udp-forwarding.sh | 7 + scripts/q7s-port-em.sh | 11 + scripts/q7s-port-local.sh | 8 + scripts/q7s-port.sh | 19 + scripts/rpi-port.sh | 10 + scripts/win-q7s-env-em.sh | 59 + scripts/win-q7s-env.sh | 59 + test/CMakeLists.txt | 4 + test/DummyParameter.h | 24 + test/TestTask.cpp | 119 + test/TestTask.h | 50 + test/gpio/CMakeLists.txt | 1 + test/gpio/DummyGpioIF.cpp | 16 + test/gpio/DummyGpioIF.h | 16 + test/gpio/GpioDummy.h | 27 + test/testtasks/CMakeLists.txt | 1 + test/testtasks/PusTcInjector.cpp | 65 + test/testtasks/PusTcInjector.h | 73 + thirdparty/CMakeLists.txt | 11 + thirdparty/gomspace-sw | 1 + thirdparty/json | 1 + thirdparty/lwgps | 1 + thirdparty/rapidcsv | 1 + thirdparty/sagittactl | 1 + thirdparty/tas/CMakeLists.txt | 9 + thirdparty/tas/crc.c | 195 ++ thirdparty/tas/hdlc.c | 90 + thirdparty/tas/tas/crc.h | 116 + thirdparty/tas/tas/hdlc.h | 52 + thirdparty/tas/tas/uart.h | 124 + thirdparty/tas/uart.c | 603 ++++ tmtc | 1 + unittest/CMakeLists.txt | 11 + unittest/controller/CMakeLists.txt | 5 + unittest/controller/testAcsController.cpp | 41 + unittest/controller/testConfigFileHandler.cpp | 65 + unittest/controller/testThermalController.cpp | 66 + unittest/hdlcEncodingRw.cpp | 38 + unittest/main.cpp | 13 + unittest/meineTestDaten.txt | 3 + unittest/mocks/CMakeLists.txt | 4 + unittest/mocks/EventManagerMock.cpp | 63 + unittest/mocks/EventManagerMock.h | 29 + unittest/mocks/HouseKeepingMock.cpp | 12 + unittest/mocks/HouseKeepingMock.h | 18 + unittest/mpsocTests.cpp | 14 + unittest/printChar.cpp | 11 + unittest/printChar.h | 6 + unittest/rebootLogic/.gitignore | 2 + unittest/rebootLogic/.vscode/settings.json | 51 + unittest/rebootLogic/CMakeLists.txt | 29 + unittest/rebootLogic/README.md | 16 + unittest/rebootLogic/src/CMakeLists.txt | 14 + unittest/rebootLogic/src/CoreController.cpp | 530 +++ unittest/rebootLogic/src/CoreController.h | 79 + unittest/rebootLogic/src/HasActionsIF.h | 9 + unittest/rebootLogic/src/SdCardManager.cpp | 5 + unittest/rebootLogic/src/SdCardManager.h | 23 + unittest/rebootLogic/src/conf.h | 1 + unittest/rebootLogic/src/definitions.h | 6 + unittest/rebootLogic/src/event.cpp | 21 + unittest/rebootLogic/src/event.h | 41 + unittest/rebootLogic/src/fsfw/CMakeLists.txt | 1 + unittest/rebootLogic/src/fsfw/FSFW.h | 9 + .../src/fsfw/serviceinterface/CMakeLists.txt | 4 + .../fsfw/serviceinterface/ServiceInterface.h | 13 + .../ServiceInterfaceBuffer.cpp | 252 ++ .../serviceinterface/ServiceInterfaceBuffer.h | 159 + .../ServiceInterfaceStream.cpp | 21 + .../serviceinterface/ServiceInterfaceStream.h | 67 + .../serviceInterfaceDefintions.h | 18 + unittest/rebootLogic/src/libxiphos.cpp | 25 + unittest/rebootLogic/src/libxiphos.h | 25 + unittest/rebootLogic/src/main.cpp | 413 +++ unittest/rebootLogic/src/print.c | 10 + unittest/testEnvironment.cpp | 59 + unittest/testEnvironment.h | 36 + unittest/testGenericFilesystem.cpp | 43 + watchdog/CMakeLists.txt | 5 + watchdog/Watchdog.cpp | 304 ++ watchdog/Watchdog.h | 56 + watchdog/definitions.h | 35 + watchdog/main.cpp | 34 + watchdog/watchdogConf.h.in | 9 + 823 files changed, 115952 insertions(+), 2 deletions(-) create mode 100644 .clang-format create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .idea/cmake.xml create mode 100644 .run/Q7S FM.run.xml create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 Justfile create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 archive/PlocMpsocHandler.cpp create mode 100644 archive/PlocMpsocHandler.h create mode 100644 archive/PlocMpsocSpecialComHelperLegacy.cpp create mode 100644 archive/PlocMpsocSpecialComHelperLegacy.h create mode 100644 archive/PlocSupervisorHandler.cpp create mode 100644 archive/PlocSupervisorHandler.h create mode 100644 archive/gpio/CMakeLists.txt create mode 100644 archive/gpio/GpioCookie.cpp create mode 100644 archive/gpio/GpioCookie.h create mode 100644 archive/gpio/GpioIF.h create mode 100644 archive/gpio/LinuxLibgpioIF.cpp create mode 100644 archive/gpio/LinuxLibgpioIF.h create mode 100644 archive/gpio/gpioDefinitions.h create mode 100644 archive/tmtc/CCSDSIPCoreBridge.cpp create mode 100644 archive/tmtc/CCSDSIPCoreBridge.h create mode 160000 arduino create mode 100644 automation/Dockerfile create mode 100644 automation/Jenkinsfile create mode 100644 bsp_egse/CMakeLists.txt create mode 100644 bsp_egse/InitMission.cpp create mode 100644 bsp_egse/InitMission.h create mode 100644 bsp_egse/ObjectFactory.cpp create mode 100644 bsp_egse/ObjectFactory.h create mode 100644 bsp_egse/boardconfig/CMakeLists.txt create mode 100644 bsp_egse/boardconfig/busConf.h create mode 100644 bsp_egse/boardconfig/etl_profile.h create mode 100644 bsp_egse/boardconfig/gcov.h create mode 100644 bsp_egse/boardconfig/print.c create mode 100644 bsp_egse/boardconfig/print.h create mode 100644 bsp_egse/boardconfig/rpiConfig.h.in create mode 100644 bsp_egse/main.cpp create mode 100644 bsp_hosted/CMakeLists.txt create mode 100644 bsp_hosted/Dockerfile create mode 100644 bsp_hosted/OBSWConfig.h.in create mode 100644 bsp_hosted/boardconfig/CMakeLists.txt create mode 100644 bsp_hosted/boardconfig/etl_profile.h create mode 100644 bsp_hosted/boardconfig/gcov.h create mode 100644 bsp_hosted/boardconfig/print.c create mode 100644 bsp_hosted/boardconfig/print.h create mode 100644 bsp_hosted/comIF/ArduinoComIF.cpp create mode 100644 bsp_hosted/comIF/ArduinoComIF.h create mode 100644 bsp_hosted/comIF/ArduinoCookie.cpp create mode 100644 bsp_hosted/comIF/ArduinoCookie.h create mode 100644 bsp_hosted/comIF/CMakeLists.txt create mode 100644 bsp_hosted/fsfwconfig/CMakeLists.txt create mode 100644 bsp_hosted/fsfwconfig/FSFWConfig.h.in create mode 100644 bsp_hosted/fsfwconfig/OBSWConfig.h.in create mode 100644 bsp_hosted/fsfwconfig/events/subsystemIdRanges.h create mode 100644 bsp_hosted/fsfwconfig/events/translateEvents.cpp create mode 100644 bsp_hosted/fsfwconfig/events/translateEvents.h create mode 100644 bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.cpp create mode 100644 bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.h create mode 100644 bsp_hosted/fsfwconfig/objects/systemObjectList.h create mode 100644 bsp_hosted/fsfwconfig/objects/translateObjects.cpp create mode 100644 bsp_hosted/fsfwconfig/objects/translateObjects.h create mode 100644 bsp_hosted/fsfwconfig/pollingsequence/CMakeLists.txt create mode 100644 bsp_hosted/fsfwconfig/pollingsequence/DummyPst.cpp create mode 100644 bsp_hosted/fsfwconfig/pollingsequence/DummyPst.h create mode 100644 bsp_hosted/fsfwconfig/returnvalues/classIds.h create mode 100644 bsp_hosted/main.cpp create mode 100644 bsp_hosted/objectFactory.cpp create mode 100644 bsp_hosted/objectFactory.h create mode 100644 bsp_hosted/scheduling.cpp create mode 100644 bsp_hosted/scheduling.h create mode 100644 bsp_linux_board/CMakeLists.txt create mode 100644 bsp_linux_board/Dockerfile create mode 100644 bsp_linux_board/InitMission.cpp create mode 100644 bsp_linux_board/InitMission.h create mode 100644 bsp_linux_board/OBSWConfig.h.in create mode 100644 bsp_linux_board/ObjectFactory.cpp create mode 100644 bsp_linux_board/ObjectFactory.h create mode 100644 bsp_linux_board/boardconfig/CMakeLists.txt create mode 100644 bsp_linux_board/boardconfig/etl_profile.h create mode 100644 bsp_linux_board/boardconfig/gcov.h create mode 100644 bsp_linux_board/boardconfig/print.c create mode 100644 bsp_linux_board/boardconfig/print.h create mode 100644 bsp_linux_board/boardconfig/rpiConfig.h.in create mode 100644 bsp_linux_board/boardtest/CMakeLists.txt create mode 100644 bsp_linux_board/definitions.h create mode 100644 bsp_linux_board/gpioInit.cpp create mode 100644 bsp_linux_board/gpioInit.h create mode 100644 bsp_linux_board/main.cpp create mode 100644 bsp_q7s/CMakeLists.txt create mode 100644 bsp_q7s/OBSWConfig.h.in create mode 100644 bsp_q7s/acs/CMakeLists.txt create mode 100644 bsp_q7s/acs/StrConfigPathGetter.h create mode 100644 bsp_q7s/boardconfig/CMakeLists.txt create mode 100644 bsp_q7s/boardconfig/busConf.h create mode 100644 bsp_q7s/boardconfig/etl_profile.h create mode 100644 bsp_q7s/boardconfig/gcov.h create mode 100644 bsp_q7s/boardconfig/print.c create mode 100644 bsp_q7s/boardconfig/print.h create mode 100644 bsp_q7s/boardconfig/q7sConfig.h.in create mode 100644 bsp_q7s/boardtest/CMakeLists.txt create mode 100644 bsp_q7s/boardtest/FileSystemTest.cpp create mode 100644 bsp_q7s/boardtest/FileSystemTest.h create mode 100644 bsp_q7s/boardtest/Q7STestTask.cpp create mode 100644 bsp_q7s/boardtest/Q7STestTask.h create mode 100644 bsp_q7s/callbacks/CMakeLists.txt create mode 100644 bsp_q7s/callbacks/gnssCallback.cpp create mode 100644 bsp_q7s/callbacks/gnssCallback.h create mode 100644 bsp_q7s/callbacks/pcduSwitchCb.cpp create mode 100644 bsp_q7s/callbacks/pcduSwitchCb.h create mode 100644 bsp_q7s/callbacks/q7sGpioCallbacks.cpp create mode 100644 bsp_q7s/callbacks/q7sGpioCallbacks.h create mode 100644 bsp_q7s/callbacks/rwSpiCallback.cpp create mode 100644 bsp_q7s/callbacks/rwSpiCallback.h create mode 100644 bsp_q7s/core/CMakeLists.txt create mode 100644 bsp_q7s/core/CoreController.cpp create mode 100644 bsp_q7s/core/CoreController.h create mode 100644 bsp_q7s/core/WatchdogHandler.cpp create mode 100644 bsp_q7s/core/WatchdogHandler.h create mode 100644 bsp_q7s/core/XiphosWdtHandler.cpp create mode 100644 bsp_q7s/core/XiphosWdtHandler.h create mode 100644 bsp_q7s/core/defs.h create mode 100644 bsp_q7s/em/CMakeLists.txt create mode 100644 bsp_q7s/em/emObjectFactory.cpp create mode 100644 bsp_q7s/fmObjectFactory.cpp create mode 100644 bsp_q7s/fs/CMakeLists.txt create mode 100644 bsp_q7s/fs/FilesystemHelper.cpp create mode 100644 bsp_q7s/fs/FilesystemHelper.h create mode 100644 bsp_q7s/fs/SdCardManager.cpp create mode 100644 bsp_q7s/fs/SdCardManager.h create mode 100644 bsp_q7s/fs/helpers.cpp create mode 100644 bsp_q7s/fs/helpers.h create mode 100644 bsp_q7s/main.cpp create mode 100644 bsp_q7s/memory/CMakeLists.txt create mode 100644 bsp_q7s/memory/LocalParameterHandler.cpp create mode 100644 bsp_q7s/memory/LocalParameterHandler.h create mode 100644 bsp_q7s/memory/scratchApi.cpp create mode 100644 bsp_q7s/memory/scratchApi.h create mode 100644 bsp_q7s/objectFactory.cpp create mode 100644 bsp_q7s/objectFactory.h create mode 100644 bsp_q7s/obsw.cpp create mode 100644 bsp_q7s/obsw.h create mode 100644 bsp_q7s/scheduling.cpp create mode 100644 bsp_q7s/scheduling.h create mode 100644 bsp_q7s/simple/CMakeLists.txt create mode 100644 bsp_q7s/simple/simple.cpp create mode 100644 bsp_q7s/simple/simple.h create mode 100644 bsp_q7s/spi/Q7sSpiComIF.cpp create mode 100644 bsp_q7s/spi/Q7sSpiComIF.h create mode 100644 bsp_q7s/xadc/CMakeLists.txt create mode 100644 bsp_q7s/xadc/Xadc.cpp create mode 100644 bsp_q7s/xadc/Xadc.h create mode 100644 bsp_te0720_1cfa/CMakeLists.txt create mode 100644 bsp_te0720_1cfa/InitMission.cpp create mode 100644 bsp_te0720_1cfa/InitMission.h create mode 100644 bsp_te0720_1cfa/OBSWConfig.h.in create mode 100644 bsp_te0720_1cfa/ObjectFactory.cpp create mode 100644 bsp_te0720_1cfa/ObjectFactory.h create mode 100644 bsp_te0720_1cfa/boardconfig/CMakeLists.txt create mode 100644 bsp_te0720_1cfa/boardconfig/busConf.h create mode 100644 bsp_te0720_1cfa/boardconfig/etl_profile.h create mode 100644 bsp_te0720_1cfa/boardconfig/gcov.h create mode 100644 bsp_te0720_1cfa/boardconfig/print.c create mode 100644 bsp_te0720_1cfa/boardconfig/print.h create mode 100644 bsp_te0720_1cfa/main.cpp create mode 100755 clone-submodules-no-privlibs.sh create mode 100644 cmake/BBBCrossCompileConfig.cmake create mode 100644 cmake/BuildType.cmake create mode 100644 cmake/EiveHelpers.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake create mode 100644 cmake/GetGitRevisionDescription.cmake.in create mode 100644 cmake/HardwareOsPostConfig.cmake create mode 100644 cmake/PreProjectConfig.cmake create mode 100644 cmake/RPiCrossCompileConfig.cmake create mode 100644 cmake/Zynq7020CrossCompileConfig.cmake create mode 100644 cmake/scripts/beagleboneb/crosscompile/bbb_path_helper.sh create mode 100755 cmake/scripts/beagleboneb/crosscompile/make-debug-cfg.sh create mode 100755 cmake/scripts/beagleboneb/crosscompile/make-release-cfg.sh create mode 100755 cmake/scripts/beagleboneb/make-debug-cfg.sh create mode 100755 cmake/scripts/cmake-build-cfg.py create mode 100644 cmake/scripts/egse/egse_path_helper_win.sh create mode 100644 cmake/scripts/egse/make-debug-cfg.sh create mode 100755 cmake/scripts/host/host-make-debug.sh create mode 100755 cmake/scripts/host/host-make-release.sh create mode 100755 cmake/scripts/host/host-ninja-debug.sh create mode 100755 cmake/scripts/linux/host-make-debug.sh create mode 100755 cmake/scripts/linux/host-make-release.sh create mode 100755 cmake/scripts/linux/host-ninja-debug.sh create mode 100755 cmake/scripts/q7s/q7s-make-debug.sh create mode 100755 cmake/scripts/q7s/q7s-make-release.sh create mode 100755 cmake/scripts/q7s/q7s-make-size.sh create mode 100755 cmake/scripts/q7s/q7s-ninja-debug.sh create mode 100755 cmake/scripts/q7s/q7s-ninja-release.sh create mode 100755 cmake/scripts/rpi/make-debug-cfg.sh create mode 100755 cmake/scripts/rpi/make-release-cfg.sh create mode 100755 cmake/scripts/rpi/ninja-debug-cfg.sh create mode 100644 cmake/scripts/rpi/rpi_path_helper.sh create mode 100644 cmake/scripts/rpi/rpi_path_helper_win.sh create mode 100644 cmake/scripts/te0720-1cfa/make-debug-cfg.sh create mode 100644 cmake/scripts/te0720-1cfa/win-env-te0720-1cfa.sh create mode 100644 common/CMakeLists.txt create mode 100644 common/config/CMakeLists.txt create mode 100644 common/config/ccsdsConfig.h create mode 100644 common/config/commonConfig.cpp create mode 100644 common/config/commonConfig.h.in create mode 100644 common/config/devConf.h create mode 100644 common/config/devices/addresses.h create mode 100644 common/config/devices/gpioIds.h create mode 100644 common/config/devices/powerSwitcherList.h create mode 100644 common/config/eive/definitions.h create mode 100644 common/config/eive/eventSubsystemIds.h create mode 100644 common/config/eive/objects.h create mode 100644 common/config/eive/resultClassIds.h create mode 100644 common/config/lwgps_opts.h create mode 100644 common/config/tmtc/pusIds.h create mode 100644 doc/XSC-1542-6025-i_Q7RevB_User_Manual.pdf create mode 100644 doc/deprecated-Q7S-user-manual14042020.pdf create mode 100644 doc/img/ProcessSettings.png create mode 100644 doc/img/eive-logo.png create mode 100644 doc/img/vivado-edition.png create mode 100644 doc/img/vivado-hl-design.png create mode 100644 doc/img/xilinx-install.PNG create mode 100644 docker-compose.yml create mode 100644 dummies/AcuDummy.cpp create mode 100644 dummies/AcuDummy.h create mode 100644 dummies/BatteryDummy.cpp create mode 100644 dummies/BatteryDummy.h create mode 100644 dummies/BpxDummy.cpp create mode 100644 dummies/BpxDummy.h create mode 100644 dummies/CMakeLists.txt create mode 100644 dummies/ComCookieDummy.cpp create mode 100644 dummies/ComCookieDummy.h create mode 100644 dummies/ComIFDummy.cpp create mode 100644 dummies/ComIFDummy.h create mode 100644 dummies/CoreControllerDummy.cpp create mode 100644 dummies/CoreControllerDummy.h create mode 100644 dummies/ExecutableComIfDummy.cpp create mode 100644 dummies/ExecutableComIfDummy.h create mode 100644 dummies/GpsCtrlDummy.cpp create mode 100644 dummies/GpsCtrlDummy.h create mode 100644 dummies/GpsDhbDummy.cpp create mode 100644 dummies/GpsDhbDummy.h create mode 100644 dummies/GyroAdisDummy.cpp create mode 100644 dummies/GyroAdisDummy.h create mode 100644 dummies/GyroL3GD20Dummy.cpp create mode 100644 dummies/GyroL3GD20Dummy.h create mode 100644 dummies/ImtqDummy.cpp create mode 100644 dummies/ImtqDummy.h create mode 100644 dummies/Max31865Dummy.cpp create mode 100644 dummies/Max31865Dummy.h create mode 100644 dummies/MgmLIS3MDLDummy.cpp create mode 100644 dummies/MgmLIS3MDLDummy.h create mode 100644 dummies/MgmRm3100Dummy.cpp create mode 100644 dummies/MgmRm3100Dummy.h create mode 100644 dummies/P60DockDummy.cpp create mode 100644 dummies/P60DockDummy.h create mode 100644 dummies/PcduHandlerDummy.cpp create mode 100644 dummies/PcduHandlerDummy.h create mode 100644 dummies/PduDummy.cpp create mode 100644 dummies/PduDummy.h create mode 100644 dummies/PlPcduDummy.cpp create mode 100644 dummies/PlPcduDummy.h create mode 100644 dummies/PlocMpsocDummy.cpp create mode 100644 dummies/PlocMpsocDummy.h create mode 100644 dummies/PlocSupervisorDummy.cpp create mode 100644 dummies/PlocSupervisorDummy.h create mode 100644 dummies/RadSensorDummy.cpp create mode 100644 dummies/RadSensorDummy.h create mode 100644 dummies/RwDummy.cpp create mode 100644 dummies/RwDummy.h create mode 100644 dummies/SaDeploymentDummy.cpp create mode 100644 dummies/SaDeploymentDummy.h create mode 100644 dummies/ScexDummy.cpp create mode 100644 dummies/ScexDummy.h create mode 100644 dummies/StarTrackerDummy.cpp create mode 100644 dummies/StarTrackerDummy.h create mode 100644 dummies/SusDummy.cpp create mode 100644 dummies/SusDummy.h create mode 100644 dummies/SyrlinksDummy.cpp create mode 100644 dummies/SyrlinksDummy.h create mode 100644 dummies/TemperatureSensorInserter.cpp create mode 100644 dummies/TemperatureSensorInserter.h create mode 100644 dummies/Tmp1075Dummy.cpp create mode 100644 dummies/Tmp1075Dummy.h create mode 100644 dummies/helperFactory.cpp create mode 100644 dummies/helperFactory.h create mode 100644 generators/.gitignore create mode 100644 generators/.run/all.run.xml create mode 100644 generators/.run/events.run.xml create mode 100644 generators/.run/objects.run.xml create mode 100644 generators/.run/returnvalues.run.xml create mode 100644 generators/bsp_hosted_events.csv create mode 100644 generators/bsp_hosted_objects.csv create mode 100644 generators/bsp_hosted_returnvalues.csv create mode 100644 generators/bsp_hosted_subsystems.csv create mode 100644 generators/bsp_q7s_events.csv create mode 100644 generators/bsp_q7s_objects.csv create mode 100644 generators/bsp_q7s_returnvalues.csv create mode 100644 generators/bsp_q7s_subsystems.csv create mode 100644 generators/definitions.py create mode 100644 generators/deps/.gitignore create mode 100755 generators/deps/install_fsfwgen.sh create mode 100644 generators/events/__init__.py create mode 100644 generators/events/event_parser.py create mode 100644 generators/events/translateEvents.cpp create mode 100644 generators/events/translateEvents.h create mode 100755 generators/gen.py create mode 100644 generators/objects/__init__.py create mode 100644 generators/objects/objects.py create mode 100644 generators/objects/translateObjects.cpp create mode 100644 generators/objects/translateObjects.h create mode 100644 generators/requirements.txt create mode 100644 generators/returnvalues/__init__.py create mode 100644 generators/returnvalues/returnvalues_parser.py create mode 100644 generators/system/__init__.py create mode 100644 generators/system/eive-system.yml create mode 100755 hooks/post-checkout create mode 100644 linux/CMakeLists.txt create mode 100644 linux/ObjectFactory.cpp create mode 100644 linux/ObjectFactory.h create mode 100644 linux/acs/AcsBoardPolling.cpp create mode 100644 linux/acs/AcsBoardPolling.h create mode 100644 linux/acs/CMakeLists.txt create mode 100644 linux/acs/GPSDefinitions.h create mode 100644 linux/acs/GpsHyperionLinuxController.cpp create mode 100644 linux/acs/GpsHyperionLinuxController.h create mode 100644 linux/acs/ImtqPollingTask.cpp create mode 100644 linux/acs/ImtqPollingTask.h create mode 100644 linux/acs/RwPollingTask.cpp create mode 100644 linux/acs/RwPollingTask.h create mode 100644 linux/acs/StrComHandler.cpp create mode 100644 linux/acs/StrComHandler.h create mode 100644 linux/acs/SusPolling.cpp create mode 100644 linux/acs/SusPolling.h create mode 100644 linux/boardtest/CMakeLists.txt create mode 100644 linux/boardtest/I2cTestClass.cpp create mode 100644 linux/boardtest/I2cTestClass.h create mode 100644 linux/boardtest/LibgpiodTest.cpp create mode 100644 linux/boardtest/LibgpiodTest.h create mode 100644 linux/boardtest/SpiTestClass.cpp create mode 100644 linux/boardtest/SpiTestClass.h create mode 100644 linux/boardtest/UartTestClass.cpp create mode 100644 linux/boardtest/UartTestClass.h create mode 100644 linux/callbacks/CMakeLists.txt create mode 100644 linux/callbacks/gpioCallbacks.cpp create mode 100644 linux/callbacks/gpioCallbacks.h create mode 100644 linux/com/CMakeLists.txt create mode 100644 linux/com/SyrlinksComHandler.cpp create mode 100644 linux/com/SyrlinksComHandler.h create mode 100644 linux/fsfwconfig/CMakeLists.txt create mode 100644 linux/fsfwconfig/FSFWConfig.h.in create mode 100644 linux/fsfwconfig/events/subsystemIdRanges.h create mode 100644 linux/fsfwconfig/events/translateEvents.cpp create mode 100644 linux/fsfwconfig/events/translateEvents.h create mode 100644 linux/fsfwconfig/ipc/MissionMessageTypes.cpp create mode 100644 linux/fsfwconfig/ipc/MissionMessageTypes.h create mode 100644 linux/fsfwconfig/objects/systemObjectList.h create mode 100644 linux/fsfwconfig/objects/translateObjects.cpp create mode 100644 linux/fsfwconfig/objects/translateObjects.h create mode 100644 linux/fsfwconfig/returnvalues/classIds.h create mode 100644 linux/ipcore/AxiPtmeConfig.cpp create mode 100644 linux/ipcore/AxiPtmeConfig.h create mode 100644 linux/ipcore/CMakeLists.txt create mode 100644 linux/ipcore/PapbVcInterface.cpp create mode 100644 linux/ipcore/PapbVcInterface.h create mode 100644 linux/ipcore/PdecConfig.cpp create mode 100644 linux/ipcore/PdecConfig.h create mode 100644 linux/ipcore/PdecHandler.cpp create mode 100644 linux/ipcore/PdecHandler.h create mode 100644 linux/ipcore/Ptme.cpp create mode 100644 linux/ipcore/Ptme.h create mode 100644 linux/ipcore/PtmeConfig.cpp create mode 100644 linux/ipcore/PtmeConfig.h create mode 100644 linux/ipcore/PtmeIF.h create mode 100644 linux/ipcore/VirtualChannelIF.h create mode 100644 linux/ipcore/pdec.h create mode 100644 linux/ipcore/pdecconfigdefs.h create mode 100644 linux/payload/CMakeLists.txt create mode 100644 linux/payload/FreshMpsocHandler.cpp create mode 100644 linux/payload/FreshMpsocHandler.h create mode 100644 linux/payload/FreshSupvHandler.cpp create mode 100644 linux/payload/FreshSupvHandler.h create mode 100644 linux/payload/MpsocCommunication.cpp create mode 100644 linux/payload/MpsocCommunication.h create mode 100644 linux/payload/PlocMemoryDumper.cpp create mode 100644 linux/payload/PlocMemoryDumper.h create mode 100644 linux/payload/PlocMpsocSpecialComHelper.cpp create mode 100644 linux/payload/PlocMpsocSpecialComHelper.h create mode 100644 linux/payload/PlocSupvUartMan.cpp create mode 100644 linux/payload/PlocSupvUartMan.h create mode 100644 linux/payload/ScexDleParser.cpp create mode 100644 linux/payload/ScexDleParser.h create mode 100644 linux/payload/ScexHelper.cpp create mode 100644 linux/payload/ScexHelper.h create mode 100644 linux/payload/ScexUartReader.cpp create mode 100644 linux/payload/ScexUartReader.h create mode 100644 linux/payload/SerialCommunicationHelper.cpp create mode 100644 linux/payload/SerialCommunicationHelper.h create mode 100644 linux/payload/SerialConfig.h create mode 100644 linux/payload/plocMemDumpDefs.h create mode 100644 linux/payload/plocMpsocHelpers.cpp create mode 100644 linux/payload/plocMpsocHelpers.h create mode 100644 linux/payload/plocSupvDefs.h create mode 100644 linux/power/CMakeLists.txt create mode 100644 linux/power/CspComIF.cpp create mode 100644 linux/power/CspComIF.h create mode 100644 linux/scheduling.cpp create mode 100644 linux/scheduling.h create mode 100644 linux/tcs/CMakeLists.txt create mode 100644 linux/tcs/Max31865RtdPolling.cpp create mode 100644 linux/tcs/Max31865RtdPolling.h create mode 100644 linux/utility/CMakeLists.txt create mode 100644 linux/utility/utility.cpp create mode 100644 linux/utility/utility.h create mode 100644 misc/archive/GPIORPi.cpp create mode 100644 misc/archive/GPIORPi.h create mode 100644 misc/archive/GyroL3GD20Handler.cpp create mode 100644 misc/archive/GyroL3GD20Handler.h create mode 100644 misc/archive/Makefile create mode 100644 misc/archive/Makefile-Hosted create mode 100644 misc/archive/RPiGPIO.cpp create mode 100644 misc/archive/RPiGPIO.h create mode 100644 misc/eclipse/.cproject create mode 100644 misc/eclipse/.project create mode 100644 misc/eclipse/host/eive-linux-host-debug-cmake.launch create mode 100644 misc/eclipse/host/eive-linux-host-release-cmake.launch create mode 100644 misc/eclipse/host/eive-mingw-debug-cmake.launch create mode 100644 misc/eclipse/host/eive-mingw-release-cmake.launch create mode 100644 misc/eclipse/host/eive-unittest.launch create mode 100644 mission/CMakeLists.txt create mode 100644 mission/SolarArrayDeploymentHandler.cpp create mode 100644 mission/SolarArrayDeploymentHandler.h create mode 100644 mission/acs/CMakeLists.txt create mode 100644 mission/acs/GyrAdis1650XHandler.cpp create mode 100644 mission/acs/GyrAdis1650XHandler.h create mode 100644 mission/acs/GyrL3gCustomHandler.cpp create mode 100644 mission/acs/GyrL3gCustomHandler.h create mode 100644 mission/acs/ImtqHandler.cpp create mode 100644 mission/acs/ImtqHandler.h create mode 100644 mission/acs/MgmLis3CustomHandler.cpp create mode 100644 mission/acs/MgmLis3CustomHandler.h create mode 100644 mission/acs/MgmRm3100CustomHandler.cpp create mode 100644 mission/acs/MgmRm3100CustomHandler.h create mode 100644 mission/acs/RwHandler.cpp create mode 100644 mission/acs/RwHandler.h create mode 100644 mission/acs/SusHandler.cpp create mode 100644 mission/acs/SusHandler.h create mode 100644 mission/acs/acsBoardPolling.h create mode 100644 mission/acs/archive/GPSHyperionHandler.cpp create mode 100644 mission/acs/archive/GPSHyperionHandler.h create mode 100644 mission/acs/archive/LegacySusHandler.cpp create mode 100644 mission/acs/archive/LegacySusHandler.h create mode 100644 mission/acs/defs.cpp create mode 100644 mission/acs/defs.h create mode 100644 mission/acs/gyroAdisHelpers.cpp create mode 100644 mission/acs/gyroAdisHelpers.h create mode 100644 mission/acs/imtqHelpers.cpp create mode 100644 mission/acs/imtqHelpers.h create mode 100644 mission/acs/rwHelpers.cpp create mode 100644 mission/acs/rwHelpers.h create mode 100644 mission/acs/str/ArcsecDatalinkLayer.cpp create mode 100644 mission/acs/str/ArcsecDatalinkLayer.h create mode 100644 mission/acs/str/ArcsecJsonParamBase.cpp create mode 100644 mission/acs/str/ArcsecJsonParamBase.h create mode 100644 mission/acs/str/CMakeLists.txt create mode 100644 mission/acs/str/StarTrackerHandler.cpp create mode 100644 mission/acs/str/StarTrackerHandler.h create mode 100644 mission/acs/str/arcsecJsonKeys.h create mode 100644 mission/acs/str/strHelpers.cpp create mode 100644 mission/acs/str/strHelpers.h create mode 100644 mission/acs/str/strJsonCommands.cpp create mode 100644 mission/acs/str/strJsonCommands.h create mode 100644 mission/acs/susMax1227Helpers.h create mode 100644 mission/cfdp/CMakeLists.txt create mode 100644 mission/cfdp/CfdpFaultHandler.h create mode 100644 mission/cfdp/CfdpHandler.cpp create mode 100644 mission/cfdp/CfdpHandler.h create mode 100644 mission/cfdp/CfdpUser.cpp create mode 100644 mission/cfdp/CfdpUser.h create mode 100644 mission/cfdp/defs.h create mode 100644 mission/com/CMakeLists.txt create mode 100644 mission/com/CcsdsIpCoreHandler.cpp create mode 100644 mission/com/CcsdsIpCoreHandler.h create mode 100644 mission/com/LiveTmTask.cpp create mode 100644 mission/com/LiveTmTask.h create mode 100644 mission/com/PersistentLogTmStoreTask.cpp create mode 100644 mission/com/PersistentLogTmStoreTask.h create mode 100644 mission/com/PersistentSingleTmStoreTask.cpp create mode 100644 mission/com/PersistentSingleTmStoreTask.h create mode 100644 mission/com/SyrlinksHandler.cpp create mode 100644 mission/com/SyrlinksHandler.h create mode 100644 mission/com/TmStoreTaskBase.cpp create mode 100644 mission/com/TmStoreTaskBase.h create mode 100644 mission/com/VirtualChannel.cpp create mode 100644 mission/com/VirtualChannel.h create mode 100644 mission/com/VirtualChannelWithQueue.cpp create mode 100644 mission/com/VirtualChannelWithQueue.h create mode 100644 mission/com/defs.h create mode 100644 mission/com/syrlinksDefs.h create mode 100644 mission/config/CMakeLists.txt create mode 100644 mission/config/comCfg.cpp create mode 100644 mission/config/comCfg.h create mode 100644 mission/config/configfile.h create mode 100644 mission/config/torquer.cpp create mode 100644 mission/config/torquer.h create mode 100644 mission/controller/AcsController.cpp create mode 100644 mission/controller/AcsController.h create mode 100644 mission/controller/CMakeLists.txt create mode 100644 mission/controller/PowerController.cpp create mode 100644 mission/controller/PowerController.h create mode 100644 mission/controller/ThermalController.cpp create mode 100644 mission/controller/ThermalController.h create mode 100644 mission/controller/acs/AcsParameters.cpp create mode 100644 mission/controller/acs/AcsParameters.h create mode 100644 mission/controller/acs/ActuatorCmd.cpp create mode 100644 mission/controller/acs/ActuatorCmd.h create mode 100644 mission/controller/acs/AttitudeEstimation.cpp create mode 100644 mission/controller/acs/AttitudeEstimation.h create mode 100644 mission/controller/acs/CMakeLists.txt create mode 100644 mission/controller/acs/FusedRotationEstimation.cpp create mode 100644 mission/controller/acs/FusedRotationEstimation.h create mode 100644 mission/controller/acs/Guidance.cpp create mode 100644 mission/controller/acs/Guidance.h create mode 100644 mission/controller/acs/Igrf13Model.cpp create mode 100644 mission/controller/acs/Igrf13Model.h create mode 100644 mission/controller/acs/MultiplicativeKalmanFilter.cpp create mode 100644 mission/controller/acs/MultiplicativeKalmanFilter.h create mode 100644 mission/controller/acs/Navigation.cpp create mode 100644 mission/controller/acs/Navigation.h create mode 100644 mission/controller/acs/SensorProcessing.cpp create mode 100644 mission/controller/acs/SensorProcessing.h create mode 100644 mission/controller/acs/SensorValues.cpp create mode 100644 mission/controller/acs/SensorValues.h create mode 100644 mission/controller/acs/SusConverter.cpp create mode 100644 mission/controller/acs/SusConverter.h create mode 100644 mission/controller/acs/control/CMakeLists.txt create mode 100644 mission/controller/acs/control/Detumble.cpp create mode 100644 mission/controller/acs/control/Detumble.h create mode 100644 mission/controller/acs/control/PtgCtrl.cpp create mode 100644 mission/controller/acs/control/PtgCtrl.h create mode 100644 mission/controller/acs/control/SafeCtrl.cpp create mode 100644 mission/controller/acs/control/SafeCtrl.h create mode 100644 mission/controller/controllerdefinitions/AcsCtrlDefinitions.h create mode 100644 mission/controller/controllerdefinitions/PowerCtrlDefinitions.h create mode 100644 mission/controller/tcsDefs.h create mode 100644 mission/genericFactory.cpp create mode 100644 mission/genericFactory.h create mode 100644 mission/memory/CMakeLists.txt create mode 100644 mission/memory/NvmParameterBase.cpp create mode 100644 mission/memory/NvmParameterBase.h create mode 100644 mission/memory/SdCardMountedIF.h create mode 100644 mission/memory/definitions.h create mode 100644 mission/payload/CMakeLists.txt create mode 100644 mission/payload/PayloadPcduHandler.cpp create mode 100644 mission/payload/PayloadPcduHandler.h create mode 100644 mission/payload/RadiationSensorHandler.cpp create mode 100644 mission/payload/RadiationSensorHandler.h create mode 100644 mission/payload/ScexDeviceHandler.cpp create mode 100644 mission/payload/ScexDeviceHandler.h create mode 100644 mission/payload/defs.cpp create mode 100644 mission/payload/defs.h create mode 100644 mission/payload/payloadPcduDefinitions.h create mode 100644 mission/payload/plocSpBase.h create mode 100644 mission/payload/radSensorDefinitions.h create mode 100644 mission/payload/scexHelpers.cpp create mode 100644 mission/payload/scexHelpers.h create mode 100644 mission/persistentTmStoreDefs.h create mode 100644 mission/pollingSeqTables.cpp create mode 100644 mission/pollingSeqTables.h create mode 100644 mission/power/AcuHandler.cpp create mode 100644 mission/power/AcuHandler.h create mode 100644 mission/power/BpxBatteryHandler.cpp create mode 100644 mission/power/BpxBatteryHandler.h create mode 100644 mission/power/CMakeLists.txt create mode 100644 mission/power/CspCookie.cpp create mode 100644 mission/power/CspCookie.h create mode 100644 mission/power/GomSpacePackets.h create mode 100644 mission/power/GomspaceDeviceHandler.cpp create mode 100644 mission/power/GomspaceDeviceHandler.h create mode 100644 mission/power/P60DockHandler.cpp create mode 100644 mission/power/P60DockHandler.h create mode 100644 mission/power/PcduHandler.cpp create mode 100644 mission/power/PcduHandler.h create mode 100644 mission/power/Pdu1Handler.cpp create mode 100644 mission/power/Pdu1Handler.h create mode 100644 mission/power/Pdu2Handler.cpp create mode 100644 mission/power/Pdu2Handler.h create mode 100644 mission/power/bpxBattDefs.h create mode 100644 mission/power/defs.h create mode 100644 mission/power/gsDefs.h create mode 100644 mission/power/gsLibDefs.h create mode 100644 mission/scheduling.cpp create mode 100644 mission/scheduling.h create mode 100644 mission/sysDefs.h create mode 100644 mission/system/CMakeLists.txt create mode 100644 mission/system/DualLanePowerStateMachine.cpp create mode 100644 mission/system/DualLanePowerStateMachine.h create mode 100644 mission/system/EiveSystem.cpp create mode 100644 mission/system/EiveSystem.h create mode 100644 mission/system/SharedPowerAssemblyBase.cpp create mode 100644 mission/system/SharedPowerAssemblyBase.h create mode 100644 mission/system/acs/AcsBoardAssembly.cpp create mode 100644 mission/system/acs/AcsBoardAssembly.h create mode 100644 mission/system/acs/AcsBoardFdir.cpp create mode 100644 mission/system/acs/AcsBoardFdir.h create mode 100644 mission/system/acs/AcsSubsystem.cpp create mode 100644 mission/system/acs/AcsSubsystem.h create mode 100644 mission/system/acs/CMakeLists.txt create mode 100644 mission/system/acs/DualLaneAssemblyBase.cpp create mode 100644 mission/system/acs/DualLaneAssemblyBase.h create mode 100644 mission/system/acs/ImtqAssembly.cpp create mode 100644 mission/system/acs/ImtqAssembly.h create mode 100644 mission/system/acs/RwAssembly.cpp create mode 100644 mission/system/acs/RwAssembly.h create mode 100644 mission/system/acs/StrAssembly.cpp create mode 100644 mission/system/acs/StrAssembly.h create mode 100644 mission/system/acs/StrFdir.cpp create mode 100644 mission/system/acs/StrFdir.h create mode 100644 mission/system/acs/SusAssembly.cpp create mode 100644 mission/system/acs/SusAssembly.h create mode 100644 mission/system/acs/SusFdir.cpp create mode 100644 mission/system/acs/SusFdir.h create mode 100644 mission/system/acs/acsModeTree.cpp create mode 100644 mission/system/acs/acsModeTree.h create mode 100644 mission/system/com/CMakeLists.txt create mode 100644 mission/system/com/ComSubsystem.cpp create mode 100644 mission/system/com/ComSubsystem.h create mode 100644 mission/system/com/SyrlinksAssembly.cpp create mode 100644 mission/system/com/SyrlinksAssembly.h create mode 100644 mission/system/com/SyrlinksFdir.cpp create mode 100644 mission/system/com/SyrlinksFdir.h create mode 100644 mission/system/com/comModeTree.cpp create mode 100644 mission/system/com/comModeTree.h create mode 100644 mission/system/objects/CMakeLists.txt create mode 100644 mission/system/objects/CamSwitcher.cpp create mode 100644 mission/system/objects/CamSwitcher.h create mode 100644 mission/system/objects/PayloadSubsystem.cpp create mode 100644 mission/system/objects/PayloadSubsystem.h create mode 100644 mission/system/objects/PowerStateMachineBase.cpp create mode 100644 mission/system/objects/PowerStateMachineBase.h create mode 100644 mission/system/objects/Stack5VHandler.cpp create mode 100644 mission/system/objects/Stack5VHandler.h create mode 100644 mission/system/payload/CMakeLists.txt create mode 100644 mission/system/payload/payloadModeTree.cpp create mode 100644 mission/system/payload/payloadModeTree.h create mode 100644 mission/system/power/CMakeLists.txt create mode 100644 mission/system/power/EpsSubsystem.cpp create mode 100644 mission/system/power/EpsSubsystem.h create mode 100644 mission/system/power/GomspacePowerFdir.cpp create mode 100644 mission/system/power/GomspacePowerFdir.h create mode 100644 mission/system/power/epsModeTree.cpp create mode 100644 mission/system/power/epsModeTree.h create mode 100644 mission/system/systemTree.cpp create mode 100644 mission/system/systemTree.h create mode 100644 mission/system/tcs/CMakeLists.txt create mode 100644 mission/system/tcs/RtdFdir.cpp create mode 100644 mission/system/tcs/RtdFdir.h create mode 100644 mission/system/tcs/TcsBoardAssembly.cpp create mode 100644 mission/system/tcs/TcsBoardAssembly.h create mode 100644 mission/system/tcs/TcsSubsystem.cpp create mode 100644 mission/system/tcs/TcsSubsystem.h create mode 100644 mission/system/tcs/TmpDevFdir.cpp create mode 100644 mission/system/tcs/TmpDevFdir.h create mode 100644 mission/system/tcs/tcsModeTree.cpp create mode 100644 mission/system/tcs/tcsModeTree.h create mode 100644 mission/system/treeUtil.cpp create mode 100644 mission/system/treeUtil.h create mode 100644 mission/tcs/CMakeLists.txt create mode 100644 mission/tcs/HeaterHandler.cpp create mode 100644 mission/tcs/HeaterHandler.h create mode 100644 mission/tcs/HeaterHealthDev.cpp create mode 100644 mission/tcs/HeaterHealthDev.h create mode 100644 mission/tcs/Max31865Definitions.h create mode 100644 mission/tcs/Max31865EiveHandler.cpp create mode 100644 mission/tcs/Max31865EiveHandler.h create mode 100644 mission/tcs/Max31865PT1000Handler.cpp create mode 100644 mission/tcs/Max31865PT1000Handler.h create mode 100644 mission/tcs/Tmp1075Definitions.h create mode 100644 mission/tcs/Tmp1075Handler.cpp create mode 100644 mission/tcs/Tmp1075Handler.h create mode 100644 mission/tcs/defs.h create mode 100644 mission/tcs/max1227.cpp create mode 100644 mission/tcs/max1227.h create mode 100644 mission/tmtc/CMakeLists.txt create mode 100644 mission/tmtc/CfdpTmFunnel.cpp create mode 100644 mission/tmtc/CfdpTmFunnel.h create mode 100644 mission/tmtc/DirectTmSinkIF.h create mode 100644 mission/tmtc/PersistentTmStore.cpp create mode 100644 mission/tmtc/PersistentTmStore.h create mode 100644 mission/tmtc/PersistentTmStoreWithTmQueue.cpp create mode 100644 mission/tmtc/PersistentTmStoreWithTmQueue.h create mode 100644 mission/tmtc/PusLiveDemux.cpp create mode 100644 mission/tmtc/PusLiveDemux.h create mode 100644 mission/tmtc/PusPacketFilter.cpp create mode 100644 mission/tmtc/PusPacketFilter.h create mode 100644 mission/tmtc/PusTmFunnel.cpp create mode 100644 mission/tmtc/PusTmFunnel.h create mode 100644 mission/tmtc/PusTmRouteByFilterHelper.cpp create mode 100644 mission/tmtc/PusTmRouteByFilterHelper.h create mode 100644 mission/tmtc/Service15TmStorage.cpp create mode 100644 mission/tmtc/Service15TmStorage.h create mode 100644 mission/tmtc/TmFunnelBase.cpp create mode 100644 mission/tmtc/TmFunnelBase.h create mode 100644 mission/tmtc/TmFunnelHandler.cpp create mode 100644 mission/tmtc/TmFunnelHandler.h create mode 100644 mission/tmtc/tmFilters.cpp create mode 100644 mission/tmtc/tmFilters.h create mode 100644 mission/utility/CMakeLists.txt create mode 100644 mission/utility/DummySdCardManager.cpp create mode 100644 mission/utility/DummySdCardManager.h create mode 100644 mission/utility/Filenaming.cpp create mode 100644 mission/utility/Filenaming.h create mode 100644 mission/utility/GlobalConfigFileDefinitions.h create mode 100644 mission/utility/GlobalConfigHandler.cpp create mode 100644 mission/utility/GlobalConfigHandler.h create mode 100644 mission/utility/InitMission.h create mode 100644 mission/utility/ProgressPrinter.cpp create mode 100644 mission/utility/ProgressPrinter.h create mode 100644 mission/utility/Timestamp.cpp create mode 100644 mission/utility/Timestamp.h create mode 100644 mission/utility/compileTime.h create mode 100644 mission/utility/trace.cpp create mode 100644 mission/utility/trace.h create mode 100755 q7s-env-em.sh create mode 100755 q7s-env.sh create mode 100644 release-checklist.md create mode 100755 scripts/auto-formatter.sh create mode 100755 scripts/create-sw-update.sh create mode 100755 scripts/create-version-file.sh create mode 100755 scripts/egse-port.sh create mode 100755 scripts/install-obsw-yocto.sh create mode 100755 scripts/q7s-cp.py create mode 100755 scripts/q7s-em-udp-forwarding.sh create mode 100755 scripts/q7s-fm-udp-forwarding.sh create mode 100755 scripts/q7s-port-em.sh create mode 100755 scripts/q7s-port-local.sh create mode 100755 scripts/q7s-port.sh create mode 100755 scripts/rpi-port.sh create mode 100644 scripts/win-q7s-env-em.sh create mode 100644 scripts/win-q7s-env.sh create mode 100644 test/CMakeLists.txt create mode 100644 test/DummyParameter.h create mode 100644 test/TestTask.cpp create mode 100644 test/TestTask.h create mode 100644 test/gpio/CMakeLists.txt create mode 100644 test/gpio/DummyGpioIF.cpp create mode 100644 test/gpio/DummyGpioIF.h create mode 100644 test/gpio/GpioDummy.h create mode 100644 test/testtasks/CMakeLists.txt create mode 100644 test/testtasks/PusTcInjector.cpp create mode 100644 test/testtasks/PusTcInjector.h create mode 100644 thirdparty/CMakeLists.txt create mode 160000 thirdparty/gomspace-sw create mode 160000 thirdparty/json create mode 160000 thirdparty/lwgps create mode 160000 thirdparty/rapidcsv create mode 160000 thirdparty/sagittactl create mode 100644 thirdparty/tas/CMakeLists.txt create mode 100644 thirdparty/tas/crc.c create mode 100644 thirdparty/tas/hdlc.c create mode 100644 thirdparty/tas/tas/crc.h create mode 100644 thirdparty/tas/tas/hdlc.h create mode 100644 thirdparty/tas/tas/uart.h create mode 100644 thirdparty/tas/uart.c create mode 160000 tmtc create mode 100644 unittest/CMakeLists.txt create mode 100644 unittest/controller/CMakeLists.txt create mode 100644 unittest/controller/testAcsController.cpp create mode 100644 unittest/controller/testConfigFileHandler.cpp create mode 100644 unittest/controller/testThermalController.cpp create mode 100644 unittest/hdlcEncodingRw.cpp create mode 100644 unittest/main.cpp create mode 100644 unittest/meineTestDaten.txt create mode 100644 unittest/mocks/CMakeLists.txt create mode 100644 unittest/mocks/EventManagerMock.cpp create mode 100644 unittest/mocks/EventManagerMock.h create mode 100644 unittest/mocks/HouseKeepingMock.cpp create mode 100644 unittest/mocks/HouseKeepingMock.h create mode 100644 unittest/mpsocTests.cpp create mode 100644 unittest/printChar.cpp create mode 100644 unittest/printChar.h create mode 100644 unittest/rebootLogic/.gitignore create mode 100644 unittest/rebootLogic/.vscode/settings.json create mode 100644 unittest/rebootLogic/CMakeLists.txt create mode 100644 unittest/rebootLogic/README.md create mode 100644 unittest/rebootLogic/src/CMakeLists.txt create mode 100644 unittest/rebootLogic/src/CoreController.cpp create mode 100644 unittest/rebootLogic/src/CoreController.h create mode 100644 unittest/rebootLogic/src/HasActionsIF.h create mode 100644 unittest/rebootLogic/src/SdCardManager.cpp create mode 100644 unittest/rebootLogic/src/SdCardManager.h create mode 100644 unittest/rebootLogic/src/conf.h create mode 100644 unittest/rebootLogic/src/definitions.h create mode 100644 unittest/rebootLogic/src/event.cpp create mode 100644 unittest/rebootLogic/src/event.h create mode 100644 unittest/rebootLogic/src/fsfw/CMakeLists.txt create mode 100644 unittest/rebootLogic/src/fsfw/FSFW.h create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/CMakeLists.txt create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/ServiceInterface.h create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/ServiceInterfaceBuffer.cpp create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/ServiceInterfaceBuffer.h create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/ServiceInterfaceStream.cpp create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/ServiceInterfaceStream.h create mode 100644 unittest/rebootLogic/src/fsfw/serviceinterface/serviceInterfaceDefintions.h create mode 100644 unittest/rebootLogic/src/libxiphos.cpp create mode 100644 unittest/rebootLogic/src/libxiphos.h create mode 100644 unittest/rebootLogic/src/main.cpp create mode 100644 unittest/rebootLogic/src/print.c create mode 100644 unittest/testEnvironment.cpp create mode 100644 unittest/testEnvironment.h create mode 100644 unittest/testGenericFilesystem.cpp create mode 100644 watchdog/CMakeLists.txt create mode 100644 watchdog/Watchdog.cpp create mode 100644 watchdog/Watchdog.h create mode 100644 watchdog/definitions.h create mode 100644 watchdog/main.cpp create mode 100644 watchdog/watchdogConf.h.in diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ada61da --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: Google +IndentWidth: 2 +--- +Language: Cpp +ColumnLimit: 100 +ReflowComments: true +--- diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6dfa6b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +/build* +generators +misc +tmtc +doc + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb6a49f --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +/build* +/cmake-build* + +# Eclipse +.settings +.metadata +.project +.cproject +!misc/eclipse/**/.cproject +!misc/eclipse/**/.project + +#vscode +/.vscode + +# IntelliJ +/.idea/* + +# Python +__pycache__ + +# CLion +!/.idea/cmake.xml + +generators/*.db + +# Clangd LSP +/compile_commands.json +/.cache diff --git a/.gitmodules b/.gitmodules index 4b96622..e1ea03a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,24 @@ +[submodule "arduino"] + path = arduino + url = https://egit.irs.uni-stuttgart.de/eive/eive_arduino_interface.git [submodule "fsfw"] path = fsfw - url = https://egit.irs.uni-stuttgart.de/fsfw/fsfw.git + url = https://egit.irs.uni-stuttgart.de/eive/fsfw.git +[submodule "tmtc"] + path = tmtc + url = https://egit.irs.uni-stuttgart.de/eive/eive-tmtc.git +[submodule "thirdparty/lwgps"] + path = thirdparty/lwgps + url = https://github.com/rmspacefish/lwgps.git +[submodule "thirdparty/json"] + path = thirdparty/json + url = https://github.com/nlohmann/json.git +[submodule "thirdparty/rapidcsv"] + path = thirdparty/rapidcsv + url = https://github.com/d99kris/rapidcsv.git +[submodule "thirdparty/gomspace-sw"] + path = thirdparty/gomspace-sw + url = https://egit.irs.uni-stuttgart.de/eive/gomspace-sw.git +[submodule "thirdparty/sagittactl"] + path = thirdparty/sagittactl + url = https://egit.irs.uni-stuttgart.de/eive/sagittactl.git diff --git a/.idea/cmake.xml b/.idea/cmake.xml new file mode 100644 index 0000000..eff02c3 --- /dev/null +++ b/.idea/cmake.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.run/Q7S FM.run.xml b/.run/Q7S FM.run.xml new file mode 100644 index 0000000..ea4ae08 --- /dev/null +++ b/.run/Q7S FM.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7a863b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2362 @@ +Change Log +======= + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/). + +The [milestone](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/milestones) +list yields a list of all related PRs for each release. + +Starting at v2.0.0, this project will adhere to semantic versioning and the the following changes +will consitute of a breaking change warranting a new major release: + +- The TMTC interface changes in any shape of form. +- The behaviour of the OBSW changes in a major shape or form relevant for operations + +# [unreleased] + +# [v8.2.1] 2025-02-07 + +Patch release with EM changes only. + +## Changed + +- BPX battery is not added anymore for EM build, dummy is used by default now. + +## Fixed + +- Small fix for `q7s-cp.py` script: Add `-O` argument. + +# [v8.2.0] 2024-06-26 + +## Changed + +- STR quaternions are now used by the `MEKF` by default +- Changed nominal `SUS Assembly` side to `B Side`. +- Changed source for state machine of detumbling to SUS and MGM only. +- Changed `FusedRotRateData` dataset to always display rotation rate from SUS and MGM. +- Solution from `QUEST` will be set to invalid if sun vector and magnetic field vector are too close + to each other. +- Changed collection intervals of dataset collection + - `GPS Controller`: `GPS Set` to 60s + - `MTQ Handler`: `HK with Torque`, `HK without Torque` to 60s + - `RW Handler`: `Status Set` to 30s + - `STR Handler`: `Solution Set` to 30s + - `ACS Controller`: `MGM Sensor`, `MGM Processed`, `SUS Sensor`, `SUS Processed`, `GYR Sensor`, + `GPS Processed` to 60s + - `ACS Controller` at or below `IDLE`: `CTRL Values`, `ACT Commands`, `Attitude Estimation`, + `Fused Rotation Rate` to 30s + - `ACS Controller` above `IDLE`: `CTRL Values`, `ACT Commands`, `Attitude Estimation`, + `Fused Rotation Rate` to 10s + +## Fixed + +- Added null termination for PLOC MPSoC image taking command which could possibly lead to + default target filenames. + +# [v8.1.1] 2024-06-05 + +## Added + +- PLOC SUPV MPSoC update re-try logic for the `WRITE_MEMORY` command. These packets form > 98% + of all packets required for a software update, but the update mechanism is not tolerant against + occasional glitches on the RS485 communication to the PLOC SUPV. A simple re-try mechanism which + tries to re-attempt packet handling up to three times for those packets is introduced. + +# [v8.1.0] 2024-05-29 + +## Fixed + +- Small fix for transition failure handling of the MPSoC when the `START_MPSOC` action command + to the supervisor fails. +- Fixed inits of arrays within the `MEKF` not being zeros. +- Important bugfix for PLOC SUPV: The SUPV previously was able to steal packets from the special + communication helper, for example during software updates. +- Corrected sigma of STR for `MEKF`. + +## Added + +- Added new command to cancel the PLOC SUPV special communication helper. + +# [v8.0.0] 2024-05-13 + +- `eive-tmtc` v7.0.0 + +## Fixed + +- Fixed calculation for target rotation rate during pointing modes. +- Possible fix for MPSoC file read algorithm. + +## Changed + +- Reworked MPSoC handler to be compatible to new MPSoC software image and use + new device handler base class. This should increase the reliability of the communication + significantly. +- MPSoC device modes IDLE, SNAPSHOT and REPLAY are now modelled using submodes. +- Commanding a submode the device is already in will not result in a completion failure + anymore. + +## Added + +- Added `VERIFY_BOOT` command for MPSoC. +- New command for MPSoC to store camera image in chunks. + +# [v7.8.1] 2024-04-11 + +## Fixed + +- Reverted fix for wrong order in quaternion multiplication for computation of the error quaternion. + +# [v7.8.0] 2024-04-10 + +## Changed + +- Reverted lower OP limit of `PLOC` to -10°C. +- All pointing laws are now allowed to use the `MEKF` per default. +- Changed limits in `PWR Controller`. +- PUS time service: Now dumps the time before and after relative timeshift or setting absolute time +- The `GPS Controller` does not set itself to `OFF` anymore, if it has not detected a valid fix for + some time. Instead it attempts to reset both GNSS devices once. +- The maximum time to reach a fix is shortened from 30min to 15min. +- The time the reset pin of the GNSS devices is pulled is prolonged from 5ms to 10s. +- A `GPS FIX HAS CHANGED` is now only triggered if no fix change has been detected within the past + 2min. This means, this event might be thrown with a 2min delay. It is instantly thrown, if the mode + of the controller is changed. As arguments it now displays the new fix and the numer of fix changes + missed. +- The number of satellites seen and used is reset to 0, in case they are set to invalid. +- Altitude, latitude and longitude messages are not checked anymore, in case the mode message was + already invalid. + +## Added + +- PUS timeservice relative timeshift. + +## Fixed + +- Fixed wrong order in quaternion multiplication for computation of the error quaternion. +- Re-worked some FDIR logic in the FSFW. The former logic prevented events with a severity + higher than INFO if the device was in EXTERNAL CONTROL. The new logic will allow to trigger + events but still inhibit FDIR reactions if the device is in EXTERNAL CONTROL. + +# [v7.7.4] 2024-03-21 + +## Changed + +- Rotational rate limit for the GS target pointing is now seperated from controller limit. It + is also reduced to 0.75°/s now. + +## Fixed + +- Fixed wrong sign in calculation of total current within the `PWR Controller`. + +# [v7.7.3] 2024-03-18 + +- Bumped `eive-fsfw` + +## Added + +- Added parameter to disable STR input for MEKF. + +## Changed + +- If a primary heater is set to `EXTERNAL CONTROL` and `ON`, the `TCS Controller` will no + try to control the temperature of that object. +- Set lower OP limit of `PLOC` to -5°C. + +## Fixed + +- Added prevention of sign jump for target quaternion of GS pointing, which would reduce the + performance of the controller. +- Heaters set to `EXTERNAL CONTROL` no longer can be switched off by the `TCS Controller`, if + they violate the maximum burn duration of the controller. + +# [v7.7.2] 2024-03-06 + +## Fixed + +- Camera and E-band antenna now point towards the target instead of away from the target for the + pointing target mode. + +# [v7.7.1] 2024-03-06 + +- Bumped `eive-tmtc` to v6.1.1 +- Bumped `eive-fsfw` + +## Added + +- The `CoreController` now sets the leap seconds on initalization. They are stored in a persistent + file. If the file does yet not exist, it will be created. The leap seconds can be updated using an + action command. This will also update the file. + +## Fixed + +- Fixed wrong dimension of a matrix within the `MEKF`, which would lead to a seg fault, if the + star tracker was available. +- Fixed case in which control values within the `AcsController` could become NaN. + +# [v7.7.0] 2024-02-29 + +- Bumped `eive-tmtc` to v6.1.0 +- Bumped `eive-fsfw` + +## Fixed + +- PLOC SUPV sets: Added missing `PoolReadGuard` instantiations when reading boot status report + and latchup status report. +- PLOC SUPV latchup report could not be handled previously. +- Bugfix in PLOC SUPV latchup report parsing. +- Bugfix in PLOC MPSoC HK set: Set and variables were not set valid. +- The `PTG_CTRL_NO_ATTITUDE_INFORMATION` will now actually trigger a fallback into safe mode + and is triggered by the `AcsController` now. +- Fixed a corner case, in which an invalid speed command could be sent to the `RwHandler`. +- Fixed calculation of desaturation torque for faulty RWs. +- Fixed bugs within the `MEKF` and simplified the code. + +## Changed + +- `FusedRotationRate` now only uses rotation rate from QUEST and STR in higher modes +- QUEST and STR rates are now allowed per default +- Changed PTG Strat priorities to favor STR before MEKF. +- Increased message queue depth and maximum number of handled messages per cycle for + `PusServiceBase` based classes (especially PUS scheduler). +- `MathOperations` functions were moved to their appropriate classes within the `eive-fsfw` +- Changed pointing strategy for target groundstation mode to prevent blinding of the STR. This + also limits the rotation for the reference target quaternion to prevent spikes in required + rotation rates. +- Updated QUEST and Sun Vector Params to new values. +- Removed the satellites's angular momentum from desaturation calculation. +- Bumped internal `sagittactl` library to v11.11. + +## Added + +- Updated STR handler to unlock and allow using the secondary firmware slot. +- STR handling for new BlobStats TM set. +- Added new action command to update the standard deviations within the `MEKF` from the + `AcsParameters`. + +# [v7.6.1] 2024-02-05 + +## Changed + +- Guidance now uses the coordinate functions from the FSFW. +- Idle should now point the STR away from the earth + +## Fixed + +- Fixed bugs in `Guidance::comparePtg` and corrected overloading +- Detumbling State Machine is now robust to commanded mode changes. + +# [v7.6.0] 2024-01-30 + +- Bumped `eive-tmtc` to v5.13.0 +- Bumped `eive-fsfw` + +## Added + +- Added new parameter for MPSoC which allows to skip SUPV commanding. + +## Changed + +- Increased allowed mode transition time for PLOC SUPV. +- Detumbling can now be triggered from all modes of the `AcsController`. In case the + current mode is a higher pointing mode, the STR will be set to faulty, to trigger a + transition to safe first. Then, in a second step, a transition to detumble is triggered. + +## Fixed + +- If the PCDU handler fails reading data from the IPC store, it will + not try to do a deserialization anymore. +- All action commands sent by the PLOC SUPV to itself will have no sender now. +- RW speed commands get reset to 0 RPM, if the `AcsController` has changed its mode + to Safe +- Antistiction for RWs will set commanded speed to 0 RPM, if a wheel is detected as not + working +- Removed parameter to disable antistiction, as deactivating it would result in the + `AcsController` being allowed sending invalid speed commands to the RW Handler, which + would then trigger FDIR and turning off the functioning device +- `RwHandler` returnvalues would use the `INTERFACE_ID` of the `DeviceHandlerBase` +- The `AcsController` will reset its stored guidance values on mode change and lost + orientation. +- The nullspace controller will only be used if all RWs are available. +- Calculation of required rotation rate in pointing modes has been fixed to actual + calculation of rotation rate from two quaternions. +- Fixed alignment matrix and pseudo inverses of RWs, to match the wrong definition of + positive rotation. + +# [v7.5.5] 2024-01-22 + +## Fixed + +- Calculation of error quaternion was done with inverse of the required target quaternion. + +# [v7.5.4] 2024-01-16 + +## Fixed + +- Pointing strategy now actually uses fused rotation rate source instead of its valid flag. +- All datasets now get updated during pointing mode, even if the strategy is a fault one. + +# [v7.5.3] 2023-12-19 + +## Fixed + +- Set STR quaternions to invalid in device handler if the solution is not trustworthy. + +# [v7.5.2] 2023-12-14 + +## Fixed + +- Fixed faulty scaling within the QUEST algorithm. + +# [v7.5.1] 2023-12-13 + +- `eive-tmtc` v5.12.1 + +## Changed + +- Increased the maximum number of scheduled telecommands from 500 to 4000. Merry Christmas! + +## Fixed + +- Faulty mapping of input values for QUEST algorithm. +- Fixed validity check for QUEST algorithm. + +# [v7.5.0] 2023-12-06 + +- `eive-tmtc` v5.12.0 + +## Changed + +- ACS-Board default side changed to B-Side +- The TLE uploaded now gets stored in a file on the filesystem. It will always be stored on + the current active SD Card. After a reboot, the TLE will be read from the filesystem. + A filesystem change via `prefSD` on bootup, can lead to the TLE not being read, even + though it is there. +- Added action cmd to read the currently stored TLE. +- Both the `AcsController` and the `PwrController` now use the monotonic clock to calculate + the time difference. +- `ACS Controller` now has the function `performAttitudeControl` which is called prior to passing + on to the relevant mode functions. It handles all telemetry relevant functions, which were + always called, regardless of the mode. + +## Added + +- Higher ACS modes can now be entered without a running `MEKF`. Higher modes will collect their + quaternion and rotational rate depending on the available sources. +- `QUEST` attitude estimation was added to the `AcsController`. +- The fused rotational rate can now be estimated from `QUEST` and the `STR`. + +# [v7.4.1] 2023-12-06 + +## Fixed + +- Schedule SCEX again. Scheduling was removed accidentaly when Payload Task was converted to a PST. +- SCEX transition was previously 0 seconds.. which did not lead to bugs? In any case it is 5 + seconds now. + +# [v7.4.0] 2023-11-30 + +- `eive-tmtc` v5.11.0 + +## Changed + +- Rewrote the PLOC Supervisor Handler, which is now based on a new device handler base class. + Added ADC and Logging Counters telemetry set support. + +## Fixed + +- Increase allowed time for PTME writers to finish partial transfers. A duration of 200 ms was + not sufficient for cases where 3 writers write concurrently. +- Fixed state issue for PTME writer object where the writer was not reset properly after a timeout + of a partial transfer. This was a major bug blocking the whole VC if it occured. +- STR config path was previously hardcoded to `/mnt/sd0/startracker/flight-config.json`. + A new abstraction was introduces which now uses the active SD card to build the correct + config path when initializing the star tracker. + +## Added + +- PL PCDU: Add command to enable and disable channel order checks. +- Added new PUS 15 subservice `DELETE_BY_TIME_RANGE` which allows to also specify a deletion + start time when deleting packets from the persistent TM store. +- Introduced a new `RELOAD_JSON_CFG_FILE` command for the STR to reload the JSON configuration + data based on the current output of the config file path getter function. A reboot of the + device is still necessary to load the configuration to the STR. + +# [v7.3.0] 2023-11-07 + +## Changed + +- Changed PDEC addresses depending on which firmware version is used. It is suspected that + the previous addresses were invalid and not properly covered by the Linux memory protection. + The OBSW will use the old addresses for older FW versions. +- Reverted some STR ComIF behaviour back to an older software version. + +## Added + +- Always add PLOC MPSoC and PLOC SUPV components for the EM as well. + +# [v7.2.0] 2023-10-27 + +- `eive-tmtc` v5.10.1 + +## Added + +- STR: Added new TM sets: Blob, Blobs, MatchedCentroids, Contrast, AutoBlob, Centroid, Centroids +- STR: Added new mechanism where the secondary TM which is polled can now be a set instead of + being temperature only. An API is exposed which allows to add a data set to that set of secondary + telemetry, reset it back to temperature only, and read the whole set. This allows more debugging + capability. +- CFDP source handler, which allows file downlink using the standardized + CFDP interface. +- Proper back pressure handling for the CFDP handler, where the `LiveTmTask` is able to throttle + the CFDP handler. +- Added CFDP fault handler events. +- The EIVE system will command the payload OFF explicitely again when receiving the + `power::POWER_LEVEL_CRITICAL` event. + +## Fixed + +- If the PTME is driven in a way where it fills faster than it can be emptied, the interface + can become full during the process of a regular packet write. The interface of the PAPB VC + was adapted to be stateful now. Packet generation is started with a `write` call while + write transfers are advanced and completed with the `advanceWrite` call if they can not be + completed immediately. +- CFDP Space Packets SSC is now generated properly, was always 0 before. +- Host build fixes +- PL Enable set of the power controller is now set to invalid properly if the power controller + is not in normal mode. +- MPSoC debug mode. +- Possible bugfix for PL PCDU parameter JSON handling which might not have been initialized + properly from the JSON file. + +## Changed + +- Swapped RTD 9 (PLOC HPA Sensor) and RTD 11 (PLOC MPA Sensor) chip select definitions. It is + strongly suspected the cables for those devices were swapped during integration. This is probably + the easiest way to fix the issue without the need to tweak ground or other OBSW or controller + code. +- Added a 3 second delay in the EIVE system between commanding all PL components except the SUPV, + and the SUPV itself OFF when the power level becomes low or critical. +- SUS FDIR should now trigger less events. The finish event is now only triggered once the + SUS has been working properly for a minute again. It will then display the number of periods + during which the SUS was not working as well as the maximum amount of invalid messages. +- Updated battery internal resistance to new value + +# [v7.1.0] 2023-10-11 + +- Bumped `eive-tmtc` to v5.8.0. +- Activate Xiphos WDT with a timeout period of 80 seconds using the `libxiphos` API. The WDT + calls are done by the new `XiphosWdtHandler` object. + +# [v7.0.0] 2023-10-11 + +- Bumped `eive-tmtc` to v5.7.1. +- Bumped `eive-fsfw` + +## Added + +- EPS Subsystem has been added to EIVE System Tree +- Power Controller for calculating the State of Charge and FDIR regarding low SoC has been + introduced. + +## Changed + +- Changed internals for MPSoC boot process to make the code more understandable and some + parameters better configurable. This should not affect the behaviour of the OBSW, but might + make it more reliable and fix some corner cases. + +## Fixed + +- Missing `nullptr` checks for PLOC Supervisor handler, which could lead to crashes. +- SCEX bugfix for normal and transition commanding. + +# [v6.6.0] 2023-09-18 + +## Changed + +- Changed the memory initialized for the PDEC Config Memory and the PDEC RAM by using `mmap` + directly and ignoring UIO. This makes the OBSW compatible to a device tree update, where those + memory segments are marked reserved and are thus not properly accessible through the UIO API + anymore. This change should be downwards compatible to older device trees. + +# [v6.5.1] 2023-09-12 + +- Bumped `eive-tmtc` to v5.5.0. + +# [v6.5.0] 2023-09-12 + +## Changed + +- Relaxed SUS FDIR. The devices have shown to be glitchy in orbit, but still seem to deliver + sensible raw values most of the time. Some further testing is necessary, but some changes in the + code should cause the SUS devices to remain healthy for now. +- The primary and the secondary temperature sensors for the PLOC mission boards are exchanged. +- ACS parameters for the SUSMGM (FLP) safe mode have been adjusted. This safe mode is now the + default one. +- MGM3100 Startup Configuration: Ignore bit 1 of the CMM reply, which is sometimes set to + 1 in the reply for some reason. + +# [v6.4.1] 2023-08-21 + +## Fixed + +- `PDEC_CONFIG_CORRUPTED` event now actually contains the readback instead of the expected + config +- Magnetic field vector was not calculated if only MGM4 was available, but still written to + the dataset. This would result in a NaN vector. Allowance for usage of MGM4 is now checked + before entering calculation. + +# [v6.4.0] 2023-08-16 + +- `eive-tmtc`: v5.4.3 + +## Fixed + +- The handling function of the GPS data is only called once per GPS read. This should remove + the fake fix-has-changed events. +- Fix for PLOC SUPV HK set parsing. +- The timestamp for the `POSSIBLE_FILE_CORRUPTION` event will be generated properly now. + +## Changed + +- PDEC FDIR rework: A full PDEC reboot will now only be performed after a regular PDEC reset has + failed 10 times. The mechanism will reset after no PDEC reset has happended for 2 minutes. + The PDEC reset will be performed when counting 4 dirty frame events 10 seconds after the count + was incremented initially. +- GPS Fix has changed event is no longer triggered for the EM +- MGM and SUS rates now will only be calculated, if 2 valid consecutive datapoints are available. + The stored value of the last timestep will now be reset, if no actual value is available. + +## Added + +- The PLOC SUPV HK set is requested and downlinked periodically if the SUPV is on now. +- SGP4 Propagator is now used for propagating the position of EIVE. It will only work once + a TLE has been uploaded with the newly added action command for the ACS Controller. In + return the actual GPS data will be ignored once SPG4 is running. However, by setting the + according parameter, the ACS Controller can be directed to ignore the SGP4 solution. +- Skyview dataset for more GPS TM has been added +- `PDEC_CONFIG_CORRUPTED` event which is triggered when the PDEC configuration does not match the + expected configuration. P1 will contain the readback of the first word and P2 will contain the + readback of the second word. +- The MGM and SUS vectors being too close together does not prevent the usage of the safe + mode controller anymore. +- Parameter to disable usage of MGM4, which is part of the MTQ and therefore cannot be + disabled without disabling the MTQ itself. + +# [v6.3.0] 2023-08-03 + +## Fixed + +- Small SCEX fix: The temperatur check option was not passed + on for commands with a user data size larger than 1. +- SCEX: Properly check whether filesystem is usable for filesystem checks. +- ACS Controller strategy is now actually written to the dataset for detumbling. +- During detumble the fused rotation rate is now calculated. +- Detumbling is now exited when its exit value is undercut and not its entry value. +- Rotation rate of last cycle is now stored in all cases for the fused rotational rate + calculation. +- Fused rotation rate estimation during eclipse can be disabled. This will also prevent + detumbling during eclipse, as no relevant rotational rate is available for now. +- `EiveSystem`: Add a small delay between triggering an event for FDIR reboots and sending the + command to the core controller. +- PL PDU: Fixed bounds checking logic. Bound checks will only be performed for modules which are + enabled. + +## Changed + +- SCEX: Only perform filesystem checks when not in OFF mode. +- The `EiveSystem` now only sends reboot commands targetting the same image. +- Added 200 ms delay between switching HPA/MPA/TX/X8 and DRO GPIO pin OFF. +- PL PCDU ADC set is now automatically enabled for `NORMAL` mode transitions. It is automatically + disabled for `OFF` mode transitions. + +## Added + +- PL PCDU for EM build. +- SCEX: Add warning event if filesystem is unusable. + +# [v6.2.0] 2023-07-26 + +- `eive-tmtc`: v5.3.1 + +## Changed + +- STR missed reply handling is now moved to DHB rather than the COM interface. The COM IF will + still trigger an event if a reply is taking too long, and FDIR should still work via reply + timeouts. +- Re-ordered some functions of the core controller in the initialize function. +- Rad sensor is now only polled every 30 minutes instead of every device cycle to reduce wear of + the RADFET electronics. +- The SD cards will still be switched OFF on a reboot, but this is done in a non-blocking manner + now with a timeout of 10 seconds where the reboot will be performed in any case. +- ACS Controller now includes the safe mode from FLP, which will calculate its rotational rate + from SUS and MGM measurements. To accommodate these changes, low-pass filters for SUS + measurements and rates as well as MGM measurements and rates are included. Usage of the new + controller as well as settings of the low-pass filters can be handled via parameter commands. +- Simplify and fix the chip and copy protection functions in the core controller. This mechanism + now is always performed for the target chip and target copy in the reboot handlers. +- Improvement in FSFW: HK generation is now countdown based. + +## Added + +- 5 ms delay after pulling RADFET enable pin high before starting + the ADC conversion. +- Set STR time in configuration sequence to firmware mode. +- The STR `AutoThreshold` parameters are now set from the configuration JSON file at STR + startup. +- The STR handler can now handle the COM error reply and triggers an low severity event accordingly. +- Add SCEX handler for EM. +- Radiation sensor handler dummy for the EM. +- Added event for SD card information in core controller initialize function. This event will also + be triggered after the SD state machine has run, so the event will generally be triggered twice + at system boot, and once after commanding SD card switches. + +## Fixed + +- General bugs in the SD card state machine. This might fix some other known bugs for certain + combinations of switching ON and OFF SD cards and also makes the whole state machine a lot more + robust against hanging up. +- SUS dummy handler went to `MODE_NORMAL` for ON commands. +- PL PCDU dummy went to `MODE_NORMAL` for ON commands. + +# [v6.1.0] 2023-07-13 + +- `eive-tmtc`: v5.2.0 + +## Changed + +- TCS: Remove OBC IF board thermal module, which is exactly identical to OBC module and therefore + obsolete. +- Swapped PL and PS I2C, BPX battery and MGT are connected to PS I2C now for firmware versions + equal or above v4. However, this software version is compatible to both v3 and v4 of the firmware. +- The firmware version variables are global statics inititalized early during the program + runtime now. This makes it possible to check the firmware version earlier. +- The TCS controller will now always command heaters OFF when being blind for thermal + components (no sensors available), irrespective of current switch state. +- Make OBSW compatible to prospective FW version v5.0.0, where the Q7 I2C devices were + moved to a PL I2C block and the TMP sensor devices were moved to the PS I2C0. +- Made `Xadc` code a little bit more robust against errors. + +## Fixed + +- TMP1075: Set dataset invalid on shutdown explicitely +- Small fixes for TMP1075 FDIR: Use strange and missed reply counters. +- TCS controller: Last heater (S-band heater) was skipped for transition completion + checks. +- TCS controller: A helper member to track the elapsed heater control cycles was not reset + properly, which could lead to switch transitions being completed immediately. This can + lead to weird bugs like heaters being commanded ON twice and can potentially lead to + other bugs. +- TMP1075: Devices did not go to OFF mode when being set faulty. +- Update PL PCDU 1 in TCS mode tree on the EM. +- TMP1075: Possibly ignored health commands. +- Bugfix in FSFW where certain packet types could only be sent with source data fields with a + maximum size of 255 bytes. +- TCS CTRL: Limit number of heater handler messages sent in case there are not sensors available + anymore. +- Fix to allow adding real STR device for EM. + +# Added + +- Two events for heaters being commanded ON and OFF by the TCS controller +- Upper limit for burn time of TCS heaters. Currently set to 1 hour for each heater. + This mechanism will only track the burn time for heaters which were commanded by the + TCS controller. +- TCS controller is now observable by introducing a new HK dataset which exposes some internal + fields related to TCS control. + +# [v6.0.0] 2023-07-02 + +- `q7s-package` version v3.2.0 +- Important bugfixes for PTME. See `q7s-package` CHANGELOG. + +## Changed + +- Added back PTME busy bit polling. This is necessary due to changes to the AXI APB interface + to the PTME core. + +## Fixed + +- For the live channel (VC0), telemetry was still only dumped if the transmitter is active. + Please note that this fix will lead to crashes for FW versions below v3.2. + However, it might not be an issue for the oldest firmware on the satellite (v2.5.1). + +# [v5.2.0] 2023-07-02 + +## Fixed + +- The firmware information event was not triggered even when possible because of an ordering + bug in the initializer function. +- Empty dumps (no TM in time range) will now correctly be completed immediately + +## Changed + +- PTME was always reset on submode changes. The reset will now only be performed if the actual data + rate changes. +- Add back ACS board code for the EM. Now that the radiation sensor was removed, the image switching + issue has disappeared and adding back the ACS board is worth it for the GPS timekeeping. + +# [v5.1.0] 2023-06-28 + +- `eive-tmtc` version v5.1.0 + +## Changed + +- Persistent TM store dumps are now performed in chronological order. +- Increase Syrlinks RX HK rate to 5.0 seconds during a pass. +- Various robustness improvements for the heater handler. The heater handler will now only + process the command queue if it is not busy with switch commanding which reduces the amount + of possible bugs. +- The heater handler is only able to process messages stricly sequentially now but is scheduled + twice in a 0.5 second slot so something like a consecutive heater ON or OFF command can still + be handled relatively quickly. + +## Added + +- Sequence counters for PUS and CFDP packets are now stored persistently across graceful reboots. +- The PUS packet message type counter will now be incremented properly for each PUS service. +- Internal error reporter set is now enabled by default and generated every 120 seconds. + +# [v5.0.0] 2023-06-26 + +v3.3.1 and all following version will now be moved to v5.0.0 with the additional changes listed +here. This was done because the firmware update (v4.0.0) is not working right now and it is not +known when and how it will be fixed. Because of that, all updates to make the SW work with the new +firmware, which are limited to a few files will be moved to a dev branch so regular development +compatible to the old firmware can continue. + +TLDR: This version is compatible to the old firmware and some changes which only work with the new +firmware have been reverted. + +## Changed + +- Added `sync` syscall in graceful shutdown handler +- Graceful shutdown is now performed by the reboot watchdog +- There is now a separate file for the total reboot counter. The reboot watchdog has its own local + counters to determine whether a reboot is necessary. + +# [v4.0.1] 2023-06-24 + +## Fixed + +- `PusLiveDemux` packet demultiplexing bugfix where the demultiplexing did not work when there was + only one destination available. + +# [v4.0.0] 2023-06-22 + +- `eive-tmtc` version v5.0.0 +- `q7s-package` version v3.1.1 + +## Fixed + +- Important bugfixes for PTME. See `q7s-package` CHANGELOG. +- TCS fixes: Set temperature values to invalid value for MAX31865 RTD handler, SUS handler + and STR handler. Also set dataset to invakid for RTD handler. +- Fixed H parameter in SUS converter from 1 mm to 2.5 mm. + +## Changed + +- Removed PTME busy/ready signals. Those were not used anyway because register reads are used now. +- APB bus access busy checking is not done anymore as this is performed by the bus itself now. +- Core controller will now announce version and image information event in addition to reboot + event in the `inititalize` function. +- Core controller will now try to request and announce the firmware version in addition to the + OBSW version as well. +- Added core controller action to read reboot mechansm information +- GNSS reset pin will now only be asserted for 5 ms instead of 100 ms. + +## Added + +- Added PL I2C reset pin. It is not used/connected for now but could be used for FDIR procedures to + restore the PL I2C. +- Core controller now announces firmware version as well when requesting a version info event + +# [v3.3.1] 2023-06-22 + +## Fixed + +- `PusLiveDemux` packet demultiplexing bugfix where the demultiplexing did not work when there was + only one destination available. + +## Fixed + +- Fixed H parameter in SUS converter from 1 mm to 2.5 mm. + +# [v3.3.0] 2023-06-21 + +Like v3.2.0 but without the custom FM changes related to VC0. + +# [v3.2.0] 2023-06-21 + +## Fixed + +- Fix sun vector calculation +- SUS total vector was not reset to being a zero vector during eclipse due to a wrong memcpy + length. + +## Changed + +- Reverted all changes related to VC0 handling to avoid FM bug possibly related to FPGA bug. + +# [v3.1.1] 2023-06-14 + +## Fixed + +- TMP1075 bugfix where negative temperatures could not be measured because of a two's-complement + conversion bug. + +# [v3.1.0] 2023-06-14 + +- `eive-tmtc` version v4.1.0 + +## Fixed + +- TCS heater switch enumeration naming was old/wrong and was not updated in sync with the object ID + update. This lead to the TCS controller commanding the wrong heaters. + +## Changed + +- Increase number of allowed parallel HK commands to 16 + +## Added + +- Added `CONFIG_SET`, `MAN_HEATER_ON` and `MAN_HEATER_OFF` support for the BPX battery handler + +# [v3.0.0] 2023-06-11 + +- `eive-tmtc` version v4.0.0 + +## Changed + +- Adapt EM configuration to include all GomSpace PCDU devices except the ACU. For the ACU + (which broke), a dummy will still be used. +- Event Manager queue depth is configurable now. +- Do not construct and schedule broken TMP1075 device anymore. +- Do not track payload modes in system mode tables. +- ACS modes derived from system modes. +- The CMake build generator will now search for the cross-compiler binaries in the environmental + variable named `CROSS_COMPILE_BIN_PATH` first when setting up the build system. This prevents + CMake from selecting wrong cross-compilers if multiple cross-compilers with the same name are used + on the same system. +- Add ACS board for EM by default now. +- Add support for MPSoC HK packet. +- Add support for MPSoC Flash Directory Content Report. +- Dynamically enable and disable HK packets for MPSoC on `ON` and `OFF` commands. +- Add support for MPSoC Flash Directory Content Report. +- Larger allowed path and file sizes for STR and PLOC MPSoC modules. +- More robust MPSoC flash read and write command data handling. +- Increase frequency of payload handlers from 0.8 seconds to 0.5 seconds. +- Disable missed deadlines per default. Not useful in orbit, and triggers all the time on the EM + build after a number of subsequent runs, without any apparent reason (deadlines are not actually + missed, thread usage displayed is nominal) +- TM store dumpes will not be cancelled anymore if the transmitter is off. The dump can be cancelled + with an OFF command, and the PTME is perfectly capable of dumping without the transmitter being + on. +- Transmitter state is not taken into account anymore for writing into the PTME. The PTME should + be perfectly capable of generating a valid CADU, even when the transmitter is not ON for any + reason. +- OFF mode is ignores in TM store for determining whether a store will be written. The modes will + only be used to cancel a transfer. +- Handling of multiple RWs in the ACS Controller is improved and can be changed by parameter + commands. +- The Directory Listing direct dumper now has a state machine to stagger the directory listing dump. + This is required because very large dumps will overload the queue capacities in the framework. +- The PUS Service 8 now has larger queue sizes to handle more action replies. The PUS Service 1 + also has a larger queue size to handle a lot of step replies now. +- Moved PDU `vcc` and `vbat` variable from auxiliary dataset to core dataset. +- Tweak TCP/IP configuration: Only the TCP server will be included on the EM. For the FM, no + TCP/IP servers will be included by default. + +## Added + +- Add the remaining system modes. +- PLOC MPSoC flash read command working. +- BPX battery handler is added for EM by default. +- ACU dummy HK sets +- IMTQ HK sets +- IMTQ dummy now handles power switch +- Added some new ACS parameters +- Enabled decimation filter for the ADIS GYRs +- Enabled second low-pass filter for L3GD20H GYRs + +## Fixed + +- CFDP low level protocol bugfix. Requires `fsfw` update and `tmtc` update. +- Compile fix if SCEX is compiled for the EM. +- Set up Rad Sensor chip select even for EM to avoid SPI bus issues. +- Correct ADIS Gyroscope type configuration for the EM, where the 16507 type is used instead of the + 16505 type. +- Host build is working again. Added reduced live TM helper which schedules the PUS and CFDP + funnel. +- PLOC Supervisor handler now has a power switcher assigned to make PLOC power switching work + without an additional PCDU power switch command. +- The PLOC Supervisor handler now waits for the replies to the `SET_TIME` command to verify working + communication. +- The PLOC MPSoC now waits 10 cycles before going to on. These wait cycles are necessary because + the MPSoC is not ready to process commands without this additional boot time. +- Fixed correction for `GPS Altitude` in case the sensor data is out of the expected bonds. +- PLOC MPSoC special communication is now scheduled, which allows flash read and flash write + commands to work. +- Fixed the MPSoC flash write command. +- Added missing ACS parameter. +- HK TM store: The HK store dump success event was triggered for cancelled HK dumps. +- When a PUS parsing error occured while parsing a TM store file, the dump completion procedure + was always executed. +- Some smaller logic fixes in the TM store base class +- Fixed usage of C `abs` instead of C++ `std::abs`, which results in MTQ commands not being + scaled correctly between 1Am² and 0.2Am². +- TCS Heater Handler: Always trigger mode event if a heater goes `OFF` or `ON`. This event might + soon replace the `HEATER_WENT_ON` and `HEATER_WENT_OFF` events. +- Prevent spam of TCS controller heater unavailability event if all heaters are in external control. +- TCS heater switch info set contained invalid values because of a faulty `memcpy` in the TCS + controller. There is not crash risk but the heater states were invalid. +- STR datasets were not set to invalid on shutdown. +- Fixed usage of quaternion valid flag, which does not actually represent the validity of the + quaternion. +- Various fixes for the pointing modes of the `ACS Controller`. All modes should work now as + intended. +- The variance for the ADIS GYRs now represents the used `-3` version and not the `-1` version +- CFDP funnel did not route packets to live channel VC0 + +# [v2.0.5] 2023-05-11 + +- The dual lane assembly transition failed handler started new transitions towards the current mode + instead of the target mode. This means that if the dual lane assembly never reached the initial + submode (e.g. mode normal and submode dual side), it will transition back to the current mode, + which miht be `MODE_OFF`. Furthermore, this can lead to invalid internal states, so the subsequent + recovery handling becomes stuck in the custom recovery sequence when swichting power back on. +- The dual lane custom recovery handling was adapted to always perform proper power switch handling + irrespective of current or target modes. + +# [v2.0.4] 2023-04-19 + +## Fixed + +- The dual lane assembly datasets were not marked invalid properly on OFF transitions. + +# [v2.0.3] 2023-04-17 + +- eive-tmtc: v3.1.1 + +## Fixed + +- Fixed shadowing within the `SafeCtrl`, which prevented actuator commands to be calculated during + eclipse phase. +- EM build idle mode fixes for RW dummy. + +## Added + +- Add `MGT_OVERHEATING` event and fallback to system SAFE mode if the MGT is overheating for + whatever reason. + +## Changed + +- Low-pass filters can no longer be executed if no actual data is available. + +# [v2.0.2] 2023-04-16 + +- Bump patch version to 2. + +# [v2.0.1] 2023-04-16 + +- eive-tmtc: v3.1.0 + +# [v2.0.0] 2023-04-16 + +This is the version which will fly on the satellite for the initial launch phase. + +- q7s-package: v2.5.0 +- eive-tmtc: v3.0.0 +- `wire` library is now on version v10.7 as well. + +## Added + +- Added `mv`, `cp` and `rm` action helpers for the core controller for common filesystem operations. +- Extended directory listing helpers. There is now a directory listing helper which dumps the + directory listing as an action data reply immediately. For smaller directory listings, this + allows a listing without requiring a separate file downlink (which also has not been implemented + yet) + +## Changed + +- The directory listing action commands now allow compressing of either the target output file + for the directory listing into file action command, or compression in the helper which dumps + the directory listing directly. + +# [v1.45.0] 2023-04-14 + +- q7s-package: v2.5.0 +- eive-tmtc: v3.0.0 +- STR firmware was updated to v10.7. `wire` library still needs to be updated. + +## Fixed + +- Small fix for `install-obsw-yocto.sh` script +- Bugfix for STR firmware update procedure where the last remaining + bytes were not written properly. +- Bugfix for STR where an invalid reply was received for special requests + like firmware updates. +- Bugfix for shell command executors in command controller which lead to a crash. +- Important bugfix for STR solution set. Missing STR mode u8 parameter. +- Fix for STR image download. +- Possible fix for STR image upload. +- Fixed regression where the reply result for ACS board and SUS board devices was set to FAILED + even when going to OFF mode. In that case, it has to be set to OK so the device handler can + complete the OFF transition. + +## Changed + +- STR `wire` library updated to v10.3. Submodule renamed to `sagittactl`. +- Custom FDIR for TMP1075 sensors. The device handlers reject `NEEDS_RECOVERY` health commands + anyway, so it does not really make sense to use the default FDIR. +- Reject `NEEDS_RECOVERY` health commands for the heater health devices. +- Adapted some queue sizes so that EM startup works without queue errors + - Event Manager: 120 -> 160 + - UDP TMTC Bridge: 50 -> 120 + - TCP TMTC Bridge: 50 -> 120 + - Service 5: 120 -> 160, number of events handled in one cycle increased to 80 +- EM: PCDU dummy is not a device handler anymore, but a custom power switcher object. This avoids + some issues where the event manager could not send an event message to the PCDU dummy because + the FDIR event queue was too small. + +## Added + +- Add a way for the MAX31865 RTD handlers to recognize faulty/broken/off sensor devices. +- Add parameter interface for core controller +- Allow setting the preferred SD card via the new parameter interface of the core controller + with domain ID 0 and unque ID 0. +- Added action commands to reset the PDEC. Also added autonomous reset handling for the PDEC, + because there is no way so send TCs with a faulty PDEC. +- Added `I2C_REBOOT` and `PDEC_REBOOT` events for EIVE system component to ensure ground gets + informed. + +## ACS + +- Commanding from ACS Controller is now enabled. +- Safe Controller was reverted to FLP Design. This also introduces safe mode strategies. + They contain what the controller does and which data it uses. The controller will + automatically based on the available data decide on which strategy to use. If a strategy + is undesirable (e.g. the MEKF should not be used) this can be handeld via setting parameters. + +# [v1.44.1] 2023-04-12 + +- eive-tmtc: v2.22.1 + +## Fixed + +- Bugfixes and improvements for SDC state machine. Internal state was not updated correctly due + to a regression, so commanding the SDC state machine externally lead to confusing results. +- Heater states array in TCS controller was too small. +- Fixed a bug in persistent TM store, where the active file was not reset of SD card switches. + SD card switch from 0 to 1 and vice-versa works without errors from persistent TM stores now. +- Add a way for the SUS polling to detect broken or off devices by checking the retrieved + temperature for the all-ones value (0x0fff). +- Better reply result handling for the ACS board devices. +- ADIS1650X initial timeout handling now performed in device handler. +- The RW assembly and TCS board assembly now perform proper power switch handling for their + recovery handling. + +## Changed + +- Added additional logic for SDC state machine so that the SD cards are marked unusable when + the active SD card is switched or there is a transition from hot redundant to cold redundant mode. + This gives other tasks some time to register the SD cards being unusable, and therefore provides + a way for them to perform any re-initialization tasks necessary after SD card switches. +- TCS controller now only has an OFF mode and an ON mode +- The TCS controller pauses operations related to the TCS board assembly (reading sensors and + the primary control loop) while a TCS board recovery is on-going. +- Allow specifying custom OBSW update filename. This allowed keeping a cleaner file structure + where each update has a name including the version +- The files extracted during an update process are deleted after the update was performed to keep + the update directory cleaner. + +## Added + +- TCS controller: SUBMODE_NO_HEATER_CTRL (1) added for ON mode. If this submode is + commanded, all heaters will be switched off and then no further heater + commanding will be done. +- Fixed a bug in persistent TM store, where the active file was not reset of SD card switches. + SD card switch from 0 to 1 and vice-versa works without errors from persistent TM stores now. + +# [v1.44.0] 2023-04-07 + +- eive-tmtc: v2.22.0 + +## Added + +- Special I2C recovery handling. If the I2C bus is unavailable for whatever reason, the EIVE + system component will power-cycle all I2C devices by first going to the OFF/BOOT mode, then + power-cycling the 3V3 stack and rebooting the battery, and finally going back to safe mode. + If this does not restore the bus, a full reboot will be performed. This special sequence can + be commanded as well. + +## Fixed + +- RW Assembly: Correctly transition back to off when more than 1 devices is OFF. Also do this + when this was due to two devices being marked faulty. +- RW dummy and STR dummy components: Set/Update modes correctly. +- RW handlers: Bugfix for TM set retrieval and special request handling in general where the CRC + check always failed for special request. Also removed an unnecessary delay for special requests. +- RW handlers: Polling is now disabled for RWs which are off. + +## Changed + +- RW shutdown now waits for the speed to be near 0 or for a OFF transition countdown to be expired + before going to off. + +# [v1.43.2] 2023-04-05 + +## Changed + +- Adapted HK data rates to new table for LEOP SAFE mode. +- GPS controller HK is now generated periodically as well. +- Better mode combination checks for assembly components. This includes: + - IMTQ assembly + - Syrlinks assembly + - Dual Lane Assembly +- RWs are no longer commanded by the ACS Controller during safe mode. Instead the RW speed command + is set to 0 as part or the `doShutDown` of the RW handler. + +## Fixed + +- Dual lane assemblies: Fix handling when health states are overwritten. Also add better handling + when some devices are permanent faulty and some are only faulty. In that case, only the faulty + devices will be restored. +- ACS dual lane assembly: Gyro 3 helper mode was assigned to the Gyro 2 mode. + +# [v1.43.1] 2023-04-04 + +## Fixed + +- Generic HK handling: Bug where HKs were generated a lot more often than required. This is the case + if a device handler `PERFORM_OPERATION` step is performed more than once per PST cycle. +- Syrlinks now goes to `_MODE_TO_ON` when finishing the `doStartUp` transition. + +## Changed + +- Doubled GS PST interval instead of scheduling everything twice. +- Syrlinks now only has one `PERFORM_OPERATION` step, but still has two communication steps. +- PCDU components only allow setting `NEEDS_RECOVERY`, `HEALTHY` and `EXTERNAL_CONTROL` health + states now. TMP sensor components only allow `HEALTHY` , `EXTERNAL_CONTROL`, `FAULTY` and + `PERMANENT_FAULTY`. +- TCS controller now does a sanity check on the temperature values: Values below -80 C or above + 160 C are ignored. + +# [v1.43.0] 2023-04-04 + +- q7s-package: v2.4.0 +- eive-tmtc: v2.21.0 + +## Added + +- Version of thermal controller which performs basic control tasks. +- PCDU handler can now command switch of the 3V3 stack (switch ID 19) +- Set STR dev to OFF in assembly when it is faulty. +- STR: Reset data link layer and flush RX for regular commands and before performing special + commands to ensure consistent start state + +## Fixed + +- PTME was not reset after configuration changes. +- GPS health devices: ACS board assembly not reacts to health changes. +- STR COM helper: Reset reply size after returning a reply + +## Changed + +- Poll threshold configuration of the PTME IP core is now configurable via a parameter command + and is set to 0b010 (4 polls) instead of 0b001 (1 poll) per default. +- EIVE system fallback and COM system fallback: Perform general subsystem handling first, then + event reception, and finally any new transition handling. +- IMTQ MGM integration time lowered to 6 ms. This relaxes scheduling requirements a bit. +- PCDU handler switcher HK set now has additional 3V3 switcher state HK. + +# [v1.42.0] 2023-04-01 + +- eive-tmtc: v2.20.1 +- q7s-package: v2.3.0 + +## Changed + +- SCEX filename updates. Also use T as the file ID / date separator between date and time. +- COM TM store and dump handling: Introduce modes for all 4 TM VC/store tasks. The OFF mode can be + used to disable ongoing dumps or to prevent writes to the PTME VC. This allows cleaner reset + handling of the PTME. All 4 VC/store tasks were attached to the COM mode tree and are commanded + as part of the COM sequence as well to ensure consistent state with the CCSDS IP core handler. +- Added `PTME_LOCKED` boolean lock which is used to lock the PTME so it is not used by the VC tasks + anymore. This lock will be controlled by the CCSDS IP core handler and is locked when the PTME + needs to be reset. Examples for this are datarate changes. +- Simulate real PCDU in PCDU dummy by remembering commandes switch change and triggering appropriate + events. Switch feedback is still immediate. +- GomSpace devices are polled with a doubled frequency. This speeds up power switch commanding. + +## Fixed + +- Bugfix for side lane transitions of the dual lane assemblies, which only worked when the + assembly was directly commanded. +- Syrlinks Handler: Bugfix so transition command is only sent once. +- SCEX file name bug: Create file name time stamp with `strftime` similarly to how it's done + for the persistent TM store. + +## Added + +- Added GPS0 and GPS1 health device which are used by the ACS board assembly when deciding whether + to change to the other side or to go to dual side directly. Setting the health devices to faulty + should also trigger a side switch or a switch to dual mode. + +# [v1.41.0] 2023-03-28 + +- eive-tmtc: v2.20.0 +- q7s-package: v2.2.0 + +## Fixed + +- Proper Faulty/External Control handling for the dual lane assemblies. +- ACS board devices: Go to ON mode instead of going to NORMAL mode directly. +- SUS device handlers: Go to ON mode on startup instead of NORMAL mode. +- Tweaks for the delay handling for the persistent TM stores. This allows pushing the full + high datarate when dumping telemetry. The most important and interesting fix is that + there needs to be a small delay between the polling of the GPIO. Polling the GPIO + without any delay consecutively can lead to scheduling issues. +- Bump FSFW for fix of `ControllerBase` class `startTransition` implementation. +- Bump FSFW for possible fix of `PowerSwitcherComponent`: Initial mode `MODE_UNDEFINED`. + +## Changed + +- Enabled periodic hosuekeeping generation for release images. +- Project structure (linux and mission folder) is subsystem centric now. + +# [v1.40.0] 2023-03-24 + +- eive-tmtc: v2.19.4 +- q7s-packasge: v2.1.0 +- Bumped fsfwgen for bugfix: Event translation can deal with duplicate event names now. + +## Fixed + +- PAPB busy polling now implemented properly with an upper bound of how often the PAPB is allowed + to be busy before returning the BUSY returnvalue. Also propagate and check for that case properly. + Ideally, this will never be an issue and the PAPB VC interface should never block for a longer + period. +- The `mekfInvalidCounter` now resets on setting the STR to faulty. +- Improve the SD lock handling. The file handling does not need to be locked as it + is only handled by one thread. + +## Added + +- The event `MEKF_RECOVERY` will be triggered in case the `MEKF` does manage to recover itself. +- The persistent TM stores now have low priorities and behave like background threads now. This + should prevent them from blocking or slowing down the system even during dumps + (at least in theory). +- STR: Fix weird issues on datalink layer data reception which sometimes occur. +- Syrlinks FDIR: Fully allow FDIR to do more recoveries. Assembly should take care of preventing + the switch to go off. +- Allow dual lane assembly side switches. + +## Changed + +- Rework FSFW OSALs to properly support regular scheduling (NICE priorities) and real-time + scheduling. +- STR: Move datalink layer to `StrComHandler` completely. DLL is now completely hidden from + device handler. +- STR: Is now scheduled twice in ACS PST. +- `StrHelper` renamed to `StrComHandler`, is now a `DeviceHandlerIF` directly and does not wrap + a separate UART COM interface anymore. +- TCS: Local pool variables are members now. +- Syrlinks: Create dedicated COM helper which uses a ring buffer to parse the Syrlinks datalinklayer + and should make communication more reliable even on high CPU loads. +- Syrlinks: Two communication cycles per PST. +- Fine-tuning of various task priorities. +- The CSP router now is scheduled with the `SCHED_RR` policy and the same priority as the PCDU + handlers as well. +- Change project structure to be more subsystem centric for ACS and COM. + +# [v1.39.1] 2023-03-22 + +## Fixed + +- Bugfix for STR: Some action commands wrongfully declined. +- STR: No normal command handling while a special request like an image upload is active. +- RS485 data line was not enabled when the transmitter was switched on. + +# [v1.39.0] 2023-03-21 + +Requires firmware update for new FPGA design where reset line is routed into the software. +2 relevant PRs: + - https://egit.irs.uni-stuttgart.de/eive/q7s-vivado/pulls/53 + - https://egit.irs.uni-stuttgart.de/eive/q7s-vivado/pulls/54 + +eive-tmtc: v2.19.3 + +## Added + +- Added NaN and Inf check for the `MEKF`. If these are detected, the `AcsController` will reset + the `MEKF` on its own once. This way, there will not be an event spam and operators will have + to look into the reason of wrong outputs. To restore the reset ability, an action command has + been added. +- Contingency handling for non-working I2C bus bug. Reboot the system if the I2C is not working. + +## Fixed + +- Fixed transition for dual power lane assemblies: When going from dual side submode to single side + submode, perform logical commanding first, similarly to when going to OFF mode. +- GPS time is only set to valid if at least one satellite is in view. + +## Changed + +- Bugfixes for STR mode transitions: Booting to mode ON with submode FIRMWARE now works properly. + Furthermore, the submode in the NORMAL mode now should be 0 instead of some ON mode submode. +- Updated GYR bias values to newest measurements. This also corrects the ADIS values to always + consit of just one digit. +- The CCSDS IP core handler now exposes a parameter to enable the priority select mode + for the PTME core. This mode prioritizes virtual channels with a lower index, so for example + the virtual channel (VC0) will have the highest priority, while VC3 will have the lowestg + priority. This mode will be enabled by default for now, but can be set via the parameter IF with + the unique parameter ID 0. The update of this mode requires a PTME reset. Therefore, it will only + be performed when the transmitter is off to avoid weird bugs. +- Connect and handle reset line for the PTME core in the software now. +- Safe mode controller failure event now only triggers once per minute. + +# [v1.38.0] 2023-03-17 + +eive-tmtc: v2.19.2 + +## Fixed + +- SA deployment file handling: Use exceptionless API. +- Fix deadlock in SD card manager constructor: Double lock of preferred SD card lock. + +## Added + +- Failure of Safe Mode Ctrl will now trigger an event. As this can only be caused by sensors not + being in the correct mode, the assemblies should take care that this will never happen and no + additional FDIR is needed. + +## Changed + +- Telemetry relevant datasets for the RWs are now set invalid and partially reset on shotdown. +- I2C PST now has a polling frequency of 0.4 seconds instead of 0.2 seconds. +- GS PST now has a polling frequency of 0.5 seconds instead of 1 second. +- Bump FSFW: merged upstream. +- Move BPX battery scheduling to ACS PST to avoid clashes with IMTQ scheduling / polling + +# [v1.37.2] 2023-03-14 + +- Changed `PoolManager` bugfix implementation in the FSFW. +- Some tweaks for IPC and TM store configuration + +# [v1.37.1] 2023-03-14 + +This version works on the EM as well. + +eive-tmtc: v2.19.1 + +## Added + +- Added `EXECUTE_SHELL_CMD` action command for `CoreController` to execute arbitrary Linux commands. +- Added some missing PLOC commands. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/462 +- Add `PcduHandlerDummy` component. +- Added parameter for timeout until `MEKF_INVALID_MODE_VIOLATION` event is triggered. + +## Fixed + +- Pointing control of the `AcsController` was still expecting submodes instead of modes. +- Limitation of RW speeds was done before converting them to the correct unit scale. +- The Syrlinks task now has a proper name instead of `MAIN_SPI`. +- Make whole EIVE system initial transition work for the EM. This was also made possible by + always scheduling most EIVE components instead of tying the scheduling to preprocessor defines. +- Store more TCP und UDP packets. +- Bump fsfw for small tweak in local datapool manager: Re-enable or re-disabling dataset + generation will not fail anymore. +- Bump FSFW to simplify HK helpers: Was previously buggy and did not allow to use + minmum sampling frequency. Now both diagnostics and HK can use minimum + sampling frequency for requesting HK +- Bump FSFW to allow the TC/TM/IPC pools to spill to higher pools/pool pages. + +## Changed + +- Set `OBSW_ADD_TCS_CTRL` to 1. Always add TCS controller now for both EM and FM. +- generators module: Bump `fsfwgen` dependency to v0.3.1. The returnvalue CSV files are now sorted. +- generators module: Always generate subsystem ID CSV files now. +- The COM subsystem is now not commanded by the EIVE system anymore. Instead, + a separate RX_ONLY mode command will be sent at OBSW boot. After that, + commanding is done autonomously by the COM subsystem internally or by the operator. This prevents + the transmitter from going off during fallbacks to the SAFE mode, which might not always be + desired. +- Initialize switch states to a special `SWITCH_STATE_UNKNOWN` (2) variable. Return + `PowerSwitchIF::SWITCH_UNKNOWN` in switch state getter if this is the state. +- Wait 1 second before commanding SAFE mode. This ensures or at least increases the chance that + the switch states were initialized. +- Dual Lane Assemblies: The returnvalues of the dual lane power state machine FSM are not ignored + anymore. + +# [v1.37.0] 2023-03-11 + +eive-tmtc: v2.18.1 + +## Added + +- `SensorProcessing` now includes an FDIR for GPS altitude. If the measured GPS altitude is out + of bounds of the range defined in the `AcsParameters`, the altitude defaults to an altitude + set in the `AcsParameters`. +- `AcsController` will now never command a RW speed larger than the maximum allowed speed. + +## Fixed + +- `PAPB_EMPTY_SIGNAL_VC1` GPIO was not set up properly. +- Fix for heater names: HPA heater (index 7) is now the Syrlinks heater. +- `AcsParameters` setter were previously all for scalar parameters. Now vector and matrix + parameters use their respective setters. +- Several `AcsController` components had their own implementation of `AcsParameters`. This resulted + in those parameters not being updated, while the actual ones were updated. All instances of + `AcsParameters` not belonging to `AcsController` are eiter removed or replaced by pointer + instances. +- Instead of updating the `gsTargetModeControllerParameters`, the `targetModeControllerParameters` + were updated. +- Instead of updating the `idleModeControllerParameters`, the `targetModeControllerParameters` + were updated. +- Fixed Idle Mode Controller never calling `ptgLaw` and therefore never calculating control + values. +- Fixed wrong check on wether file used for persistant boolean flag on successful still existed. +- Scaling of MTQ Cmds now scales the current values to command with the current values and not + the values of the last step, which would result in undefined behaviour. +- Solved naming collision between file used for solar array deployment and confirmation for + ACS for solar array deployment. +- Fixed that scaling of RW torque would result in a zero vector unless the maximum value was exceeded. +- Bias for the GYR data was substracted within the wrong rf (sensor rf vs body rf). + +## Changed + +- Refactored TM pipeline to optimize usage of the PTME and communication downlink bandwidth. + This was done by moving the dumping of TMs to the VCs into separate threads with permanent loops. + These threads are then able to process high TM loads on demand. The PUS TM funnel will route + PUS packets to the approrpiate persisten TM stores and then demultiplex the TM to all registered + TM destinations as before. +- Service 5 now handles 40 events per cycle instead of 15 +- Remove periodic SD card check. The file system is not mounted read-only anymore when using an + ext4 filesystem +- The `detumbleCounter` now does not get hard reset anymore, if the critical rate does not get + violated anymore. Instead it is incrementally reset. +- The RW antistiction now only takes the RW speeds in account. +- ACS CTRL transition to DETUBMLE is now done in CTRL internally. No + system level handling necessary anymore. +- More fixes and improvements for SD card handling. Extend SD card setup in core controller to + create full initial state for SD card manager are core controller as early as possible, turn + execution of setup file update blocking. This might solve the issue with the SD card manager + sometimes blocking for a long time. +- Request raw MTM measurement twice for IMTQ, might reduce number of times measurement could not + be retrieved. +- Event manager and event service have larger queues now: 45 -> 120 for Service 5, 80 -> 120 for + event manager +- ACS mode changes: The ACS CTRL submodes are now modes. DETUBMLE is now submode of SAFE mode. +- EIVE system now tracks the mode of the ACS subsyste in SAFE mode. + +# [v1.36.0] 2023-03-08 + +eive-tmtc: v2.17.2 + +## Added + +- Star Tracker Assembly +- New `REBOOT_COUNTER` and `INDIVIDUAL_BOOT_COUNTS` events. The first contains the total boot count + as a u64, the second one contains the individual boot counts as 4 u16. Add new core controller + action command `ANNOUNCE_BOOT_COUNTS` with action ID 3 which triggers both events. These events + will also be triggered on each reboot. + +## Changed + +- Persistent TM stores will now create new files on each reboot. +- Fast ACS subsystem commanding: Command SUS board consecutively with other devices now +- Star Tracker: Use ground confguration for EM and flight config for FM by default. + +## Fixed + +- Command TCS controller off first for TCS subsystem transition to off. +- Health handling for TCS board assembly +- Mode fallback from IDLE mode to SAFE mode due to ACS errors/events now works properly for + the ACS subsystem +- Bugfix in IDLE transition for system. +- `std::filesystem` API usages: Avoid exceptions by using variants which return an error code + instead of throwing exceptions. +- GPS fix loss was not reported if mode is unset. +- Star Tracker: OFF to NORMAL transition now posssible. Requires FSFW bump which sets + transition source modes properly for those transitions. + FSFW PR: https://egit.irs.uni-stuttgart.de/eive/fsfw/pulls/131 +- Star Tracker JSON initialization is now done during object initization instead of redoing it + when building a command. This avoids missed deadlines issues in the ACS PST. +- Allow arbitrary submodes for dual lane boards to avoid FDIR reactions of subsystem components. + Bump FSFW to allow this. +- PUS 15 was not scheduled +- Transmitter timeout set to 2 minutes instead of 15 minutes. This will prevent to discharge the + battery in case the syrlinks starts transmitting due to detection of unintentional bitlock. This + happened e.g. on ground when the uplink to the flying latop was established. +- ACS system components are now always scheduled (EM specific) + +# [v1.35.1] 2023-03-04 + +## Fixed + +- ACS Board Assembly FDIR: Prevent permanent SAFE mode fallbacks by introducing special health + handling. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/418/files +- Watchdog fixes +- IMTQ timing fixes + +## Added + +- Add IMTQ assembly + +# [v1.35.0] 2023-03-04 + +eive-tmtc: v2.16.4 + +## Added + +- Improved the OBSW watchdog by adding a watch functionality. The watch functionality is optional + and has to be enabled specifically by the application being watched by the watchdog when + starting the watchdog. If the watch functionality is enabled and the OBSW has not pinged + the watchdog via the FIFO for 2 minutes, the watchdog will restart the OBSW service via systemd. + The primary OBSW will only activate the watch functionality if it is the OBSW inside the + `/usr/bin` directory. This allows debugging the system by leaving flashed or manually copied + debugging images 2 minutes to start the watchdog without the watch functionality. + +## Fixed + +- Bumped FSFW: `Countdown` and `Stopwatch` use new monotonic clock API now. +- IMTQ: Various fixes, most notably missing buffer time after starting MGM measurement + and corrections for actuator commanding. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/430 + +# [v1.34.0] 2023-03-03 + +eive-tmtc: v2.16.3 + +This might include the fix for the race condition where CPU usage jumped to 200 %. The race +condition was traced to the `Countdown` class, more specifically to the `getUptime` function where +the `/proc/uptime` file is read. + +## Changed + +- The SD card prefix is now set earlier inside the `CoreController` constructor +- The watchdog handling was moved outside the `CoreController` into the main loop. +- Moved polling of all SPI parts to the same PST. +- Allow quicker transition for the EIVE system component by allowing consecutive TCS and ACS + component commanding again. +- Changed a lot of lock guards to use timeouts +- Queue sizes of TCP/UDP servers increased from 20 to 50 +- Significantly simplified and improved lock guard handling in TCS and ACS board polling + tasks. + +## Fixed + +- IMTQ: Sets were filled with wrong data, e.g. Raw MTM was filled with calibrated MTM measurements. +- Set RM3100 dataset to valid. +- Fixed units in calculation of ACS control laws safe and detumble. +- Bump FSFW for change in Countdown: Use system clock instead of reading uptime from file + to prevent possible race condition. +- GPS: No fix considered a fault now after 30 minutes instead of 5 hours. +- SUS Assembly FDIR: Prevent permanent SAFE mode fallbacks by introducing special health + handling. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/418/files + +## Added + +- Added Syrlinks Assembly object to allow recovery handling and to fix faulty FDIR behaviour. + +# [v1.33.0] 2023-03-01 + +eive-tmtc: v2.16.2 + +## Changed + +- Move ACS board polling to separate worker thread. +- Move SUS board polling to separate worker thread. + +## Fixed + +- Linux GPS handler now checks the individual `*_SET` flags when analysing the `gpsd` struct. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/400 + +# [v1.32.0] 2023-02-24 + +eive-tmtc: v2.16.1 + +## Fixed + +- ADIS1650X: Added missing MDL_RANG pool entry for configuration set +- Bumped FSFW for bugfix in health service: No execution complete for targeted health announce + command. +- Removed matrix determinant calculation as part of the `MEKF`, which would take about + 300ms of runtime +- Resetting the `MEKF` now also actually resets its stored state +- Bumped FSFW for bugfix in destination handler: Better error handling and able to process + destination folder path. + +## Changed + +- Added basic persistent TM store for PUS telemetry and basic interface to dump and delete + telemetry. Implementation is based on a timed rotating files, with the addition that files + might be generated more often if the maximum file size of 8192 bytes is exceeded. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/320/files +- Commented out commanding of actuators as part of the `AcsController` +- Collection sets of the `AcsController` now get updated before running the actual ACS + algorithm +- `GpsController` now always gets scheduled +- The `CoreController` now initializes the initial clock from the time file as early as possible + (in the constructor) if possible, which should usually be the case. + +## Added + +- Added basic persistent TM store for PUS telemetry and basic interface to dump and delete + telemetry. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/320/files +- `ExecutableComIfDummy` class to have a dummy for classes like the RTD polling class. +- Added `AcsController` action command to confirm solar array deployment, which then deletes + two files +- Added `AcsController` action command to reset `MEKF` +- `GpsCtrlDummy` now initializes the `gpsSet` +- `RwDummy` now initializes with a non faulty state + + +# [v1.31.1] 2023-02-23 + +## Fixed + +- ADIS1650X configuration set was empty because the local pool variables were not registered. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/402 +- ACS Controller: Correction for size of MEKF dataset and some optimization and fixes + for actuator control which lead to a crash. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/403 + +# [v1.31.0] 2023-02-23 + +eive-tmtc: v2.16.0 + +## Fixed + +- Usage of floats as iterators and using them to calculate a uint8_t index in `SusConverter` +- Removed unused variables in the `AcsController` +- Remove shadowing variables inside ACS assembly classes. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/385 + +## Changed + +COM PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/364 + +* Moved transmitter timer and handling of carrier and bitlock event from CCSDS handler to COM + subsystem +* Added parameter command to be able to change the transmitter timeout +* Solves [#362](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/362) +* Solves [#360](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/360) +* Solves [#361](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/361) +* Solves [#386](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/386) +- All `targetQuat` functions in `Guidance` now return the target quaternion (target + in ECI frame), which is passed on to `CtrlValData`. +- Moved polling sequence table definitions and source code to `mission/core` folder. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/395 + +## Added + +- `MEKF` now returns an unique returnvalue depending on why the function terminates. These + returnvalues are used in the `AcsController` to determine on how to procede with its + perform functions. In case the `MEKF` did terminate before estimating the quaternion + and rotational rate, an info event will be triggered. Another info event can only be + triggered after the `MEKF` has run successfully again. If the `AcsController` tries to + perform any pointing mode and the `MEKF` fails, the `performPointingCtrl` function will + set the RWs to the last RW speeds and set a zero dipole vector. If the `MEKF` does not + recover within 5 cycles (2 mins) the `AcsController` triggers another event, resulting in + the `AcsSubsystem` being commanded to `SAFE`. +- `MekfData` now includes `mekfStatus` +- `CtrlValData` now includes `tgtRotRate` + +# [v1.30.0] 2023-02-22 + +eive-tmtc: v2.14.0 + +Event IDs for PDEC handler have changed in a breaking manner. + +## Added and Fixed + +- PDEC: Added basic FDIR to limit the number of allowed TC interrupts and to allow complete task + lockups in the case an IRQ is immediately re-raised by the PDEC module. This is done by only + allowing a certain number of handled IRQs (whether they yield a valid TC or not) during + time windows of one second. Right now, 800 IRQs/TCs are allowed per time window. + This time window is reset if a TC reception timeout after 500ms occurs. TBD whether the maximum + allowed number will be a configurable parameter. If the number of occured IRQs is exceeded, + an event is triggered and the task is delayed for 400 ms. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/393 + +# [v1.29.1] 2023-02-21 + +## Fixed + +- Limit number of handled messages for core TM handlers: + - https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/391 + - https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/390 + - https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/389 +- HeaterHandler better handling for faulty message reception + Issue: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/issues/388 +- Disable stopwatch in MAX31865 polling task + +# [v1.29.0] 2023-02-21 + +eive-tmtc: v2.13.0 + +## Changed + +- Refactored IMTQ handlers to also perform low level I2C communication tasks in separate thread. + This avoids the various delays needed for I2C communication with that device inside the ACS PST. + (e.g. 1 ms delay between each transfer, or 10 ms integration delay for MGM measurements). + +## Added + +- Added new heater info set for the TCS controller. This set contains the heater switch states + and the current draw. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/351 +- The HeaterHandler now exposes a mode which reflects whether the heater power + is on or off. It also triggers mode events for its heater children objects + which show whether the specific heaters are on or off. The heater handler + will be part of the TCS tree. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/351 + +# [v1.28.1] 2023-02-21 + +## Fixed + +- Patch version which compiles for EM +- CFDP Funnel bugfix: CCSDS wrapping was buggy and works properly now. +- PDEC: Some adaptions to prevent task lockups on invalid FAR states. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/393 +- CMakeLists.txt fix which broke CI/CD builds when server could not retrieve full git SHA. +- Possible regression in the MAX31865 polling task: Using a `ManualCsLockGuard` for reconfiguring + and then polling the sensor is problematic, invalid sensor values will be read. + CS probably needs to be de-asserted or some other HW/SPI specific issue. Letting the SPI ComIF + do the locking does the job. + +## Changed + +- Add `-Wshadow=local` shadowing warnings and fixed all of them +- Updated generated CSV files: Support for skip directive and explicit + "No description" info string +- The polling threads for actuator polling now have a slightly higher priority than the ACS PST + to ensure timing requirements are met. + +## Added + +- git post checkout hook which initializes and updates the submodules + automatically. + +# [v1.28.0] 2023-02-17 + +eive-tmtc: v2.12.7 + +## Added + +- In case the ACS Controller does recognize more than one RW to be invalid and therefore not + available, it does not perform pointing control but aborts shortly after `sensorProcessing`. If the + problem persits for 5 ACS cycles, the `MULTIPLE_RW_INVALID` event is triggered, which invokes the + transition of the `AcsSubsystem` to safe mode. + +## Changed + +- Igrf13 model vector now outputs as uT instead of nT +- Changed timings for `AcsPst`, more time for sun sensors. +- Added values for MGM sensor fusion +- Refactored RW Software: Polling runs in separate thread, all RWs are now polled in under 60 ms. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/381 +- Bumped FSFW to allow initializing child modes in `SubsystemBase` derived objects. + +## Fixed + +- Fixed values for GYR sensor fusion +- Fixed speed types for `rwHandlingParameter` +- Pseudo inverse used for allocating torque to RWs and RW antistiction now actually consider the + state of the RWs + +# [v1.27.2] 2023-02-14 + +Reaction Wheel handling was determined to be (quasi) broken and needs to be fixed in future release +to be usable by ACS controller. + +eive-tmtc: v2.12.6 + +## Added + +- Function for the ACS controller to command MTQ and RWs called by all subroutines +- RwHandler now handles commanding of RW speeds via RwSpeedActuationSet +- Tracing supports which allows checking whether threads are running as usual. + +## Changed + +- Remove 2 TCS threads. +- Move low level polling into ACS PST, move high level device handlers into TCS system task. +- ActCmds now returns command vectors as integers as required by the actuators + and scales them to the appropriate range +- All RwHandler are now polled five times per ACS cycle +- Remove 2 TCS threads. Move low level polling into ACS PST, move high level device handlers into + TCS system task. +- Further reduce number of threads: + 1. Remove PUS low priority task, move assigned threads to the generic system task + 2. Group events and verification tasks into PUS high priority task + 3. Group all other components into PUS medium priority task + 4. Add SCEX device handler to PL task, remove dedicated thread + +## Removed + +- lwgps dependency not compiled anymore, is not used + +# [v1.27.1] 2023-02-13 + +## Fixed + +- Fix for SPI ComIF: Set transfer size to 0 for failed transfers +- Fix shadowing issue with locks in MAX31865 low level handler + +# [v1.27.0] 2023-02-13 + +eive-tmtc: v2.12.5 + +Added EIVE system top mode component. Currently, only SAFE and IDLE mode are +implemented, and the system does not do more than commanding TCS and ACS +into the correct modes. It does not have a lot of mode tracking capabilities +yet because the ACS controller might alternate between SAFE and DETUMBLE. +It takes around 5-10 seconds for the EIVE system to reach the SAFE mode. + +The new system is used at software boot to command the satellite into safe mode +on each reboot. This behaviour can be disabled with the +`OBSW_SWITCH_TO_NORMAL_MODE_AFTER_STARTUP` flag. + +## Added + +- New EIVE system component like explained above. + +## Changed + +- The satellite now commands itself into SAFE mode on each reboot, which + triggers a lot of events on each SW reboot. The TCS subsystem will commanded + to NORMAL mode immediately while the ACS subsystem will be commanded to + SAFE mode. The payload subsystem will be commanded OFF. +- `RELEASE_BUILD` flag moved to `commonConfig.h` +- The ACS subsystem transitions are now staggered: The SUS board assembly + is commanded as a separate transition. This reduces the risk of long bus lockups. +- No INFO mode event translations for release builds to reduce number of + printouts. +- More granular locking inside the MAX31865 low level read handler. + +## Fixed + +- More DHB thermal module fixes. +- ACS PST frequency extended to 0.8 seconds in debug builds to avoid SPI + bus lockups. +- Local datapool fixes for the `PlocSupervisorHandler` + +# [v1.26.4] 2023-02-10 + +eive-tmtc: v2.12.3 + +## Fixed + +- `SdCardManager.cpp` `isSdCardUsable`: Use `ext4` instead of `vfat` to check read-only state. + +# [v1.26.3] 2023-02-09 + +eive-tmtc: v2.12.2 + +## Added + +- First version of a TCS controller heater control loop, but + the loop is disabled for now. + +## Changed + +- Reworked dummy handling for the TCS controller. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/325 +- Generator scripts now generate files for hosted and for Q7S build. + +## Fixed + +- GPS Controller: Set fix value to 0 when switching off to allow + `GPS_FIX_CHANGE` to work when switching the GPS back on. + +# [v1.26.2] 2023-02-08 + +## Changed + +- ACS Controller scheduling is now configurable via the `eive/definitions.h` file. Also ensured + that scheduling is done in big blocks to reduce risk of missed deadlines. +- Replaced chained locks for polling new sensor data to the `AcsController`. +- Made TM store even larger. + +## Fixed + +- Bugfix for PDEC handler which causes the PIR register of the PDEC to never + be cleared on release builds. The dummy variable used to read the register + needs to be declared volatile to avoid compiler optimizations. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/374 +- Bumped FSFW for fix of possible memory leaks in TCP/IP TMTC bridge + inside the FSFW. + +## Added + +- Create TCS controller for EM build. + +# [v1.26.1] 2023-02-08 + +- Initialize parameter helper in ACS controller. + +# [v1.26.0] 2023-02-08 + +eive-tmtc v2.12.1 + +## Changed + +### ACS + +- Readded calibration matrices for MGM calibration. +- Added calculation of satellite velocity vector from GPS position data +- Added detumble mode using GYR values +- Added inertial pointing mode +- Added nadir pointing mode +- Added ground station target mode +- Added antistiction for RWs +- Added `sunTargetSafe` differentiation for LEOP +- Added check for existance of `SD_0_SKEWED_PTG_FILE` and `SD_1_SKEWED_PTG_FILE` to determine + which `sunTargetSafe` to use +- Added `gpsVelocity` and `gpsPosition` to `gpsProcessed` +- Removed deprecated `OutputValues` +- Added `HasParametersIF` to `AcsParameters` +- Added `ReceivesParameterMessagesIF` and `ParameterHelper` to `AcsController` +- Updated `AcsParameters` with actual values and changed structure +- Sun vector model and magnetic field vector model calculations are always executed now +- `domainId` is now used as identifier for parameter structs +- Changed onboard GYR value handling from deg/s to rad/s + +## Fixed + +- Single sourcing the version information into `CMakeLists.txt`. The `git describe` functionality + is only used to retrieve the git SHA hash now. Also removed `OBSWVersion.h` accordingly. +- Build system: Fixed small bug, where the version itself was + stored as the git SHA hash in `commonConfig.h`. This will be + an empty string now for regular versions. +- Bump FSFW for important fix in PUS mode service. + +### ACS + +- Bugfixes in 'SensorProcessing' where previously MGM values would be calibrated before being + transformed in body RF. However, the calibration values are in the body RF. Also fixed the + validity flag of 'mgmVecTotDerivative'. +- Fixed calculation of model sun vector +- Fixed calculation of model magnetic field vector +- Fixed MEKF algorithm +- Fixed several variable initializations +- Fixed several variable types +- Fixed use of `sunMagAngleMin` for safe mode +- Fixed MEKF not using correct `sampleTime` +- Fixed assignment of `SUS0` and `SUS6` calibration matrices due to wiring being mixed up +- Various smaller bugfixes + +# [v1.25.0] 2023-02-06 + +eive-tmtc version: v2.12.0 + +## Changed + +- Updated Subsystem mode IDs to avoid clashes with regular device handler modes. + +## Fixed + +- `GpsHyperionLinuxController`: Fix `gpsd` polling by continuously calling `gps_read` in one cycle + until it does not have any data left anymore. Also, the data is now polled in a permanent loop, + where controller handling is done on 0.2 second timeouts. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/368 + +# [v1.24.0] 2023-02-03 + +- eive-tmtc v2.10.0 +- `AcsSubsystem`: OFF, SAFE and DETUMBLE mode were tested. Auto-transitions SAFE <-> DETUMBLE tested + as well. Other modes still need to be tested. + +## Fixed + +- `AcsController`: Parameter fix in `DetumbleParameter`. +- Set GPS set entries to invalid on MODE_OFF command. +- Bump FSFW for bugfix in `setNormalDatapoolEntriesInvalid` where the validity was not set to false + properly +- Fixed usage of uint instead of int for commanding MTQ. Also fixed the range in which the ACS Ctrl + commands the MTQ to match the actual commanding range. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/363 +- Regression: Revert swap of SUS0 and SUS6. Those devices are on separate power lines. In a + future fix, the calibration matrices of SUS0 and SUS6 will be swapped. + +## Changed + +- Update ACS scheduling to represent the actual ACS design. There is one ACS PST now for all + timing sensitive ACS operations. In the debug builds, the new ACS polling sequence table + will have a period of 0.6 seconds, but will remain 0.4 seconds for the release build. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/365 +- `ACS::SensorValues` is now an ACS controller member to reduce the risk of stack overflow. +- ACS Subsystem Sequence Mode IDs updated. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/365 + TMTC PR: https://egit.irs.uni-stuttgart.de/eive/eive-tmtc/pulls/130 +- Update and tweak ACS subsystem to represent the actual ACS design +- Event handling in the ACS subsystem for events triggered by the ACS controller. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/365 + +# [v1.23.1] 2023-02-02 + +TMTC rev: 15adb9bf2ec68304a4f87b8dd418c1a8353283a3 + +## Fixed + +- Bugfix in FSFW where the sequence flags of the PUS packets were set to continuation segment (0b00) + instead of unsegmented (0b11). +- Bugfix in FSFW where the MGM RM3100 value Z axis data was parse incorrectly. + PR: https://egit.irs.uni-stuttgart.de/eive/fsfw/pulls/123 + +# [v1.23.0] 2023-02-01 + +TMTC version: v2.9.0 + +## Changed + +- Bumped FSFW to include improvements and bugfix for Health Service. The health service now + supports the announce all health info command. + PR: https://egit.irs.uni-stuttgart.de/fsfw/fsfw/pulls/725 + +## Fixed + +- Bumped FSFW to include fixes in the time service. + PR: https://egit.irs.uni-stuttgart.de/fsfw/fsfw/pulls/726 +- The CCSDS handler starts the transmitter timer each time it is commanded to MODE_ON and times + out the timer when the handler is commanded to MODE_OFF +- If the timer is timed out the CCSDS handler will disable the TX clock which will cause the + syrlinks to got to standby mode +- PDEC handler now parses the FAR register also in interrupt mode + + +# [v1.22.1] 2023-01-30 + +## Changed + +- Updated FSFW to include addition where the `SO_REUSEADDR` option is set + on the TCP server, which should improve its ergonomics. + +# [v1.22.0] 2023-01-28 + +TMTC version: v2.6.1 + +## Added + +- First COM subsystem implementation. It mirrors the Syrlinks mode/submodes but also takes + care of commanding the CCSDS handler. It expects the Syrlinks submodes as mode commands. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/358 +- The CCSDS handler has has a new submode (3) to configure the default datarate. +- Default datarate parameter commanding moved to COM subsystem. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/358 + +# [v1.21.0] 2023-01-26 + +TMTC version: v2.5.0 +Syrlinks PR: PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/353 + +## Fixed + +- The `OBSW_SYRLINKS_SIMULATED` flag is set to 0 for for both EM and FM. +- MGM4 handling in ACS sensor processing: Bugfix in `mulScalar` operation + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/354 +- Subsystem ID clash: CORE subsystem ID was the same as Syrlinks subsystem ID. + +## Changed + +- Startracker temperature set and PCDU switcher set are diagnostic now +- `SyrlinksHkHandler` renamed to `SyrlinksHandler` to better reflect that it does more than + just HK and is also responsible for setting the TX mode of the device. +- `SyrlinksHandler`: Go to startup immediately because the Syrlinks device should always be on + by default. +- `SyrlinksHandler`: Go to normal mode at startup. + +## Added + +- The Syrlinks handler has submodes for the TX mode now: RX Only (0), RX and TX default + datarate (1), RX and TX Low Rate (2), RX and TX High Rate (3) and TX Carrier Wave (4). + The submodes apply for both ON and NORMAL mode. The default datarate can be updated using + a parameter command (domain ID 0 and unique ID 0) with value 0 for low rate and 1 for high rate. +- The Syrlinks handler always sets TX to standby when switching off +- The Syrlinks handler triggers a new TX_ON event when the transmitter was switched on successfully + and a TX_OFF event when it was switched off successfully. +- Startracker temperature set and PCDU switcher set are diagnostic now +- The CCSDS handler can accept mode commands now. It accepts ON and OFF commands. Furthermore + it has a submode for low datarate (1) and high datarate (2) for the ON command. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/352 + +# [v1.20.0] 2023-01-24 + +## Added + +- The Q7S SW now checks for a file named `boot_delay_secs.txt` in the home directory. + If it exists and the file is empty, it will delay for 6 seconds before continuing + with the regular boot. It can also try to read delay seconds from the file. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/340. +- Basic TCS Subsystem component. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/319 +- Expose base set of STR periodic housekeeping packets + +## Changed + +- Moved some PDEC/PTME configuration to `common/config/eive/definitions.h` + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/319 +- The ACS Controller Gyro Sets (raw and processed) and the MEKF dataset are diagnostics now. +- Bumped FSFW for Service 11 improvement which includes size and CRC check for contained TC +- Syrlinks module now always included for both EM and FM +- SA Deployment: Allow specifying the switch interval and the initial channel. This allows testing + the new deployment procedure where each channel is burned for half of the whole burn duration. + It also allows burning only one channel for the whole burn duration. The autonomous mechanism + was adapted to burn each channel for half of the burn time by default. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/347 + TMTC PR: https://egit.irs.uni-stuttgart.de/eive/eive-tmtc/pulls/127 +- `Max31865RtdLowlevelHandler.cpp`: For each RTD device, the config is now re-written before + every read. This seems to fix some issue with invalid temperature sensor readings. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/345 + +## Fixed + +- `GyroADIS1650XHandler`: Updated handler to determine correct dynamic range from `RANG_MDL` + register readout. This is because ADIS16505-3BMLZ devices are used on the ACS board and the + previous range setting was wrong. Also fixed a small error properly set internal state + on shut-down. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/342 +- Syrlinks Handler: Read RX frequency shift as 24 bit signed number now. Also include + validity handling for datasets. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/350 +- `GyroADIS1650XHandler`: Changed calculation of angular rate to be sensitivity based instead of + max. range based, as previous fix still left an margin of error between ADIS16505 sensors + and L3GD20 sensors. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/346 + +# [v1.19.0] 2023-01-10 + +## Changed + +- 5V stack is now off by default + +## Fixed + +- PLOC SUPV: Minor adaptions and important bugfix for UART manager +- Allow cloning and building the hosted OBSW version without proprietary libraries, + which also avoids the need to have a Gitea account. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/337 + +## Added + +- First version of ACS controller + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/329 +- Allow commanding the 5V stack internally in software + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/334 +- Add automatic 5V stack commanding for all connected devices + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/335 + +# [v1.18.0] 2022-12-01 + +## Changed + +- PLOC Supervisor: Changes baudrate to 921600 +- Renamed `/dev/ul-plsv` to `/dev/ploc_supv`, is not a UART lite anymore +- Renamed `/dev/i2c_eive` to `/dev/i2c_pl` and `/dev/i2c-2` to `/dev/i2c_ps`. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/328 + +# [v1.17.0] 2022-11-28 + +## Added + +- PLOC Supervisor Update: Update SW to use newest PLOC SUPV version by TAS + PR 1: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/316 + PR 2: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/324 + PR 3: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/326 + +# [v1.16.0] 2022-11-18 + +- It is now possible to compile Linux components for the hosted build conditionally + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/322 +- ACS Subsystem. PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/231 +- Payload Subsystem. PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/231 +- Add IRQ mode for PDEC handler. PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/310 +- Extended TM funnels to allow multiple TM recipients. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/312 +- DHB: Transitions to normal mode now possible directly, which simplifies subsystem implementations + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/313 +- MAX3185 Low Level Handler and Device Handler: Simplifications and bugfixes to allow switching + off without triggering unrequested replies + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/313 +- Add remaining missing TMP1075 device handlers. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/318 + +# [v1.15.0] 2022-10-27 + +- Consistent device file naming +- Remove rad sensor from EM build, lead to weird bugs on EM which + prevented `xsc_boot_copy` from working properly +- CFDP closure handling is now working + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/307 +- Safety mechanism for SD card handling on graceful reboots + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/308 +- Solar Array Deployment handler update + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/305 +- IMTQ updates as preparation for ACS controller expansion + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/306 +- P60 Module: Reduce number of set IDs, use same set IDs for core, auxiliary + and config HK set across the three PCDU modules + +# [v1.14.1] 11.10.2022 + +- Various bugfixes and regression fixes +- General file handling at program initialization now works properly again +- Scratch buffer preferred SD card handling works again +- Use scoped locks in TCS controller to avoid deadlocks + +# [v1.14.0] 10.10.2022 + +- Provide full SW update capability for the OBSW. + This includes very basic CFDP integration, a software update + procedure specified in detail in the README and some high level + commands to make this easier for operators. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/302 +- Update for FSFW: `HasReturnvaluesIF` class replaced by namespace `returnvalue` +- Add some GomSpace clients as a submodule dependency. Use this dependency to deserialize the + GomSpace TM tables +- Add API to retrieve GomSpace device parameter tables + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/287 +- Add API to save and load GomSpace config tables + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/293 +- Increase number of allowed consescutive action commands from 3 to 16 + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/294 +- Fix for EM SW: Always create ACS Task +- Added Scex device handler and Scex uart reader + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/303 +- ACS Subsystem. PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/228 + +# [v1.13.0] 24.08.2022 + +- Added first version of ACS Controller with gathers MGM data in a set +- Some tweaks for IMTQ handler + +# [v1.12.1] 05.07.2022 + +- Disable periodic TCS controller HK generation by default + +# [v1.12.0] 04.07.2022 + +## Added + +- Dummy components to run OBSW without relying on external hardware + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/266 +- Basic Thermal Controller + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/266 +- PUS11 TC scheduler + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/259 +- Regular reboot command + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/242 +- Commands for individual RTD devices + PR: https://egit.irs.uni-stuttgart.de/eive/eive-tmtc/pulls/84 +- `RwAssembly` added to system components. Assembly works in principle, + issues making 4 consecutives RWs communicate at once.. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/224 +- Adds a yocto helper script which is able to install the release build binaries + (OBSW and Watchdog) into the `q7s-yocto` repository as long as the `q7s-package` + or `q7s-yocto` repo was cloned in the same directory the EIVE OBSW repo. + This makes updating the root filesystem a lot easier. It also creates and installs a + version file. + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/248 +- Create the generic image by default for the Q7S build. The unique binary with the + username appended at the end is created as a side-product now + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/248 + +## Fixed + +- `q7s-cp.py` bugfix + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/256 +- Generator scripts output now produce platform-independent artifacts + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/267 + +### Heater + +- Adds `HealthIF` to heaters. Heaters are own system object with queues now which allows to set them faulty. +- SW will attempt to shut down heaters which are on but marked faulty +- Some simplifications for `HeaterHandler`, use `std::vector` instead of `std::unordered_map` for primary container. Using the heater indexes 0 to 7 allows to use natural array indexing +- Some additional input sanity checks in `executeAction` + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/236 + +## Changed + +- CCSDS handler improvements + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/268 +- Build unittest as default side product of hosted builds + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/244 +- Let CI/CD build host build and run unittest side product in same step +- Catch2 pre-installed in CI/CD docker container, Xiphos SDK installed in CI/CD docker + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/247 +- Sun Sensors have names denoting their location and poiting in the satellite now + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/245 +- Better RTD names denoting their purpose (and location consequently) + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/246 + +# [v1.11.0] + +## Fixed + +- Host build working again + +## Added + +- Custom Syrlinks FDIR which disabled most of the default FDIR functionality + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/232 +- Custom Gomspace FDIR which disabled most of the default FDIR functionality +- Custom Syrlinks FDIR which disabled most of the default FDIR functionality + +## Changed + +- PCDU handler only called once in PST, but can handle multiple messages now + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/221 + Bugfix: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/235 +- Update rootfs base of Linux, all related OBSW changes +- Add `/usr/local/bin` to PATH. All shell scripts are there now +- Add Syrlinks and TMP devices to Software by default +- Update GPS Linux Hyperion Handler to use socket interface. Still allows switching + back to SHM interface, but the SHM interface is a possible cause of SW crashes +- Updated code for changed FSFW HAL GPIO API: `readGpio` prototype has changed + PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/240 and + https://egit.irs.uni-stuttgart.de/eive/fsfw/pulls/76 + +### GPS + +PRs: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/239 + +- Rename GPS device to `/dev/gps0` +- Use gpsd version 3.17 now. Includes API changes + +### EM and FM splitup & Build Workflow improvements + +PR: https://egit.irs.uni-stuttgart.de/eive/eive-obsw/pulls/238 + +- Split up `bsp_q7s` in separate EM and FM build with module loading set to different + default values. The EM object factory is unique which allows building a parallel setup + with dummy components +- All major BSPs have an own `OBSWConfig.h.in` file which simplifies the file significantly +- Renamed Q7S primary build folders: + - `cmake-build-debug-q7s` for primary development build + - `cmake-build-release-q7s` for primary release build + - `cmake-build-debug-q7s-em` for primary development build of the EM software + - `cmake-build-release-q7s-em` for primary release build of the EM software +- Refactored Q7S helper script handling. It is now intended and preferred to copy the environment + script to the same folder level as the `eive-obsw` and source it. This will also + add the path containing the shell helper scripts to `PATH` +- The actual helper shell scripts were renamed as well to `q7s--.sh` + +# [v1.10.1] + +Version bump + +# [v1.10.0] + +For all releases equal or prior to v1.10.0, +see [milestones](https://egit.irs.uni-stuttgart.de/eive/eive-obsw/milestones) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2914b88 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,581 @@ +# ############################################################################## +# CMake support for the EIVE OBSW +# +# Author: R. Mueller +# ############################################################################## + +# ############################################################################## +# Pre-Project preparation +# ############################################################################## +cmake_minimum_required(VERSION 3.13) + +set(OBSW_VERSION_MAJOR 8) +set(OBSW_VERSION_MINOR 2) +set(OBSW_VERSION_REVISION 1) + +# set(CMAKE_VERBOSE TRUE) + +option( + EIVE_HARDCODED_TOOLCHAIN_FILE + "\ +For Linux Board Target BSPs, a default toolchain file will be set. Should be set to OFF \ +if a different toolchain file is set externally" + ON) + +if(NOT FSFW_OSAL) + set(FSFW_OSAL + linux + CACHE STRING "OS for the FSFW.") +endif() + +if(TGT_BSP) + if(TGT_BSP MATCHES "arm/q7s" + OR TGT_BSP MATCHES "arm/raspberrypi" + OR TGT_BSP MATCHES "arm/beagleboneblack") + option(LINUX_CROSS_COMPILE ON) + endif() + if(TGT_BSP MATCHES "arm/raspberrypi" OR TGT_BSP MATCHES "arm/beagleboneblack") + option(EIVE_BUILD_GPSD_GPS_HANDLER "Build GPSD dependent GPS Handler" OFF) + elseif(TGT_BSP MATCHES "arm/q7s") + option(EIVE_Q7S_EM "Build configuration for the EM" OFF) + option(EIVE_BUILD_GPSD_GPS_HANDLER "Build GPSD dependent GPS Handler" ON) + endif() + option(EIVE_CREATE_UNIQUE_OBSW_BIN "Append username to generated binary name" + ON) +else() + option(EIVE_CREATE_UNIQUE_OBSW_BIN "Append username to generated binary name" + OFF) +endif() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# Perform steps like loading toolchain files where applicable. +include(PreProjectConfig) +pre_project_config() + +# Project Name +project(eive-obsw) + +# Specify the C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +include(EiveHelpers) + +option(EIVE_ADD_ETL_LIB "Add ETL library" ON) +option(EIVE_ADD_JSON_LIB "Add JSON library" ON) + +set(OBSW_MAX_SCHEDULED_TCS 4000) + +if(EIVE_Q7S_EM) + set(OBSW_Q7S_EM + 1 + CACHE STRING "Q7S EM configuration") + set(OBSW_Q7S_FM 0) + set(OBSW_STAR_TRACKER_GROUND_CONFIG 1) +else() + set(OBSW_Q7S_EM + 0 + CACHE STRING "Q7S EM configuration") + set(OBSW_Q7S_FM 1) + set(OBSW_STAR_TRACKER_GROUND_CONFIG 0) +endif() + +set(OBSW_ADD_TMTC_TCP_SERVER + ${OBSW_Q7S_EM} + CACHE STRING "Add TCP TMTC Server") +set(OBSW_ADD_TMTC_UDP_SERVER + 0 + CACHE STRING "Add UDP TMTC Server") +set(OBSW_ADD_MGT + ${OBSW_Q7S_FM} + CACHE STRING "Add MGT module") +set(OBSW_ADD_BPX_BATTERY_HANDLER + ${OBSW_Q7S_FM} + CACHE STRING "Add BPX battery module") +set(OBSW_ADD_STAR_TRACKER + 1 + CACHE STRING "Add Startracker module") +set(OBSW_ADD_SUN_SENSORS + ${OBSW_Q7S_FM} + CACHE STRING "Add sun sensor module") +set(OBSW_ADD_SUS_BOARD_ASS + ${OBSW_Q7S_FM} + CACHE STRING "Add sun sensor board assembly") +set(OBSW_ADD_THERMAL_TEMP_INSERTER + ${OBSW_Q7S_EM} + CACHE STRING "Add thermal sensor temperature inserter") +set(OBSW_ADD_ACS_BOARD + 1 + CACHE STRING "Add ACS board module") +set(OBSW_ADD_GPS_CTRL + ${OBSW_Q7S_FM} + CACHE STRING "Add GPS controllers") +set(OBSW_ADD_CCSDS_IP_CORES + 1 + CACHE STRING "Add CCSDS IP cores") +set(OBSW_TM_TO_PTME + 1 + CACHE STRING "Send telemetry to PTME IP core") +set(OBSW_TC_FROM_PDEC + 1 + CACHE STRING "Poll telecommand from PDEC IP core") +set(OBSW_ADD_TCS_CTRL + 1 + CACHE STRING "Add TCS controllers") +set(OBSW_ADD_HEATERS + 1 + CACHE STRING "Add TCS heaters") +set(OBSW_ADD_PLOC_SUPERVISOR + 1 + CACHE STRING "Add PLOC supervisor handler") +set(OBSW_ADD_SA_DEPL + ${OBSW_Q7S_FM} + CACHE STRING "Add SA deployment handler") +set(OBSW_ADD_PLOC_MPSOC + 1 + CACHE STRING "Add MPSoC handler") +set(OBSW_ADD_ACS_CTRL + ${OBSW_Q7S_FM} + CACHE STRING "Add ACS controller") +set(OBSW_ADD_RTD_DEVICES + ${OBSW_Q7S_FM} + CACHE STRING "Add RTD devices") +set(OBSW_ADD_RAD_SENSORS + ${OBSW_Q7S_FM} + CACHE STRING "Add Rad Sensor module") +set(OBSW_ADD_PL_PCDU + 1 + CACHE STRING "Add Payload PCDU modukle") +set(OBSW_ADD_SYRLINKS + 1 + CACHE STRING "Add Syrlinks module") +set(OBSW_ADD_TMP_DEVICES + 1 + CACHE STRING "Add TMP devices") +set(OBSW_ADD_GOMSPACE_PCDU + 1 + CACHE STRING "Add GomSpace PCDU modules") +set(OBSW_ADD_GOMSPACE_ACU + ${OBSW_Q7S_FM} + CACHE STRING "Add GomSpace ACU submodule") +set(OBSW_ADD_RW + ${OBSW_Q7S_FM} + CACHE STRING "Add RW modules") +set(OBSW_ADD_SCEX_DEVICE + 1 + CACHE STRING "Add Solar Cell Experiment module") +set(OBSW_SYRLINKS_SIMULATED + 0 + CACHE STRING "Syrlinks is simulated") + +# ############################################################################## +# Pre-Sources preparation +# ############################################################################## + +# Version handling +set(GIT_VER_HANDLING_OK FALSE) +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git) + determine_version_with_git("--exclude" "docker_*") + set(GIT_INFO + ${GIT_INFO} + CACHE STRING "Version information retrieved with git describe") + if(GIT_INFO) + set(GIT_INFO + ${GIT_INFO} + CACHE STRING "Version information retrieved with git describe") + # CMakeLists.txt is now single source of information. list(GET GIT_INFO 1 + # OBSW_VERSION_MAJOR) list(GET GIT_INFO 2 OBSW_VERSION_MINOR) list(GET + # GIT_INFO 3 OBSW_VERSION_REVISION) + list(LENGTH GIT_INFO LIST_LEN) + if(LIST_LEN GREATER 4) + list(GET GIT_INFO 4 OBSW_VERSION_CST_GIT_SHA1) + endif() + set(GIT_VER_HANDLING_OK TRUE) + else() + set(GIT_VER_HANDLING_OK FALSE) + endif() +endif() + +# Set names and variables +set(OBSW_NAME ${CMAKE_PROJECT_NAME}) +set(WATCHDOG_NAME eive-watchdog) +set(SIMPLE_OBSW_NAME eive-simple) +set(UNITTEST_NAME eive-unittest) +set(LIB_FSFW_NAME fsfw) +set(LIB_EIVE_MISSION eive-mission) +set(LIB_ETL_TARGET etl::etl) +set(LIB_CSP_NAME libcsp) +set(LIB_LWGPS_NAME lwgps) +set(LIB_ARCSEC wire) +set(LIB_GOMSPACE_CLIENTS gs_clients) +set(LIB_GOMSPACE_CSP gs_csp) + +set(THIRD_PARTY_FOLDER thirdparty) +set(LIB_CXX_FS -lstdc++fs) +set(LIB_CATCH2 Catch2) +set(LIB_GPS gps) +set(LIB_JSON_NAME nlohmann_json::nlohmann_json) +set(LIB_DUMMIES dummies) + +# Set path names +set(FSFW_PATH fsfw) +set(TEST_PATH test) +set(UNITTEST_PATH unittest) +set(LINUX_PATH linux) +set(LIB_GOMSPACE_PATH ${THIRD_PARTY_FOLDER}/gomspace-sw) +set(COMMON_PATH common) +set(DUMMY_PATH dummies) +set(WATCHDOG_PATH watchdog) +set(COMMON_CONFIG_PATH ${COMMON_PATH}/config) +set(UNITTEST_CFG_PATH ${UNITTEST_PATH}/testcfg) + +set(LIB_EIVE_MISSION_PATH mission) +set(LIB_ETL_PATH ${THIRD_PARTY_FOLDER}/etl) +set(LIB_CATCH2_PATH ${THIRD_PARTY_FOLDER}/Catch2) +set(LIB_LWGPS_PATH ${THIRD_PARTY_FOLDER}/lwgps) +set(LIB_ARCSEC_PATH ${THIRD_PARTY_FOLDER}/sagittactl) +set(LIB_JSON_PATH ${THIRD_PARTY_FOLDER}/json) + +set(FSFW_WARNING_SHADOW_LOCAL_GCC OFF) +set(EIVE_ADD_LINUX_FILES OFF) +set(FSFW_ADD_TMSTORAGE ON) + +set(FSFW_ADD_COORDINATES ON) +set(FSFW_ADD_SGP4_PROPAGATOR ON) + +# Analyse different OS and architecture/target options, determine BSP_PATH, +# display information about compiler etc. +pre_source_hw_os_config() + +if(TGT_BSP) + set(LIBGPS_VERSION_MAJOR 3) + # I assume a newer version than 3.17 will be installed on other Linux board + # than the Q7S + set(LIBGPS_VERSION_MINOR 20) + if(TGT_BSP MATCHES "arm/q7s" + OR TGT_BSP MATCHES "arm/raspberrypi" + OR TGT_BSP MATCHES "arm/beagleboneblack" + OR TGT_BSP MATCHES "arm/egse" + OR TGT_BSP MATCHES "arm/te0720-1cfa") + find_library(${LIB_GPS} gps) + set(FSFW_CONFIG_PATH "linux/fsfwconfig") + if(NOT BUILD_Q7S_SIMPLE_MODE) + set(EIVE_ADD_LINUX_FILES TRUE) + set(EIVE_ADD_LINUX_FSFWCONFIG TRUE) + set(ADD_GOMSPACE_CSP TRUE) + set(ADD_GOMSPACE_CLIENTS TRUE) + set(FSFW_HAL_ADD_LINUX ON) + set(FSFW_HAL_LINUX_ADD_LIBGPIOD ON) + set(FSFW_HAL_LINUX_ADD_PERIPHERAL_DRIVERS ON) + endif() + elseif(UNIX) + set(EIVE_ADD_LINUX_FILES ON) + endif() + + if(TGT_BSP MATCHES "arm/raspberrypi") + # Used by configure file + set(RASPBERRY_PI ON) + set(FSFW_HAL_ADD_RASPBERRY_PI ON) + endif() + + if(TGT_BSP MATCHES "arm/egse") + # Used by configure file + set(EGSE ON) + set(FSFW_HAL_LINUX_ADD_LIBGPIOD OFF) + set(OBSW_ADD_STAR_TRACKER 1) + set(OBSW_DEBUG_STARTRACKER 1) + endif() + + if(TGT_BSP MATCHES "arm/beagleboneblack") + # Used by configure file + set(BEAGLEBONEBLACK ON) + endif() + + if(TGT_BSP MATCHES "arm/q7s") + # Used by configure file + set(XIPHOS_Q7S ON) + set(LIBGPS_VERSION_MAJOR 3) + set(LIBGPS_VERSION_MINOR 17) + endif() + + if(TGT_BSP MATCHES "arm/te0720-1cfa") + set(TE0720_1CFA ON) + endif() +else() + # Required by FSFW library + set(FSFW_CONFIG_PATH "${BSP_PATH}/fsfwconfig") + if(UNIX) + set(EIVE_ADD_LINUX_FILES ON) + endif() +endif() + +include(BuildType) +set_build_type() + +set(FSFW_DEBUG_INFO 0) +set(OBSW_ENABLE_PERIODIC_HK 1) +set(Q7S_CHECK_FOR_ALREADY_RUNNING_IMG 0) +if(RELEASE_BUILD MATCHES 0) + set(FSFW_DEBUG_INFO 1) + set(OBSW_ENABLE_PERIODIC_HK 0) + set(Q7S_CHECK_FOR_ALREADY_RUNNING_IMG 1) +endif() + +# Configuration files +configure_file(${COMMON_CONFIG_PATH}/commonConfig.h.in commonConfig.h) +configure_file(${FSFW_CONFIG_PATH}/FSFWConfig.h.in FSFWConfig.h) +configure_file(${BSP_PATH}/OBSWConfig.h.in OBSWConfig.h) +if(TGT_BSP MATCHES "arm/q7s") + configure_file(${BSP_PATH}/boardconfig/q7sConfig.h.in q7sConfig.h) +elseif(TGT_BSP MATCHES "arm/raspberrypi" OR TGT_BSP MATCHES "arm/egse") + configure_file(${BSP_PATH}/boardconfig/rpiConfig.h.in rpiConfig.h) +endif() + +configure_file(${WATCHDOG_PATH}/watchdogConf.h.in watchdogConf.h) + +# Set common config path for FSFW +set(FSFW_ADDITIONAL_INC_PATHS "${COMMON_PATH}/config" + ${CMAKE_CURRENT_BINARY_DIR}) + +# ############################################################################## +# Executable and Sources +# ############################################################################## + +# global compiler options need to be set before adding executables +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # Remove unused sections. + add_compile_options("-ffunction-sections" "-fdata-sections") + + # Removed unused sections. + add_link_options("-Wl,--gc-sections") + +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(COMPILER_FLAGS "/permissive-") +endif() + +add_library(${LIB_EIVE_MISSION}) +add_library(${LIB_DUMMIES}) + +# Add main executable +add_executable(${OBSW_NAME}) +set(OBSW_BIN_NAME ${CMAKE_PROJECT_NAME}) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(WARNING_FLAGS + "-Wall" + "-Wextra" + "-Wimplicit-fallthrough=1" + "-Wno-unused-parameter" + "-Wno-psabi" + "-Wshadow=local" + "-Wduplicated-cond" # check for duplicate conditions + "-Wduplicated-branches" # check for duplicate branches + "-Wlogical-op" # Search for bitwise operations instead of logical + "-Wnull-dereference" # Search for NULL dereference + "-Wundef" # Warn if undefind marcos are used + "-Wformat=2" # Format string problem detection + "-Wformat-overflow=2" # Formatting issues in printf + "-Wformat-truncation=2" # Formatting issues in printf + "-Wformat-security" # Search for dangerous printf operations + "-Wstrict-overflow=3" # Warn if integer overflows might happen + "-Warray-bounds=2" # Some array bounds violations will be found + "-Wshift-overflow=2" # Search for bit left shift overflows ( + + EIVE On-Board Software +====== + +# Index + +1. [General](#general) +2. [Prerequisites](#prereq) +3. [Building the Software](#build) +4. [Useful and Common Host Commands](#host-commands) +5. [Setting up Prerequisites](#set-up-prereq) +6. [Remote Debugging](#remote-debugging) +6. [Remote Reset](#remote-reset) +8. [TMTC testing](#tmtc-testing) +9. [Direct Debugging](#direct-debugging) +10. [Transfering Files to the Q7S](#file-transfer) +11. [Q7S OBC](#q7s) +12. [Static Code Analysis](#static-code-analysis) +13. [Eclipse](#eclipse) +14. [CLion](#clion) +14. [Running the OBSW on a Raspberry Pi](#rpi) +15. [Running OBSW on EGSE](#egse) +16. [Manually preparing sysroots to compile gpsd](#gpsd) +17. [FSFW](#fsfw) +18. [Coding Style](#coding-style) + +# General information + +Target systems: + +* OBC with Linux OS + * Xiphos Q7S + * Based on Zynq-7020 SoC (xc7z020clg484-2) + * Dual-core ARM Cortex-A9 + * 766 MHz + * Artix-7 FPGA (85K pogrammable logic cells) + * Datasheet at https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Arbeitsdaten/08_Used%20Components/Q7S&fileid=340648 + * Also a lot of information about the Q7S can be found on + the [Xiphos Traq Platform](https://trac2.xiphos.ca/eive-q7). Press on index to find all + relevant pages. The most recent datasheet can be found + [here](https://trac2.xiphos.ca/manual/wiki/Q7RevB/UserManual). + * Linux OS built with Yocto 2.5. SDK and root filesystem can be rebuilt with + [yocto](https://egit.irs.uni-stuttgart.de/eive/q7s-yocto) + * [Linux Kernel](https://github.com/XiphosSystemsCorp/linux-xlnx.git) . EIVE version can be found + [here](https://github.com/spacefisch/linux-xlnx) . Pre-compiled files can be + found [here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/q7s-linux-components&fileid=777299). + * Q7S base project can be found [here](https://egit.irs.uni-stuttgart.de/eive/q7s-base) + * Minimal base project files and Xiphos SDK can be found + [here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/xiphos-q7s-sdk&fileid=510908) +* Host System + * Generic software components which are not dependant on hardware can also + be run on a host system. All host code is contained in the `bsp_hosted` folder + * Tested for Linux (Ubuntu 20.04) and Windows 10 +* Raspberry Pi + * EIVE OBC can be built for Raspberry Pi as well (either directly on Raspberry Pi or by installing a cross compiler) + +The steps in the primary README are related to the main OBC target Q7S. +The CMake build system can be used to generate build systems as well (see helper scripts in `cmake/scripts`: + +- Linux Raspberry Pi: See special section below. Uses the `bsp_linux_board` folder +- Linux Trenz TE7020_1CFA: Uses the `bsp_te0720_1cfa` folder +- Linux Host: Uses the `bsp_hosted` BSP folder and the CMake Unix Makefiles generator. +- Windows Host: Uses the `bsp_hosted` BSP folder, the CMake MinGW Makefiles generator and MSYS2. + +# Prerequisites + +There is a separate [prerequisites](#set-up-prereq) which specifies how to set up all +prerequisites. + +## Building the OBSW and flashing it on the Q7S + +1. ARM cross-compiler installed, either as part of [Vivado 2018.2 installation](#vivado) or + as a [separate download](#arm-toolchain). The Xiphos SDK also installs a cross-compiler, + but its version is currently too old to compile the OBSW (7.3.0). +2. [Q7S sysroot](#sysroot) on local development machine. It is installed by the Xiphos SDK +3. Recommended: Eclipse or [Vivado 2018.2 SDK](#vivado) for OBSW development +3. [TCF agent](https://wiki.eclipse.org/TCF) running on Q7S + +## Hardware Design + +1. [Vivado 2018.2](#vivado) for programmable logic design + +# Building the software + +## CMake + +When using Windows, run theses steps in MSYS2. + +1. Clone the repository with + + ```sh + git clone https://egit.irs.uni-stuttgart.de/eive/eive-obsw.git + ``` + +2. Update all the submodules + + ```sh + git submodule update --init + ``` + +3. Create two system variables to pass the system root path and the cross-compiler path to the + build system. You only need to do this once when setting up the build system. + Example for Unix: + + ```sh + export CROSS_COMPILE_BIN_PATH= + export ZYNQ_7020_SYSROOT= + ``` + +4. Ensure that the cross-compiler is working with + `${CROSS_COMPILE_BIN_PATH}/arm-linux-gnueabihf-gcc --version` and that + the sysroot environmental variables have been set like specified in the + [root filesystem chapter](#sysroot). + +5. Run the CMake configuration to create the build system in a `build-Debug-Q7S` folder. + Add `-G "MinGW Makefiles` in MinGW64 on Windows. + + ```sh + mkdir cmake-build-debug-q7s && cd cmake-build-debug-q7s + cmake -DTGT_BSP="arm/q7s" -DCMAKE_BUILD_TYPE=Debug .. + cmake --build . -j + ``` + + Please note that you can also use provided shell scripts to perform these commands. + ```sh + cp scripts/q7s-env.sh .. + cp scripts/q7s-env-em.sh .. + ``` + + Adapt these scripts for your needs by editing the `CROSS_COMPILE_BIN_PATH` + and `ZYNQ_7020_SYSROOT`. After that, you can run the following commands to set up + the FM build + + ```sh + cd .. + ./q7s-env.sh + q7s-make-debug.sh + ``` + + You can build the EM setup by running + + ```sh + export EIVE_Q7S_EM=1 + ``` + + or by running the `q7s-env-em.sh` script instead before setting up the build + configuration. + + The shell scripts will invoke a Python script which in turn invokes CMake with the correct + arguments to configure CMake for Q7S cross-compilation. You can look into the command + output to see which commands were run exactly. + + There are also different values for `-DTGT_BSP` to build for the Raspberry Pi + or the Beagle Bone Black: `arm/raspberrypi` and `arm/beagleboneblack`. + +6. Build the software with + + ```sh + cd cmake-build-debug-q7s + cmake --build . -j + ``` + +## Preparing and executing an OBSW update + +A OBSW update consists of a `xz` compressed file `eive-sw-update.tar.xz` +which contains the following two files: + +1. Stripped OBSW binary `eive-obsw-stripped` +2. OBSW version text file with the name `obsw_version.txt` + +These files can be created manually: + +1. Build the release image inside `cmake-build-release-q7s` +2. Switch into the build directory +3. Run the following command to create the version file + + ```sh + git describe --tags --always --exclude docker_* > obsw_version.txt + ``` + + You can also use the `create-version-file.sh` helper shell script + located in the `scripts` folder to do this. + +4. Set the Q7S user as the file owner for both files + + ```sh + sudo chown root:root eive-obsw-stripped + sudo chown root:root obsw_version.txt + ``` + +5. Run the following command to create the compressed archive + + ```sh + tar -cJvf eive-sw-update.tar.xz eive-obsw-stripped obsw_version.txt + ``` + +You can also use the helper script `create-sw-update.sh` inside the build folder +after sourcing the `q7s-env.sh` helper script to perform all steps including +a rebuild. + +After creating these files, they need to be transferred onto the Q7S +to either the `/mnt/sd0/bin` or `/mnt/sd1/bin` folder if the OBSW update +is performed from the SD card. It can also be transferred to the `/tmp` folder +to perform the update from a temporary directory, which does not rely on any +of the SD cards being on and mounted. However, all files in the temporary +directory will be deleted if the Linux OS is rebooted for any reason. + +After both files are in place (this is checked by the OBSW), the example command +sequence is used by the OBSW to write the OBSW update to the QSPI chip 0 and +slot 0 using SD card 0: + +```sh +tar -xJvf eive-update.tar.xz +xsc_mount_copy 0 0 +cp eive-obsw-stripped /tmp/mntupdate-xdi-qspi0-nom-rootfs/usr/bin/eive-obsw +cp obsw_update.txt /tmp/mntupdate-xdi-qspi0-nom-rootfs/usr/share/obsw_update.txt +writeprotect 0 0 1 +``` + +Some context information about the used commands: + +1. It mounts the target chip and copy combination into the `/tmp` folder + using the `xsc_mount_copy ` utility. This also unlocks the + writeprotection for the chip. The mount point name inside `/tmp` depends + on which chip and copy is used + + - Chip 0 Copy 0: `/tmp/mntupdate-xdi-qspi0-nom-rootfs` + - Chip 0 Copy 1: `/tmp/mntupdate-xdi-qspi0-gold-rootfs` + - Slot 1 Copy 0: `/tmp/mntupdate-xdi-qspi1-nom-rootfs` + - Slot 1 Copy 1: `/tmp/mntupdate-xdi-qspi1-gold-rootfs` + +2. Writing the file with a regular `cp ` command +3. Enabling the writeprotection using the `writeprotect 1` utility. + +## Build for the Q7S target root filesystem with `yocto` + +The EIVE root filesystem will contain the EIVE OBSW and the Watchdog component. +It is currently generated with `yocto`, but the tool can not compile the primary +OBSW due to toolchain version incompatibility. Therefore, the OBSW components +are currently compiled using the toolchain specified in this README (e.g. installed by Vivado). + +However, it is still possible to install the two components using yocto. A few helper files were +provided to make this process easier. The following steps can be used to install the OBSW +components and a version file to the yocto sources for the generation of the complete EIVE root +file system image. The steps here are shown for Ubuntu, you can use the according Windows +helper scripts as well. + +1. Copy the `q7s-env.sh` script to the same layer as the `eive-obsw`. + + ```sh + cp scripts/q7s-env.sh .. + cd .. + ./q7s-env.sh + q7s-make-release.sh + ``` + +2. Compile the OBSW components in release mode + + ```sh + cd cmake-build-release-q7s + cmake --build . -j + ``` + +3. Make sure the [`q7s-yocto`](https://egit.irs.uni-stuttgart.de/eive/q7s-yocto) + repository or the [`q7s-package`](https://egit.irs.uni-stuttgart.de/eive/q7s-package.git) + repository and its `q7s-yocto` submodule were cloned in the same directory layer as + the `eive-obsw`. + +4. Run the install script to install the files into `q7s-yocto`. + + ```sh + install-obsw-yocto.sh + ``` + +5. Navigate into the `q7s-yocto` repo and review the changes. You can then add and push those + changes. + +6. You can now rebuild the root filesystem with the updated OBSW using `yocto`. This probably needs + to be done on another machine or in a VM. The [`q7s-yocto`](https://egit.irs.uni-stuttgart.de/eive/q7s-yocto) + repository contains details on how to best do this. + +## Building in Xilinx SDK 2018.2 + +1. Open Xilinx SDK 2018.2 +2. Import project + * File → Import → C/C++ → Existing Code as Makefile Project +3. Set build command. Replace \ with either debug or release. + * When on Linux right click project → Properties → C/C++ Build → Set build command to `make -j` + * -j causes the compiler to use all available cores + * The target is used to either compile the debug or the optimized release build. + * On windows create a make target additionally (Windows → Show View → Make Target) + * Right click eive_obsw → New + * Target name: all + * Uncheck "Same as the target name" + * Uncheck "Use builder settings" + * As build command type: `cmake --build .` + * In the Behaviour tab, you can enable build acceleration +4. Run build command by double clicking the created target or by right clicking + the project folder and selecting Build Project. + +# Useful and Common Commands + +## Build generation + +Replace `Debug` with `Release` for release build. Add `-G "MinGW Makefiles` or `-G "Ninja"` +on Windows or when `ninja` should be used. You can build with `cmake --build . -j` after +build generation. You can finds scripts in `cmake/scripts` to perform the build commands +automatically. + +### Q7S OBSW + +The EIVE OBSW is the default target if no target is specified. + +**Debug** + +```sh +mkdir cmake-build-debug-q7s && cd cmake-build-debug-q7s +cmake -DTGT_BSP=arm/q7s -DCMAKE_BUILD_TYPE=Debug .. +cmake --build . -j +``` + +**Release** + +```sh +mkdir cmake-build-release-q7s && cd cmake-build-release-q7s +cmake -DTGT_BSP=arm/q7s -DCMAKE_BUILD_TYPE=Release .. +cmake --build . -j +``` + +### Hosted OBSW + +You can also use the FSFW OSAL `host` to build on Windows or for generic OSes. +You can use the `clone-submodules-no-privlibs.sh` script to only clone the required (non-private) +submodules required to build the hosted OBSW. + +```sh +mkdir cmake-build-debug && cd cmake-build-debug +cmake -DFSFW_OSAL=host -DCMAKE_BUILD_TYPE=Debug .. +cmake --build . -j +``` + +You can also use the `linux` OSAL: + +```sh +mkdir cmake-build-debug && cd cmake-build-debug +cmake -DFSFW_OSAL=linux -DCMAKE_BUILD_TYPE=Debug .. +cmake --build . -j +``` + +Please note that some additional Linux setup might be necessary. +You can find more information in the [Linux section of the FSFW example](https://egit.irs.uni-stuttgart.de/fsfw/fsfw-example-linux-mcu/src/branch/mueller/master/doc/README-linux.md#raising-message-queue-size-limit) + +### Q7S Watchdog + +The watchdog will be built along side the primary OBSW binary. + +### Unittests + +To build the unittests, the corresponding target must be specified in the build command. +The configure steps do not need to be repeated if the folder has already been configured. + +```sh +mkdir cmake-build-debug && cd cmake-build-debug +cmake .. +cmake --build . --target eive-unittests -j +``` + +## Connect to EIVE flatsat + +### DNS + +```sh +ssh eive@flatsat.eive.absatvirt.lw +``` + +### IPv6 +```sh +ssh eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 +``` + +### IPv4 + +```sh +ssh eive@192.168.199.227 +``` + +## Connecting to the serial console or ssh console + +A serial console session is up permanently in a `tmux` session + +### Serial console + +You can check whether the sessions exist with `tmux ls`. +This is the command to connect to the serial interface of the FM using the +RS422 interface of the flight preparation panel: + +```sh +tmux a -t q7s-fm-fpp +``` + +If the session does not exist, you can create it like this + +```sh +tmux new -s q7s-fm-fpp -t /bin/bash +launch-q7s-fpp +``` + +Other useful tmux commands: +- Enable scroll mode: You can press `ctrl + b` and then `[` (`AltGr + 8`) to enable scroll mode. + You can quit scroll mode with `q`. +- Kill a tmux session: run `ctrl + b` and then `k`. +- Detach from a tmux session: run `ctrl + b` and then `d` +- [Pipe last 3000 lines](https://unix.stackexchange.com/questions/26548/write-all-tmux-scrollback-to-a-file) + into file for copying or analysis: + 1. `ctrl + b` and `:` + 2. `capture-pane -S -3000` + `enter` + 3. `save-buffer /tmp/tmux-output.txt` + `enter` + + +### SSH console + +You can use the following command to connect to the Q7S with `ssh`: + +```sh +q7s-fm-ssh +``` + +## Set up all port forwarding at once + +Port forwarding is necessary for remote-debugging using the `tcf-agent`, copying files +with `scp` & `q7s-cp.py` and sending TMTC commands. +You can specify the `-L` option multiple times to set up all port forwarding at once. + +```sh +ssh -L 1534:192.168.155.55:1534 \ + -L 1535:192.168.155.55:22 \ + -L 1536:192.168.155.55:7301 \ + -L 1537:127.0.0.1:7100 \ + -L 1538:192.168.133.10:1534 \ + -L 1539:192.168.133.10:22 \ + -L 1540:192.168.133.10:7301 \ + eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 \ + -t 'CONSOLE_PREFIX="[Q7S Tunnel]" /bin/bash' +``` + +There is also a shell script called `q7s-port.sh` which can be used to achieve the same. + +# Setting up prerequisites + +## Getting system root for Linux cross-compilation + +Cross-compiling any program for an embedded Linux board generally required parts of the target root +file system on the development/host computer. For the Q7S, you can install the cross-compilation +root file system by simply installing the SDK. You can find the most recent SDK +[here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/xiphos-q7s-sdk). + +If you are compiling for the Q7S or the TE7020, the `ZYNQ_7020_SYSROOT` environment variable +must be set to the location of the SDK compile sysroot. Here is an example on how to do this +in Ubuntu, assuming the SDK was installed in the default location + +```sh +export ZYNQ_7020_SYSROOT="/opt/xiphos/sdk/ark/sysroots/cortexa9hf-neon-xiphos-linux-gnueabi" +``` + +If you are comiling for the Raspberry Pi, you have to set the `LINUX_ROOTFS` environmental +variable instead. You can find a base root filesystem for the Raspberry Pi +[here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/rootfs). + +## Installing Vivado and the Xilinx development tools + +It's also possible to perform debugging with a normal Eclipse installation by installing +the TCF plugin and downloading the cross-compiler as specified in the section below. However, +if you want to generate the `*.xdi` files necessary to update the firmware, you need to +installed Vivado with the SDK core tools. + +* Install Vivado 2018.2 and + [Xilinx SDK](https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/vivado-design-tools/archive.html). + Install the Vivado Design Suite - HLx Editions - 2018.2 Full Product Installation instead of + the updates. It is recommended to use the installer. + +* Install settings. In the Devices selection, it is sufficient to pick SoC → Zynq-7000:
+ +
+ +
+ +
+ +* For supported OS refer to https://www.xilinx.com/support/documentation/sw_manuals/xilinx2018_2/ug973-vivado-release-notes-install-license.pdf . + Installation was tested on Windows and Ubuntu 21.04. +* Add path of linux cross-compiler to permanent environment variables (`.bashrc` file in Linux): + `\SDK\2018.2\gnu\aarch32\nt\gcc-arm-linux-gnueabi\bin` + or set up path each time before debugging. + +### Installing on Linux - Device List Issue + +When installing on Ubuntu, the installer might get stuck at the `Generating installed device list` +step. When this happens, you can kill the installation process (might be necessara to kill a process +twice) and generate this list manually with the following commands, according to +[this forum entry](https://forums.xilinx.com/t5/Installation-and-Licensing/Vivado-2018-3-Final-Processing-hangs-at-Generating-installed/m-p/972114#M25861). + +1. Install the following library + ```sh + sudo apt install libncurses5 + ``` + +2. Execute the following command + + ```sh + sudo /Vivado/2018.2/bin/vivado -nolog -nojournal -mode batch -source + /.xinstall/Vivado_2018.2/scripts/xlpartinfo.tcl -tclargs + /Vivado/2018.2/data/parts/installed_devices.txt + ``` + +For Linux, you can also download a more recent version of the +[Linaro 8.3.0 cross-compiler](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads) +from [here](https://developer.arm.com/-/media/Files/downloads/gnu-a/8.3-2019.03/binrel/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf.tar.xz?revision=e09a1c45-0ed3-4a8e-b06b-db3978fd8d56&la=en&hash=93ED4444B8B3A812B893373B490B90BBB28FD2E3) + +### Compatibility issues with wayland on more recent Linux distributions + +If Vivado crashes and you find following lines in the `hs_err_pid*` files: + +```sh +# +# An unexpected error has occurred (11) +# +Stack: +/opt/Xilinx/Vivado/2017.4/tps/lnx64/jre/lib/amd64/server/libjvm.so(+0x923da9) [0x7f666cf5eda9] +/opt/Xilinx/Vivado/2017.4/tps/lnx64/jre/lib/amd64/server/libjvm.so(JVM_handle_linux_signal+0xb6) [0x7f666cf653f6] +/opt/Xilinx/Vivado/2017.4/tps/lnx64/jre/lib/amd64/server/libjvm.so(+0x9209d3) [0x7f666cf5b9d3] +/lib/x86_64-linux-gnu/libc.so.6(+0x35fc0) [0x7f66a993efc0] +/opt/Xilinx/Vivado/2017.4/tps/lnx64/jre/lib/amd64/libawt_xawt.so(+0x42028) [0x7f664e24d028] +... +``` + +You can [solve this](https://forums.xilinx.com/t5/Design-Entry/Bug-Vivado-2017-4-crashing-on-rightclick-in-console-log/td-p/881811) +by logging in with `xorg` like specified [here](https://www.maketecheasier.com/switch-xorg-wayland-ubuntu1710/). + +### Using `docnav` on more recent Linux versions + +If you want to use `docnav` for navigating Xilinx documentation, it is recommended to install +it as a standalone version from [here](https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/documentation-nav.html). +This is because the `docnav` installed as part of version 2018.2 requires `libpng12`, which is not part of +more recent disitributions anymore. + +## Installing toolchain without Vivado + +You can download the toolchains for Windows and Linux +[from the EIVE cloud](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/tools). + +## Installing CMake and MSYS2 on Windows + +1. Install [MSYS2](https://www.msys2.org/) and [CMake](https://cmake.org/download/) first. + +2. Open the MinGW64 console. It is recommended to set up aliases in `.bashrc` to navigate to the + software repository quickly + +3. Run the following commands in MinGW64 + + ```sh + pacman -Syu + ``` + + It is recommended to install the full base development toolchain + + ```sh + pacman -S base-devel + ``` + + It is also possible to only install required packages + + ```sh + pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-make mingw-w64-x86_64-gcc mingw-w64-x86_64-gdb python3 + ``` + +## Installing CMake on Linux + +1. Run the following command + + ```sh + sudo apt-get install cmake + ```` + +### Updating system root for CI + +If the system root is updated, it needs to be manually updated on the buggy file server. +If access on `buggy.irs.uni-stuttgart.de` is possible with `ssh` and the rootfs in the cloud +[was updated](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/rootfs&fileid=831849) +as well, you can update the rootfs like this: + +```sh +cd /var/www/eive/tools +wget https://eive-cloud.irs.uni-stuttgart.de/index.php/s/SyXpdBBQX32xPgE/download/cortexa9hf-neon-xiphos-linux-gnueabi.tar.gz +``` + +## Setting up UNIX environment for real-time functionalities + +Please note that on most UNIX environments (e.g. Ubuntu), the real time functionalities +used by the UNIX pthread module are restricted, which will lead to permission errors when creating +these tasks and configuring real-time properites like scheduling priorities. + +To solve this issues, try following steps: + +1. Edit the /etc/security/limits.conf +file and add following lines at the end: +```sh + hard rtprio 99 + soft rtprio 99 +``` +The soft limit can also be set in the console with `ulimit -Sr` if the hard +limit has been increased, but it is recommended to add it to the file as well for convenience. +If adding the second line is not desired for security reasons, +the soft limit needs to be set for each session. If using an IDE like eclipse +in that case, the IDE needs to be started from the console after setting +the soft limit higher there. After adding the two lines to the file, +the computer needs to be restarted. + +It is also recommended to perform the following change so that the unlockRealtime +script does not need to be run anymore each time. The following steps +raise the maximum allowed message queue length to a higher number permanently, which is +required for some framework components. The recommended values for the new message +length is 130. + +2. Edit the /etc/sysctl.conf file + + ```sh + sudo nano /etc/sysctl.conf + ``` + + Append at end: + ```sh + fs/mqueue/msg_max = + ``` + + Apply changes with: + ```sh + sudo sysctl -p + ``` + + A possible solution which only persists for the current session is + ```sh + echo | sudo tee /proc/sys/fs/mqueue/msg_max + ``` + +## TCF-Agent + +Most of the steps specified here were already automated + +1. On reboot, some steps have to be taken on the Q7S. Set static IP address and netmask + + ```sh + ifconfig eth0 192.168.133.10 + ifconfig eth0 netmask 255.255.255.0 + ``` + +2. `tcfagent` application should run automatically but this can be checked with + ```sh + systemctl status tcfagent + ``` + +3. If the agent is not running, check whether `agent` is located inside `usr/bin`. + You can run it manually there. To perform auto-start on boot, have a look at the start-up + application section. + +# Remote Debugging + +Open SSH connection to flatsat PC: + +```sh +ssh eive@flatsat.eive.absatvirt.lw +``` + +or + +```sh +ssh eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 +``` + +or + +```sh +ssh eive@192.168.199.227 +``` + +If the static IP address of the Q7S has already been set, +you can access it with ssh + +```sh +ssh root@192.168.133.10 +``` + +If this has not been done yet, you can access the serial +console of the Q7S like this + +```sh +picocom -b 115200 /dev/q7sSerial +``` + +The flatsat has the aliases and shell scripts `q7s_ssh` and `q7s_serial` for this task as well. +If the serial port is blocked for some reason, you can kill +the process using it with `q7s_kill`. + +You can use `AltGr` + `X` to exit the picocom session. + +To debug an application, first make sure a static IP address is assigned to the Q7S. Run ifconfig +on the Q7S serial console. + +```sh +ifconfig +``` + +Set IP address and netmask with + +```sh +ifconfig eth0 192.168.133.10 +ifconfig eth0 netmask 255.255.255.0 +``` + +To launch application from Xilinx SDK setup port fowarding on the development machine +(not on the flatsat!) + +```sh +ssh -L 1534:192.168.133.10:1534 eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 -t bash +``` + +This forwards any requests to localhost:1534 to the port 1534 of the Q7S with the IP address +192.168.133.10. This needs to be done every time, so it is recommended to create an +alias or shell script to do this quickly. + +Note: When now setting up a debug session in the Xilinx SDK or Eclipse, the host must be set +to localhost instead of the IP address of the Q7S. + +# Remote Reset +1. Launch xilinx hardware server on flatsat with alias +```` +launch-hwserver-xilinx +```` +2. On host PC start xsc +3. In xsct console type the follwing command to connect to the hardware server (replace with the IP address of the flatsat PC. Can be found out with ifconfig) +```` +connect -url tcp::3121 +```` +4. The following command will list all available devices +```` +targets +```` +5. Connect to the APU of the Q7S +```` +target +```` +6. Perform reset +```` +rst +```` + +# TMTC testing + +The OBSW supports sending PUS TM packets via TCP or the PDEC IP Core which transmits the data as +CADU frames. To make the CADU frames receivabel by the +[TMTC porgram](https://egit.irs.uni-stuttgart.de/eive/eive-tmtc), a python script is running as +`systemd` service named `tmtc_bridge` on the flatsat PC which forwards TCP commands to the TCP +server of the OBC and reads CADU frames from a serial interface. + +You can check whether the service is running the following command on the flatsat PC + +```sh +systemctl status tmtc_bridge +``` + +The PUS packets transported with the CADU frames are extracted +and forwared to the TMTC program's TCP client. The code of the TMTC bridge can be found +[here](https://egit.irs.uni-stuttgart.de/eive/tmtc-bridge). To connect the TMTC program to the +TMTC-bridge a port forwarding from a host must be set up with the following command: + +```sh +ssh -L 1537:127.0.0.1:7100 eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 -t bash +``` + +You can print the output of the `systemd` service with + +```sh +journalctl -u tmtc_bridge +``` + +This can be helpful to determine whether any TCs arrive or TMs are coming back. + +Note: The encoding of the TM packets and conversion of CADU frames takes some time. +Thus the replies are received with a larger delay compared to a direct TCP connection. + +# Direct Debugging + +1. Assign static IP address to Q7S + * Open serial console of Q7S (Accessible via the micro-USB of the PIM, see also Q7S user + manual chapter 10.3) + * Baudrate 115200 + * Login to Q7S: + * user: root + * pw: root + +2. Connect Q7S to workstation via ethernet +3. Make sure the netmask of the ehternet interface of the workstation matches the netmask of the Q7S + * When IP address is set to 192.168.133.10 and the netmask is 255.255.255.0, an example IP address for the workstation + is 192.168.133.2 +4. Make sure th `tcf-agent` is running by checking `systemctl status tcf-agent` +5. In Xilinx SDK 2018.2 right click on project → Debug As → Debug Configurations +6. Right click Xilinx C/C++ applicaton (System Debugger) → New → +7. Set Debug Type to Linux Application Debug and Connectin to Linux Agent +8. Click New +9. Give connection a name +10. Set Host to static IP address of Q7S. e.g. 192.168.133.10 +11. Test connection (This ensures the TCF Agent is running on the Q7S) +12. Select Application tab + * Project Name: eive_obsw + * Local File Path: Path to OBSW application image with debug symbols (non-stripped) + * Remote File Path: `/tmp/` + +# Transfering Files to the Q7S + +To transfer files from the local machine to the Q7S, use port forwarding + +```sh +ssh -L 1535:192.168.133.10:22 eive@2001:7c0:2018:1099:babe:0:e1fe:f1a5 +``` + +An `example` file can be copied like this + +```sh +scp -P 1535 example root@localhost:/tmp +``` + +Copying a file from Q7S to flatsat PC +```` +scp -P 22 root@192.168.133.10:/tmp/kernel-config /tmp +```` + +From a windows machine files can be copied with putty tools (note: use IPv4 address) +```` +pscp -scp -P 22 eive@192.168.199.227:/example-file +```` + +A helper script named `q7s-cp.py` can be used together with the `q7s-port.sh` +script to make this process easier. + +# Q7S OBC + +## Launching an application at start-up - deprecated + +This way to enable auto-startup is deprecated. It is instead recommended to tweak the yocto +recipes file for the related `systemd` service to enable auto-startup with `SYSTEMD_AUTO_ENABLE`. + +You can also do the steps performed here on a host computer inside the `q7s-rootfs` directory +of the [Q7S base repository](https://egit.irs.uni-stuttgart.de/eive/q7s-base). This might +be more convenient while also allowing to update all images at once with the finished `rootfs.xdi`. + +Load the root partiton from the flash memory (there are to nor-flash memories and each flash holds +two xdi images). Note: It is not possible to modify the currently loaded root partition, e.g. +creating directories. To do this, the parition needs to be mounted. + +1. Disable write protection of the desired root partition + + ```sh + writeprotect 0 0 0 # unlocks nominal image on nor-flash 0 + ``` + +2. Mount the root partition + + ```sh + xsc_mount_copy 0 0 # Mounts the nominal image from nor-flash 0 + ``` + The mounted partition will be located inside the `/tmp` folder + +3. Copy the executable to `/usr/bin` + +4. Make sure the permissions to execute the application are set + + ```sh + chmod +x application + ``` + +5. Create systemd service in `/etc/systemd/system`. The following shows an example service. + + ```sh + cat > example.service + [Unit] + Description=Example Service + StartLimitIntervalSec=0 + + [Service] + Type=simple + Restart=always + RestartSec=1 + User=root + ExecStart=/usr/bin/application + + [Install] + WantedBy=multi-user.target + ``` + +6. Enable the service. This is normally done with `systemctl enable ` which would create + a symlink in the `multi-user.target.wants` directory. However, this is not possible + when the service is created for a mounted root partition. It is also not possible during run + time because symlinks can't be created in a read-only filesystem. Therefore, relative symlinks + are used like this: + + ```sh + cd etc/systemd/system/multi-user.target.wants/ + ln -s ../example.service example.service + ``` + + You can check the symlinnks with `ls -l` + +7. The modified root partition is written back when the partion is locked again. + ```sh + writeprotect 0 0 1 + ``` +8. Now verify the application start by booting from the modified image + ```sh + xsc_boot_copy 0 0 + ```` + +9. After booting verify if the service is running + ```sh + systemctl status example + ``` + +## Current user systemd services + +The following custom `systemd` services are currently running on the Q7S and can be found in +the `/etc/systemd/system` folder. +You can query that status of a service by running `systemctl status `. + +### `eive-watchdog` + +The watchdog will create a pipe at `/tmp/watchdog-pipe` which can be used both by the watchdog and +the EIVE OBSW. The watchdog will only read from this pipe while the OBSW will only write +to this pipe. The watchdog checks for basic ASCII commands as a first basic feature set. +The most important functionality is that the watchdog cant detect if a timeout +has happened. This can happen beause the OBSW is hanging (or at least the CoreController thread) or +there is simply now OBSW running on the system. It does to by checking whether the FIFO is +regulary written to, which means the EIVE OBSW is alive. + +If the EIVE OBSW is alive, a special file called `/tmp/obsw-running` will be created. +This file can be used by any other software component to query whether the EIVE OBSW is running. +The EIVE OBSW itself can be configured to check whether this file exists, which prevents two +EIVE OBSW instances running on the Q7S at once. + +If a timeout occurs, this special file will be deleted as well. +The watchdog and its configuration will be directly integrated into this repostory, which +makes adaptions easy. + +### `tcf-agent` + +This starts the `/usr/bin/tcf-agent` program to allows remote debugging + +### `eive-early-config` + +This is a configuration script which runs early after `local-fs.target` and `sysinit.target` +Currently only pipes the output of `xsc_boot_copy` into the file `/tmp/curr_copy.txt` which can be +used by other software components to read the current chip and copy. + +### `eive-post-ntpd-config` + +This is a configuration scripts which runs after the Network Time Protocol has run. This script +currently sets the static IP address `192.168.133.10` and starts the `can` interface. + +## Initial boot delay + +You can create a file named `boot_delays_secs.txt` inside the home folder to delay the OBSW boot +for 6 seconds if the file is empty of for the number of seconds specified inside the file. This +can be helpful if something inside the OBSW leads to an immediate crash of the OBC. + +## PCDU + +Connect to serial console of P60 Dock +```` +picocom -b 500000 /dev/ttyUSBx +```` +General information +```` +cmp ident +```` +List parameter table: +x values: 1,2 or 4 +```` +param list x +```` +Table 4 lists HK parameters +Changing parameters +First switch to table where parameter shall be changed (here table id is 1) +```` +p60-dock # param mem 1 +p60-dock # param set out_en[0] 1 +p60-dock # param get out_en[0] +GET out_en[0] = 1 +```` + +## Core commands + +Display currently running image: + +```sh +xsc_boot_copy +``` + +Rebooting currently running image: + +```sh +xsc_boot_copy -r +``` + +### Setting time on Q7S +Setting date and time (only timezone UTC available) +```` +timedatectl set-time 'YYYY-MM-DD HH:MM:SS' +```` +Setting UNIX time +```` +date +%s -s @1626337522 +```` +This only sets the system time and does not updating the time of the real time clock. To harmonize +the system time with the real time clock run +```` +hwclock -w +```` +Reading the real time clock +```` +hwclock --show +```` + +## pa3tool Host Tool + +The `pa3tool` is a host tool to interface with the ProASIC3 on the Q7S board. It was +installed on the clean room PC but it can also be found +[on the Traq platform](https://trac2.xiphos.ca/manual/attachment/wiki/WikiStart/libpa3-1.3.4.tar.gz). + +For more information, see Q7S datasheet. + +## Creating files with cat and echo + +The only folder which can be written in the root filesystem is the `tmp` folder. + +You can create a simple file with initial content with `echo` + +```sh +echo "Hallo Welt" > /tmp/test.txt +cat /tmp/test.txt +``` + +For more useful combinations, see this [link](https://www.freecodecamp.org/news/the-cat-command-in-linux-how-to-create-a-text-file-with-cat-or-touch/). + +## Using the scratch buffer of the ProASIC3 + +The ProASIC3 has a 1024 byte scratch buffer. The values in this scratch buffer will survive +a reboot, so this buffer can be used as an alternative to the SD cards to exchange information +between images or to store mission critical information. + +You can use `xsc_scratch --help` for more information. + +Write to scratch buffer: + +```sh +xsc_scratch write TEST "1" +``` + +Read from scratch buffer: + +```sh +xsc_scratch read TEST +``` + +Read all keys: + +```sh +xsc_scratch read + +``` + +Get fill count: + +```sh +xsc_scratch read | wc -c +``` + +## Custom device names in Linux with the `udev` module + +You can assign custom device names using the Linux `udev` system. +This works by specifying a rules file inside the `/etc/udev/rules.d` folder +which creates a SYMLINK if certain device properties are true. + +Each rule is a new line inside a rules file. +For example, the rule + +```txt +SUBSYSTEM=="tty", ATTRS{interface}=="Dual RS232-HS", ATTRS{bInterfaceNumber}=="01", SYMLINK+="ploc_supv +``` + +Will create a symlink `/dev/ploc_supv` if a connected USB device has the +same `interface` and `bInterfaceNumber` properties as shown above. + +You can list the properties for a given connected device using `udevadm`. +For example, you can do this for a connected example device `/dev/ttyUSB0` +by using + +```txt +udevadm info -a /dev/ttyUSB0 +``` + +## Using `system` when debugging + +Please note that when using a `system` call in C++/C code and debugging, a new thread will be +spawned which will appear on the left in Eclipse or Xilinx SDK as a `sh` program. +The debugger might attach to this child process automatically, depending on debugger configuration, +and the process needs to be selected and continued/started manually. You can enable or disable +this behaviour by selecting or deselecting the `Attach Process Children` option in the Remote +Application Configuration for the TCF plugin like shown in the following picture + +
+ +## Libgpiod + +Detect all gpio device files: +```` +gpiodetect +```` +Get info about a specific gpio group: +```` +gpioinfo +```` +The following sets the gpio 18 from gpio group gpiochip7 to high level. +```` +gpioset gpiochip7 18=1 +```` +Setting the gpio to low. +```` +gpioset gpiochip7 18=0 +```` +Show options for setting gpios. +```` +gpioset -h +```` +To get the state of a gpio: +```` +gpioget +```` +Example to get state: +gpioget gpiochip7 14 + +Both the MIOs and EMIOs can be accessed via the zynq_gpio instance which +comprises 118 pins (54 MIOs and 64 EMIOs). + +## Xilinx UARTLIE + +Get info about ttyUL* devices +```` +cat /proc/tty/driver +```` + +## I2C + +Getting information about some I2C device + +```sh +ls /sys/class/i2c-dev/i2c-0/device/device/driver +``` +This shows the memory mapping of `/dev/i2c-0`. + +You can use the `i2cdetect` utility to scan for I2C devices. +For example, to do this for bus 0 (`/dev/i2c-0`), you can use + +```sh +i2cdetect -r -y 0 +``` + +## CAN + +```sh +ip link set can0 down +ip link set can0 type can loopback off +ip link set can0 up type can bitrate 1000000 +``` + +Following command sends 8 bytes to device with id 99 (for petalinux) +```` +cansend can0 -i99 99 88 77 11 33 11 22 99 +```` +For Q7S use this: +```` +cansend can0 5A1#11.22.33.44.55.66.77.88 +```` +Turn loopback mode on: +```` +ip link set can0 type can bitrate 1000000 loopback on +```` +Reading data from CAN: +```` +candump can0 +```` + +## Dump content of file in hex +```` +cat file.bin | hexdump -C +```` +All content will be printed with +```` +cat file.bin | hexdump -v +```` +To print only the first X bytes of a file +```` +cat file.bin | hexdump -v -n X +```` + +## Preparation of a fresh rootfs and SD card + +See [q7s-package repository README](https://egit.irs.uni-stuttgart.de/eive/q7s-package) + +# Running cppcheck on the Software + +Static code analysis can be useful to find bugs. +`cppcheck` can be used for this purpose. On Windows you can use MinGW64 to do this. + +```sh +pacman -S mingw-w64-x86_64-cppcheck +``` + +On Ubuntu, install with + +```sh +sudo apt-get install cppcheck +``` + +You can use the Eclipse integration or you can perform the scanning manually from the command line. +CMake will be used for this. + +Run the CMake build generation commands specified above but supply +`-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to the build generation. Invoking the build command will +generate a `compile_commands.json` file which can be used by cppcheck. + +```sh +cppcheck --project=compile_commands.json --xml 2> report.xml +``` + +Finally, you can convert the generated `.xml` file to HTML with the following command + +```sh +cppcheck-htmlreport --file=report.xml --report-dir=cppcheck --source-dir=.. +``` + +# CLion + +CLion is the recommended IDE for the development of the hosted version of EIVE. +You can also set up CLion for cross-compilation of the primary OBSW. + +There is a shared `.idea/cmake.xml` file to get started with this. +To make cross-compilation work, two special environment variables +need to be set: + +- `ZYNQ_7020_ROOTFS` pointing to the root filesystem +- `CROSS_COMPILE` pointing to the the full path of the cross-compiler + without the specific tool suffix. For example, if the the cross-compiler + tools are located at `/opt/q7s-gcc/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin`, + this variable would be set + to `/opt/q7s-gcc/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf` + +# Eclipse + +When using Eclipse, there are two special build variables in the project properties +→ C/C++ Build → Build Variables called `Q7S_SYSROOT` or `RPI_SYSROOT`. You can set +the sysroot path in those variables to get any additional includes like `gpiod.h` in the +Eclipse indexer. + +## Setting up default Eclipse for Q7S projects - TCF agent + +The [TCF agent](https://wiki.eclipse.org/TCF) can be used to perform remote debugging on the Q7S. + +1. Copy the `.cproject` file and the `.project` file inside the `misc/eclipse` folder into the + repo root + + ```sh + cd eive-obsw + cp misc/eclipse/.cproject . + cp misc/eclipse/.project . + ``` + +2. Open the repo in Eclipse as a folder. + +3. Install the TCF agent plugin in Eclipse from + the [releases](https://www.eclipse.org/tcf/downloads.php). Go to + Help → Install New Software and use the download page, for + example https://download.eclipse.org/tools/tcf/releases/1.7/1.7.0/ to search for the plugin and + install it. You can find the newest version [here](https://www.eclipse.org/tcf/downloads.php) + +4. Go to Window → Perspective → Open Perspective and open the **Target Explorer Perspective**. + Here, the Q7S should show up if the local port forwarding was set up as explained previously. + Please note that you have to connect to `localhost` and port `1534` with port forwaring set up. + +5. A launch configuration was provided, but it might be necessary to adapt it for your own needs. + Alternatively: + + - Create a new **TCF Remote Application** by pressing the cogs button at the top or going to + Run → Debug Configurations → Remote Application and creating a new one there. + + - Set up the correct image in the main tab (it might be necessary to send the image to the + Q7S manually once) and file transfer properties + + - It is also recommended to link the correct Eclipse project. + +After that, comfortable remote debugging should be possible with the Debug button. + +A build configuration and a shell helper script has been provided to set up the path variables and +build the Q7S binary on Windows, but a launch configuration needs to be newly created because the +IP address and path settings differ from machine to machine. + +# Running the EIVE OBSW on a Raspberry Pi + +Special section for running the EIVE OBSW on the Raspberry Pi. +The Raspberry Pi build uses the `bsp_rpi` BSP folder, and a very similar cross-compiler. + +For running the software on a Raspberry Pi, it is recommended to follow the steps specified in +[the fsfw example](https://egit.irs.uni-stuttgart.de/fsfw/fsfw_example/src/branch/mueller/master/doc/README-rpi.md#top) +and using the TCF agent to have a similar set-up process also required for the Q7S. +You should run the following command first on your Raspberry Pi + +```sh +sudo apt-get install gpiod libgpiod-dev +``` + +to install the required GPIO libraries before cloning the system root folder. + +# Running OBSW on EGSE +The EGSE is a test system from arcsec build arround a raspberry pi 4 to test the star tracker. The IP address of the EGSE (raspberry pi) is 192.168.18.31. An ssh session can be opened with +```` +ssh pi@192.168.18.31 +```` +Password: raspberry + +To run the obsw perform the following steps: +1. Build the cmake EGSE Configuration + * the sysroots for the EGSE can be found [here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/egse&fileid=1190471) + * toolchain for linux host can be downloaded from [here](https://github.com/Pro/raspi-toolchain) + * toolchain for windows host from [here](https://gnutoolchains.com/raspberry/) (the raspios-buster-armhf toolchain is the right one for the EGSE) +2. Disable the ser2net systemd service on the EGSE +````sh +$ sudo systemctl stop ser2net.service +```` +3. Power on the star tracker by running +````sh +$ ~/powerctrl/enable0.sh` +```` +4. Run portforwarding script for tmtc tcp connection and tcf agent on host PC +````sh +$ ./scripts/egse-port.sh +```` +5. The star tracker can be powered off by running +````sh +$ ~/powerctrl/disable0.sh +```` + +# Manually preparing sysroots to compile gpsd +Copy all header files from [here](https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/Software/gpsd&fileid=1189985) to the /usr/include directory and all static libraries to /usr/lib. + +# Flight Software Framework (FSFW) + +An EIVE fork of the FSFW is submodules into this repository. +To add the master upstream branch and merge changes and updates from it +into the fork, run the following command in the fsfw folder first: + +```sh +git remote add upstream https://egit.irs.uni-stuttgart.de/fsfw/fsfw.git +git remote update --prune +``` + +After that, an update can be merged by running + +```sh +git merge upstream/master +``` + +Alternatively, changes from other upstreams (forks) and branches can be merged like that +in the same way. + +# Coding Style + +* the formatting is based on the clang-format tools + +## Setting up auto-formatter with clang-format in Xilinx SDK + +1. Help → Install New Software → Add +2. In location insert the link http://www.cppstyle.com/luna +3. The software package CppStyle should now be available for installation +4. On windows download the clang-formatting tools from https://llvm.org/builds/. On linux clang-format can be installed with the package manager. +5. Navigate to Preferences → C/C++ → CppStyle +6. Insert the path to the clang-format executable +7. Under C/C++ → Code Style → Formatter, change the formatter to CppStyle (clang-format) +8. Code can now be formatted with the clang tool by using the key combination Ctrl + Shift + f + +## Setting up auto-fromatter with clang-format in eclipse +1. Help → Eclipse market place → Search for "Cppstyle" and install +2. On windows download the clang-formatting tools from https://llvm.org/builds/. On linux clang-format can be installed with the package manager. +3. Navigate to Preferences → C/C++ → CppStyle +4. Insert the path to the clang-format executable +5. Under C/C++ → Code Style → Formatter, change the formatter to CppStyle (clang-format) +6. Code can now be formatted with the clang tool by using the key combination Ctrl + Shift + f diff --git a/archive/PlocMpsocHandler.cpp b/archive/PlocMpsocHandler.cpp new file mode 100644 index 0000000..ecb0bed --- /dev/null +++ b/archive/PlocMpsocHandler.cpp @@ -0,0 +1,1559 @@ +#include +#include +#include + +#include "OBSWConfig.h" +#include "fsfw/datapool/PoolReadGuard.h" +#include "fsfw/globalfunctions/CRC.h" +#include "fsfw/ipc/QueueFactory.h" +#include "fsfw/parameters/HasParametersIF.h" + +PlocMpsocHandler::PlocMpsocHandler(object_id_t objectId, object_id_t uartComIFid, + CookieIF* comCookie, + PlocMpsocSpecialComHelperLegacy* plocMPSoCHelper, + Gpio uartIsolatorSwitch, object_id_t supervisorHandler) + : DeviceHandlerBase(objectId, uartComIFid, comCookie), + hkReport(this), + specialComHelper(plocMPSoCHelper), + uartIsolatorSwitch(uartIsolatorSwitch), + supervisorHandler(supervisorHandler), + commandActionHelper(this) { + if (comCookie == nullptr) { + sif::error << "PlocMPSoCHandler: Invalid communication cookie" << std::endl; + } + eventQueue = QueueFactory::instance()->createMessageQueue(10); + commandActionHelperQueue = QueueFactory::instance()->createMessageQueue(10); + spParams.maxSize = sizeof(commandBuffer); + spParams.buf = commandBuffer; +} + +PlocMpsocHandler::~PlocMpsocHandler() {} + +ReturnValue_t PlocMpsocHandler::initialize() { + ReturnValue_t result = returnvalue::OK; + result = DeviceHandlerBase::initialize(); + if (result != returnvalue::OK) { + return result; + } + uartComIf = dynamic_cast(communicationInterface); + if (uartComIf == nullptr) { + sif::warning << "PlocMPSoCHandler::initialize: Invalid uart com if" << std::endl; + return ObjectManagerIF::CHILD_INIT_FAILED; + } + + EventManagerIF* manager = ObjectManager::instance()->get(objects::EVENT_MANAGER); + if (manager == nullptr) { +#if FSFW_CPP_OSTREAM_ENABLED == 1 + sif::error << "PlocMPSoCHandler::initialize: Invalid event manager" << std::endl; +#endif + return ObjectManagerIF::CHILD_INIT_FAILED; + ; + } + result = manager->registerListener(eventQueue->getId()); + if (result != returnvalue::OK) { + return result; + } + result = manager->subscribeToEvent( + eventQueue->getId(), + event::getEventId(PlocMpsocSpecialComHelperLegacy::MPSOC_FLASH_WRITE_FAILED)); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + result = manager->subscribeToEvent( + eventQueue->getId(), + event::getEventId(PlocMpsocSpecialComHelperLegacy::MPSOC_FLASH_WRITE_SUCCESSFUL)); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + result = manager->subscribeToEvent( + eventQueue->getId(), + event::getEventId(PlocMpsocSpecialComHelperLegacy::MPSOC_FLASH_READ_SUCCESSFUL)); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + result = manager->subscribeToEvent( + eventQueue->getId(), + event::getEventId(PlocMpsocSpecialComHelperLegacy::MPSOC_FLASH_READ_FAILED)); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + + result = specialComHelper->setComIF(communicationInterface); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + specialComHelper->setComCookie(comCookie); + specialComHelper->setSequenceCount(&sequenceCount); + result = commandActionHelper.initialize(); + if (result != returnvalue::OK) { + return ObjectManagerIF::CHILD_INIT_FAILED; + } + return result; +} + +void PlocMpsocHandler::performOperationHook() { + if (commandIsPending and cmdCountdown.hasTimedOut()) { + sif::warning << "PlocMpsocHandler: Command " << getPendingCommand() << " has timed out" + << std::endl; + commandIsPending = false; + // TODO: Better returnvalue? + cmdDoneHandler(false, returnvalue::FAILED); + } + EventMessage event; + for (ReturnValue_t result = eventQueue->receiveMessage(&event); result == returnvalue::OK; + result = eventQueue->receiveMessage(&event)) { + switch (event.getMessageId()) { + case EventMessage::EVENT_MESSAGE: + handleEvent(&event); + break; + default: + sif::debug << "PlocMPSoCHandler::performOperationHook: Did not subscribe to this event" + << " message" << std::endl; + break; + } + } + CommandMessage message; + for (ReturnValue_t result = commandActionHelperQueue->receiveMessage(&message); + result == returnvalue::OK; result = commandActionHelperQueue->receiveMessage(&message)) { + result = commandActionHelper.handleReply(&message); + if (result == returnvalue::OK) { + continue; + } + } +} + +ReturnValue_t PlocMpsocHandler::executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) { + ReturnValue_t result = returnvalue::OK; + switch (actionId) { + case mpsoc::SET_UART_TX_TRISTATE: { + uartIsolatorSwitch.pullLow(); + return EXECUTION_FINISHED; + break; + } + case mpsoc::RELEASE_UART_TX: { + uartIsolatorSwitch.pullHigh(); + return EXECUTION_FINISHED; + break; + default: + break; + } + } + + if (specialComHelperExecuting) { + return mpsoc::MPSOC_HELPER_EXECUTING; + } + + switch (actionId) { + case mpsoc::TC_FLASH_WRITE_FULL_FILE: { + mpsoc::FlashBasePusCmd flashWritePusCmd; + result = flashWritePusCmd.extractFields(data, size); + if (result != returnvalue::OK) { + return result; + } + result = specialComHelper->startFlashWrite(flashWritePusCmd.getObcFile(), + flashWritePusCmd.getMPSoCFile()); + if (result != returnvalue::OK) { + return result; + } + specialComHelperExecuting = true; + return EXECUTION_FINISHED; + } + case mpsoc::TC_FLASH_READ_FULL_FILE: { + mpsoc::FlashReadPusCmd flashReadPusCmd; + result = flashReadPusCmd.extractFields(data, size); + if (result != returnvalue::OK) { + return result; + } + result = specialComHelper->startFlashRead(flashReadPusCmd.getObcFile(), + flashReadPusCmd.getMPSoCFile(), + flashReadPusCmd.getReadSize()); + if (result != returnvalue::OK) { + return result; + } + specialComHelperExecuting = true; + return EXECUTION_FINISHED; + } + case (mpsoc::OBSW_RESET_SEQ_COUNT): { + sequenceCount = 0; + return EXECUTION_FINISHED; + } + default: + break; + } + // For longer commands, do not set these. + commandIsPending = true; + cmdCountdown.resetTimer(); + return DeviceHandlerBase::executeAction(actionId, commandedBy, data, size); +} + +void PlocMpsocHandler::doStartUp() { + if (startupState == StartupState::IDLE) { + startupState = StartupState::HW_INIT; + } + if (startupState == StartupState::HW_INIT) { + if (handleHwStartup()) { + startupState = StartupState::DONE; + } + } + if (startupState == StartupState::DONE) { + setMode(_MODE_TO_ON); + hkReport.setReportingEnabled(true); + powerState = PowerState::IDLE; + startupState = StartupState::IDLE; + } +} + +void PlocMpsocHandler::doShutDown() { + if (handleHwShutdown()) { + hkReport.setReportingEnabled(false); + setMode(_MODE_POWER_DOWN); + commandIsPending = false; + sequenceCount = 0; + powerState = PowerState::IDLE; + startupState = StartupState::IDLE; + } +} + +ReturnValue_t PlocMpsocHandler::buildNormalDeviceCommand(DeviceCommandId_t* id) { + if (not commandIsPending and not specialComHelperExecuting) { + *id = mpsoc::TC_GET_HK_REPORT; + commandIsPending = true; + return buildCommandFromCommand(*id, nullptr, 0); + } + return NOTHING_TO_SEND; +} + +ReturnValue_t PlocMpsocHandler::buildTransitionDeviceCommand(DeviceCommandId_t* id) { + return NOTHING_TO_SEND; +} + +ReturnValue_t PlocMpsocHandler::buildCommandFromCommand(DeviceCommandId_t deviceCommand, + const uint8_t* commandData, + size_t commandDataLen) { + spParams.buf = commandBuffer; + ReturnValue_t result = returnvalue::OK; + switch (deviceCommand) { + case (mpsoc::TC_MEM_WRITE): { + result = prepareTcMemWrite(commandData, commandDataLen); + break; + } + case (mpsoc::TC_MEM_READ): { + result = prepareTcMemRead(commandData, commandDataLen); + break; + } + case (mpsoc::TC_FLASHDELETE): { + result = prepareTcFlashDelete(commandData, commandDataLen); + break; + } + case (mpsoc::TC_REPLAY_START): { + result = prepareTcReplayStart(commandData, commandDataLen); + break; + } + case (mpsoc::TC_REPLAY_STOP): { + result = prepareTcReplayStop(); + break; + } + case (mpsoc::TC_DOWNLINK_PWR_ON): { + result = prepareTcDownlinkPwrOn(commandData, commandDataLen); + break; + } + case (mpsoc::TC_DOWNLINK_PWR_OFF): { + result = prepareTcDownlinkPwrOff(); + break; + } + case (mpsoc::TC_REPLAY_WRITE_SEQUENCE): { + result = prepareTcReplayWriteSequence(commandData, commandDataLen); + break; + } + case (mpsoc::TC_GET_HK_REPORT): { + result = prepareTcGetHkReport(); + break; + } + case (mpsoc::TC_FLASH_GET_DIRECTORY_CONTENT): { + result = prepareTcGetDirContent(commandData, commandDataLen); + break; + } + case (mpsoc::TC_MODE_REPLAY): { + result = prepareTcModeReplay(); + break; + } + case (mpsoc::TC_MODE_IDLE): { + result = prepareTcModeIdle(); + break; + } + case (mpsoc::TC_CAM_CMD_SEND): { + result = prepareTcCamCmdSend(commandData, commandDataLen); + break; + } + case (mpsoc::TC_CAM_TAKE_PIC): { + result = prepareTcCamTakePic(commandData, commandDataLen); + break; + } + case (mpsoc::TC_SIMPLEX_SEND_FILE): { + result = prepareTcSimplexSendFile(commandData, commandDataLen); + break; + } + case (mpsoc::TC_DOWNLINK_DATA_MODULATE): { + result = prepareTcDownlinkDataModulate(commandData, commandDataLen); + break; + } + case (mpsoc::TC_MODE_SNAPSHOT): { + result = prepareTcModeSnapshot(); + break; + } + default: + sif::debug << "PlocMPSoCHandler::buildCommandFromCommand: Command not implemented" + << std::endl; + result = DeviceHandlerIF::COMMAND_NOT_IMPLEMENTED; + break; + } + + if (result == returnvalue::OK) { + /** + * Flushing the receive buffer to make sure there are no data left from a faulty reply. + */ + uartComIf->flushUartRxBuffer(comCookie); + } + + return result; +} + +void PlocMpsocHandler::fillCommandAndReplyMap() { + this->insertInCommandMap(mpsoc::TC_MEM_WRITE); + this->insertInCommandMap(mpsoc::TC_MEM_READ); + this->insertInCommandMap(mpsoc::TC_FLASHDELETE); + insertInCommandMap(mpsoc::TC_FLASH_WRITE_FULL_FILE); + insertInCommandMap(mpsoc::TC_FLASH_READ_FULL_FILE); + this->insertInCommandMap(mpsoc::TC_REPLAY_START); + this->insertInCommandMap(mpsoc::TC_REPLAY_STOP); + this->insertInCommandMap(mpsoc::TC_DOWNLINK_PWR_ON); + this->insertInCommandMap(mpsoc::TC_DOWNLINK_PWR_OFF); + this->insertInCommandMap(mpsoc::TC_REPLAY_WRITE_SEQUENCE); + this->insertInCommandMap(mpsoc::TC_MODE_REPLAY); + this->insertInCommandMap(mpsoc::TC_MODE_IDLE); + this->insertInCommandMap(mpsoc::TC_CAM_CMD_SEND); + this->insertInCommandMap(mpsoc::TC_GET_HK_REPORT); + this->insertInCommandMap(mpsoc::RELEASE_UART_TX); + this->insertInCommandMap(mpsoc::SET_UART_TX_TRISTATE); + this->insertInCommandMap(mpsoc::TC_CAM_TAKE_PIC); + this->insertInCommandMap(mpsoc::TC_FLASH_GET_DIRECTORY_CONTENT); + this->insertInCommandMap(mpsoc::TC_SIMPLEX_SEND_FILE); + this->insertInCommandMap(mpsoc::TC_DOWNLINK_DATA_MODULATE); + this->insertInCommandMap(mpsoc::TC_MODE_SNAPSHOT); + this->insertInReplyMap(mpsoc::ACK_REPORT, 3, nullptr, mpsoc::SIZE_ACK_REPORT); + this->insertInReplyMap(mpsoc::EXE_REPORT, 3, nullptr, mpsoc::SIZE_EXE_REPORT); + this->insertInReplyMap(mpsoc::TM_MEMORY_READ_REPORT, 2, nullptr, mpsoc::SIZE_TM_MEM_READ_REPORT); + this->insertInReplyMap(mpsoc::TM_GET_HK_REPORT, 5, nullptr, mpsoc::SIZE_TM_HK_REPORT); + this->insertInReplyMap(mpsoc::TM_CAM_CMD_RPT, 2, nullptr, mpsoc::SP_MAX_SIZE); + this->insertInReplyMap(mpsoc::TM_FLASH_DIRECTORY_CONTENT, 2, nullptr, mpsoc::SP_MAX_SIZE); +} + +ReturnValue_t PlocMpsocHandler::scanForReply(const uint8_t* start, size_t remainingSize, + DeviceCommandId_t* foundId, size_t* foundLen) { + ReturnValue_t result = returnvalue::OK; + + SpacePacketReader spacePacket; + spacePacket.setReadOnlyData(start, remainingSize); + if (DEBUG_MPSOC_COMMUNICATION) { + sif::debug << "RECV MPSOC packet. APID 0x" << std::hex << std::setw(3) << spacePacket.getApid() + << std::dec << " Size " << spacePacket.getFullPacketLen() << " SSC " + << spacePacket.getSequenceCount() << std::endl; + } + if (spacePacket.isNull()) { + return returnvalue::FAILED; + } + auto res = spacePacket.checkSize(); + if (res != returnvalue::OK) { + return res; + } + uint16_t apid = spacePacket.getApid(); + + auto handleDedicatedReply = [&](DeviceCommandId_t replyId) { + *foundLen = spacePacket.getFullPacketLen(); + foundPacketLen = *foundLen; + *foundId = replyId; + }; + switch (apid) { + case (mpsoc::apid::ACK_SUCCESS): + *foundLen = mpsoc::SIZE_ACK_REPORT; + *foundId = mpsoc::ACK_REPORT; + break; + case (mpsoc::apid::ACK_FAILURE): + *foundLen = mpsoc::SIZE_ACK_REPORT; + *foundId = mpsoc::ACK_REPORT; + break; + case (mpsoc::apid::TM_MEMORY_READ_REPORT): + *foundLen = tmMemReadReport.rememberRequestedSize; + *foundId = mpsoc::TM_MEMORY_READ_REPORT; + break; + case (mpsoc::apid::TM_CAM_CMD_RPT): + handleDedicatedReply(mpsoc::TM_CAM_CMD_RPT); + break; + case (mpsoc::apid::TM_HK_GET_REPORT): { + handleDedicatedReply(mpsoc::TM_GET_HK_REPORT); + break; + } + case (mpsoc::apid::TM_FLASH_DIRECTORY_CONTENT): { + handleDedicatedReply(mpsoc::TM_FLASH_DIRECTORY_CONTENT); + break; + } + case (mpsoc::apid::EXE_SUCCESS): + *foundLen = mpsoc::SIZE_EXE_REPORT; + *foundId = mpsoc::EXE_REPORT; + break; + case (mpsoc::apid::EXE_FAILURE): + *foundLen = mpsoc::SIZE_EXE_REPORT; + *foundId = mpsoc::EXE_REPORT; + break; + default: { + sif::debug << "PlocMPSoCHandler::scanForReply: Reply has invalid APID 0x" << std::hex + << std::setfill('0') << std::setw(2) << apid << std::dec << std::endl; + *foundLen = remainingSize; + return mpsoc::INVALID_APID; + } + } + + uint16_t recvSeqCnt = ((*(start + 2) << 8) | *(start + 3)) & PACKET_SEQUENCE_COUNT_MASK; + if (recvSeqCnt != sequenceCount) { + triggerEvent(MPSOC_HANDLER_SEQUENCE_COUNT_MISMATCH, sequenceCount, recvSeqCnt); + sequenceCount = recvSeqCnt; + } + // This sequence count ping pong does not make any sense but it is how the MPSoC expects it. + sequenceCount++; + + return result; +} + +ReturnValue_t PlocMpsocHandler::interpretDeviceReply(DeviceCommandId_t id, const uint8_t* packet) { + ReturnValue_t result = returnvalue::OK; + + switch (id) { + case mpsoc::ACK_REPORT: { + result = handleAckReport(packet); + break; + } + case (mpsoc::TM_MEMORY_READ_REPORT): { + result = handleMemoryReadReport(packet); + break; + } + case (mpsoc::TM_GET_HK_REPORT): { + result = handleGetHkReport(packet); + break; + } + case (mpsoc::TM_CAM_CMD_RPT): { + result = handleCamCmdRpt(packet); + break; + } + case (mpsoc::TM_FLASH_DIRECTORY_CONTENT): { + result = verifyPacket(packet, foundPacketLen); + if (result == mpsoc::CRC_FAILURE) { + sif::warning << "PLOC MPSoC: Flash directory content reply invalid CRC" << std::endl; + } + /** Send data to commanding queue */ + handleDeviceTm(packet + mpsoc::DATA_FIELD_OFFSET, + foundPacketLen - mpsoc::DATA_FIELD_OFFSET - mpsoc::CRC_SIZE, + mpsoc::TM_FLASH_DIRECTORY_CONTENT); + nextReplyId = mpsoc::EXE_REPORT; + return result; + } + case (mpsoc::EXE_REPORT): { + result = handleExecutionReport(packet); + break; + } + default: { + sif::debug << "PlocMPSoCHandler::interpretDeviceReply: Unknown device reply id" << std::endl; + return DeviceHandlerIF::UNKNOWN_DEVICE_REPLY; + } + } + + return result; +} + +void PlocMpsocHandler::setNormalDatapoolEntriesInvalid() { + PoolReadGuard pg(&hkReport); + hkReport.setValidity(false, true); +} + +uint32_t PlocMpsocHandler::getTransitionDelayMs(Mode_t modeFrom, Mode_t modeTo) { return 15000; } + +ReturnValue_t PlocMpsocHandler::initializeLocalDataPool(localpool::DataPool& localDataPoolMap, + LocalDataPoolManager& poolManager) { + localDataPoolMap.emplace(mpsoc::poolid::STATUS, &peStatus); + localDataPoolMap.emplace(mpsoc::poolid::MODE, &peMode); + localDataPoolMap.emplace(mpsoc::poolid::DOWNLINK_PWR_ON, &peDownlinkPwrOn); + localDataPoolMap.emplace(mpsoc::poolid::DOWNLINK_REPLY_ACTIIVE, &peDownlinkReplyActive); + localDataPoolMap.emplace(mpsoc::poolid::DOWNLINK_JESD_SYNC_STATUS, &peDownlinkJesdSyncStatus); + localDataPoolMap.emplace(mpsoc::poolid::DOWNLINK_DAC_STATUS, &peDownlinkDacStatus); + localDataPoolMap.emplace(mpsoc::poolid::CAM_STATUS, &peCameraStatus); + localDataPoolMap.emplace(mpsoc::poolid::CAM_SDI_STATUS, &peCameraSdiStatus); + localDataPoolMap.emplace(mpsoc::poolid::CAM_FPGA_TEMP, &peCameraFpgaTemp); + localDataPoolMap.emplace(mpsoc::poolid::CAM_SOC_TEMP, &peCameraSocTemp); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_TEMP, &peSysmonTemp); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCINT, &peSysmonVccInt); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCAUX, &peSysmonVccAux); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCBRAM, &peSysmonVccBram); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCPAUX, &peSysmonVccPaux); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCPINT, &peSysmonVccPint); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCCPDRO, &peSysmonVccPdro); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_MB12V, &peSysmonMb12V); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_MB3V3, &peSysmonMb3V3); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_MB1V8, &peSysmonMb1V8); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC12V, &peSysmonVcc12V); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC5V, &peSysmonVcc5V); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC3V3, &peSysmonVcc3V3); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC3V3VA, &peSysmonVcc3V3VA); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC2V5DDR, &peSysmonVcc2V5DDR); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC1V2DDR, &peSysmonVcc1V2DDR); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC0V9, &peSysmonVcc0V9); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_VCC0V6VTT, &peSysmonVcc0V6VTT); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_SAFE_COTS_CUR, &peSysmonSafeCotsCur); + localDataPoolMap.emplace(mpsoc::poolid::SYSMON_NVM4_XO_CUR, &peSysmonNvm4XoCur); + localDataPoolMap.emplace(mpsoc::poolid::SEM_UNCORRECTABLE_ERRS, &peSemUncorrectableErrs); + localDataPoolMap.emplace(mpsoc::poolid::SEM_CORRECTABLE_ERRS, &peSemCorrectableErrs); + localDataPoolMap.emplace(mpsoc::poolid::SEM_STATUS, &peSemStatus); + localDataPoolMap.emplace(mpsoc::poolid::REBOOT_MPSOC_REQUIRED, &peRebootMpsocRequired); + poolManager.subscribeForRegularPeriodicPacket( + subdp::RegularHkPeriodicParams(hkReport.getSid(), false, 10.0)); + return returnvalue::OK; +} + +void PlocMpsocHandler::handleEvent(EventMessage* eventMessage) { + object_id_t objectId = eventMessage->getReporter(); + switch (objectId) { + case objects::PLOC_MPSOC_HELPER: { + specialComHelperExecuting = false; + break; + } + default: + sif::debug << "PlocMPSoCHandler::handleEvent: Did not subscribe to this event" << std::endl; + break; + } +} + +ReturnValue_t PlocMpsocHandler::prepareTcMemWrite(const uint8_t* commandData, + size_t commandDataLen) { + ReturnValue_t result = returnvalue::OK; + mpsoc::TcMemWrite tcMemWrite(spParams, sequenceCount); + result = tcMemWrite.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcMemWrite); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcMemRead(const uint8_t* commandData, + size_t commandDataLen) { + ReturnValue_t result = returnvalue::OK; + mpsoc::TcMemRead tcMemRead(spParams, sequenceCount); + result = tcMemRead.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcMemRead); + tmMemReadReport.rememberRequestedSize = tcMemRead.getMemLen() * 4 + TmMemReadReport::FIX_SIZE; + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcFlashDelete(const uint8_t* commandData, + size_t commandDataLen) { + if (commandDataLen > config::MAX_PATH_SIZE + config::MAX_FILENAME_SIZE) { + return mpsoc::NAME_TOO_LONG; + } + ReturnValue_t result = returnvalue::OK; + mpsoc::TcFlashDelete tcFlashDelete(spParams, sequenceCount); + std::string filename = std::string(reinterpret_cast(commandData), commandDataLen); + result = tcFlashDelete.setPayload(filename); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcFlashDelete); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcReplayStart(const uint8_t* commandData, + size_t commandDataLen) { + ReturnValue_t result = returnvalue::OK; + mpsoc::TcReplayStart tcReplayStart(spParams, sequenceCount); + result = tcReplayStart.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcReplayStart); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcReplayStop() { + mpsoc::TcReplayStop tcReplayStop(spParams, sequenceCount); + finishTcPrep(tcReplayStop); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcDownlinkPwrOn(const uint8_t* commandData, + size_t commandDataLen) { + ReturnValue_t result = returnvalue::OK; + mpsoc::TcDownlinkPwrOn tcDownlinkPwrOn(spParams, sequenceCount); + result = tcDownlinkPwrOn.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcDownlinkPwrOn); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcDownlinkPwrOff() { + mpsoc::TcDownlinkPwrOff tcDownlinkPwrOff(spParams, sequenceCount); + finishTcPrep(tcDownlinkPwrOff); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcGetHkReport() { + mpsoc::TcGetHkReport tcGetHkReport(spParams, sequenceCount); + finishTcPrep(tcGetHkReport); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcReplayWriteSequence(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcReplayWriteSeq tcReplayWriteSeq(spParams, sequenceCount); + ReturnValue_t result = tcReplayWriteSeq.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcReplayWriteSeq); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcModeReplay() { + mpsoc::TcModeReplay tcModeReplay(spParams, sequenceCount); + finishTcPrep(tcModeReplay); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcModeIdle() { + mpsoc::TcModeIdle tcModeIdle(spParams, sequenceCount); + finishTcPrep(tcModeIdle); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcCamCmdSend(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcCamcmdSend tcCamCmdSend(spParams, sequenceCount); + ReturnValue_t result = tcCamCmdSend.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcCamCmdSend); + nextReplyId = mpsoc::TM_CAM_CMD_RPT; + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcCamTakePic(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcCamTakePic tcCamTakePic(spParams, sequenceCount); + ReturnValue_t result = tcCamTakePic.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcCamTakePic); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcSimplexSendFile(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcSimplexSendFile tcSimplexSendFile(spParams, sequenceCount); + ReturnValue_t result = tcSimplexSendFile.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcSimplexSendFile); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcGetDirContent(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcGetDirContent tcGetDirContent(spParams, sequenceCount); + ReturnValue_t result = tcGetDirContent.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcGetDirContent); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcDownlinkDataModulate(const uint8_t* commandData, + size_t commandDataLen) { + mpsoc::TcDownlinkDataModulate tcDownlinkDataModulate(spParams, sequenceCount); + ReturnValue_t result = tcDownlinkDataModulate.setPayload(commandData, commandDataLen); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(tcDownlinkDataModulate); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::prepareTcModeSnapshot() { + mpsoc::TcModeSnapshot tcModeSnapshot(spParams, sequenceCount); + finishTcPrep(tcModeSnapshot); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::finishTcPrep(mpsoc::TcBase& tcBase) { + nextReplyId = mpsoc::ACK_REPORT; + ReturnValue_t result = tcBase.finishPacket(); + if (result != returnvalue::OK) { + return result; + } + rawPacket = commandBuffer; + rawPacketLen = tcBase.getFullPacketLen(); + sequenceCount++; + + if (DEBUG_MPSOC_COMMUNICATION) { + sif::debug << "SEND MPSOC packet. APID 0x" << std::hex << std::setw(3) << tcBase.getApid() + << " Size " << std::dec << tcBase.getFullPacketLen() << " SSC " + << tcBase.getSeqCount() << std::endl; + } + cmdCountdown.resetTimer(); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::verifyPacket(const uint8_t* start, size_t foundLen) { + if (CRC::crc16ccitt(start, foundLen) != 0) { + return mpsoc::CRC_FAILURE; + } + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::handleAckReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, mpsoc::SIZE_ACK_REPORT); + if (result == mpsoc::CRC_FAILURE) { + sif::warning << "PlocMPSoCHandler::handleAckReport: CRC failure" << std::endl; + nextReplyId = mpsoc::NONE; + replyRawReplyIfnotWiretapped(data, mpsoc::SIZE_ACK_REPORT); + triggerEvent(MPSOC_HANDLER_CRC_FAILURE); + sendFailureReport(mpsoc::ACK_REPORT, mpsoc::CRC_FAILURE); + disableAllReplies(); + return IGNORE_REPLY_DATA; + } + + uint16_t apid = (*(data) << 8 | *(data + 1)) & APID_MASK; + + switch (apid) { + case mpsoc::apid::ACK_FAILURE: { + DeviceCommandId_t commandId = getPendingCommand(); + uint16_t status = mpsoc::getStatusFromRawData(data); + sif::warning << "MPSoC ACK Failure: " << mpsoc::getStatusString(status) << std::endl; + if (commandId != DeviceHandlerIF::NO_COMMAND_ID) { + triggerEvent(ACK_FAILURE, commandId, status); + } + sendFailureReport(mpsoc::ACK_REPORT, status); + disableAllReplies(); + nextReplyId = mpsoc::NONE; + result = IGNORE_REPLY_DATA; + break; + } + case mpsoc::apid::ACK_SUCCESS: { + setNextReplyId(); + break; + } + default: { + sif::debug << "PlocMPSoCHandler::handleAckReport: Invalid APID in Ack report" << std::endl; + result = returnvalue::FAILED; + break; + } + } + + return result; +} + +ReturnValue_t PlocMpsocHandler::handleExecutionReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, mpsoc::SIZE_EXE_REPORT); + if (result == mpsoc::CRC_FAILURE) { + sif::warning << "PlocMPSoCHandler::handleExecutionReport: CRC failure" << std::endl; + nextReplyId = mpsoc::NONE; + return result; + } + + uint16_t apid = (*(data) << 8 | *(data + 1)) & APID_MASK; + + switch (apid) { + case (mpsoc::apid::EXE_SUCCESS): { + cmdDoneHandler(true, result); + break; + } + case (mpsoc::apid::EXE_FAILURE): { + DeviceCommandId_t commandId = getPendingCommand(); + if (commandId == DeviceHandlerIF::NO_COMMAND_ID) { + sif::debug << "PlocMPSoCHandler::handleExecutionReport: Unknown command id" << std::endl; + } + uint16_t status = mpsoc::getStatusFromRawData(data); + sif::warning << "MPSoC EXE Failure: " << mpsoc::getStatusString(status) << std::endl; + triggerEvent(EXE_FAILURE, commandId, status); + sendFailureReport(mpsoc::EXE_REPORT, mpsoc::RECEIVED_EXE_FAILURE); + result = IGNORE_REPLY_DATA; + cmdDoneHandler(false, mpsoc::RECEIVED_EXE_FAILURE); + break; + } + default: { + sif::warning << "PlocMPSoCHandler::handleExecutionReport: Unknown APID" << std::endl; + result = returnvalue::FAILED; + break; + } + } + nextReplyId = mpsoc::NONE; + return result; +} + +ReturnValue_t PlocMpsocHandler::handleMemoryReadReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + result = verifyPacket(data, tmMemReadReport.rememberRequestedSize); + if (result == mpsoc::CRC_FAILURE) { + sif::warning << "PlocMPSoCHandler::handleMemoryReadReport: Memory read report has invalid crc" + << std::endl; + } + uint16_t memLen = + *(data + mpsoc::MEM_READ_RPT_LEN_OFFSET) << 8 | *(data + mpsoc::MEM_READ_RPT_LEN_OFFSET + 1); + /** Send data to commanding queue */ + handleDeviceTm(data + mpsoc::DATA_FIELD_OFFSET, mpsoc::SIZE_MEM_READ_RPT_FIX + memLen * 4, + mpsoc::TM_MEMORY_READ_REPORT); + nextReplyId = mpsoc::EXE_REPORT; + return result; +} + +ReturnValue_t PlocMpsocHandler::handleGetHkReport(const uint8_t* data) { + ReturnValue_t result = verifyPacket(data, foundPacketLen); + if (result != returnvalue::OK) { + return result; + } + SpacePacketReader packetReader(data, foundPacketLen); + const uint8_t* dataStart = data + 6; + PoolReadGuard pg(&hkReport); + size_t deserLen = mpsoc::SIZE_TM_HK_REPORT; + SerializeIF::Endianness endianness = SerializeIF::Endianness::NETWORK; + result = SerializeAdapter::deSerialize(&hkReport.status.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.mode.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.downlinkPwrOn.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.downlinkReplyActive.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.downlinkJesdSyncStatus.value, &dataStart, + &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.downlinkDacStatus.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.camStatus.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.camSdiStatus.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.camFpgaTemp.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.sysmonTemp.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccInt.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccAux.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccBram.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccPaux.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccPint.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVccPdro.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.sysmonMb12V.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.sysmonMb3V3.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.sysmonMb1V8.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc12V.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.sysmonVcc5V.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc3V3.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc3V3VA.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc2V5DDR.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc1V2DDR.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc0V9.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonVcc0V6VTT.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonSafeCotsCur.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.sysmonNvm4XoCur.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.semUncorrectableErrs.value, &dataStart, + &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + result = SerializeAdapter::deSerialize(&hkReport.semCorrectableErrs.value, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + result = + SerializeAdapter::deSerialize(&hkReport.semStatus.value, &dataStart, &deserLen, endianness); + if (result != returnvalue::OK) { + return result; + } + // Skip the weird filename + dataStart += 256; + result = SerializeAdapter::deSerialize(&hkReport.rebootMpsocRequired, &dataStart, &deserLen, + endianness); + if (result != returnvalue::OK) { + return result; + } + hkReport.setValidity(true, true); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocHandler::handleCamCmdRpt(const uint8_t* data) { + ReturnValue_t result = verifyPacket(data, foundPacketLen); + if (result == mpsoc::CRC_FAILURE) { + sif::warning << "PlocMPSoCHandler::handleCamCmdRpt: CRC failure" << std::endl; + } + SpacePacketReader packetReader(data, foundPacketLen); + const uint8_t* dataFieldPtr = data + mpsoc::SPACE_PACKET_HEADER_SIZE + sizeof(uint16_t); + std::string camCmdRptMsg(reinterpret_cast(dataFieldPtr), + foundPacketLen - mpsoc::SPACE_PACKET_HEADER_SIZE - sizeof(uint16_t) - 3); +#if OBSW_DEBUG_PLOC_MPSOC == 1 + uint8_t ackValue = *(packetReader.getFullData() + packetReader.getFullPacketLen() - 2); + sif::info << "PlocMPSoCHandler: CamCmdRpt message: " << camCmdRptMsg << std::endl; + sif::info << "PlocMPSoCHandler: CamCmdRpt Ack value: 0x" << std::hex + << static_cast(ackValue) << std::endl; +#endif /* OBSW_DEBUG_PLOC_MPSOC == 1 */ + handleDeviceTm(packetReader.getPacketData() + sizeof(uint16_t), + packetReader.getPacketDataLen() - 1, mpsoc::TM_CAM_CMD_RPT); + return result; +} + +ReturnValue_t PlocMpsocHandler::enableReplyInReplyMap(DeviceCommandMap::iterator command, + uint8_t expectedReplies, bool useAlternateId, + DeviceCommandId_t alternateReplyID) { + ReturnValue_t result = returnvalue::OK; + + uint8_t enabledReplies = 0; + + auto enableThreeReplies = [&](DeviceCommandId_t replyId) { + enabledReplies = 3; + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, replyId); + if (result != returnvalue::OK) { + sif::debug << "PlocMPSoCHandler::enableReplyInReplyMap: Reply with id " + << mpsoc::TM_MEMORY_READ_REPORT << " not in replyMap" << std::endl; + return result; + } + return returnvalue::OK; + }; + switch (command->first) { + case mpsoc::TC_MEM_WRITE: + case mpsoc::TC_FLASHDELETE: + case mpsoc::TC_REPLAY_START: + case mpsoc::TC_REPLAY_STOP: + case mpsoc::TC_DOWNLINK_PWR_ON: + case mpsoc::TC_DOWNLINK_PWR_OFF: + case mpsoc::TC_REPLAY_WRITE_SEQUENCE: + case mpsoc::TC_MODE_REPLAY: + case mpsoc::TC_MODE_IDLE: + case mpsoc::TC_CAM_TAKE_PIC: + case mpsoc::TC_SIMPLEX_SEND_FILE: + case mpsoc::TC_DOWNLINK_DATA_MODULATE: + case mpsoc::TC_MODE_SNAPSHOT: + enabledReplies = 2; + break; + case mpsoc::TC_GET_HK_REPORT: { + result = enableThreeReplies(mpsoc::TM_GET_HK_REPORT); + if (result != returnvalue::OK) { + return result; + } + break; + } + case mpsoc::TC_MEM_READ: { + result = enableThreeReplies(mpsoc::TM_MEMORY_READ_REPORT); + if (result != returnvalue::OK) { + return result; + } + break; + } + case mpsoc::TC_CAM_CMD_SEND: { + result = enableThreeReplies(mpsoc::TM_CAM_CMD_RPT); + if (result != returnvalue::OK) { + return result; + } + break; + } + case mpsoc::TC_FLASH_GET_DIRECTORY_CONTENT: { + result = enableThreeReplies(mpsoc::TM_FLASH_DIRECTORY_CONTENT); + if (result != returnvalue::OK) { + return result; + } + break; + } + case mpsoc::OBSW_RESET_SEQ_COUNT: + break; + default: + sif::debug << "PlocMPSoCHandler::enableReplyInReplyMap: Unknown command id" << std::endl; + break; + } + + /** + * Every command causes at least one acknowledgment and one execution report. Therefore both + * replies will be enabled here. + */ + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, mpsoc::ACK_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocMPSoCHandler::enableReplyInReplyMap: Reply with id " << mpsoc::ACK_REPORT + << " not in replyMap" << std::endl; + } + + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, mpsoc::EXE_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocMPSoCHandler::enableReplyInReplyMap: Reply with id " << mpsoc::EXE_REPORT + << " not in replyMap" << std::endl; + } + + switch (command->first) { + case mpsoc::TC_REPLAY_WRITE_SEQUENCE: { + DeviceReplyIter iter = deviceReplyMap.find(mpsoc::EXE_REPORT); + // Overwrite delay cycles because replay write sequence command can required up to + // 30 seconds for execution + iter->second.delayCycles = mpsoc::TC_WRITE_SEQ_EXECUTION_DELAY; + break; + } + case mpsoc::TC_DOWNLINK_PWR_ON: { + DeviceReplyIter iter = deviceReplyMap.find(mpsoc::EXE_REPORT); + iter->second.delayCycles = mpsoc::TC_DOWNLINK_PWR_ON_EXECUTION_DELAY; + break; + } + case mpsoc::TC_CAM_TAKE_PIC: { + DeviceReplyIter iter = deviceReplyMap.find(mpsoc::EXE_REPORT); + iter->second.delayCycles = mpsoc::TC_CAM_TAKE_PIC_EXECUTION_DELAY; + break; + } + case mpsoc::TC_SIMPLEX_SEND_FILE: { + DeviceReplyIter iter = deviceReplyMap.find(mpsoc::EXE_REPORT); + iter->second.delayCycles = mpsoc::TC_SIMPLEX_SEND_FILE_DELAY; + break; + } + default: + break; + } + + return returnvalue::OK; +} + +void PlocMpsocHandler::setNextReplyId() { + switch (getPendingCommand()) { + case mpsoc::TC_MEM_READ: + nextReplyId = mpsoc::TM_MEMORY_READ_REPORT; + break; + case mpsoc::TC_FLASH_GET_DIRECTORY_CONTENT: { + nextReplyId = mpsoc::TM_FLASH_DIRECTORY_CONTENT; + break; + } + case mpsoc::TC_GET_HK_REPORT: { + nextReplyId = mpsoc::TM_GET_HK_REPORT; + break; + } + default: + /* If no telemetry is expected the next reply is always the execution report */ + nextReplyId = mpsoc::EXE_REPORT; + break; + } +} + +size_t PlocMpsocHandler::getNextReplyLength(DeviceCommandId_t commandId) { + size_t replyLen = 0; + + if (nextReplyId == mpsoc::NONE) { + return replyLen; + } + + DeviceReplyIter iter = deviceReplyMap.find(nextReplyId); + + if (iter != deviceReplyMap.end()) { + if (iter->second.delayCycles == 0) { + /* Reply inactive */ + return replyLen; + } + switch (nextReplyId) { + case mpsoc::TM_MEMORY_READ_REPORT: { + replyLen = tmMemReadReport.rememberRequestedSize; + break; + } + case mpsoc::TM_CAM_CMD_RPT: + // Read acknowledgment, camera and execution report in one go because length of camera + // report is not fixed + replyLen = mpsoc::SP_MAX_SIZE; + break; + case mpsoc::TM_FLASH_DIRECTORY_CONTENT: + // I think the reply size is not fixed either. + replyLen = mpsoc::SP_MAX_SIZE; + break; + default: { + replyLen = iter->second.replyLen; + break; + } + } + } else { + sif::debug << "PlocMPSoCHandler::getNextReplyLength: No entry for reply with reply id " + << std::hex << nextReplyId << " in deviceReplyMap" << std::endl; + } + + return replyLen; +} + +ReturnValue_t PlocMpsocHandler::doSendReadHook() { + // Prevent DHB from polling UART during commands executed by the mpsoc helper task + if (specialComHelperExecuting) { + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +MessageQueueIF* PlocMpsocHandler::getCommandQueuePtr() { return commandActionHelperQueue; } + +void PlocMpsocHandler::stepSuccessfulReceived(ActionId_t actionId, uint8_t step) { return; } + +void PlocMpsocHandler::stepFailedReceived(ActionId_t actionId, uint8_t step, + ReturnValue_t returnCode) { + switch (actionId) { + case supv::START_MPSOC: { + sif::warning << "PlocMPSoCHandler::stepFailedReceived: Failed to start MPSoC" << std::endl; + break; + } + case supv::SHUTDOWN_MPSOC: { + triggerEvent(MPSOC_SHUTDOWN_FAILED); + sif::warning << "PlocMPSoCHandler::stepFailedReceived: Failed to shutdown MPSoC" << std::endl; + break; + } + default: + sif::debug << "PlocMPSoCHandler::stepFailedReceived: Received unexpected action reply" + << std::endl; + break; + } + powerState = PowerState::SUPV_FAILED; +} + +void PlocMpsocHandler::dataReceived(ActionId_t actionId, const uint8_t* data, uint32_t size) { + return; +} + +void PlocMpsocHandler::completionSuccessfulReceived(ActionId_t actionId) { + if (actionId == supv::ACK_REPORT) { + // I seriously don't know why this happens.. + // sif::warning + // << "PlocMpsocHandler::completionSuccessfulReceived: Only received ACK report. Consider + // " + // "increasing the MPSoC boot timer." + // << std::endl; + } else if (actionId != supv::EXE_REPORT) { + sif::warning << "PlocMpsocHandler::completionSuccessfulReceived: Did not expect the action " + << "ID " << actionId << std::endl; + return; + } + switch (powerState) { + case PowerState::PENDING_STARTUP: { + mpsocBootTransitionCd.resetTimer(); + powerState = PowerState::DONE; + break; + } + case PowerState::PENDING_SHUTDOWN: { + powerState = PowerState::DONE; + break; + } + default: { + break; + } + } +} + +void PlocMpsocHandler::completionFailedReceived(ActionId_t actionId, ReturnValue_t returnCode) { + handleActionCommandFailure(actionId); +} + +void PlocMpsocHandler::handleDeviceTm(const uint8_t* data, size_t dataSize, + DeviceCommandId_t replyId) { + ReturnValue_t result = returnvalue::OK; + + if (wiretappingMode == RAW) { + /* Data already sent in doGetRead() */ + return; + } + + DeviceReplyMap::iterator iter = deviceReplyMap.find(replyId); + if (iter == deviceReplyMap.end()) { + sif::debug << "PlocMPSoCHandler::handleDeviceTM: Unknown reply id" << std::endl; + return; + } + MessageQueueId_t queueId = iter->second.command->second.sendReplyTo; + + if (queueId == NO_COMMANDER) { + return; + } + + result = actionHelper.reportData(queueId, replyId, data, dataSize); + if (result != returnvalue::OK) { + sif::debug << "PlocMPSoCHandler::handleDeviceTM: Failed to report data" << std::endl; + } +} + +void PlocMpsocHandler::disableAllReplies() { + using namespace mpsoc; + DeviceReplyMap::iterator iter; + + /* Disable ack reply */ + iter = deviceReplyMap.find(ACK_REPORT); + DeviceReplyInfo* info = &(iter->second); + info->delayCycles = 0; + info->command = deviceCommandMap.end(); + + DeviceCommandId_t commandId = getPendingCommand(); + + auto disableCommandWithReply = [&](DeviceCommandId_t replyId) { + iter = deviceReplyMap.find(replyId); + info = &(iter->second); + info->delayCycles = 0; + info->active = false; + info->command = deviceCommandMap.end(); + }; + /* If the command expects a telemetry packet the appropriate tm reply will be disabled here */ + switch (commandId) { + case TC_MEM_WRITE: + case TC_FLASHDELETE: + case TC_REPLAY_START: + case TC_REPLAY_STOP: + case TC_DOWNLINK_PWR_ON: + case TC_DOWNLINK_PWR_OFF: + case TC_REPLAY_WRITE_SEQUENCE: + case TC_MODE_REPLAY: + case TC_MODE_IDLE: + case TC_CAM_TAKE_PIC: + case TC_SIMPLEX_SEND_FILE: + case TC_DOWNLINK_DATA_MODULATE: + case TC_MODE_SNAPSHOT: + break; + case TC_MEM_READ: { + disableCommandWithReply(TM_MEMORY_READ_REPORT); + break; + } + case TC_GET_HK_REPORT: { + disableCommandWithReply(TM_GET_HK_REPORT); + break; + } + case TC_FLASH_GET_DIRECTORY_CONTENT: { + disableCommandWithReply(TM_FLASH_DIRECTORY_CONTENT); + break; + } + case TC_CAM_CMD_SEND: { + disableCommandWithReply(TM_CAM_CMD_RPT); + break; + } + default: { + sif::debug << "PlocMPSoCHandler::disableAllReplies: Unknown command id: " << commandId + << std::endl; + break; + } + } + + /* We always need to disable the execution report reply here */ + disableExeReportReply(); + nextReplyId = mpsoc::NONE; +} + +void PlocMpsocHandler::sendFailureReport(DeviceCommandId_t replyId, ReturnValue_t status) { + DeviceReplyIter iter = deviceReplyMap.find(replyId); + if (iter == deviceReplyMap.end()) { + sif::debug << "PlocMPSoCHandler::sendFailureReport: Reply not in reply map" << std::endl; + return; + } + DeviceCommandInfo* info = &(iter->second.command->second); + if (info == nullptr) { + sif::debug << "PlocMPSoCHandler::sendFailureReport: Reply has no active command" << std::endl; + return; + } + if (info->sendReplyTo != NO_COMMANDER) { + actionHelper.finish(false, info->sendReplyTo, iter->first, status); + } + info->isExecuting = false; +} + +void PlocMpsocHandler::disableExeReportReply() { + DeviceReplyIter iter = deviceReplyMap.find(mpsoc::EXE_REPORT); + DeviceReplyInfo* info = &(iter->second); + info->delayCycles = 0; + info->command = deviceCommandMap.end(); + /* Expected replies is set to one here. The value will be set to 0 in replyToReply() */ + info->command->second.expectedReplies = 0; +} + +void PlocMpsocHandler::stopSpecialComHelper() { + if (specialComHelper != nullptr) { + specialComHelper->stopProcess(); + } + specialComHelperExecuting = false; +} + +bool PlocMpsocHandler::handleHwStartup() { +#if OBSW_MPSOC_JTAG_BOOT == 1 + uartIsolatorSwitch.pullHigh(); + startupState = StartupState::WAIT_CYCLES; + return true; +#endif + if (powerState == PowerState::IDLE) { + if (skipSupvCommandingToOn) { + powerState = PowerState::DONE; + } else { + if (supv::SUPV_ON) { + commandActionHelper.commandAction(supervisorHandler, supv::START_MPSOC); + supvTransitionCd.resetTimer(); + powerState = PowerState::PENDING_STARTUP; + } else { + triggerEvent(SUPV_NOT_ON, 1); + // Set back to OFF for now, failing the transition. + setMode(MODE_OFF); + } + } + } + if (powerState == PowerState::SUPV_FAILED) { + setMode(MODE_OFF); + powerState = PowerState::IDLE; + return false; + } + if (powerState == PowerState::PENDING_STARTUP) { + if (supvTransitionCd.hasTimedOut()) { + // Process with transition nonetheless.. + triggerEvent(SUPV_REPLY_TIMEOUT); + powerState = PowerState::DONE; + } else { + return false; + } + } + if (powerState == PowerState::DONE) { + if (mpsocBootTransitionCd.hasTimedOut()) { + // Wait a bit for the MPSoC to fully boot. + uartIsolatorSwitch.pullHigh(); + powerState = PowerState::IDLE; + } else { + return false; + } + } + return true; +} + +bool PlocMpsocHandler::handleHwShutdown() { + stopSpecialComHelper(); + uartIsolatorSwitch.pullLow(); +#if OBSW_MPSOC_JTAG_BOOT == 1 + powerState = PowerState::DONE; + return true; +#endif + + if (powerState == PowerState::IDLE) { + if (supv::SUPV_ON) { + commandActionHelper.commandAction(supervisorHandler, supv::SHUTDOWN_MPSOC); + supvTransitionCd.resetTimer(); + powerState = PowerState::PENDING_SHUTDOWN; + } else { + triggerEvent(SUPV_NOT_ON, 0); + powerState = PowerState::DONE; + } + } + if (powerState == PowerState::PENDING_SHUTDOWN) { + if (supvTransitionCd.hasTimedOut()) { + powerState = PowerState::DONE; + // Process with transition nonetheless.. + triggerEvent(SUPV_REPLY_TIMEOUT); + return true; + } else { + // Wait till power state is OFF. + return false; + } + } + return true; +} + +void PlocMpsocHandler::handleActionCommandFailure(ActionId_t actionId) { + switch (actionId) { + case supv::ACK_REPORT: + case supv::EXE_REPORT: + break; + default: + sif::warning << "PlocMPSoCHandler::handleActionCommandFailure: Did not expect the action ID " + << actionId << std::endl; + return; + } + switch (powerState) { + case PowerState::PENDING_STARTUP: { + sif::info << "PlocMPSoCHandler::handleActionCommandFailure: MPSoC boot command failed" + << std::endl; + // This is commonly the case when the MPSoC is already operational. Thus the power state is + // set to on here + break; + } + case PowerState::PENDING_SHUTDOWN: { + // FDIR will intercept event and switch PLOC power off + triggerEvent(MPSOC_SHUTDOWN_FAILED); + sif::warning << "PlocMPSoCHandler::handleActionCommandFailure: Failed to shutdown MPSoC" + << std::endl; + break; + } + default: + break; + } + powerState = PowerState::SUPV_FAILED; + return; +} + +LocalPoolDataSetBase* PlocMpsocHandler::getDataSetHandle(sid_t sid) { + if (sid == hkReport.getSid()) { + return &hkReport; + } + return nullptr; +} + +bool PlocMpsocHandler::dontCheckQueue() { + // The TC and TMs need to be handled strictly sequentially, so while a command is pending, + // more specifically while replies are still expected, do not check the queue. + return commandIsPending; +} + +void PlocMpsocHandler::cmdDoneHandler(bool success, ReturnValue_t result) { + commandIsPending = false; + auto commandIter = deviceCommandMap.find(getPendingCommand()); + if (commandIter != deviceCommandMap.end()) { + commandIter->second.isExecuting = false; + if (commandIter->second.sendReplyTo != MessageQueueIF::NO_QUEUE) { + actionHelper.finish(success, commandIter->second.sendReplyTo, getPendingCommand(), result); + } + } + disableAllReplies(); +} + +ReturnValue_t PlocMpsocHandler::checkModeCommand(Mode_t commandedMode, Submode_t commandedSubmode, + uint32_t* msToReachTheMode) { + if (commandedMode != MODE_OFF) { + PoolReadGuard pg(&enablePl); + if (pg.getReadResult() == returnvalue::OK) { + if (enablePl.plUseAllowed.isValid() and not enablePl.plUseAllowed.value) { + return NON_OP_STATE_OF_CHARGE; + } + } + } + return DeviceHandlerBase::checkModeCommand(commandedMode, commandedSubmode, msToReachTheMode); +} + +ReturnValue_t PlocMpsocHandler::getParameter(uint8_t domainId, uint8_t uniqueId, + ParameterWrapper* parameterWrapper, + const ParameterWrapper* newValues, + uint16_t startAtIndex) { + if (uniqueId == mpsoc::ParamId::SKIP_SUPV_ON_COMMANDING) { + uint8_t value = 0; + newValues->getElement(&value); + if (value > 1) { + return HasParametersIF::INVALID_VALUE; + } + parameterWrapper->set(skipSupvCommandingToOn); + return returnvalue::OK; + } + return DeviceHandlerBase::getParameter(domainId, uniqueId, parameterWrapper, newValues, + startAtIndex); +} diff --git a/archive/PlocMpsocHandler.h b/archive/PlocMpsocHandler.h new file mode 100644 index 0000000..f3f2485 --- /dev/null +++ b/archive/PlocMpsocHandler.h @@ -0,0 +1,322 @@ +#ifndef BSP_Q7S_DEVICES_PLOC_PLOCMPSOCHANDLER_H_ +#define BSP_Q7S_DEVICES_PLOC_PLOCMPSOCHANDLER_H_ + +#include +#include +#include +#include + +#include "fsfw/action/CommandActionHelper.h" +#include "fsfw/action/CommandsActionsIF.h" +#include "fsfw/devicehandlers/DeviceHandlerBase.h" +#include "fsfw/tmtcservices/SourceSequenceCounter.h" +#include "fsfw_hal/linux/gpio/Gpio.h" +#include "fsfw_hal/linux/serial/SerialComIF.h" + +static constexpr bool DEBUG_MPSOC_COMMUNICATION = true; + +/** + * @brief This is the device handler for the MPSoC of the payload computer. + * + * @details The PLOC uses the space packet protocol for communication. Each command will be + * answered with at least one acknowledgment and one execution report. + * Flight manual: + * https://egit.irs.uni-stuttgart.de/redmine/projects/eive-flight-manual/wiki/PLOC_MPSoC ICD: + * https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_TAS-ILH-IRS/ICD-PLOC/ILH&fileid=1030263 + * + * @note The sequence count in the space packets must be incremented with each received and sent + * packet otherwise the MPSoC will reply with an acknowledgment failure report. + * + * NOTE: This is not an example for a good device handler, DO NOT USE THIS AS A REFERENCE HANDLER. + * @author J. Meier, R. Mueller + */ +class PlocMpsocHandler : public DeviceHandlerBase, public CommandsActionsIF { + public: + /** + * @brief Constructor + * + * @param ojectId Object ID of the MPSoC handler + * @param uartcomIFid Object ID of the UART communication interface + * @param comCookie UART communication cookie + * @param plocMPSoCHelper Pointer to MPSoC helper object + * @param uartIsolatorSwitch Gpio object representing the GPIO connected to the UART isolator + * module in the programmable logic + * @param supervisorHandler Object ID of the supervisor handler + */ + PlocMpsocHandler(object_id_t objectId, object_id_t uartComIFid, CookieIF* comCookie, + PlocMpsocSpecialComHelperLegacy* plocMPSoCHelper, Gpio uartIsolatorSwitch, + object_id_t supervisorHandler); + virtual ~PlocMpsocHandler(); + virtual ReturnValue_t initialize() override; + ReturnValue_t executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) override; + void performOperationHook() override; + MessageQueueIF* getCommandQueuePtr() override; + void stepSuccessfulReceived(ActionId_t actionId, uint8_t step) override; + void stepFailedReceived(ActionId_t actionId, uint8_t step, ReturnValue_t returnCode) override; + void dataReceived(ActionId_t actionId, const uint8_t* data, uint32_t size) override; + void completionSuccessfulReceived(ActionId_t actionId) override; + void completionFailedReceived(ActionId_t actionId, ReturnValue_t returnCode) override; + + protected: + void doStartUp() override; + void doShutDown() override; + ReturnValue_t buildNormalDeviceCommand(DeviceCommandId_t* id) override; + ReturnValue_t buildTransitionDeviceCommand(DeviceCommandId_t* id) override; + void fillCommandAndReplyMap() override; + ReturnValue_t buildCommandFromCommand(DeviceCommandId_t deviceCommand, const uint8_t* commandData, + size_t commandDataLen) override; + ReturnValue_t scanForReply(const uint8_t* start, size_t remainingSize, DeviceCommandId_t* foundId, + size_t* foundLen) override; + ReturnValue_t interpretDeviceReply(DeviceCommandId_t id, const uint8_t* packet) override; + void setNormalDatapoolEntriesInvalid() override; + uint32_t getTransitionDelayMs(Mode_t modeFrom, Mode_t modeTo) override; + ReturnValue_t initializeLocalDataPool(localpool::DataPool& localDataPoolMap, + LocalDataPoolManager& poolManager) override; + ReturnValue_t enableReplyInReplyMap(DeviceCommandMap::iterator command, + uint8_t expectedReplies = 1, bool useAlternateId = false, + DeviceCommandId_t alternateReplyID = 0) override; + size_t getNextReplyLength(DeviceCommandId_t deviceCommand) override; + ReturnValue_t doSendReadHook() override; + LocalPoolDataSetBase* getDataSetHandle(sid_t sid) override; + bool dontCheckQueue() override; + + private: + static const uint8_t SUBSYSTEM_ID = SUBSYSTEM_ID::PLOC_MPSOC_HANDLER; + + //! [EXPORT] : [COMMENT] PLOC crc failure in telemetry packet + static const Event MEMORY_READ_RPT_CRC_FAILURE = MAKE_EVENT(1, severity::LOW); + //! [EXPORT] : [COMMENT] PLOC receive acknowledgment failure report + //! P1: Command Id which leads the acknowledgment failure report + //! P2: The status field inserted by the MPSoC into the data field + static const Event ACK_FAILURE = MAKE_EVENT(2, severity::LOW); + //! [EXPORT] : [COMMENT] PLOC receive execution failure report + //! P1: Command Id which leads the execution failure report + //! P2: The status field inserted by the MPSoC into the data field + static const Event EXE_FAILURE = MAKE_EVENT(3, severity::LOW); + //! [EXPORT] : [COMMENT] PLOC reply has invalid crc + static const Event MPSOC_HANDLER_CRC_FAILURE = MAKE_EVENT(4, severity::LOW); + //! [EXPORT] : [COMMENT] Packet sequence count in received space packet does not match expected + //! count P1: Expected sequence count P2: Received sequence count + static const Event MPSOC_HANDLER_SEQUENCE_COUNT_MISMATCH = MAKE_EVENT(5, severity::LOW); + //! [EXPORT] : [COMMENT] Supervisor fails to shutdown MPSoC. Requires to power off the PLOC and + //! thus also to shutdown the supervisor. + static const Event MPSOC_SHUTDOWN_FAILED = MAKE_EVENT(6, severity::HIGH); + //! [EXPORT] : [COMMENT] SUPV not on for boot or shutdown process. P1: 0 for OFF transition, 1 for + //! ON transition. + static constexpr Event SUPV_NOT_ON = event::makeEvent(SUBSYSTEM_ID, 7, severity::LOW); + static constexpr Event SUPV_REPLY_TIMEOUT = event::makeEvent(SUBSYSTEM_ID, 8, severity::LOW); + + static const uint16_t APID_MASK = 0x7FF; + static const uint16_t PACKET_SEQUENCE_COUNT_MASK = 0x3FFF; + + mpsoc::HkReport hkReport; + Countdown mpsocBootTransitionCd = Countdown(6500); + Countdown supvTransitionCd = Countdown(3000); + + MessageQueueIF* eventQueue = nullptr; + MessageQueueIF* commandActionHelperQueue = nullptr; + + SourceSequenceCounter sequenceCount = SourceSequenceCounter(0); + + uint8_t commandBuffer[mpsoc::MAX_COMMAND_SIZE]; + SpacePacketCreator creator; + ploc::SpTcParams spParams = ploc::SpTcParams(creator); + + PoolEntry peStatus = PoolEntry(); + PoolEntry peMode = PoolEntry(); + PoolEntry peDownlinkPwrOn = PoolEntry(); + PoolEntry peDownlinkReplyActive = PoolEntry(); + PoolEntry peDownlinkJesdSyncStatus = PoolEntry(); + PoolEntry peDownlinkDacStatus = PoolEntry(); + PoolEntry peCameraStatus = PoolEntry(); + PoolEntry peCameraSdiStatus = PoolEntry(); + PoolEntry peCameraFpgaTemp = PoolEntry(); + PoolEntry peCameraSocTemp = PoolEntry(); + PoolEntry peSysmonTemp = PoolEntry(); + PoolEntry peSysmonVccInt = PoolEntry(); + PoolEntry peSysmonVccAux = PoolEntry(); + PoolEntry peSysmonVccBram = PoolEntry(); + PoolEntry peSysmonVccPaux = PoolEntry(); + PoolEntry peSysmonVccPint = PoolEntry(); + PoolEntry peSysmonVccPdro = PoolEntry(); + PoolEntry peSysmonMb12V = PoolEntry(); + PoolEntry peSysmonMb3V3 = PoolEntry(); + PoolEntry peSysmonMb1V8 = PoolEntry(); + PoolEntry peSysmonVcc12V = PoolEntry(); + PoolEntry peSysmonVcc5V = PoolEntry(); + PoolEntry peSysmonVcc3V3 = PoolEntry(); + PoolEntry peSysmonVcc3V3VA = PoolEntry(); + PoolEntry peSysmonVcc2V5DDR = PoolEntry(); + PoolEntry peSysmonVcc1V2DDR = PoolEntry(); + PoolEntry peSysmonVcc0V9 = PoolEntry(); + PoolEntry peSysmonVcc0V6VTT = PoolEntry(); + PoolEntry peSysmonSafeCotsCur = PoolEntry(); + PoolEntry peSysmonNvm4XoCur = PoolEntry(); + PoolEntry peSemUncorrectableErrs = PoolEntry(); + PoolEntry peSemCorrectableErrs = PoolEntry(); + PoolEntry peSemStatus = PoolEntry(); + PoolEntry peRebootMpsocRequired = PoolEntry(); + + /** + * This variable is used to store the id of the next reply to receive. This is necessary + * because the PLOC sends as reply to each command at least one acknowledgment and execution + * report. + */ + DeviceCommandId_t nextReplyId = mpsoc::NONE; + + SerialComIF* uartComIf = nullptr; + + PlocMpsocSpecialComHelperLegacy* specialComHelper = nullptr; + Gpio uartIsolatorSwitch; + object_id_t supervisorHandler = 0; + CommandActionHelper commandActionHelper; + + // Used to block incoming commands when MPSoC helper class is currently executing a command + bool specialComHelperExecuting = false; + bool commandIsPending = false; + + struct TmMemReadReport { + static const uint8_t FIX_SIZE = 14; + size_t rememberRequestedSize = 0; + }; + + TmMemReadReport tmMemReadReport; + Countdown cmdCountdown = Countdown(15000); + + struct TelemetryBuffer { + uint16_t length = 0; + uint8_t buffer[mpsoc::SP_MAX_SIZE]; + }; + + size_t foundPacketLen = 0; + TelemetryBuffer tmBuffer; + + enum class StartupState { IDLE, HW_INIT, DONE } startupState = StartupState::IDLE; + enum class PowerState { IDLE, PENDING_STARTUP, PENDING_SHUTDOWN, SUPV_FAILED, DONE }; + + PowerState powerState = PowerState::IDLE; + + uint8_t skipSupvCommandingToOn = false; + + /** + * @brief Handles events received from the PLOC MPSoC helper + */ + void handleEvent(EventMessage* eventMessage); + + ReturnValue_t prepareTcMemWrite(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcMemRead(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcFlashDelete(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcReplayStart(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcReplayStop(); + ReturnValue_t prepareTcDownlinkPwrOn(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcDownlinkPwrOff(); + ReturnValue_t prepareTcGetHkReport(); + ReturnValue_t prepareTcGetDirContent(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcReplayWriteSequence(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcCamCmdSend(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcModeIdle(); + ReturnValue_t prepareTcCamTakePic(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcSimplexSendFile(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcDownlinkDataModulate(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareTcModeSnapshot(); + ReturnValue_t finishTcPrep(mpsoc::TcBase& tcBase); + + /** + * @brief This function checks the crc of the received PLOC reply. + * + * @param start Pointer to the first byte of the reply. + * @param foundLen Pointer to the length of the whole packet. + * + * @return returnvalue::OK if CRC is ok, otherwise CRC_FAILURE. + */ + ReturnValue_t verifyPacket(const uint8_t* start, size_t foundLen); + + /** + * @brief This function handles the acknowledgment report. + * + * @param data Pointer to the data holding the acknowledgment report. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleAckReport(const uint8_t* data); + + /** + * @brief This function handles the data of a execution report. + * + * @param data Pointer to the received data packet. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleExecutionReport(const uint8_t* data); + + /** + * @brief This function handles the memory read report. + * + * @param data Pointer to the data buffer holding the memory read report. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleMemoryReadReport(const uint8_t* data); + + ReturnValue_t handleGetHkReport(const uint8_t* data); + ReturnValue_t handleCamCmdRpt(const uint8_t* data); + + /** + * @brief Depending on the current active command, this function sets the reply id of the + * next reply after a successful acknowledgment report has been received. This is + * required by the function getNextReplyLength() to identify the length of the next + * reply to read. + */ + void setNextReplyId(); + + /** + * @brief This function handles action message replies in case the telemetry has been + * requested by another object. + * + * @param data Pointer to the telemetry data. + * @param dataSize Size of telemetry in bytes. + * @param replyId Id of the reply. This will be added to the ActionMessage. + */ + void handleDeviceTm(const uint8_t* data, size_t dataSize, DeviceCommandId_t replyId); + + /** + * @brief In case an acknowledgment failure reply has been received this function disables + * all previously enabled commands and resets the exepected replies variable of an + * active command. + */ + void disableAllReplies(); + + /** + * @brief This function sends a failure report if the active action was commanded by an other + * object. + * + * @param replyId The id of the reply which signals a failure. + * @param status A status byte which gives information about the failure type. + */ + void sendFailureReport(DeviceCommandId_t replyId, ReturnValue_t status); + + /** + * @brief This function disables the execution report reply. Within this function also the + * the variable expectedReplies of an active command will be set to 0. + */ + void disableExeReportReply(); + + ReturnValue_t prepareTcModeReplay(); + + void cmdDoneHandler(bool success, ReturnValue_t result); + bool handleHwStartup(); + bool handleHwShutdown(); + void stopSpecialComHelper(); + + void handleActionCommandFailure(ActionId_t actionId); + + pwrctrl::EnablePl enablePl = pwrctrl::EnablePl(objects::POWER_CONTROLLER); + ReturnValue_t checkModeCommand(Mode_t commandedMode, Submode_t commandedSubmode, + uint32_t* msToReachTheMode) override; + + ReturnValue_t getParameter(uint8_t domainId, uint8_t uniqueId, ParameterWrapper* parameterWrapper, + const ParameterWrapper* newValues, uint16_t startAtIndex) override; +}; + +#endif /* BSP_Q7S_DEVICES_PLOC_PLOCMPSOCHANDLER_H_ */ diff --git a/archive/PlocMpsocSpecialComHelperLegacy.cpp b/archive/PlocMpsocSpecialComHelperLegacy.cpp new file mode 100644 index 0000000..5dcaeb1 --- /dev/null +++ b/archive/PlocMpsocSpecialComHelperLegacy.cpp @@ -0,0 +1,545 @@ +#include +#include +#include +#include + +#include +#include + +#ifdef XIPHOS_Q7S +#include "bsp_q7s/fs/FilesystemHelper.h" +#endif + +#include "mission/utility/Timestamp.h" + +using namespace ploc; + +PlocMpsocSpecialComHelperLegacy::PlocMpsocSpecialComHelperLegacy(object_id_t objectId) + : SystemObject(objectId) { + spParams.buf = commandBuffer; + spParams.maxSize = sizeof(commandBuffer); +} + +PlocMpsocSpecialComHelperLegacy::~PlocMpsocSpecialComHelperLegacy() {} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::initialize() { +#ifdef XIPHOS_Q7S + sdcMan = SdCardManager::instance(); + if (sdcMan == nullptr) { + sif::warning << "PlocMPSoCHelper::initialize: Invalid SD Card Manager" << std::endl; + return returnvalue::FAILED; + } +#endif + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::performOperation(uint8_t operationCode) { + ReturnValue_t result = returnvalue::OK; + semaphore.acquire(); + while (true) { +#if OBSW_THREAD_TRACING == 1 + trace::threadTrace(opCounter, "PLOC MPSOC Helper"); +#endif + switch (internalState) { + case InternalState::IDLE: { + semaphore.acquire(); + break; + } + case InternalState::FLASH_WRITE: { + result = performFlashWrite(); + if (result == returnvalue::OK) { + triggerEvent(MPSOC_FLASH_WRITE_SUCCESSFUL, sequenceCount->get()); + } else { + triggerEvent(MPSOC_FLASH_WRITE_FAILED, sequenceCount->get()); + } + internalState = InternalState::IDLE; + break; + } + case InternalState::FLASH_READ: { + result = performFlashRead(); + if (result == returnvalue::OK) { + triggerEvent(MPSOC_FLASH_READ_SUCCESSFUL, sequenceCount->get()); + } else { + triggerEvent(MPSOC_FLASH_READ_FAILED, sequenceCount->get()); + } + internalState = InternalState::IDLE; + break; + } + default: + sif::debug << "PlocMPSoCHelper::performOperation: Invalid state" << std::endl; + break; + } + } +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::setComIF( + DeviceCommunicationIF* communicationInterface_) { + uartComIF = dynamic_cast(communicationInterface_); + if (uartComIF == nullptr) { + sif::warning << "PlocMPSoCHelper::initialize: Invalid uart com if" << std::endl; + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +void PlocMpsocSpecialComHelperLegacy::setComCookie(CookieIF* comCookie_) { comCookie = comCookie_; } + +void PlocMpsocSpecialComHelperLegacy::setSequenceCount(SourceSequenceCounter* sequenceCount_) { + sequenceCount = sequenceCount_; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::startFlashWrite(std::string obcFile, + std::string mpsocFile) { + if (internalState != InternalState::IDLE) { + return returnvalue::FAILED; + } + ReturnValue_t result = startFlashReadOrWriteBase(std::move(obcFile), std::move(mpsocFile)); + if (result != returnvalue::OK) { + return result; + } + internalState = InternalState::FLASH_WRITE; + return semaphore.release(); +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::startFlashRead(std::string obcFile, + std::string mpsocFile, + size_t readFileSize) { + if (internalState != InternalState::IDLE) { + return returnvalue::FAILED; + } + ReturnValue_t result = startFlashReadOrWriteBase(std::move(obcFile), std::move(mpsocFile)); + if (result != returnvalue::OK) { + return result; + } + flashReadAndWrite.totalReadSize = readFileSize; + internalState = InternalState::FLASH_READ; + return semaphore.release(); +} + +void PlocMpsocSpecialComHelperLegacy::resetHelper() { + spParams.buf = commandBuffer; + terminate = false; + uartComIF->flushUartRxBuffer(comCookie); +} + +void PlocMpsocSpecialComHelperLegacy::stopProcess() { terminate = true; } + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::performFlashWrite() { + ReturnValue_t result = returnvalue::OK; + std::ifstream file(flashReadAndWrite.obcFile, std::ifstream::binary); + if (file.bad()) { + return returnvalue::FAILED; + } + result = flashfopen(mpsoc::FileAccessModes::WRITE | mpsoc::FileAccessModes::OPEN_ALWAYS); + if (result != returnvalue::OK) { + return result; + } + // Set position of next character to end of file input stream + file.seekg(0, file.end); + // tellg returns position of character in input stream + size_t remainingSize = file.tellg(); + size_t dataLength = 0; + size_t bytesRead = 0; + while (remainingSize > 0) { + if (terminate) { + return returnvalue::OK; + } + // The minus 4 is necessary for unknown reasons. Maybe some bug in the ILH software? + if (remainingSize > mpsoc::MAX_FLASH_WRITE_DATA_SIZE - 4) { + dataLength = mpsoc::MAX_FLASH_WRITE_DATA_SIZE - 4; + } else { + dataLength = remainingSize; + } + if (file.bad() or not file.is_open()) { + return FILE_WRITE_ERROR; + } + file.seekg(bytesRead, file.beg); + file.read(reinterpret_cast(fileBuf.data()), dataLength); + bytesRead += dataLength; + remainingSize -= dataLength; + mpsoc::TcFlashWrite tc(spParams, *sequenceCount); + result = tc.setPayload(fileBuf.data(), dataLength); + if (result != returnvalue::OK) { + return result; + } + result = tc.finishPacket(); + if (result != returnvalue::OK) { + return result; + } + (*sequenceCount)++; + result = handlePacketTransmissionNoReply(tc); + if (result != returnvalue::OK) { + return result; + } + } + result = flashfclose(); + if (result != returnvalue::OK) { + return result; + } + return result; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::performFlashRead() { + std::error_code e; + std::ofstream ofile(flashReadAndWrite.obcFile, std::ios::trunc | std::ios::binary); + if (ofile.bad()) { + return returnvalue::FAILED; + } + ReturnValue_t result = flashfopen(mpsoc::FileAccessModes::READ); + if (result != returnvalue::OK) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return result; + } + size_t readSoFar = 0; + size_t nextReadSize = mpsoc::MAX_FLASH_READ_DATA_SIZE; + while (readSoFar < flashReadAndWrite.totalReadSize) { + if (terminate) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return returnvalue::OK; + } + nextReadSize = mpsoc::MAX_FLASH_READ_DATA_SIZE; + if (flashReadAndWrite.totalReadSize - readSoFar < mpsoc::MAX_FLASH_READ_DATA_SIZE) { + nextReadSize = flashReadAndWrite.totalReadSize - readSoFar; + } + if (ofile.bad() or not ofile.is_open()) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return FILE_READ_ERROR; + } + mpsoc::TcFlashRead flashReadRequest(spParams, *sequenceCount); + result = flashReadRequest.setPayload(nextReadSize); + if (result != returnvalue::OK) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return result; + } + result = flashReadRequest.finishPacket(); + if (result != returnvalue::OK) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return result; + } + (*sequenceCount)++; + result = handlePacketTransmissionFlashRead(flashReadRequest, ofile, nextReadSize); + if (result != returnvalue::OK) { + std::filesystem::remove(flashReadAndWrite.obcFile, e); + return result; + } + readSoFar += nextReadSize; + } + result = flashfclose(); + if (result != returnvalue::OK) { + return result; + } + return result; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::flashfopen(uint8_t mode) { + spParams.buf = commandBuffer; + mpsoc::FlashFopen flashFopen(spParams, *sequenceCount); + ReturnValue_t result = flashFopen.setPayload(flashReadAndWrite.mpsocFile, mode); + if (result != returnvalue::OK) { + return result; + } + result = flashFopen.finishPacket(); + if (result != returnvalue::OK) { + return result; + } + (*sequenceCount)++; + result = handlePacketTransmissionNoReply(flashFopen); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::flashfclose() { + spParams.buf = commandBuffer; + mpsoc::FlashFclose flashFclose(spParams, *sequenceCount); + ReturnValue_t result = flashFclose.finishPacket(); + if (result != returnvalue::OK) { + return result; + } + (*sequenceCount)++; + result = handlePacketTransmissionNoReply(flashFclose); + if (result != returnvalue::OK) { + return result; + } + return result; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handlePacketTransmissionFlashRead( + mpsoc::TcFlashRead& tc, std::ofstream& ofile, size_t expectedReadLen) { + ReturnValue_t result = sendCommand(tc); + if (result != returnvalue::OK) { + return result; + } + result = handleAck(); + if (result != returnvalue::OK) { + return result; + } + result = handleTmReception(); + if (result != returnvalue::OK) { + return result; + } + + // We have the nominal case where the flash read report appears first, or the case where we + // get an EXE failure immediately. + if (spReader.getApid() == mpsoc::apid::TM_FLASH_READ_REPORT) { + result = handleFlashReadReply(ofile, expectedReadLen); + if (result != returnvalue::OK) { + return result; + } + return handleExe(); + } else if (spReader.getApid() == mpsoc::apid::EXE_FAILURE) { + handleExeFailure(); + } else { + triggerEvent(MPSOC_EXE_INVALID_APID, spReader.getApid(), static_cast(internalState)); + sif::warning << "PLOC MPSoC: Expected execution report " + << "but received space packet with apid " << std::hex << spReader.getApid() + << std::endl; + } + return returnvalue::FAILED; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handlePacketTransmissionNoReply(ploc::SpTcBase& tc) { + ReturnValue_t result = sendCommand(tc); + if (result != returnvalue::OK) { + return result; + } + result = handleAck(); + if (result != returnvalue::OK) { + return result; + } + return handleExe(); +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::sendCommand(ploc::SpTcBase& tc) { + ReturnValue_t result = returnvalue::OK; + result = uartComIF->sendMessage(comCookie, tc.getFullPacket(), tc.getFullPacketLen()); + if (result != returnvalue::OK) { + sif::warning << "PlocMPSoCHelper::sendCommand: Failed to send command" << std::endl; + triggerEvent(MPSOC_SENDING_COMMAND_FAILED, result, static_cast(internalState)); + return result; + } + return result; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handleAck() { + ReturnValue_t result = returnvalue::OK; + result = handleTmReception(); + if (result != returnvalue::OK) { + return result; + } + result = checkReceivedTm(); + if (result != returnvalue::OK) { + return result; + } + uint16_t apid = spReader.getApid(); + if (apid != mpsoc::apid::ACK_SUCCESS) { + handleAckApidFailure(spReader); + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +void PlocMpsocSpecialComHelperLegacy::handleAckApidFailure(const ploc::SpTmReader& reader) { + uint16_t apid = reader.getApid(); + if (apid == mpsoc::apid::ACK_FAILURE) { + uint16_t status = mpsoc::getStatusFromRawData(reader.getFullData()); + sif::warning << "PLOC MPSoC ACK Failure: " << mpsoc::getStatusString(status) << std::endl; + triggerEvent(MPSOC_ACK_FAILURE_REPORT, static_cast(internalState), status); + } else { + triggerEvent(MPSOC_ACK_INVALID_APID, apid, static_cast(internalState)); + sif::warning << "PlocMPSoCHelper::handleAckApidFailure: Expected acknowledgement report " + << "but received space packet with apid " << std::hex << apid << std::endl; + } +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handleExe() { + ReturnValue_t result = returnvalue::OK; + + result = handleTmReception(); + if (result != returnvalue::OK) { + return result; + } + result = checkReceivedTm(); + if (result != returnvalue::OK) { + return result; + } + uint16_t apid = spReader.getApid(); + if (apid == mpsoc::apid::EXE_FAILURE) { + handleExeFailure(); + return returnvalue::FAILED; + } else if (apid != mpsoc::apid::EXE_SUCCESS) { + triggerEvent(MPSOC_EXE_INVALID_APID, apid, static_cast(internalState)); + sif::warning << "PLOC MPSoC: Expected execution report " + << "but received space packet with apid " << std::hex << apid << std::endl; + } + return returnvalue::OK; +} + +void PlocMpsocSpecialComHelperLegacy::handleExeFailure() { + uint16_t status = mpsoc::getStatusFromRawData(spReader.getFullData()); + sif::warning << "PLOC MPSoC EXE Failure: " << mpsoc::getStatusString(status) << std::endl; + triggerEvent(MPSOC_EXE_FAILURE_REPORT, static_cast(internalState)); +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handleTmReception() { + ReturnValue_t result = returnvalue::OK; + tmCountdown.resetTimer(); + size_t readBytes = 0; + size_t currentBytes = 0; + uint32_t usleepDelay = 5; + size_t fullPacketLen = 0; + while (true) { + if (tmCountdown.hasTimedOut()) { + triggerEvent(MPSOC_READ_TIMEOUT, tmCountdown.getTimeoutMs()); + return returnvalue::FAILED; + } + result = receive(tmBuf.data() + readBytes, 6, ¤tBytes); + if (result != returnvalue::OK) { + return result; + } + spReader.setReadOnlyData(tmBuf.data(), tmBuf.size()); + fullPacketLen = spReader.getFullPacketLen(); + readBytes += currentBytes; + if (readBytes == 6) { + break; + } + usleep(usleepDelay); + if (usleepDelay < 200000) { + usleepDelay *= 4; + } + } + while (true) { + if (tmCountdown.hasTimedOut()) { + triggerEvent(MPSOC_READ_TIMEOUT, tmCountdown.getTimeoutMs()); + return returnvalue::FAILED; + } + result = receive(tmBuf.data() + readBytes, fullPacketLen - readBytes, ¤tBytes); + readBytes += currentBytes; + if (fullPacketLen == readBytes) { + break; + } + usleep(usleepDelay); + if (usleepDelay < 200000) { + usleepDelay *= 4; + } + } + // arrayprinter::print(tmBuf.data(), readBytes); + return result; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::handleFlashReadReply(std::ofstream& ofile, + size_t expectedReadLen) { + ReturnValue_t result = checkReceivedTm(); + if (result != returnvalue::OK) { + return result; + } + uint16_t apid = spReader.getApid(); + if (apid != mpsoc::apid::TM_FLASH_READ_REPORT) { + triggerEvent(MPSOC_FLASH_READ_PACKET_ERROR, FlashReadErrorType::FLASH_READ_APID_ERROR); + sif::warning << "PLOC MPSoC Flash Read: Unexpected APID" << std::endl; + return result; + } + const uint8_t* packetData = spReader.getPacketData(); + size_t deserDummy = spReader.getPacketDataLen() - mpsoc::CRC_SIZE; + uint32_t receivedReadLen = 0; + // I think this is buggy, weird stuff in the short name field. + // std::string receivedShortName = std::string(reinterpret_cast(packetData), 12); + // if (receivedShortName != flashReadAndWrite.mpsocFile.substr(0, 11)) { + // sif::warning << "PLOC MPSoC Flash Read: Missmatch between request file name and " + // "received file name" + // << std::endl; + // triggerEvent(MPSOC_FLASH_READ_PACKET_ERROR, FlashReadErrorType::FLASH_READ_FILENAME_ERROR); + // return returnvalue::FAILED; + // } + packetData += 12; + result = SerializeAdapter::deSerialize(&receivedReadLen, &packetData, &deserDummy, + SerializeIF::Endianness::NETWORK); + if (result != returnvalue::OK) { + return result; + } + if (receivedReadLen != expectedReadLen) { + sif::warning << "PLOC MPSoC Flash Read: Missmatch between request read length and " + "received read length" + << std::endl; + triggerEvent(MPSOC_FLASH_READ_PACKET_ERROR, FlashReadErrorType::FLASH_READ_READLEN_ERROR); + return returnvalue::FAILED; + } + ofile.write(reinterpret_cast(packetData), receivedReadLen); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::fileCheck(std::string obcFile) { +#ifdef XIPHOS_Q7S + ReturnValue_t result = FilesystemHelper::checkPath(obcFile); + if (result != returnvalue::OK) { + return result; + } +#elif defined(TE0720_1CFA) + if (not std::filesystem::exists(obcFile)) { + sif::warning << "PlocMPSoCHelper::startFlashWrite: File " << obcFile << "does not exist" + << std::endl; + return returnvalue::FAILED; + } +#endif + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::startFlashReadOrWriteBase(std::string obcFile, + std::string mpsocFile) { + ReturnValue_t result = fileCheck(obcFile); + if (result != returnvalue::OK) { + return result; + } + + flashReadAndWrite.obcFile = std::move(obcFile); + flashReadAndWrite.mpsocFile = std::move(mpsocFile); + resetHelper(); + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::checkReceivedTm() { + ReturnValue_t result = spReader.checkSize(); + if (result != returnvalue::OK) { + sif::error << "PLOC MPSoC: Size check on received TM failed" << std::endl; + triggerEvent(MPSOC_TM_SIZE_ERROR); + return result; + } + result = spReader.checkCrc(); + if (result != returnvalue::OK) { + sif::warning << "PLOC MPSoC: CRC check failed" << std::endl; + triggerEvent(MPSOC_TM_CRC_MISSMATCH, *sequenceCount); + return result; + } + uint16_t recvSeqCnt = spReader.getSequenceCount(); + if (recvSeqCnt != *sequenceCount) { + triggerEvent(MPSOC_HELPER_SEQ_CNT_MISMATCH, *sequenceCount, recvSeqCnt); + *sequenceCount = recvSeqCnt; + } + // This sequence count ping pong does not make any sense but it is how the MPSoC expects it. + (*sequenceCount)++; + return returnvalue::OK; +} + +ReturnValue_t PlocMpsocSpecialComHelperLegacy::receive(uint8_t* data, size_t requestBytes, + size_t* readBytes) { + ReturnValue_t result = returnvalue::OK; + uint8_t* buffer = nullptr; + result = uartComIF->requestReceiveMessage(comCookie, requestBytes); + if (result != returnvalue::OK) { + sif::warning << "PlocMPSoCHelper::receive: Failed to request reply" << std::endl; + triggerEvent(MPSOC_HELPER_REQUESTING_REPLY_FAILED, result, + static_cast(static_cast(internalState))); + return returnvalue::FAILED; + } + result = uartComIF->readReceivedMessage(comCookie, &buffer, readBytes); + if (result != returnvalue::OK) { + sif::warning << "PlocMPSoCHelper::receive: Failed to read received message" << std::endl; + triggerEvent(MPSOC_HELPER_READING_REPLY_FAILED, result, static_cast(internalState)); + return returnvalue::FAILED; + } + if (*readBytes > 0) { + std::memcpy(data, buffer, *readBytes); + } + return result; +} diff --git a/archive/PlocMpsocSpecialComHelperLegacy.h b/archive/PlocMpsocSpecialComHelperLegacy.h new file mode 100644 index 0000000..e3d5370 --- /dev/null +++ b/archive/PlocMpsocSpecialComHelperLegacy.h @@ -0,0 +1,200 @@ +#ifndef BSP_Q7S_DEVICES_PLOCMPSOCHELPER_H_ +#define BSP_Q7S_DEVICES_PLOCMPSOCHELPER_H_ + +#include +#include + +#include + +#include "OBSWConfig.h" +#include "fsfw/devicehandlers/CookieIF.h" +#include "fsfw/objectmanager/SystemObject.h" +#include "fsfw/osal/linux/BinarySemaphore.h" +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw/tasks/ExecutableObjectIF.h" +#include "fsfw/tmtcservices/SourceSequenceCounter.h" +#include "fsfw_hal/linux/serial/SerialComIF.h" +#ifdef XIPHOS_Q7S +#include "bsp_q7s/fs/SdCardManager.h" +#endif + +/** + * @brief Helper class for MPSoC of PLOC intended to accelerate large data transfers between + * MPSoC and OBC. + * @author J. Meier + */ +class PlocMpsocSpecialComHelperLegacy : public SystemObject, public ExecutableObjectIF { + public: + static const uint8_t SUBSYSTEM_ID = SUBSYSTEM_ID::PLOC_MPSOC_HELPER; + + //! [EXPORT] : [COMMENT] Flash write fails + static const Event MPSOC_FLASH_WRITE_FAILED = MAKE_EVENT(0, severity::LOW); + //! [EXPORT] : [COMMENT] Flash write successful + static const Event MPSOC_FLASH_WRITE_SUCCESSFUL = MAKE_EVENT(1, severity::INFO); + //! [EXPORT] : [COMMENT] Communication interface returned failure when trying to send the command + //! to the MPSoC + //! P1: Return value returned by the communication interface sendMessage function + //! P2: Internal state of MPSoC helper + static const Event MPSOC_SENDING_COMMAND_FAILED = MAKE_EVENT(2, severity::LOW); + //! [EXPORT] : [COMMENT] Request receive message of communication interface failed + //! P1: Return value returned by the communication interface requestReceiveMessage function + //! P2: Internal state of MPSoC helper + static const Event MPSOC_HELPER_REQUESTING_REPLY_FAILED = MAKE_EVENT(3, severity::LOW); + //! [EXPORT] : [COMMENT] Reading receive message of communication interface failed + //! P1: Return value returned by the communication interface readingReceivedMessage function + //! P2: Internal state of MPSoC helper + static const Event MPSOC_HELPER_READING_REPLY_FAILED = MAKE_EVENT(4, severity::LOW); + //! [EXPORT] : [COMMENT] Did not receive acknowledgment report + //! P1: Number of bytes missing + //! P2: Internal state of MPSoC helper + static const Event MPSOC_MISSING_ACK = MAKE_EVENT(5, severity::LOW); + //! [EXPORT] : [COMMENT] Did not receive execution report + //! P1: Number of bytes missing + //! P2: Internal state of MPSoC helper + static const Event MPSOC_MISSING_EXE = MAKE_EVENT(6, severity::LOW); + //! [EXPORT] : [COMMENT] Received acknowledgment failure report + //! P1: Internal state of MPSoC + static const Event MPSOC_ACK_FAILURE_REPORT = MAKE_EVENT(7, severity::LOW); + //! [EXPORT] : [COMMENT] Received execution failure report + //! P1: Internal state of MPSoC + static const Event MPSOC_EXE_FAILURE_REPORT = MAKE_EVENT(8, severity::LOW); + //! [EXPORT] : [COMMENT] Expected acknowledgment report but received space packet with other apid + //! P1: Apid of received space packet + //! P2: Internal state of MPSoC + static const Event MPSOC_ACK_INVALID_APID = MAKE_EVENT(9, severity::LOW); + //! [EXPORT] : [COMMENT] Expected execution report but received space packet with other apid + //! P1: Apid of received space packet + //! P2: Internal state of MPSoC + static const Event MPSOC_EXE_INVALID_APID = MAKE_EVENT(10, severity::LOW); + //! [EXPORT] : [COMMENT] Received sequence count does not match expected sequence count + //! P1: Expected sequence count + //! P2: Received sequence count + static const Event MPSOC_HELPER_SEQ_CNT_MISMATCH = MAKE_EVENT(11, severity::LOW); + static const Event MPSOC_TM_SIZE_ERROR = MAKE_EVENT(12, severity::LOW); + static const Event MPSOC_TM_CRC_MISSMATCH = MAKE_EVENT(13, severity::LOW); + static const Event MPSOC_FLASH_READ_PACKET_ERROR = MAKE_EVENT(14, severity::LOW); + static const Event MPSOC_FLASH_READ_FAILED = MAKE_EVENT(15, severity::LOW); + static const Event MPSOC_FLASH_READ_SUCCESSFUL = MAKE_EVENT(16, severity::INFO); + static const Event MPSOC_READ_TIMEOUT = MAKE_EVENT(17, severity::LOW); + + enum FlashReadErrorType : uint32_t { + FLASH_READ_APID_ERROR = 0, + FLASH_READ_FILENAME_ERROR = 1, + FLASH_READ_READLEN_ERROR = 2 + }; + + PlocMpsocSpecialComHelperLegacy(object_id_t objectId); + virtual ~PlocMpsocSpecialComHelperLegacy(); + + ReturnValue_t initialize() override; + ReturnValue_t performOperation(uint8_t operationCode = 0) override; + + ReturnValue_t setComIF(DeviceCommunicationIF* communicationInterface_); + void setComCookie(CookieIF* comCookie_); + + /** + * @brief Starts flash write sequence + * + * @param obcFile File where to read from the data + * @param mpsocFile The file of the MPSoC where should be written to + * + * @return returnvalue::OK if successful, otherwise error return value + */ + ReturnValue_t startFlashWrite(std::string obcFile, std::string mpsocFile); + /** + * + * @param obcFile Full target file name on OBC + * @param mpsocFile The file on the MPSoC which should be copied ot the OBC + * @param readFileSize The size of the file on the MPSoC. + * @return + */ + ReturnValue_t startFlashRead(std::string obcFile, std::string mpsocFile, size_t readFileSize); + + /** + * @brief Can be used to interrupt a running data transfer. + */ + void stopProcess(); + + /** + * @brief Sets the sequence count object responsible for the sequence count handling + */ + void setSequenceCount(SourceSequenceCounter* sequenceCount_); + + private: + static const uint8_t INTERFACE_ID = CLASS_ID::PLOC_MPSOC_HELPER; + + //! [EXPORT] : [COMMENT] File error occured for file transfers from OBC to the MPSoC. + static const ReturnValue_t FILE_WRITE_ERROR = MAKE_RETURN_CODE(0xA0); + //! [EXPORT] : [COMMENT] File error occured for file transfers from MPSoC to OBC. + static const ReturnValue_t FILE_READ_ERROR = MAKE_RETURN_CODE(0xA1); + + // Maximum number of times the communication interface retries polling data from the reply + // buffer + static const int RETRIES = 10000; + + struct FlashInfo { + std::string obcFile; + std::string mpsocFile; + }; + + struct FlashRead : public FlashInfo { + size_t totalReadSize = 0; + }; + + struct FlashRead flashReadAndWrite; +#if OBSW_THREAD_TRACING == 1 + uint32_t opCounter = 0; +#endif + + enum class InternalState { IDLE, FLASH_WRITE, FLASH_READ }; + + InternalState internalState = InternalState::IDLE; + + BinarySemaphore semaphore; +#ifdef XIPHOS_Q7S + SdCardManager* sdcMan = nullptr; +#endif + uint8_t commandBuffer[mpsoc::MAX_COMMAND_SIZE]; + SpacePacketCreator creator; + ploc::SpTcParams spParams = ploc::SpTcParams(creator); + + Countdown tmCountdown = Countdown(5000); + + std::array fileBuf{}; + std::array tmBuf{}; + + bool terminate = false; + + /** + * Communication interface of MPSoC responsible for low level access. Must be set by the + * MPSoC Handler. + */ + SerialComIF* uartComIF = nullptr; + // Communication cookie. Must be set by the MPSoC Handler + CookieIF* comCookie = nullptr; + // Sequence count, must be set by Ploc MPSoC Handler + SourceSequenceCounter* sequenceCount = nullptr; + ploc::SpTmReader spReader; + + void resetHelper(); + ReturnValue_t performFlashWrite(); + ReturnValue_t performFlashRead(); + ReturnValue_t flashfopen(uint8_t accessMode); + ReturnValue_t flashfclose(); + ReturnValue_t handlePacketTransmissionNoReply(ploc::SpTcBase& tc); + ReturnValue_t handlePacketTransmissionFlashRead(mpsoc::TcFlashRead& tc, std::ofstream& ofile, + size_t expectedReadLen); + ReturnValue_t handleFlashReadReply(std::ofstream& ofile, size_t expectedReadLen); + ReturnValue_t sendCommand(ploc::SpTcBase& tc); + ReturnValue_t receive(uint8_t* data, size_t requestBytes, size_t* readBytes); + ReturnValue_t handleAck(); + ReturnValue_t handleExe(); + ReturnValue_t startFlashReadOrWriteBase(std::string obcFile, std::string mpsocFile); + ReturnValue_t fileCheck(std::string obcFile); + void handleAckApidFailure(const ploc::SpTmReader& reader); + void handleExeFailure(); + ReturnValue_t handleTmReception(); + ReturnValue_t checkReceivedTm(); +}; + +#endif /* BSP_Q7S_DEVICES_PLOCMPSOCHELPER_H_ */ diff --git a/archive/PlocSupervisorHandler.cpp b/archive/PlocSupervisorHandler.cpp new file mode 100644 index 0000000..67ec707 --- /dev/null +++ b/archive/PlocSupervisorHandler.cpp @@ -0,0 +1,2027 @@ +#include "PlocSupervisorHandler.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "OBSWConfig.h" +#include "eive/definitions.h" +#include "fsfw/datapool/PoolReadGuard.h" +#include "fsfw/globalfunctions/CRC.h" +#include "fsfw/ipc/QueueFactory.h" +#include "fsfw/timemanager/Clock.h" + +using namespace supv; +using namespace returnvalue; + +PlocSupervisorHandler::PlocSupervisorHandler(object_id_t objectId, CookieIF* comCookie, + Gpio uartIsolatorSwitch, power::Switch_t powerSwitch, + PlocSupvUartManager& supvHelper) + : DeviceHandlerBase(objectId, supvHelper.getObjectId(), comCookie), + uartIsolatorSwitch(uartIsolatorSwitch), + hkset(this), + bootStatusReport(this), + latchupStatusReport(this), + countersReport(this), + adcReport(this), + powerSwitch(powerSwitch), + uartManager(supvHelper) { + if (comCookie == nullptr) { + sif::error << "PlocSupervisorHandler: Invalid com cookie" << std::endl; + } + spParams.buf = commandBuffer; + spParams.maxSize = sizeof(commandBuffer); + eventQueue = QueueFactory::instance()->createMessageQueue(EventMessage::EVENT_MESSAGE_SIZE * 5); +} + +PlocSupervisorHandler::~PlocSupervisorHandler() {} + +ReturnValue_t PlocSupervisorHandler::initialize() { + ReturnValue_t result = returnvalue::OK; + result = DeviceHandlerBase::initialize(); + if (result != returnvalue::OK) { + return result; + } +#ifdef XIPHOS_Q7S + sdcMan = SdCardManager::instance(); +#endif /* TE0720_1CFA */ + + result = eventSubscription(); + if (result != returnvalue::OK) { + return result; + } + return result; +} + +void PlocSupervisorHandler::performOperationHook() { + if (normalCommandIsPending and normalCmdCd.hasTimedOut()) { + // Event, FDIR, printout? Leads to spam though and normally should not happen.. + normalCommandIsPending = false; + } + if (commandIsPending and cmdCd.hasTimedOut()) { + // Event, FDIR, printout? Leads to spam though and normally should not happen.. + commandIsPending = false; + + // if(iter->second.sendReplyTo != NO_COMMANDER) { + // actionHelper.finish(true, iter->second.sendReplyTo, iter->first, returnvalue::OK); + // } + disableAllReplies(); + } + EventMessage event; + for (ReturnValue_t result = eventQueue->receiveMessage(&event); result == returnvalue::OK; + result = eventQueue->receiveMessage(&event)) { + switch (event.getMessageId()) { + case EventMessage::EVENT_MESSAGE: + handleEvent(&event); + break; + default: + sif::debug << "PlocSupervisorHandler::performOperationHook: Did not subscribe to this event" + << " message" << std::endl; + break; + } + } +} + +ReturnValue_t PlocSupervisorHandler::executeAction(ActionId_t actionId, + MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) { + using namespace supv; + ReturnValue_t result = returnvalue::OK; + + switch (actionId) { + default: + break; + } + + if (uartManager.longerRequestActive()) { + return result::SUPV_HELPER_EXECUTING; + } + + result = acceptExternalDeviceCommands(); + if (result != returnvalue::OK) { + return result; + } + + switch (actionId) { + case PERFORM_UPDATE: { + if (size > config::MAX_PATH_SIZE + config::MAX_FILENAME_SIZE) { + return result::FILENAME_TOO_LONG; + } + shutdownCmdSent = false; + UpdateParams params; + result = extractUpdateCommand(data, size, params); + if (result != returnvalue::OK) { + return result; + } + result = uartManager.performUpdate(params); + if (result != returnvalue::OK) { + return result; + } + return EXECUTION_FINISHED; + } + case CONTINUE_UPDATE: { + shutdownCmdSent = false; + uartManager.initiateUpdateContinuation(); + return EXECUTION_FINISHED; + } + case MEMORY_CHECK_WITH_FILE: { + shutdownCmdSent = false; + UpdateParams params; + result = extractBaseParams(&data, size, params); + if (result != returnvalue::OK) { + return result; + } + if (not std::filesystem::exists(params.file)) { + return HasFileSystemIF::FILE_DOES_NOT_EXIST; + } + uartManager.performMemCheck(params.file, params.memId, params.startAddr); + return EXECUTION_FINISHED; + } + default: + break; + } + return DeviceHandlerBase::executeAction(actionId, commandedBy, data, size); +} + +void PlocSupervisorHandler::doStartUp() { + if (startupState == StartupState::OFF) { + bootTimeout.resetTimer(); + startupState = StartupState::BOOTING; + } + if (startupState == StartupState::BOOTING) { + if (bootTimeout.hasTimedOut()) { + uartIsolatorSwitch.pullHigh(); + uartManager.start(); + if (SET_TIME_DURING_BOOT) { + startupState = StartupState::SET_TIME; + } else { + startupState = StartupState::ON; + } + } + } + if (startupState == StartupState::TIME_WAS_SET) { + startupState = StartupState::ON; + } + if (startupState == StartupState::ON) { + hkset.setReportingEnabled(true); + supv::SUPV_ON = true; + setMode(_MODE_TO_ON); + } +} + +void PlocSupervisorHandler::doShutDown() { + setMode(_MODE_POWER_DOWN); + hkset.setReportingEnabled(false); + hkset.setValidity(false, true); + shutdownCmdSent = false; + packetInBuffer = false; + nextReplyId = supv::NONE; + uartManager.stop(); + uartIsolatorSwitch.pullLow(); + disableAllReplies(); + supv::SUPV_ON = false; + startupState = StartupState::OFF; +} + +ReturnValue_t PlocSupervisorHandler::buildNormalDeviceCommand(DeviceCommandId_t* id) { + if (not normalCommandIsPending) { + *id = GET_HK_REPORT; + normalCommandIsPending = true; + normalCmdCd.resetTimer(); + return buildCommandFromCommand(*id, nullptr, 0); + } + return NOTHING_TO_SEND; +} + +ReturnValue_t PlocSupervisorHandler::buildTransitionDeviceCommand(DeviceCommandId_t* id) { + if (startupState == StartupState::SET_TIME) { + *id = supv::SET_TIME_REF; + startupState = StartupState::WAIT_FOR_TIME_REPLY; + return buildCommandFromCommand(*id, nullptr, 0); + } + return NOTHING_TO_SEND; +} + +ReturnValue_t PlocSupervisorHandler::buildCommandFromCommand(DeviceCommandId_t deviceCommand, + const uint8_t* commandData, + size_t commandDataLen) { + using namespace supv; + ReturnValue_t result = returnvalue::FAILED; + spParams.buf = commandBuffer; + switch (deviceCommand) { + case GET_HK_REPORT: { + prepareEmptyCmd(Apid::HK, static_cast(tc::HkId::GET_REPORT)); + result = returnvalue::OK; + break; + } + case START_MPSOC: { + sif::info << "PLOC SUPV: Starting MPSoC" << std::endl; + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::START_MPSOC)); + result = returnvalue::OK; + break; + } + case SHUTDOWN_MPSOC: { + sif::info << "PLOC SUPV: Shutting down MPSoC" << std::endl; + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::SHUTDOWN_MPSOC)); + result = returnvalue::OK; + break; + } + case SEL_MPSOC_BOOT_IMAGE: { + prepareSelBootImageCmd(commandData); + result = returnvalue::OK; + break; + } + case RESET_MPSOC: { + sif::info << "PLOC SUPV: Resetting MPSoC" << std::endl; + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::RESET_MPSOC)); + result = returnvalue::OK; + break; + } + case SET_TIME_REF: { + result = prepareSetTimeRefCmd(); + break; + } + case SET_BOOT_TIMEOUT: { + prepareSetBootTimeoutCmd(commandData); + result = returnvalue::OK; + break; + } + case SET_MAX_RESTART_TRIES: { + prepareRestartTriesCmd(commandData); + result = returnvalue::OK; + break; + } + case DISABLE_PERIOIC_HK_TRANSMISSION: { + prepareDisableHk(); + result = returnvalue::OK; + break; + } + case GET_BOOT_STATUS_REPORT: { + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::GET_BOOT_STATUS_REPORT)); + result = returnvalue::OK; + break; + } + case ENABLE_LATCHUP_ALERT: { + result = prepareLatchupConfigCmd(commandData, deviceCommand); + break; + } + case DISABLE_LATCHUP_ALERT: { + result = prepareLatchupConfigCmd(commandData, deviceCommand); + break; + } + case SET_ALERT_LIMIT: { + result = prepareSetAlertLimitCmd(commandData); + break; + } + case GET_LATCHUP_STATUS_REPORT: { + prepareEmptyCmd(Apid::LATCHUP_MON, static_cast(tc::LatchupMonId::GET_STATUS_REPORT)); + result = returnvalue::OK; + break; + } + case RUN_AUTO_EM_TESTS: { + result = prepareRunAutoEmTest(commandData); + break; + } + case SET_GPIO: { + result = prepareSetGpioCmd(commandData, commandDataLen); + break; + } + case FACTORY_RESET: { + result = prepareFactoryResetCmd(commandData, commandDataLen); + break; + } + case READ_GPIO: { + result = prepareReadGpioCmd(commandData, commandDataLen); + break; + } + case SET_SHUTDOWN_TIMEOUT: { + prepareSetShutdownTimeoutCmd(commandData); + result = returnvalue::OK; + break; + } + case FACTORY_FLASH: { + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::FACTORY_FLASH)); + result = returnvalue::OK; + break; + } + case RESET_PL: { + prepareEmptyCmd(Apid::BOOT_MAN, static_cast(tc::BootManId::RESET_PL)); + result = returnvalue::OK; + break; + } + case SET_ADC_ENABLED_CHANNELS: { + prepareSetAdcEnabledChannelsCmd(commandData); + result = returnvalue::OK; + break; + } + case SET_ADC_WINDOW_AND_STRIDE: { + prepareSetAdcWindowAndStrideCmd(commandData); + result = returnvalue::OK; + break; + } + case SET_ADC_THRESHOLD: { + prepareSetAdcThresholdCmd(commandData); + result = returnvalue::OK; + break; + } + case WIPE_MRAM: { + result = prepareWipeMramCmd(commandData); + break; + } + case REQUEST_ADC_REPORT: { + prepareEmptyCmd(Apid::ADC_MON, static_cast(tc::AdcMonId::REQUEST_ADC_SAMPLE)); + result = returnvalue::OK; + break; + } + case REQUEST_LOGGING_COUNTERS: { + prepareEmptyCmd(Apid::DATA_LOGGER, + static_cast(tc::DataLoggerServiceId::REQUEST_COUNTERS)); + result = returnvalue::OK; + break; + } + default: + sif::debug << "PlocSupervisorHandler::buildCommandFromCommand: Command not implemented" + << std::endl; + result = DeviceHandlerIF::COMMAND_NOT_IMPLEMENTED; + break; + } + commandIsPending = true; + cmdCd.resetTimer(); + return result; +} + +void PlocSupervisorHandler::fillCommandAndReplyMap() { + // Command only + insertInCommandMap(GET_HK_REPORT); + insertInCommandMap(START_MPSOC); + insertInCommandMap(SHUTDOWN_MPSOC); + insertInCommandMap(SEL_MPSOC_BOOT_IMAGE); + insertInCommandMap(SET_BOOT_TIMEOUT); + insertInCommandMap(SET_MAX_RESTART_TRIES); + insertInCommandMap(RESET_MPSOC); + insertInCommandMap(WIPE_MRAM); + insertInCommandMap(SET_TIME_REF); + insertInCommandMap(DISABLE_PERIOIC_HK_TRANSMISSION); + insertInCommandMap(GET_BOOT_STATUS_REPORT); + insertInCommandMap(ENABLE_LATCHUP_ALERT); + insertInCommandMap(DISABLE_LATCHUP_ALERT); + insertInCommandMap(SET_ALERT_LIMIT); + insertInCommandMap(GET_LATCHUP_STATUS_REPORT); + insertInCommandMap(RUN_AUTO_EM_TESTS); + insertInCommandMap(SET_GPIO); + insertInCommandMap(READ_GPIO); + insertInCommandMap(FACTORY_RESET); + insertInCommandMap(MEMORY_CHECK); + insertInCommandMap(SET_SHUTDOWN_TIMEOUT); + insertInCommandMap(FACTORY_FLASH); + insertInCommandMap(SET_ADC_ENABLED_CHANNELS); + insertInCommandMap(SET_ADC_THRESHOLD); + insertInCommandMap(SET_ADC_WINDOW_AND_STRIDE); + insertInCommandMap(RESET_PL); + insertInCommandMap(REQUEST_ADC_REPORT); + insertInCommandMap(REQUEST_LOGGING_COUNTERS); + + // ACK replies, use countdown for them + insertInReplyMap(ACK_REPORT, 0, nullptr, SIZE_ACK_REPORT, false, &acknowledgementReportTimeout); + insertInReplyMap(EXE_REPORT, 0, nullptr, SIZE_EXE_REPORT, false, &executionReportTimeout); + insertInReplyMap(MEMORY_CHECK, 5, nullptr, 0, false); + + // TM replies + insertInReplyMap(HK_REPORT, 3, &hkset); + insertInReplyMap(BOOT_STATUS_REPORT, 3, &bootStatusReport, SIZE_BOOT_STATUS_REPORT); + insertInReplyMap(LATCHUP_REPORT, 3, &latchupStatusReport, SIZE_LATCHUP_STATUS_REPORT); + insertInReplyMap(COUNTERS_REPORT, 3, &countersReport, SIZE_COUNTERS_REPORT); + insertInReplyMap(ADC_REPORT, 3, &adcReport, SIZE_ADC_REPORT); +} + +ReturnValue_t PlocSupervisorHandler::enableReplyInReplyMap(DeviceCommandMap::iterator command, + uint8_t expectedReplies, + bool useAlternateId, + DeviceCommandId_t alternateReplyID) { + ReturnValue_t result = OK; + + uint8_t enabledReplies = 0; + + switch (command->first) { + case GET_HK_REPORT: { + enabledReplies = 3; + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, HK_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " << HK_REPORT + << " not in replyMap" << std::endl; + } + break; + } + case GET_BOOT_STATUS_REPORT: { + enabledReplies = 3; + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, + BOOT_STATUS_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " + << BOOT_STATUS_REPORT << " not in replyMap" << std::endl; + } + break; + } + case GET_LATCHUP_STATUS_REPORT: { + enabledReplies = 3; + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, LATCHUP_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " + << LATCHUP_REPORT << " not in replyMap" << std::endl; + } + break; + } + case REQUEST_LOGGING_COUNTERS: { + enabledReplies = 3; + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, COUNTERS_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " + << COUNTERS_REPORT << " not in replyMap" << std::endl; + } + break; + } + case REQUEST_ADC_REPORT: { + enabledReplies = 3; + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, ADC_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " << ADC_REPORT + << " not in replyMap" << std::endl; + } + break; + } + case FIRST_MRAM_DUMP: { + enabledReplies = 2; // expected replies will be increased in handleMramDumpPacket + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, FIRST_MRAM_DUMP); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " + << FIRST_MRAM_DUMP << " not in replyMap" << std::endl; + } + break; + } + case CONSECUTIVE_MRAM_DUMP: { + enabledReplies = 2; // expected replies will be increased in handleMramDumpPacket + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, + CONSECUTIVE_MRAM_DUMP); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " + << CONSECUTIVE_MRAM_DUMP << " not in replyMap" << std::endl; + } + break; + } + case MEMORY_CHECK: { + enabledReplies = 3; + result = + DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, MEMORY_CHECK); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " << MEMORY_CHECK + << " not in replyMap" << std::endl; + } + break; + } + case START_MPSOC: + case SHUTDOWN_MPSOC: + case SEL_MPSOC_BOOT_IMAGE: + case SET_BOOT_TIMEOUT: + case SET_MAX_RESTART_TRIES: + case RESET_MPSOC: + case SET_TIME_REF: + case ENABLE_LATCHUP_ALERT: + case DISABLE_LATCHUP_ALERT: + case SET_ALERT_LIMIT: + case SET_ADC_ENABLED_CHANNELS: + case SET_ADC_WINDOW_AND_STRIDE: + case SET_ADC_THRESHOLD: + case RUN_AUTO_EM_TESTS: + case WIPE_MRAM: + case SET_GPIO: + case FACTORY_RESET: + case READ_GPIO: + case DISABLE_PERIOIC_HK_TRANSMISSION: + case SET_SHUTDOWN_TIMEOUT: + case FACTORY_FLASH: + case ENABLE_AUTO_TM: + case DISABLE_AUTO_TM: + case RESET_PL: + enabledReplies = 2; + break; + default: + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Unknown command id" << std::endl; + break; + } + + /** + * Every command causes at least one acknowledgment and one execution report. Therefore both + * replies will be enabled here. + */ + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, ACK_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " << ACK_REPORT + << " not in replyMap" << std::endl; + } + + setExecutionTimeout(command->first); + + result = DeviceHandlerBase::enableReplyInReplyMap(command, enabledReplies, true, EXE_REPORT); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::enableReplyInReplyMap: Reply with id " << EXE_REPORT + << " not in replyMap" << std::endl; + } + + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::scanForReply(const uint8_t* start, size_t remainingSize, + DeviceCommandId_t* foundId, size_t* foundLen) { + using namespace supv; + + tmReader.setData(start, remainingSize); + uint16_t apid = tmReader.getModuleApid(); + if (DEBUG_PLOC_SUPV) { + handlePacketPrint(); + } + + switch (apid) { + case (Apid::TMTC_MAN): { + switch (tmReader.getServiceId()) { + case (static_cast(supv::tm::TmtcId::ACK)): + case (static_cast(supv::tm::TmtcId::NAK)): { + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::ACK_REPORT; + return OK; + } + case (static_cast(supv::tm::TmtcId::EXEC_ACK)): + case (static_cast(supv::tm::TmtcId::EXEC_NAK)): { + *foundLen = tmReader.getFullPacketLen(); + *foundId = EXE_REPORT; + return OK; + } + } + break; + } + case (Apid::HK): { + if (tmReader.getServiceId() == static_cast(supv::tm::HkId::REPORT)) { + normalCommandIsPending = false; + // Yeah apparently this is needed?? + disableCommand(GET_HK_REPORT); + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::HK_REPORT; + return OK; + } else if (tmReader.getServiceId() == static_cast(supv::tm::HkId::HARDFAULTS)) { + handleBadApidServiceCombination(SUPV_UNINIMPLEMENTED_TM, apid, tmReader.getServiceId()); + return INVALID_DATA; + } + break; + } + case (Apid::BOOT_MAN): { + if (tmReader.getServiceId() == + static_cast(supv::tm::BootManId::BOOT_STATUS_REPORT)) { + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::BOOT_STATUS_REPORT; + return OK; + } + break; + } + case (Apid::ADC_MON): { + if (tmReader.getServiceId() == static_cast(supv::tm::AdcMonId::ADC_REPORT)) { + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::ADC_REPORT; + return OK; + } + break; + } + case (Apid::MEM_MAN): { + if (tmReader.getServiceId() == + static_cast(supv::tm::MemManId::UPDATE_STATUS_REPORT)) { + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::UPDATE_STATUS_REPORT; + return OK; + } + break; + } + case (Apid::DATA_LOGGER): { + if (tmReader.getServiceId() == + static_cast(supv::tm::DataLoggerId::COUNTERS_REPORT)) { + *foundLen = tmReader.getFullPacketLen(); + *foundId = ReplyId::COUNTERS_REPORT; + return OK; + } + } + } + handleBadApidServiceCombination(SUPV_UNKNOWN_TM, apid, tmReader.getServiceId()); + *foundLen = remainingSize; + return INVALID_DATA; +} + +void PlocSupervisorHandler::handlePacketPrint() { + if (tmReader.getModuleApid() == Apid::TMTC_MAN) { + if ((tmReader.getServiceId() == static_cast(supv::tm::TmtcId::ACK)) or + (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::NAK))) { + AcknowledgmentReport ack(tmReader); + ReturnValue_t result = ack.parse(); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler: Parsing ACK failed" << std::endl; + } + if (REDUCE_NORMAL_MODE_PRINTOUT and ack.getRefModuleApid() == (uint8_t)supv::Apid::HK and + ack.getRefServiceId() == (uint8_t)supv::tc::HkId::GET_REPORT) { + return; + } + const char* printStr = "???"; + if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::ACK)) { + printStr = "ACK"; + + } else if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::NAK)) { + printStr = "NAK"; + } + sif::debug << "PlocSupervisorHandler: RECV " << printStr << " for APID Module ID " + << (int)ack.getRefModuleApid() << " Service ID " << (int)ack.getRefServiceId() + << " Seq Count " << ack.getRefSequenceCount() << std::endl; + return; + } else if ((tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_ACK)) or + (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_NAK))) { + ExecutionReport exe(tmReader); + ReturnValue_t result = exe.parse(); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler: Parsing EXE failed" << std::endl; + } + const char* printStr = "???"; + if (REDUCE_NORMAL_MODE_PRINTOUT and exe.getRefModuleApid() == (uint8_t)supv::Apid::HK and + exe.getRefServiceId() == (uint8_t)supv::tc::HkId::GET_REPORT) { + return; + } + if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_ACK)) { + printStr = "ACK EXE"; + + } else if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_NAK)) { + printStr = "NAK EXE"; + } + sif::debug << "PlocSupervisorHandler: RECV " << printStr << " for APID Module ID " + << (int)exe.getRefModuleApid() << " Service ID " << (int)exe.getRefServiceId() + << " Seq Count " << exe.getRefSequenceCount() << std::endl; + return; + } + } + sif::debug << "PlocSupervisorHandler: RECV PACKET Size " << tmReader.getFullPacketLen() + << " Module APID " << (int)tmReader.getModuleApid() << " Service ID " + << (int)tmReader.getServiceId() << std::endl; +} +ReturnValue_t PlocSupervisorHandler::interpretDeviceReply(DeviceCommandId_t id, + const uint8_t* packet) { + using namespace supv; + ReturnValue_t result = returnvalue::OK; + + switch (id) { + case ACK_REPORT: { + result = handleAckReport(packet); + break; + } + case (HK_REPORT): { + result = handleHkReport(packet); + break; + } + case (BOOT_STATUS_REPORT): { + result = handleBootStatusReport(packet); + break; + } + case (COUNTERS_REPORT): { + result = genericHandleTm("COUNTERS", packet, countersReport); +#if OBSW_VERBOSE_LEVEL >= 1 && OBSW_DEBUG_PLOC_SUPERVISOR == 1 + countersReport.printSet(); +#endif + break; + } + case (LATCHUP_REPORT): { + result = handleLatchupStatusReport(packet); + break; + } + case (ADC_REPORT): { + result = genericHandleTm("ADC", packet, adcReport); +#if OBSW_VERBOSE_LEVEL >= 1 && OBSW_DEBUG_PLOC_SUPERVISOR == 1 + adcReport.printSet(); +#endif + break; + } + case (EXE_REPORT): { + result = handleExecutionReport(packet); + break; + } + case (UPDATE_STATUS_REPORT): { + // TODO: handle status report here + break; + } + default: { + sif::debug << "PlocSupervisorHandler::interpretDeviceReply: Unknown device reply id" + << std::endl; + return DeviceHandlerIF::UNKNOWN_DEVICE_REPLY; + } + } + + return result; +} + +ReturnValue_t PlocSupervisorHandler::initializeLocalDataPool(localpool::DataPool& localDataPoolMap, + LocalDataPoolManager& poolManager) { + localDataPoolMap.emplace(supv::NUM_TMS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TEMP_PS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TEMP_PL, new PoolEntry({0})); + localDataPoolMap.emplace(supv::HK_SOC_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::NVM0_1_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::NVM3_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MISSION_IO_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::FMC_STATE, &fmcStateEntry); + localDataPoolMap.emplace(supv::NUM_TCS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TEMP_SUP, &tempSupEntry); + localDataPoolMap.emplace(supv::UPTIME, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CPULOAD, new PoolEntry({0})); + localDataPoolMap.emplace(supv::AVAILABLEHEAP, new PoolEntry({0})); + + localDataPoolMap.emplace(supv::BR_SOC_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::POWER_CYCLES, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BOOT_AFTER_MS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BOOT_TIMEOUT_MS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::ACTIVE_NVM, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BP0_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BP1_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BP2_STATE, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BOOT_STATE, &bootStateEntry); + localDataPoolMap.emplace(supv::BOOT_CYCLES, &bootCyclesEntry); + + localDataPoolMap.emplace(supv::LATCHUP_ID, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT0, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT1, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT2, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT3, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT4, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT5, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CNT6, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_MSEC, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_SEC, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_MIN, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_HOUR, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_DAY, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_MON, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_TIME_YEAR, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LATCHUP_RPT_IS_SET, new PoolEntry({0})); + + localDataPoolMap.emplace(supv::SIGNATURE, new PoolEntry()); + localDataPoolMap.emplace(supv::LATCHUP_HAPPENED_CNTS, &latchupCounters); + localDataPoolMap.emplace(supv::ADC_DEVIATION_TRIGGERS_CNT, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TC_RECEIVED_CNT, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TM_AVAILABLE_CNT, new PoolEntry({0})); + localDataPoolMap.emplace(supv::SUPERVISOR_BOOTS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MPSOC_BOOTS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MPSOC_BOOT_FAILED_ATTEMPTS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MPSOC_POWER_UP, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MPSOC_UPDATES, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MPSOC_HEARTBEAT_RESETS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::CPU_WDT_RESETS, new PoolEntry({0})); + localDataPoolMap.emplace(supv::PS_HEARTBEATS_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::PL_HEARTBEATS_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::EB_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::BM_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::LM_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::AM_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::TCTMM_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::MM_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::HK_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::DL_TASK_LOST, new PoolEntry({0})); + localDataPoolMap.emplace(supv::RWS_TASKS_LOST, new PoolEntry(3)); + + localDataPoolMap.emplace(supv::ADC_RAW, &adcRawEntry); + localDataPoolMap.emplace(supv::ADC_ENG, &adcEngEntry); + + poolManager.subscribeForRegularPeriodicPacket( + subdp::RegularHkPeriodicParams(hkset.getSid(), false, 10.0)); + return returnvalue::OK; +} + +void PlocSupervisorHandler::handleEvent(EventMessage* eventMessage) { + ReturnValue_t result = returnvalue::OK; + object_id_t objectId = eventMessage->getReporter(); + Event event = eventMessage->getEvent(); + switch (objectId) { + case objects::PLOC_SUPERVISOR_HELPER: { + // After execution of update procedure, PLOC is in a state where it draws approx. 700 mA of + // current. To leave this state the shutdown MPSoC command must be sent here. + if (event == PlocSupvUartManager::SUPV_UPDATE_FAILED || + event == PlocSupvUartManager::SUPV_UPDATE_SUCCESSFUL || + event == PlocSupvUartManager::SUPV_CONTINUE_UPDATE_FAILED || + event == PlocSupvUartManager::SUPV_CONTINUE_UPDATE_SUCCESSFUL || + event == PlocSupvUartManager::SUPV_MEM_CHECK_FAIL || + event == PlocSupvUartManager::SUPV_MEM_CHECK_OK) { + // Wait for a short period for the uart state machine to adjust + // TaskFactory::delayTask(5); + if (not shutdownCmdSent) { + shutdownCmdSent = true; + result = this->executeAction(supv::SHUTDOWN_MPSOC, NO_COMMANDER, nullptr, 0); + if (result != returnvalue::OK) { + triggerEvent(SUPV_MPSOC_SHUTDOWN_BUILD_FAILED); + sif::warning << "PlocSupervisorHandler::handleEvent: Failed to build MPSoC shutdown " + "command" + << std::endl; + return; + } + } + } + break; + } + default: + sif::debug << "PlocMPSoCHandler::handleEvent: Did not subscribe to this event" << std::endl; + break; + } +} + +void PlocSupervisorHandler::setExecutionTimeout(DeviceCommandId_t command) { + using namespace supv; + switch (command) { + case FIRST_MRAM_DUMP: + case CONSECUTIVE_MRAM_DUMP: + executionReportTimeout.setTimeout(MRAM_DUMP_EXECUTION_TIMEOUT); + break; + case COPY_ADC_DATA_TO_MRAM: + executionReportTimeout.setTimeout(COPY_ADC_TO_MRAM_TIMEOUT); + break; + default: + executionReportTimeout.setTimeout(EXECUTION_DEFAULT_TIMEOUT); + break; + } +} + +ReturnValue_t PlocSupervisorHandler::verifyPacket(const uint8_t* start, size_t foundLen) { + if (CRC::crc16ccitt(start, foundLen) != 0) { + return result::CRC_FAILURE; + } + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::handleAckReport(const uint8_t* data) { + using namespace supv; + ReturnValue_t result = returnvalue::OK; + + if (not tmReader.verifyCrc()) { + sif::error << "PlocSupervisorHandler::handleAckReport: CRC failure" << std::endl; + nextReplyId = supv::NONE; + replyRawReplyIfnotWiretapped(data, supv::SIZE_ACK_REPORT); + triggerEvent(SUPV_CRC_FAILURE_EVENT); + sendFailureReport(supv::ACK_REPORT, result::CRC_FAILURE); + disableAllReplies(); + return returnvalue::OK; + } + AcknowledgmentReport ack(tmReader); + result = ack.parse(); + if (result != returnvalue::OK) { + nextReplyId = supv::NONE; + replyRawReplyIfnotWiretapped(data, supv::SIZE_ACK_REPORT); + triggerEvent(SUPV_CRC_FAILURE_EVENT); + sendFailureReport(supv::ACK_REPORT, result); + disableAllReplies(); + return result; + } + if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::NAK)) { + DeviceCommandId_t commandId = getPendingCommand(); + if (commandId != DeviceHandlerIF::NO_COMMAND_ID) { + triggerEvent(SUPV_ACK_FAILURE, commandId, static_cast(ack.getStatusCode())); + } + ack.printStatusInformation(); + printAckFailureInfo(ack.getStatusCode(), commandId); + sendFailureReport(supv::ACK_REPORT, result::RECEIVED_ACK_FAILURE); + disableAllReplies(); + nextReplyId = supv::NONE; + result = IGNORE_REPLY_DATA; + } else if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::ACK)) { + setNextReplyId(); + } + return result; +} + +ReturnValue_t PlocSupervisorHandler::handleExecutionReport(const uint8_t* data) { + using namespace supv; + ReturnValue_t result = returnvalue::OK; + + if (not tmReader.verifyCrc()) { + nextReplyId = supv::NONE; + return result::CRC_FAILURE; + } + ExecutionReport report(tmReader); + result = report.parse(); + if (result != OK) { + nextReplyId = supv::NONE; + return result; + } + if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_ACK)) { + result = handleExecutionSuccessReport(report); + } else if (tmReader.getServiceId() == static_cast(supv::tm::TmtcId::EXEC_NAK)) { + handleExecutionFailureReport(report); + } + commandIsPending = false; + nextReplyId = supv::NONE; + return result; +} + +ReturnValue_t PlocSupervisorHandler::handleHkReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, tmReader.getFullPacketLen()); + + if (result == result::CRC_FAILURE) { + sif::error << "PlocSupervisorHandler::handleHkReport: Hk report has invalid crc" << std::endl; + return result; + } + + uint16_t offset = supv::PAYLOAD_OFFSET; + hkset.tempPs = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.tempPl = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.tempSup = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + size_t size = sizeof(hkset.uptime.value); + result = SerializeAdapter::deSerialize(&hkset.uptime, data + offset, &size, + SerializeIF::Endianness::BIG); + offset += 8; + hkset.cpuLoad = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.availableHeap = *(data + offset) << 24 | *(data + offset + 1) << 16 | + *(data + offset + 2) << 8 | *(data + offset + 3); + offset += 4; + hkset.numTcs = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.numTms = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.socState = *(data + offset) << 24 | *(data + offset + 1) << 16 | *(data + offset + 2) << 8 | + *(data + offset + 3); + offset += 4; + hkset.nvm0_1_state = *(data + offset); + offset += 1; + hkset.nvm3_state = *(data + offset); + offset += 1; + hkset.missionIoState = *(data + offset); + offset += 1; + hkset.fmcState = *(data + offset); + offset += 1; + + nextReplyId = supv::EXE_REPORT; + hkset.setValidity(true, true); + +#if OBSW_VERBOSE_LEVEL >= 1 && OBSW_DEBUG_PLOC_SUPERVISOR == 1 + sif::info << "PlocSupervisorHandler::handleHkReport: temp_ps: " << hkset.tempPs << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: temp_pl: " << hkset.tempPl << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: temp_sup: " << hkset.tempSup << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: uptime: " << hkset.uptime << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: cpu_load: " << hkset.cpuLoad << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: available_heap: " << hkset.availableHeap + << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: num_tcs: " << hkset.numTcs << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: num_tms: " << hkset.numTms << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: soc_state: " << hkset.socState << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: nvm0_1_state: " + << static_cast(hkset.nvm0_1_state.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: nvm3_state: " + << static_cast(hkset.nvm3_state.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: mission_io_state: " + << static_cast(hkset.missionIoState.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleHkReport: fmc_state: " + << static_cast(hkset.fmcState.value) << std::endl; + +#endif + + return result; +} + +ReturnValue_t PlocSupervisorHandler::handleBootStatusReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, tmReader.getFullPacketLen()); + + if (result == result::CRC_FAILURE) { + sif::error << "PlocSupervisorHandler::handleBootStatusReport: Boot status report has invalid" + " crc" + << std::endl; + return result; + } + + const uint8_t* payloadStart = tmReader.getPayloadStart(); + uint16_t offset = 0; + bootStatusReport.socState = payloadStart[0]; + offset += 1; + bootStatusReport.powerCycles = payloadStart[1]; + offset += 1; + bootStatusReport.bootAfterMs = *(payloadStart + offset) << 24 | + *(payloadStart + offset + 1) << 16 | + *(payloadStart + offset + 2) << 8 | *(payloadStart + offset + 3); + offset += 4; + bootStatusReport.bootTimeoutMs = *(payloadStart + offset) << 24 | + *(payloadStart + offset + 1) << 16 | + *(payloadStart + offset + 2) << 8 | *(payloadStart + offset + 3); + offset += 4; + bootStatusReport.activeNvm = *(payloadStart + offset); + offset += 1; + bootStatusReport.bp0State = *(payloadStart + offset); + offset += 1; + bootStatusReport.bp1State = *(payloadStart + offset); + offset += 1; + bootStatusReport.bp2State = *(payloadStart + offset); + offset += 1; + bootStatusReport.bootState = *(payloadStart + offset); + offset += 1; + bootStatusReport.bootCycles = *(payloadStart + offset); + + nextReplyId = supv::EXE_REPORT; + bootStatusReport.setValidity(true, true); + +#if OBSW_VERBOSE_LEVEL >= 1 && OBSW_DEBUG_PLOC_SUPERVISOR == 1 + sif::info << "PlocSupervisorHandler::handleBootStatusReport: SoC State (0 - off, 1 - booting, 2 " + "- Update, 3 " + "- operating, 4 - Shutdown, 5 - Reset): " + << static_cast(bootStatusReport.socState.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: Power Cycles: " + << static_cast(bootStatusReport.powerCycles.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: BootAfterMs: " + << bootStatusReport.bootAfterMs << " ms" << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: BootTimeoutMs: " << std::dec + << bootStatusReport.bootTimeoutMs << " ms" << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: Active NVM: " + << static_cast(bootStatusReport.activeNvm.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: BP0: " + << static_cast(bootStatusReport.bp0State.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: BP1: " + << static_cast(bootStatusReport.bp1State.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: BP2: " + << static_cast(bootStatusReport.bp2State.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: Boot state: " + << static_cast(bootStatusReport.bootState.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleBootStatusReport: Boot cycles: " + << static_cast(bootStatusReport.bootCycles.value) << std::endl; +#endif + + return result; +} + +ReturnValue_t PlocSupervisorHandler::handleLatchupStatusReport(const uint8_t* data) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, tmReader.getFullPacketLen()); + + if (result == result::CRC_FAILURE) { + sif::error << "PlocSupervisorHandler::handleLatchupStatusReport: Latchup status report has " + << "invalid crc" << std::endl; + return result; + } + + const uint8_t* payloadData = tmReader.getPayloadStart(); + uint16_t offset = 0; + latchupStatusReport.id = *(payloadData + offset); + offset += 1; + latchupStatusReport.cnt0 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt1 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt2 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt3 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt4 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt5 = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + offset += 2; + latchupStatusReport.cnt6 = *(payloadData + offset) << 8 | *(data + offset + 1); + offset += 2; + uint16_t msec = *(payloadData + offset) << 8 | *(payloadData + offset + 1); + latchupStatusReport.isSet = msec >> supv::LatchupStatusReport::IS_SET_BIT_POS; + latchupStatusReport.timeMsec = msec & (~(1 << latchupStatusReport.IS_SET_BIT_POS)); + offset += 2; + latchupStatusReport.timeSec = *(payloadData + offset); + offset += 1; + latchupStatusReport.timeMin = *(payloadData + offset); + offset += 1; + latchupStatusReport.timeHour = *(payloadData + offset); + offset += 1; + latchupStatusReport.timeDay = *(payloadData + offset); + offset += 1; + latchupStatusReport.timeMon = *(payloadData + offset); + offset += 1; + latchupStatusReport.timeYear = *(payloadData + offset); + + nextReplyId = supv::EXE_REPORT; + +#if OBSW_VERBOSE_LEVEL >= 1 && OBSW_DEBUG_PLOC_SUPERVISOR == 1 + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Latchup ID: " + << static_cast(latchupStatusReport.id.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT0: " + << latchupStatusReport.cnt0 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT1: " + << latchupStatusReport.cnt1 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT2: " + << latchupStatusReport.cnt2 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT3: " + << latchupStatusReport.cnt3 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT4: " + << latchupStatusReport.cnt4 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT5: " + << latchupStatusReport.cnt5 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: CNT6: " + << latchupStatusReport.cnt6 << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Sec: " + << static_cast(latchupStatusReport.timeSec.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Min: " + << static_cast(latchupStatusReport.timeMin.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Hour: " + << static_cast(latchupStatusReport.timeHour.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Day: " + << static_cast(latchupStatusReport.timeDay.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Mon: " + << static_cast(latchupStatusReport.timeMon.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Year: " + << static_cast(latchupStatusReport.timeYear.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: Msec: " + << static_cast(latchupStatusReport.timeMsec.value) << std::endl; + sif::info << "PlocSupervisorHandler::handleLatchupStatusReport: isSet: " + << static_cast(latchupStatusReport.isSet.value) << std::endl; +#endif + + return result; +} + +ReturnValue_t PlocSupervisorHandler::genericHandleTm(const char* contextString, const uint8_t* data, + LocalPoolDataSetBase& set) { + ReturnValue_t result = returnvalue::OK; + + result = verifyPacket(data, tmReader.getFullPacketLen()); + + if (result == result::CRC_FAILURE) { + sif::warning << "PlocSupervisorHandler: " << contextString << " report has " + << "invalid CRC" << std::endl; + return result; + } + + const uint8_t* dataField = data + supv::PAYLOAD_OFFSET; + PoolReadGuard pg(&set); + if (pg.getReadResult() != returnvalue::OK) { + return result; + } + set.setValidityBufferGeneration(false); + size_t size = set.getSerializedSize(); + result = set.deSerialize(&dataField, &size, SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler: Deserialization failed" << std::endl; + } + set.setValidityBufferGeneration(true); + set.setValidity(true, true); + nextReplyId = supv::EXE_REPORT; + return result; +} + +void PlocSupervisorHandler::setNextReplyId() { + switch (getPendingCommand()) { + case supv::GET_HK_REPORT: + nextReplyId = supv::HK_REPORT; + break; + case supv::GET_BOOT_STATUS_REPORT: + nextReplyId = supv::BOOT_STATUS_REPORT; + break; + case supv::GET_LATCHUP_STATUS_REPORT: + nextReplyId = supv::LATCHUP_REPORT; + break; + case supv::FIRST_MRAM_DUMP: + nextReplyId = supv::FIRST_MRAM_DUMP; + break; + case supv::CONSECUTIVE_MRAM_DUMP: + nextReplyId = supv::CONSECUTIVE_MRAM_DUMP; + break; + case supv::REQUEST_LOGGING_COUNTERS: + nextReplyId = supv::COUNTERS_REPORT; + break; + case supv::REQUEST_ADC_REPORT: + nextReplyId = supv::ADC_REPORT; + break; + default: + /* If no telemetry is expected the next reply is always the execution report */ + nextReplyId = supv::EXE_REPORT; + break; + } +} + +size_t PlocSupervisorHandler::getNextReplyLength(DeviceCommandId_t commandId) { + size_t replyLen = 0; + + if (nextReplyId == supv::NONE) { + return replyLen; + } + + if (nextReplyId == supv::FIRST_MRAM_DUMP || nextReplyId == supv::CONSECUTIVE_MRAM_DUMP) { + /** + * Try to read 20 MRAM packets. If reply is larger, the packets will be read with the + * next doSendRead call. The command will be as long active as the packet with the sequence + * count indicating the last packet has not been received. + */ + replyLen = supv::MAX_PACKET_SIZE * 20; + return replyLen; + } + + DeviceReplyIter iter = deviceReplyMap.find(nextReplyId); + if (iter != deviceReplyMap.end()) { + if ((iter->second.delayCycles == 0 && iter->second.countdown == nullptr) || + (not iter->second.active && iter->second.countdown != nullptr)) { + /* Reply inactive */ + return replyLen; + } + replyLen = iter->second.replyLen; + } else { + sif::debug << "PlocSupervisorHandler::getNextReplyLength: No entry for reply with reply id " + << std::hex << nextReplyId << " in deviceReplyMap" << std::endl; + } + + return replyLen; +} + +void PlocSupervisorHandler::doOffActivity() {} + +void PlocSupervisorHandler::handleDeviceTm(const uint8_t* data, size_t dataSize, + DeviceCommandId_t replyId) { + ReturnValue_t result = returnvalue::OK; + + if (wiretappingMode == RAW) { + /* Data already sent in doGetRead() */ + return; + } + + DeviceReplyMap::iterator iter = deviceReplyMap.find(replyId); + if (iter == deviceReplyMap.end()) { + sif::debug << "PlocSupervisorHandler::handleDeviceTM: Unknown reply id" << std::endl; + return; + } + MessageQueueId_t queueId = iter->second.command->second.sendReplyTo; + + if (queueId == NO_COMMANDER) { + return; + } + + result = actionHelper.reportData(queueId, replyId, data, dataSize); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler::handleDeviceTM: Failed to report data" << std::endl; + } +} + +ReturnValue_t PlocSupervisorHandler::prepareEmptyCmd(uint16_t apid, uint8_t serviceId) { + supv::NoPayloadPacket packet(spParams, apid, serviceId); + ReturnValue_t result = packet.buildPacket(); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSelBootImageCmd(const uint8_t* commandData) { + supv::MPSoCBootSelect packet(spParams); + ReturnValue_t result = + packet.buildPacket(commandData[0], commandData[1], commandData[2], commandData[3]); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetTimeRefCmd() { + Clock::TimeOfDay_t time; + ReturnValue_t result = Clock::getDateAndTime(&time); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler::prepareSetTimeRefCmd: Failed to get current time" + << std::endl; + return result::GET_TIME_FAILURE; + } + supv::SetTimeRef packet(spParams); + result = packet.buildPacket(&time); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareDisableHk() { + supv::DisablePeriodicHkTransmission packet(spParams); + ReturnValue_t result = packet.buildPacket(); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetBootTimeoutCmd(const uint8_t* commandData) { + supv::SetBootTimeout packet(spParams); + uint32_t timeout = *(commandData) << 24 | *(commandData + 1) << 16 | *(commandData + 2) << 8 | + *(commandData + 3); + ReturnValue_t result = packet.buildPacket(timeout); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareRestartTriesCmd(const uint8_t* commandData) { + uint8_t restartTries = *(commandData); + supv::SetRestartTries packet(spParams); + ReturnValue_t result = packet.buildPacket(restartTries); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareLatchupConfigCmd(const uint8_t* commandData, + DeviceCommandId_t deviceCommand) { + ReturnValue_t result = returnvalue::OK; + uint8_t latchupId = *commandData; + if (latchupId > 6) { + return result::INVALID_LATCHUP_ID; + } + switch (deviceCommand) { + case (supv::ENABLE_LATCHUP_ALERT): { + supv::LatchupAlert packet(spParams); + result = packet.buildPacket(true, latchupId); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + break; + } + case (supv::DISABLE_LATCHUP_ALERT): { + supv::LatchupAlert packet(spParams); + result = packet.buildPacket(false, latchupId); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + break; + } + default: { + sif::debug << "PlocSupervisorHandler::prepareLatchupConfigCmd: Invalid command id" + << std::endl; + result = returnvalue::FAILED; + break; + } + } + return result; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetAlertLimitCmd(const uint8_t* commandData) { + uint8_t offset = 0; + uint8_t latchupId = *commandData; + offset += 1; + uint32_t dutycycle = *(commandData + offset) << 24 | *(commandData + offset + 1) << 16 | + *(commandData + offset + 2) << 8 | *(commandData + offset + 3); + if (latchupId > 6) { + return result::INVALID_LATCHUP_ID; + } + supv::SetAlertlimit packet(spParams); + ReturnValue_t result = packet.buildPacket(latchupId, dutycycle); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetAdcEnabledChannelsCmd(const uint8_t* commandData) { + uint16_t ch = *(commandData) << 8 | *(commandData + 1); + supv::SetAdcEnabledChannels packet(spParams); + ReturnValue_t result = packet.buildPacket(ch); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetAdcWindowAndStrideCmd(const uint8_t* commandData) { + uint8_t offset = 0; + uint16_t windowSize = *(commandData + offset) << 8 | *(commandData + offset + 1); + offset += 2; + uint16_t stridingStepSize = *(commandData + offset) << 8 | *(commandData + offset + 1); + supv::SetAdcWindowAndStride packet(spParams); + ReturnValue_t result = packet.buildPacket(windowSize, stridingStepSize); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetAdcThresholdCmd(const uint8_t* commandData) { + uint32_t threshold = *(commandData) << 24 | *(commandData + 1) << 16 | *(commandData + 2) << 8 | + *(commandData + 3); + supv::SetAdcThreshold packet(spParams); + ReturnValue_t result = packet.buildPacket(threshold); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareRunAutoEmTest(const uint8_t* commandData) { + uint8_t test = *commandData; + if (test != 1 && test != 2) { + return result::INVALID_TEST_PARAM; + } + supv::RunAutoEmTests packet(spParams); + ReturnValue_t result = packet.buildPacket(test); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareSetGpioCmd(const uint8_t* commandData, + size_t commandDataLen) { + if (commandDataLen < 3) { + return HasActionsIF::INVALID_PARAMETERS; + } + uint8_t port = *commandData; + uint8_t pin = *(commandData + 1); + uint8_t val = *(commandData + 2); + supv::SetGpio packet(spParams); + ReturnValue_t result = packet.buildPacket(port, pin, val); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareReadGpioCmd(const uint8_t* commandData, + size_t commandDataLen) { + if (commandDataLen < 2) { + return HasActionsIF::INVALID_PARAMETERS; + } + uint8_t port = *commandData; + uint8_t pin = *(commandData + 1); + supv::ReadGpio packet(spParams); + ReturnValue_t result = packet.buildPacket(port, pin); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareFactoryResetCmd(const uint8_t* commandData, + size_t len) { + FactoryReset resetCmd(spParams); + if (len < 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + ReturnValue_t result = resetCmd.buildPacket(commandData[0]); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(resetCmd); + return returnvalue::OK; +} + +void PlocSupervisorHandler::finishTcPrep(TcBase& tc) { + nextReplyId = supv::ACK_REPORT; + rawPacket = commandBuffer; + rawPacketLen = tc.getFullPacketLen(); + if (DEBUG_PLOC_SUPV) { + sif::debug << "PLOC SUPV: SEND PACKET Size " << tc.getFullPacketLen() << " Module APID " + << (int)tc.getModuleApid() << " Service ID " << (int)tc.getServiceId() << std::endl; + } +} + +ReturnValue_t PlocSupervisorHandler::prepareSetShutdownTimeoutCmd(const uint8_t* commandData) { + uint32_t timeout = 0; + ReturnValue_t result = returnvalue::OK; + size_t size = sizeof(timeout); + result = + SerializeAdapter::deSerialize(&timeout, &commandData, &size, SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning + << "PlocSupervisorHandler::prepareSetShutdownTimeoutCmd: Failed to deserialize timeout" + << std::endl; + return result; + } + supv::SetShutdownTimeout packet(spParams); + result = packet.buildPacket(timeout); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +void PlocSupervisorHandler::disableAllReplies() { + using namespace supv; + DeviceReplyMap::iterator iter; + + /* Disable ack reply */ + iter = deviceReplyMap.find(ACK_REPORT); + if (iter == deviceReplyMap.end()) { + return; + } + DeviceReplyInfo* info = &(iter->second); + if (info == nullptr) { + return; + } + info->delayCycles = 0; + info->command = deviceCommandMap.end(); + + DeviceCommandId_t commandId = getPendingCommand(); + + /* If the command expects a telemetry packet the appropriate tm reply will be disabled here */ + switch (commandId) { + case GET_HK_REPORT: { + disableReply(GET_HK_REPORT); + break; + } + case FIRST_MRAM_DUMP: + case CONSECUTIVE_MRAM_DUMP: { + disableReply(commandId); + break; + } + case REQUEST_ADC_REPORT: { + disableReply(ADC_REPORT); + break; + } + case GET_BOOT_STATUS_REPORT: { + disableReply(BOOT_STATUS_REPORT); + break; + } + case GET_LATCHUP_STATUS_REPORT: { + disableReply(LATCHUP_REPORT); + break; + } + case REQUEST_LOGGING_COUNTERS: { + disableReply(COUNTERS_REPORT); + break; + } + default: { + break; + } + } + + /* We must always disable the execution report reply here */ + disableExeReportReply(); +} + +void PlocSupervisorHandler::disableReply(DeviceCommandId_t replyId) { + DeviceReplyMap::iterator iter = deviceReplyMap.find(replyId); + if (iter == deviceReplyMap.end()) { + return; + } + DeviceReplyInfo* info = &(iter->second); + info->delayCycles = 0; + info->active = false; + info->command = deviceCommandMap.end(); +} + +void PlocSupervisorHandler::sendFailureReport(DeviceCommandId_t replyId, ReturnValue_t status) { + DeviceReplyIter iter = deviceReplyMap.find(replyId); + + if (iter == deviceReplyMap.end()) { + sif::debug << "PlocSupervisorHandler::sendFailureReport: Reply not in reply map" << std::endl; + return; + } + + DeviceCommandInfo* info = &(iter->second.command->second); + + if (info == nullptr) { + sif::debug << "PlocSupervisorHandler::sendFailureReport: Reply has no active command" + << std::endl; + return; + } + + if (info->sendReplyTo != NO_COMMANDER) { + actionHelper.finish(false, info->sendReplyTo, iter->first, status); + } + info->isExecuting = false; +} + +void PlocSupervisorHandler::disableExeReportReply() { + DeviceReplyIter iter = deviceReplyMap.find(supv::EXE_REPORT); + if (iter == deviceReplyMap.end()) { + return; + } + DeviceReplyInfo* info = &(iter->second); + info->delayCycles = 0; + info->command = deviceCommandMap.end(); + info->active = false; + /* Expected replies is set to one here. The value will set to 0 in replyToReply() */ + info->command->second.expectedReplies = 1; +} + +ReturnValue_t PlocSupervisorHandler::handleMramDumpPacket(DeviceCommandId_t id) { + ReturnValue_t result = returnvalue::FAILED; + + // Prepare packet for downlink + if (packetInBuffer) { + uint16_t packetLen = readSpacePacketLength(spacePacketBuffer); + result = verifyPacket(spacePacketBuffer, ccsds::HEADER_LEN + packetLen + 1); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler::handleMramDumpPacket: CRC failure" << std::endl; + return result; + } + result = handleMramDumpFile(id); + if (result != returnvalue::OK) { + DeviceCommandMap::iterator iter = deviceCommandMap.find(id); + if (iter != deviceCommandMap.end()) { + actionHelper.finish(false, iter->second.sendReplyTo, id, result); + } + disableAllReplies(); + nextReplyId = supv::NONE; + return result; + } + packetInBuffer = false; + receivedMramDumpPackets++; + if (expectedMramDumpPackets == receivedMramDumpPackets) { + nextReplyId = supv::EXE_REPORT; + } + increaseExpectedMramReplies(id); + return returnvalue::OK; + } + return result; +} + +void PlocSupervisorHandler::increaseExpectedMramReplies(DeviceCommandId_t id) { + DeviceReplyMap::iterator mramDumpIter = deviceReplyMap.find(id); + DeviceReplyMap::iterator exeReportIter = deviceReplyMap.find(supv::EXE_REPORT); + if (mramDumpIter == deviceReplyMap.end()) { + sif::debug << "PlocSupervisorHandler::increaseExpectedMramReplies: Dump MRAM reply not " + << "in reply map" << std::endl; + return; + } + if (exeReportIter == deviceReplyMap.end()) { + sif::debug << "PlocSupervisorHandler::increaseExpectedMramReplies: Execution report not " + << "in reply map" << std::endl; + return; + } + DeviceReplyInfo* mramReplyInfo = &(mramDumpIter->second); + if (mramReplyInfo == nullptr) { + sif::debug << "PlocSupervisorHandler::increaseExpectedReplies: MRAM reply info nullptr" + << std::endl; + return; + } + DeviceReplyInfo* exeReplyInfo = &(exeReportIter->second); + if (exeReplyInfo == nullptr) { + sif::debug << "PlocSupervisorHandler::increaseExpectedReplies: Execution reply info" + << " nullptr" << std::endl; + return; + } + DeviceCommandInfo* info = &(mramReplyInfo->command->second); + if (info == nullptr) { + sif::debug << "PlocSupervisorHandler::increaseExpectedReplies: Command info nullptr" + << std::endl; + return; + } + uint8_t sequenceFlags = spacePacketBuffer[2] >> 6; + if (sequenceFlags != static_cast(ccsds::SequenceFlags::LAST_SEGMENT) && + (sequenceFlags != static_cast(ccsds::SequenceFlags::UNSEGMENTED))) { + // Command expects at least one MRAM packet more and the execution report + info->expectedReplies = 2; + mramReplyInfo->countdown->resetTimer(); + } else { + // Command expects the execution report + info->expectedReplies = 1; + mramReplyInfo->active = false; + } + exeReplyInfo->countdown->resetTimer(); + return; +} + +ReturnValue_t PlocSupervisorHandler::handleMramDumpFile(DeviceCommandId_t id) { +#ifdef XIPHOS_Q7S + if (not sdcMan->getActiveSdCard()) { + return HasFileSystemIF::FILESYSTEM_INACTIVE; + } +#endif + ReturnValue_t result = returnvalue::OK; + uint16_t packetLen = readSpacePacketLength(spacePacketBuffer); + uint8_t sequenceFlags = readSequenceFlags(spacePacketBuffer); + if (id == supv::FIRST_MRAM_DUMP) { + if (sequenceFlags == static_cast(ccsds::SequenceFlags::FIRST_SEGMENT) || + (sequenceFlags == static_cast(ccsds::SequenceFlags::UNSEGMENTED))) { + result = createMramDumpFile(); + if (result != returnvalue::OK) { + return result; + } + } + } + if (not std::filesystem::exists(activeMramFile)) { + sif::warning << "PlocSupervisorHandler::handleMramDumpFile: MRAM file does not exist" + << std::endl; + return result::MRAM_FILE_NOT_EXISTS; + } + std::ofstream file(activeMramFile, std::ios_base::app | std::ios_base::out); + file.write(reinterpret_cast(spacePacketBuffer + ccsds::HEADER_LEN), packetLen - 1); + file.close(); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::prepareWipeMramCmd(const uint8_t* commandData) { + uint32_t start = 0; + uint32_t stop = 0; + size_t size = sizeof(start) + sizeof(stop); + SerializeAdapter::deSerialize(&start, &commandData, &size, SerializeIF::Endianness::BIG); + SerializeAdapter::deSerialize(&stop, &commandData, &size, SerializeIF::Endianness::BIG); + if ((stop - start) <= 0) { + return result::INVALID_MRAM_ADDRESSES; + } + supv::MramCmd packet(spParams); + ReturnValue_t result = packet.buildPacket(start, stop, supv::MramCmd::MramAction::WIPE); + if (result != returnvalue::OK) { + return result; + } + finishTcPrep(packet); + return returnvalue::OK; +} + +uint16_t PlocSupervisorHandler::readSpacePacketLength(uint8_t* spacePacket) { + return spacePacket[4] << 8 | spacePacket[5]; +} + +uint8_t PlocSupervisorHandler::readSequenceFlags(uint8_t* spacePacket) { + return spacePacketBuffer[2] >> 6; +} + +ReturnValue_t PlocSupervisorHandler::createMramDumpFile() { + ReturnValue_t result = returnvalue::OK; + std::string timeStamp; + result = getTimeStampString(timeStamp); + if (result != returnvalue::OK) { + return result; + } + + std::string filename = "mram-dump--" + timeStamp + ".bin"; + +#ifdef XIPHOS_Q7S + const char* currentMountPrefix = sdcMan->getCurrentMountPrefix(); +#else + const char* currentMountPrefix = "/mnt/sd0"; +#endif /* BOARD_TE0720 == 0 */ + if (currentMountPrefix == nullptr) { + return returnvalue::FAILED; + } + + // Check if path to PLOC directory exists + if (not std::filesystem::exists(std::string(currentMountPrefix) + "/" + supervisorFilePath)) { + sif::warning << "PlocSupervisorHandler::createMramDumpFile: Supervisor path does not exist" + << std::endl; + return result::PATH_DOES_NOT_EXIST; + } + activeMramFile = std::string(currentMountPrefix) + "/" + supervisorFilePath + "/" + filename; + // Create new file + std::ofstream file(activeMramFile, std::ios_base::out); + file.close(); + + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::getTimeStampString(std::string& timeStamp) { + Clock::TimeOfDay_t time; + ReturnValue_t result = Clock::getDateAndTime(&time); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler::getTimeStampString: Failed to get current time" + << std::endl; + return result::GET_TIME_FAILURE; + } + timeStamp = std::to_string(time.year) + "-" + std::to_string(time.month) + "-" + + std::to_string(time.day) + "--" + std::to_string(time.hour) + "-" + + std::to_string(time.minute) + "-" + std::to_string(time.second); + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::extractUpdateCommand(const uint8_t* commandData, size_t size, + supv::UpdateParams& params) { + size_t remSize = size; + if (size > (config::MAX_FILENAME_SIZE + config::MAX_PATH_SIZE + sizeof(params.memId)) + + sizeof(params.startAddr) + sizeof(params.bytesWritten) + sizeof(params.seqCount) + + sizeof(uint8_t)) { + sif::warning << "PlocSupervisorHandler::extractUpdateCommand: Data size too big" << std::endl; + return result::INVALID_LENGTH; + } + ReturnValue_t result = returnvalue::OK; + result = extractBaseParams(&commandData, size, params); + result = SerializeAdapter::deSerialize(¶ms.bytesWritten, &commandData, &remSize, + SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler::extractUpdateCommand: Failed to deserialize bytes " + "already written" + << std::endl; + return result; + } + result = SerializeAdapter::deSerialize(¶ms.seqCount, &commandData, &remSize, + SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning + << "PlocSupervisorHandler::extractUpdateCommand: Failed to deserialize start sequence count" + << std::endl; + return result; + } + uint8_t delMemRaw = 0; + result = SerializeAdapter::deSerialize(&delMemRaw, &commandData, &remSize, + SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning + << "PlocSupervisorHandler::extractUpdateCommand: Failed to deserialize whether to delete " + "memory" + << std::endl; + return result; + } + params.deleteMemory = delMemRaw; + return returnvalue::OK; +} + +ReturnValue_t PlocSupervisorHandler::extractBaseParams(const uint8_t** commandData, size_t& remSize, + supv::UpdateParams& params) { + bool nullTermFound = false; + for (size_t idx = 0; idx < remSize; idx++) { + if ((*commandData)[idx] == '\0') { + nullTermFound = true; + break; + } + } + if (not nullTermFound) { + return returnvalue::FAILED; + } + params.file = std::string(reinterpret_cast(*commandData)); + if (params.file.size() > (config::MAX_FILENAME_SIZE + config::MAX_PATH_SIZE)) { + sif::warning << "PlocSupervisorHandler::extractUpdateCommand: Filename too long" << std::endl; + return result::FILENAME_TOO_LONG; + } + *commandData += params.file.size() + SIZE_NULL_TERMINATOR; + remSize -= (params.file.size() + SIZE_NULL_TERMINATOR); + params.memId = **commandData; + *commandData += 1; + remSize -= 1; + ReturnValue_t result = SerializeAdapter::deSerialize(¶ms.startAddr, commandData, &remSize, + SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler::extractBaseParams: Failed to deserialize start address" + << std::endl; + return result; + } + return result; +} + +ReturnValue_t PlocSupervisorHandler::eventSubscription() { + ReturnValue_t result = returnvalue::OK; + EventManagerIF* manager = ObjectManager::instance()->get(objects::EVENT_MANAGER); + if (manager == nullptr) { +#if FSFW_CPP_OSTREAM_ENABLED == 1 + sif::error << "PlocSupervisorHandler::eventSubscritpion: Invalid event manager" << std::endl; +#endif + return ObjectManagerIF::CHILD_INIT_FAILED; + ; + } + result = manager->registerListener(eventQueue->getId()); + if (result != returnvalue::OK) { + return result; + } + result = manager->subscribeToEventRange( + eventQueue->getId(), event::getEventId(PlocSupvUartManager::SUPV_UPDATE_FAILED), + event::getEventId(PlocSupvUartManager::SUPV_MEM_CHECK_FAIL)); + if (result != returnvalue::OK) { +#if FSFW_CPP_OSTREAM_ENABLED == 1 + sif::warning << "PlocSupervisorHandler::eventSubscritpion: Failed to subscribe to events from " + " ploc supervisor helper" + << std::endl; +#endif + return ObjectManagerIF::CHILD_INIT_FAILED; + } + return result; +} + +ReturnValue_t PlocSupervisorHandler::handleExecutionSuccessReport(ExecutionReport& report) { + DeviceCommandId_t commandId = getPendingCommand(); + DeviceCommandMap::iterator iter = deviceCommandMap.find(commandId); + if (iter != deviceCommandMap.end() and iter->second.sendReplyTo != NO_COMMANDER) { + actionHelper.finish(true, iter->second.sendReplyTo, iter->first, returnvalue::OK); + iter->second.isExecuting = false; + } + commandIsPending = false; + switch (commandId) { + case supv::READ_GPIO: { + // TODO: Fix + uint16_t gpioState = report.getStatusCode(); +#if OBSW_DEBUG_PLOC_SUPERVISOR == 1 + sif::info << "PlocSupervisorHandler: Read GPIO TM, State: " << gpioState << std::endl; +#endif /* OBSW_DEBUG_PLOC_SUPERVISOR == 1 */ + if (iter != deviceCommandMap.end() and iter->second.sendReplyTo == NO_COMMAND_ID) { + return returnvalue::OK; + } + uint8_t data[sizeof(gpioState)]; + size_t size = 0; + ReturnValue_t result = SerializeAdapter::serialize(&gpioState, data, &size, sizeof(gpioState), + SerializeIF::Endianness::BIG); + if (result != returnvalue::OK) { + sif::debug << "PlocSupervisorHandler: Failed to deserialize GPIO state" << std::endl; + } + result = actionHelper.reportData(iter->second.sendReplyTo, commandId, data, sizeof(data)); + if (result != returnvalue::OK) { + sif::warning << "PlocSupervisorHandler: Read GPIO, failed to report data" << std::endl; + } + break; + } + case supv::SET_TIME_REF: { + // We could only allow proper bootup when the time was set successfully, but + // this makes debugging difficult. + + if (startupState == StartupState::WAIT_FOR_TIME_REPLY) { + startupState = StartupState::TIME_WAS_SET; + } + break; + } + default: + break; + } + return returnvalue::OK; +} + +void PlocSupervisorHandler::handleExecutionFailureReport(ExecutionReport& report) { + using namespace supv; + DeviceCommandId_t commandId = getPendingCommand(); + report.printStatusInformation(); + if (commandId != DeviceHandlerIF::NO_COMMAND_ID) { + triggerEvent(SUPV_EXE_FAILURE, commandId, static_cast(report.getStatusCode())); + } + sendFailureReport(EXE_REPORT, result::RECEIVED_EXE_FAILURE); + disableExeReportReply(); +} + +void PlocSupervisorHandler::handleBadApidServiceCombination(Event event, unsigned int apid, + unsigned int serviceId) { + const char* printString = ""; + if (event == SUPV_UNKNOWN_TM) { + printString = "PlocSupervisorHandler: Unknown"; + } else if (event == SUPV_UNINIMPLEMENTED_TM) { + printString = "PlocSupervisorHandler: Unimplemented"; + } + triggerEvent(event, apid, tmReader.getServiceId()); + sif::warning << printString << " APID service combination 0x" << std::setw(2) << std::setfill('0') + << std::hex << apid << ", 0x" << std::setw(2) << serviceId << std::endl; +} + +void PlocSupervisorHandler::printAckFailureInfo(uint16_t statusCode, DeviceCommandId_t commandId) { + switch (commandId) { + case (supv::SET_TIME_REF): { + sif::warning + << "PlocSupervisoHandler: Setting time failed. Make sure the OBC has a valid time" + << std::endl; + break; + } + default: + break; + } +} + +ReturnValue_t PlocSupervisorHandler::getSwitches(const uint8_t** switches, + uint8_t* numberOfSwitches) { + if (powerSwitch == power::NO_SWITCH) { + return DeviceHandlerBase::NO_SWITCH; + } + *numberOfSwitches = 1; + *switches = &powerSwitch; + return returnvalue::OK; +} + +uint32_t PlocSupervisorHandler::getTransitionDelayMs(Mode_t modeFrom, Mode_t modeTo) { + return 7000; +} + +void PlocSupervisorHandler::disableCommand(DeviceCommandId_t cmd) { + auto commandIter = deviceCommandMap.find(GET_HK_REPORT); + commandIter->second.isExecuting = false; +} + +ReturnValue_t PlocSupervisorHandler::checkModeCommand(Mode_t commandedMode, + Submode_t commandedSubmode, + uint32_t* msToReachTheMode) { + if (commandedMode != MODE_OFF) { + PoolReadGuard pg(&enablePl); + if (pg.getReadResult() == returnvalue::OK) { + if (enablePl.plUseAllowed.isValid() and not enablePl.plUseAllowed.value) { + return NON_OP_STATE_OF_CHARGE; + } + } + } + return DeviceHandlerBase::checkModeCommand(commandedMode, commandedSubmode, msToReachTheMode); +} diff --git a/archive/PlocSupervisorHandler.h b/archive/PlocSupervisorHandler.h new file mode 100644 index 0000000..85c3d94 --- /dev/null +++ b/archive/PlocSupervisorHandler.h @@ -0,0 +1,389 @@ +#ifndef MISSION_DEVICES_PLOCSUPERVISORHANDLER_H_ +#define MISSION_DEVICES_PLOCSUPERVISORHANDLER_H_ + +#include +#include +#include + +#include "OBSWConfig.h" +#include "devices/powerSwitcherList.h" +#include "fsfw/devicehandlers/DeviceHandlerBase.h" +#include "fsfw/timemanager/Countdown.h" +#include "fsfw_hal/linux/gpio/Gpio.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" +#include "fsfw_hal/linux/serial/SerialComIF.h" + +#ifdef XIPHOS_Q7S +#include "bsp_q7s/fs/SdCardManager.h" +#endif + +using supv::ExecutionReport; +using supv::TcBase; + +static constexpr bool DEBUG_PLOC_SUPV = true; +static constexpr bool REDUCE_NORMAL_MODE_PRINTOUT = true; + +/** + * @brief This is the device handler for the supervisor of the PLOC which is programmed by + * Thales. + * + * @details The PLOC uses the space packet protocol for communication. On each command the PLOC + * answers with at least one acknowledgment and one execution report. + * Flight manual: + * https://egit.irs.uni-stuttgart.de/redmine/projects/eive-flight-manual/wiki/PLOC_Commands + * ILH ICD: https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/ + * Arbeitsdaten/08_Used%20Components/PLOC&fileid=940960 + * @author J. Meier + */ +class PlocSupervisorHandler : public DeviceHandlerBase { + public: + PlocSupervisorHandler(object_id_t objectId, CookieIF* comCookie, Gpio uartIsolatorSwitch, + power::Switch_t powerSwitch, PlocSupvUartManager& supvHelper); + virtual ~PlocSupervisorHandler(); + + virtual ReturnValue_t initialize() override; + void performOperationHook() override; + ReturnValue_t executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) override; + + protected: + void doStartUp() override; + void doShutDown() override; + ReturnValue_t buildNormalDeviceCommand(DeviceCommandId_t* id) override; + ReturnValue_t buildTransitionDeviceCommand(DeviceCommandId_t* id) override; + void fillCommandAndReplyMap() override; + ReturnValue_t buildCommandFromCommand(DeviceCommandId_t deviceCommand, const uint8_t* commandData, + size_t commandDataLen) override; + ReturnValue_t scanForReply(const uint8_t* start, size_t remainingSize, DeviceCommandId_t* foundId, + size_t* foundLen) override; + ReturnValue_t interpretDeviceReply(DeviceCommandId_t id, const uint8_t* packet) override; + uint32_t getTransitionDelayMs(Mode_t modeFrom, Mode_t modeTo) override; + ReturnValue_t initializeLocalDataPool(localpool::DataPool& localDataPoolMap, + LocalDataPoolManager& poolManager) override; + ReturnValue_t enableReplyInReplyMap(DeviceCommandMap::iterator command, + uint8_t expectedReplies = 1, bool useAlternateId = false, + DeviceCommandId_t alternateReplyID = 0) override; + size_t getNextReplyLength(DeviceCommandId_t deviceCommand) override; + // ReturnValue_t doSendReadHook() override; + void doOffActivity() override; + + private: + static const uint16_t APID_MASK = 0x7FF; + static const uint16_t PACKET_SEQUENCE_COUNT_MASK = 0x3FFF; + static const uint8_t EXE_STATUS_OFFSET = 10; + static const uint8_t SIZE_NULL_TERMINATOR = 1; + // 5 s + static const uint32_t EXECUTION_DEFAULT_TIMEOUT = 5000; + // 70 S + static const uint32_t ACKNOWLEDGE_DEFAULT_TIMEOUT = 5000; + // 60 s + static const uint32_t MRAM_DUMP_EXECUTION_TIMEOUT = 60000; + // 70 s + static const uint32_t COPY_ADC_TO_MRAM_TIMEOUT = 70000; + // 60 s + static const uint32_t MRAM_DUMP_TIMEOUT = 60000; + + enum class StartupState : uint8_t { + OFF, + BOOTING, + SET_TIME, + WAIT_FOR_TIME_REPLY, + TIME_WAS_SET, + ON + }; + + static constexpr bool SET_TIME_DURING_BOOT = true; + + StartupState startupState = StartupState::OFF; + + uint8_t commandBuffer[supv::MAX_COMMAND_SIZE]; + SpacePacketCreator creator; + supv::TcParams spParams = supv::TcParams(creator); + + /** + * This variable is used to store the id of the next reply to receive. This is necessary + * because the PLOC sends as reply to each command at least one acknowledgment and execution + * report. + */ + DeviceCommandId_t nextReplyId = supv::NONE; + + SerialComIF* uartComIf = nullptr; + LinuxLibgpioIF* gpioComIF = nullptr; + Gpio uartIsolatorSwitch; + bool shutdownCmdSent = false; + // Yeah, I am using an extra variable because I once again don't know + // what the hell the base class is doing and I don't care anymore. + bool normalCommandIsPending = false; + // True men implement their reply timeout handling themselves! + Countdown normalCmdCd = Countdown(2000); + bool commandIsPending = false; + Countdown cmdCd = Countdown(2000); + + supv::HkSet hkset; + supv::BootStatusReport bootStatusReport; + supv::LatchupStatusReport latchupStatusReport; + supv::CountersReport countersReport; + supv::AdcReport adcReport; + + const power::Switch_t powerSwitch = power::NO_SWITCH; + supv::TmBase tmReader; + + PlocSupvUartManager& uartManager; + MessageQueueIF* eventQueue = nullptr; + + /** Number of expected replies following the MRAM dump command */ + uint32_t expectedMramDumpPackets = 0; + uint32_t receivedMramDumpPackets = 0; + /** Set to true as soon as a complete space packet is present in the spacePacketBuffer */ + bool packetInBuffer = false; + + /** This buffer is used to concatenate space packets received in two different read steps */ + uint8_t spacePacketBuffer[supv::MAX_PACKET_SIZE]; + +#ifdef XIPHOS_Q7S + SdCardManager* sdcMan = nullptr; +#endif + + // Path to supervisor specific files on SD card + std::string supervisorFilePath = "ploc/supervisor"; + std::string activeMramFile; + + Countdown executionReportTimeout = Countdown(EXECUTION_DEFAULT_TIMEOUT, false); + Countdown acknowledgementReportTimeout = Countdown(ACKNOWLEDGE_DEFAULT_TIMEOUT, false); + // Vorago nees some time to boot properly + Countdown bootTimeout = Countdown(supv::BOOT_TIMEOUT_MS); + Countdown mramDumpTimeout = Countdown(MRAM_DUMP_TIMEOUT); + + PoolEntry adcRawEntry = PoolEntry(16); + PoolEntry adcEngEntry = PoolEntry(16); + PoolEntry latchupCounters = PoolEntry(7); + PoolEntry fmcStateEntry = PoolEntry(1); + PoolEntry bootStateEntry = PoolEntry(1); + PoolEntry bootCyclesEntry = PoolEntry(1); + PoolEntry tempSupEntry = PoolEntry(1); + + /** + * @brief Adjusts the timeout of the execution report dependent on command + */ + void setExecutionTimeout(DeviceCommandId_t command); + + void handlePacketPrint(); + + /** + * @brief Handles event messages received from the supervisor helper + */ + void handleEvent(EventMessage* eventMessage); + + ReturnValue_t getSwitches(const uint8_t** switches, uint8_t* numberOfSwitches); + + /** + * @brief This function checks the crc of the received PLOC reply. + * + * @param start Pointer to the first byte of the reply. + * @param foundLen Pointer to the length of the whole packet. + * + * @return returnvalue::OK if CRC is ok, otherwise CRC_FAILURE. + */ + ReturnValue_t verifyPacket(const uint8_t* start, size_t foundLen); + + /** + * @brief This function handles the acknowledgment report. + * + * @param data Pointer to the data holding the acknowledgment report. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleAckReport(const uint8_t* data); + + /** + * @brief This function handles the data of a execution report. + * + * @param data Pointer to the received data packet. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleExecutionReport(const uint8_t* data); + + /** + * @brief This function handles the housekeeping report. This means verifying the CRC of the + * reply and filling the appropriate dataset. + * + * @param data Pointer to the data buffer holding the housekeeping read report. + * + * @return returnvalue::OK if successful, otherwise an error code. + */ + ReturnValue_t handleHkReport(const uint8_t* data); + + /** + * @brief This function calls the function to check the CRC of the received boot status report + * and fills the associated dataset with the boot status information. + */ + ReturnValue_t handleBootStatusReport(const uint8_t* data); + + ReturnValue_t handleLatchupStatusReport(const uint8_t* data); + ReturnValue_t handleCounterReport(const uint8_t* data); + void handleBadApidServiceCombination(Event result, unsigned int apid, unsigned int serviceId); + ReturnValue_t handleAdcReport(const uint8_t* data); + ReturnValue_t genericHandleTm(const char* contextString, const uint8_t* data, + LocalPoolDataSetBase& set); + + void disableCommand(DeviceCommandId_t cmd); + + /** + * @brief Depending on the current active command, this function sets the reply id of the + * next reply after a successful acknowledgment report has been received. This is + * required by the function getNextReplyLength() to identify the length of the next + * reply to read. + */ + void setNextReplyId(); + + /** + * @brief This function handles action message replies in case the telemetry has been + * requested by another object. + * + * @param data Pointer to the telemetry data. + * @param dataSize Size of telemetry in bytes. + * @param replyId Id of the reply. This will be added to the ActionMessage. + */ + void handleDeviceTm(const uint8_t* data, size_t dataSize, DeviceCommandId_t replyId); + + /** + * @brief This function prepares a space packet which does not transport any data in the + * packet data field apart from the crc. + */ + ReturnValue_t prepareEmptyCmd(uint16_t apid, uint8_t serviceId); + + /** + * @brief This function initializes the space packet to select the boot image of the MPSoC. + */ + ReturnValue_t prepareSelBootImageCmd(const uint8_t* commandData); + + ReturnValue_t prepareDisableHk(); + + /** + * @brief This function fills the commandBuffer with the data to update the time of the + * PLOC supervisor. + */ + ReturnValue_t prepareSetTimeRefCmd(); + + /** + * @brief This function fills the commandBuffer with the data to change the boot timeout + * value in the PLOC supervisor. + */ + ReturnValue_t prepareSetBootTimeoutCmd(const uint8_t* commandData); + + ReturnValue_t prepareRestartTriesCmd(const uint8_t* commandData); + + ReturnValue_t prepareFactoryResetCmd(const uint8_t* commandData, size_t len); + + /** + * @brief This function fills the command buffer with the packet to enable or disable the + * watchdogs on the PLOC. + */ + void prepareWatchdogsEnableCmd(const uint8_t* commandData); + + /** + * @brief This function fills the command buffer with the packet to set the watchdog timer + * of one of the three watchdogs (PS, PL, INT). + */ + ReturnValue_t prepareWatchdogsConfigTimeoutCmd(const uint8_t* commandData); + + ReturnValue_t prepareLatchupConfigCmd(const uint8_t* commandData, + DeviceCommandId_t deviceCommand); + ReturnValue_t prepareSetAlertLimitCmd(const uint8_t* commandData); + ReturnValue_t prepareSetAdcEnabledChannelsCmd(const uint8_t* commandData); + ReturnValue_t prepareSetAdcWindowAndStrideCmd(const uint8_t* commandData); + ReturnValue_t prepareSetAdcThresholdCmd(const uint8_t* commandData); + ReturnValue_t prepareRunAutoEmTest(const uint8_t* commandData); + ReturnValue_t prepareWipeMramCmd(const uint8_t* commandData); + ReturnValue_t prepareSetGpioCmd(const uint8_t* commandData, size_t commandDataLen); + ReturnValue_t prepareReadGpioCmd(const uint8_t* commandData, size_t commandDataLen); + + /** + * @brief Copies the content of a space packet to the command buffer. + */ + void finishTcPrep(TcBase& tc); + + /** + * @brief In case an acknowledgment failure reply has been received this function disables + * all previously enabled commands and resets the exepected replies variable of an + * active command. + */ + void disableAllReplies(); + + void disableReply(DeviceCommandId_t replyId); + + /** + * @brief This function sends a failure report if the active action was commanded by an other + * object. + * + * @param replyId The id of the reply which signals a failure. + * @param status A status byte which gives information about the failure type. + */ + void sendFailureReport(DeviceCommandId_t replyId, ReturnValue_t status); + + /** + * @brief This function disables the execution report reply. Within this function also the + * the variable expectedReplies of an active command will be set to 0. + */ + void disableExeReportReply(); + + /** + * @brief This function generates the Service 8 packets for the MRAM dump data. + */ + ReturnValue_t handleMramDumpPacket(DeviceCommandId_t id); + + /** + * @brief With this function the number of expected replies following an MRAM dump command + * will be increased. This is necessary to release the command in case not all replies + * have been received. + */ + void increaseExpectedMramReplies(DeviceCommandId_t id); + + /** + * @brief Writes the data of the MRAM dump to a file. The file will be created when receiving + * the first packet. + */ + ReturnValue_t handleMramDumpFile(DeviceCommandId_t id); + + /** + * @brief Extracts the length field of a spacePacket referenced by the spacePacket pointer. + * + * @param spacePacket Pointer to the buffer holding the space packet. + * + * @return The value stored in the length field of the data field. + */ + uint16_t readSpacePacketLength(uint8_t* spacePacket); + + /** + * @brief Extracts the sequence flags from a space packet referenced by the spacePacket + * pointer. + * + * @param spacePacket Pointer to the buffer holding the space packet. + * + * @return uint8_t where the two least significant bits hold the sequence flags. + */ + uint8_t readSequenceFlags(uint8_t* spacePacket); + + ReturnValue_t createMramDumpFile(); + ReturnValue_t getTimeStampString(std::string& timeStamp); + + ReturnValue_t prepareSetShutdownTimeoutCmd(const uint8_t* commandData); + + ReturnValue_t extractUpdateCommand(const uint8_t* commandData, size_t size, + supv::UpdateParams& params); + ReturnValue_t extractBaseParams(const uint8_t** commandData, size_t& remSize, + supv::UpdateParams& params); + ReturnValue_t eventSubscription(); + + ReturnValue_t handleExecutionSuccessReport(ExecutionReport& report); + void handleExecutionFailureReport(ExecutionReport& report); + + void printAckFailureInfo(uint16_t statusCode, DeviceCommandId_t commandId); + + pwrctrl::EnablePl enablePl = pwrctrl::EnablePl(objects::POWER_CONTROLLER); + ReturnValue_t checkModeCommand(Mode_t commandedMode, Submode_t commandedSubmode, + uint32_t* msToReachTheMode) override; +}; + +#endif /* MISSION_DEVICES_PLOCSUPERVISORHANDLER_H_ */ diff --git a/archive/gpio/CMakeLists.txt b/archive/gpio/CMakeLists.txt new file mode 100644 index 0000000..9b20b35 --- /dev/null +++ b/archive/gpio/CMakeLists.txt @@ -0,0 +1,12 @@ +target_sources(${TARGET_NAME} PUBLIC + GpioCookie.cpp + LinuxLibgpioIF.cpp +) + +target_link_libraries(${TARGET_NAME} PUBLIC + gpiod +) + + + + diff --git a/archive/gpio/GpioCookie.cpp b/archive/gpio/GpioCookie.cpp new file mode 100644 index 0000000..b1bb2db --- /dev/null +++ b/archive/gpio/GpioCookie.cpp @@ -0,0 +1,32 @@ +#include "GpioCookie.h" + +#include + +GpioCookie::GpioCookie() {} + +ReturnValue_t GpioCookie::addGpio(gpioId_t gpioId, GpioBase* gpioConfig) { + if (gpioConfig == nullptr) { + sif::debug << "GpioCookie::addGpio: gpioConfig is nullpointer" << std::endl; + return HasReturnvaluesIF::RETURN_FAILED; + } + auto gpioMapIter = gpioMap.find(gpioId); + if (gpioMapIter == gpioMap.end()) { + auto statusPair = gpioMap.emplace(gpioId, gpioConfig); + if (statusPair.second == false) { +#if FSFW_VERBOSE_LEVEL >= 1 + sif::error << "GpioCookie::addGpio: Failed to add GPIO " << gpioId << " to GPIO map" + << std::endl; +#endif + return HasReturnvaluesIF::RETURN_FAILED; + } + return HasReturnvaluesIF::RETURN_OK; + } +#if FSFW_VERBOSE_LEVEL >= 1 + sif::error << "GpioCookie::addGpio: GPIO already exists in GPIO map " << std::endl; +#endif + return HasReturnvaluesIF::RETURN_FAILED; +} + +GpioMap GpioCookie::getGpioMap() const { return gpioMap; } + +GpioCookie::~GpioCookie() {} diff --git a/archive/gpio/GpioCookie.h b/archive/gpio/GpioCookie.h new file mode 100644 index 0000000..9295a4a --- /dev/null +++ b/archive/gpio/GpioCookie.h @@ -0,0 +1,39 @@ +#ifndef LINUX_GPIO_GPIOCOOKIE_H_ +#define LINUX_GPIO_GPIOCOOKIE_H_ + +#include +#include + +#include "GpioIF.h" +#include "gpioDefinitions.h" + +/** + * @brief Cookie for the GpioIF. Allows the GpioIF to determine which + * GPIOs to initialize and whether they should be configured as in- or + * output. + * @details One GpioCookie can hold multiple GPIO configurations. To add a new + * GPIO configuration to a GpioCookie use the GpioCookie::addGpio + * function. + * + * @author J. Meier + */ +class GpioCookie : public CookieIF { + public: + GpioCookie(); + + virtual ~GpioCookie(); + + ReturnValue_t addGpio(gpioId_t gpioId, GpioBase* gpioConfig); + /** + * @brief Get map with registered GPIOs. + */ + GpioMap getGpioMap() const; + + private: + /** + * Returns a copy of the internal GPIO map. + */ + GpioMap gpioMap; +}; + +#endif /* LINUX_GPIO_GPIOCOOKIE_H_ */ diff --git a/archive/gpio/GpioIF.h b/archive/gpio/GpioIF.h new file mode 100644 index 0000000..045af55 --- /dev/null +++ b/archive/gpio/GpioIF.h @@ -0,0 +1,54 @@ +#ifndef LINUX_GPIO_GPIOIF_H_ +#define LINUX_GPIO_GPIOIF_H_ + +#include +#include + +#include "gpioDefinitions.h" + +class GpioCookie; + +/** + * @brief This class defines the interface for objects requiring the control + * over GPIOs. + * @author J. Meier + */ +class GpioIF : public HasReturnvaluesIF { + public: + virtual ~GpioIF(){}; + + /** + * @brief Called by the GPIO using object. + * @param cookie Cookie specifying informations of the GPIOs required + * by a object. + */ + virtual ReturnValue_t addGpios(GpioCookie* cookie) = 0; + + /** + * @brief By implementing this function a child must provide the + * functionality to pull a certain GPIO to high logic level. + * + * @param gpioId A unique number which specifies the GPIO to drive. + * @return Returns RETURN_OK for success. This should never return RETURN_FAILED. + */ + virtual ReturnValue_t pullHigh(gpioId_t gpioId) = 0; + + /** + * @brief By implementing this function a child must provide the + * functionality to pull a certain GPIO to low logic level. + * + * @param gpioId A unique number which specifies the GPIO to drive. + */ + virtual ReturnValue_t pullLow(gpioId_t gpioId) = 0; + + /** + * @brief This function requires a child to implement the functionality to read the state of + * an ouput or input gpio. + * + * @param gpioId A unique number which specifies the GPIO to read. + * @param gpioState State of GPIO will be written to this pointer. + */ + virtual ReturnValue_t readGpio(gpioId_t gpioId, int* gpioState) = 0; +}; + +#endif /* LINUX_GPIO_GPIOIF_H_ */ diff --git a/archive/gpio/LinuxLibgpioIF.cpp b/archive/gpio/LinuxLibgpioIF.cpp new file mode 100644 index 0000000..e5dcc1f --- /dev/null +++ b/archive/gpio/LinuxLibgpioIF.cpp @@ -0,0 +1,295 @@ +#include "LinuxLibgpioIF.h" + +#include +#include +#include +#include + +#include + +#include "GpioCookie.h" + +LinuxLibgpioIF::LinuxLibgpioIF(object_id_t objectId) : SystemObject(objectId) { + struct gpiod_chip* chip = gpiod_chip_open_by_label("/amba_pl/gpio@42030000"); + + sif::debug << chip->name << std::endl; +} + +LinuxLibgpioIF::~LinuxLibgpioIF() {} + +ReturnValue_t LinuxLibgpioIF::addGpios(GpioCookie* gpioCookie) { + ReturnValue_t result; + if (gpioCookie == nullptr) { + sif::error << "LinuxLibgpioIF::initialize: Invalid cookie" << std::endl; + return RETURN_FAILED; + } + + GpioMap mapToAdd = gpioCookie->getGpioMap(); + + /* Check whether this ID already exists in the map and remove duplicates */ + result = checkForConflicts(mapToAdd); + if (result != RETURN_OK) { + return result; + } + + result = configureGpios(mapToAdd); + if (result != RETURN_OK) { + return RETURN_FAILED; + } + + /* Register new GPIOs in gpioMap */ + gpioMap.insert(mapToAdd.begin(), mapToAdd.end()); + + return RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::configureGpios(GpioMap& mapToAdd) { + for (auto& gpioConfig : mapToAdd) { + switch (gpioConfig.second->gpioType) { + case (gpio::GpioTypes::NONE): { + return GPIO_INVALID_INSTANCE; + } + case (gpio::GpioTypes::GPIOD_REGULAR): { + GpiodRegular* regularGpio = dynamic_cast(gpioConfig.second); + if (regularGpio == nullptr) { + return GPIO_INVALID_INSTANCE; + } + configureRegularGpio(gpioConfig.first, regularGpio); + break; + } + case (gpio::GpioTypes::CALLBACK): { + auto gpioCallback = dynamic_cast(gpioConfig.second); + if (gpioCallback->callback == nullptr) { + return GPIO_INVALID_INSTANCE; + } + gpioCallback->callback(gpioConfig.first, gpio::GpioOperation::WRITE, + gpioCallback->initValue, gpioCallback->callbackArgs); + } + } + } + return RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::configureRegularGpio(gpioId_t gpioId, GpiodRegular* regularGpio) { + std::string chipname; + unsigned int lineNum; + struct gpiod_chip* chip; + gpio::Direction direction; + std::string consumer; + struct gpiod_line* lineHandle; + int result = 0; + + chipname = regularGpio->chipname; + chip = gpiod_chip_open_by_name(chipname.c_str()); + if (!chip) { + sif::error << "LinuxLibgpioIF::configureGpios: Failed to open chip " << chipname + << ". Gpio ID: " << gpioId << std::endl; + return RETURN_FAILED; + } + + lineNum = regularGpio->lineNum; + lineHandle = gpiod_chip_get_line(chip, lineNum); + if (!lineHandle) { + sif::error << "LinuxLibgpioIF::configureGpios: Failed to open line for GPIO with id " << gpioId + << std::endl; + gpiod_chip_close(chip); + return RETURN_FAILED; + } + + direction = regularGpio->direction; + consumer = regularGpio->consumer; + /* Configure direction and add a description to the GPIO */ + switch (direction) { + case (gpio::OUT): { + result = gpiod_line_request_output(lineHandle, consumer.c_str(), regularGpio->initValue); + if (result < 0) { + sif::error << "LinuxLibgpioIF::configureGpios: Failed to request line " << lineNum + << " from GPIO instance with ID: " << gpioId << std::endl; + gpiod_line_release(lineHandle); + return RETURN_FAILED; + } + break; + } + case (gpio::IN): { + result = gpiod_line_request_input(lineHandle, consumer.c_str()); + if (result < 0) { + sif::error << "LinuxLibgpioIF::configureGpios: Failed to request line " << lineNum + << " from GPIO instance with ID: " << gpioId << std::endl; + gpiod_line_release(lineHandle); + return RETURN_FAILED; + } + break; + } + default: { + sif::error << "LinuxLibgpioIF::configureGpios: Invalid direction specified" << std::endl; + return GPIO_INVALID_INSTANCE; + } + } + /** + * Write line handle to GPIO configuration instance so it can later be used to set or + * read states of GPIOs. + */ + regularGpio->lineHandle = lineHandle; + return RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::pullHigh(gpioId_t gpioId) { + gpioMapIter = gpioMap.find(gpioId); + if (gpioMapIter == gpioMap.end()) { + sif::warning << "LinuxLibgpioIF::driveGpio: Unknown GPIOD ID " << gpioId << std::endl; + return UNKNOWN_GPIO_ID; + } + + if (gpioMapIter->second->gpioType == gpio::GpioTypes::GPIOD_REGULAR) { + return driveGpio(gpioId, dynamic_cast(gpioMapIter->second), 1); + } else { + auto gpioCallback = dynamic_cast(gpioMapIter->second); + if (gpioCallback->callback == nullptr) { + return GPIO_INVALID_INSTANCE; + } + gpioCallback->callback(gpioMapIter->first, gpio::GpioOperation::WRITE, 1, + gpioCallback->callbackArgs); + } + return GPIO_TYPE_FAILURE; +} + +ReturnValue_t LinuxLibgpioIF::pullLow(gpioId_t gpioId) { + gpioMapIter = gpioMap.find(gpioId); + if (gpioMapIter == gpioMap.end()) { + sif::warning << "LinuxLibgpioIF::driveGpio: Unknown GPIOD ID " << gpioId << std::endl; + return UNKNOWN_GPIO_ID; + } + + if (gpioMapIter->second->gpioType == gpio::GpioTypes::GPIOD_REGULAR) { + return driveGpio(gpioId, dynamic_cast(gpioMapIter->second), 0); + } else { + auto gpioCallback = dynamic_cast(gpioMapIter->second); + if (gpioCallback->callback == nullptr) { + return GPIO_INVALID_INSTANCE; + } + gpioCallback->callback(gpioMapIter->first, gpio::GpioOperation::WRITE, 0, + gpioCallback->callbackArgs); + } + return GPIO_TYPE_FAILURE; +} + +ReturnValue_t LinuxLibgpioIF::driveGpio(gpioId_t gpioId, GpiodRegular* regularGpio, + unsigned int logicLevel) { + if (regularGpio == nullptr) { + return GPIO_TYPE_FAILURE; + } + + int result = gpiod_line_set_value(regularGpio->lineHandle, logicLevel); + if (result < 0) { + sif::warning << "LinuxLibgpioIF::driveGpio: Failed to pull GPIO with ID " << gpioId + << " to logic level " << logicLevel << std::endl; + return DRIVE_GPIO_FAILURE; + } + + return RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::readGpio(gpioId_t gpioId, int* gpioState) { + gpioMapIter = gpioMap.find(gpioId); + if (gpioMapIter == gpioMap.end()) { + sif::warning << "LinuxLibgpioIF::readGpio: Unknown GPIOD ID " << gpioId << std::endl; + return UNKNOWN_GPIO_ID; + } + + if (gpioMapIter->second->gpioType == gpio::GpioTypes::GPIOD_REGULAR) { + GpiodRegular* regularGpio = dynamic_cast(gpioMapIter->second); + if (regularGpio == nullptr) { + return GPIO_TYPE_FAILURE; + } + *gpioState = gpiod_line_get_value(regularGpio->lineHandle); + } else { + } + + return RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::checkForConflicts(GpioMap& mapToAdd) { + ReturnValue_t status = HasReturnvaluesIF::RETURN_OK; + ReturnValue_t result = HasReturnvaluesIF::RETURN_OK; + for (auto& gpioConfig : mapToAdd) { + switch (gpioConfig.second->gpioType) { + case (gpio::GpioTypes::GPIOD_REGULAR): { + auto regularGpio = dynamic_cast(gpioConfig.second); + if (regularGpio == nullptr) { + return GPIO_TYPE_FAILURE; + } + /* Check for conflicts and remove duplicates if necessary */ + result = checkForConflictsRegularGpio(gpioConfig.first, regularGpio, mapToAdd); + if (result != HasReturnvaluesIF::RETURN_OK) { + status = result; + } + break; + } + case (gpio::GpioTypes::CALLBACK): { + auto callbackGpio = dynamic_cast(gpioConfig.second); + if (callbackGpio == nullptr) { + return GPIO_TYPE_FAILURE; + } + /* Check for conflicts and remove duplicates if necessary */ + result = checkForConflictsCallbackGpio(gpioConfig.first, callbackGpio, mapToAdd); + if (result != HasReturnvaluesIF::RETURN_OK) { + status = result; + } + break; + } + default: { + } + } + } + return status; +} + +ReturnValue_t LinuxLibgpioIF::checkForConflictsRegularGpio(gpioId_t gpioIdToCheck, + GpiodRegular* gpioToCheck, + GpioMap& mapToAdd) { + /* Cross check with private map */ + gpioMapIter = gpioMap.find(gpioIdToCheck); + if (gpioMapIter != gpioMap.end()) { + if (gpioMapIter->second->gpioType != gpio::GpioTypes::GPIOD_REGULAR) { + sif::warning << "LinuxLibgpioIF::checkForConflicts: ID already exists for different " + "GPIO type" + << gpioIdToCheck << ". Removing duplicate." << std::endl; + mapToAdd.erase(gpioIdToCheck); + return HasReturnvaluesIF::RETURN_OK; + } + auto ownRegularGpio = dynamic_cast(gpioMapIter->second); + if (ownRegularGpio == nullptr) { + return GPIO_TYPE_FAILURE; + } + + /* Remove element from map to add because a entry for this GPIO + already exists */ + sif::warning << "LinuxLibgpioIF::checkForConflictsRegularGpio: Duplicate GPIO definition" + << " detected. Duplicate will be removed from map to add." << std::endl; + mapToAdd.erase(gpioIdToCheck); + } + return HasReturnvaluesIF::RETURN_OK; +} + +ReturnValue_t LinuxLibgpioIF::checkForConflictsCallbackGpio(gpioId_t gpioIdToCheck, + GpioCallback* callbackGpio, + GpioMap& mapToAdd) { + /* Cross check with private map */ + gpioMapIter = gpioMap.find(gpioIdToCheck); + if (gpioMapIter != gpioMap.end()) { + if (gpioMapIter->second->gpioType != gpio::GpioTypes::CALLBACK) { + sif::warning << "LinuxLibgpioIF::checkForConflicts: ID already exists for different " + "GPIO type" + << gpioIdToCheck << ". Removing duplicate." << std::endl; + mapToAdd.erase(gpioIdToCheck); + return HasReturnvaluesIF::RETURN_OK; + } + + /* Remove element from map to add because a entry for this GPIO + already exists */ + sif::warning << "LinuxLibgpioIF::checkForConflictsRegularGpio: Duplicate GPIO definition" + << " detected. Duplicate will be removed from map to add." << std::endl; + mapToAdd.erase(gpioIdToCheck); + } + return HasReturnvaluesIF::RETURN_OK; +} diff --git a/archive/gpio/LinuxLibgpioIF.h b/archive/gpio/LinuxLibgpioIF.h new file mode 100644 index 0000000..1c974ef --- /dev/null +++ b/archive/gpio/LinuxLibgpioIF.h @@ -0,0 +1,75 @@ +#ifndef LINUX_GPIO_LINUXLIBGPIOIF_H_ +#define LINUX_GPIO_LINUXLIBGPIOIF_H_ + +#include +#include +#include + +class GpioCookie; + +/** + * @brief This class implements the GpioIF for a linux based system. The + * implementation is based on the libgpiod lib which requires linux 4.8 + * or higher. + * @note The Petalinux SDK from Xilinx supports libgpiod since Petalinux + * 2019.1. + */ +class LinuxLibgpioIF : public GpioIF, public SystemObject { + public: + static const uint8_t gpioRetvalId = CLASS_ID::LINUX_LIBGPIO_IF; + + static constexpr ReturnValue_t UNKNOWN_GPIO_ID = + HasReturnvaluesIF::makeReturnCode(gpioRetvalId, 1); + static constexpr ReturnValue_t DRIVE_GPIO_FAILURE = + HasReturnvaluesIF::makeReturnCode(gpioRetvalId, 2); + static constexpr ReturnValue_t GPIO_TYPE_FAILURE = + HasReturnvaluesIF::makeReturnCode(gpioRetvalId, 3); + static constexpr ReturnValue_t GPIO_INVALID_INSTANCE = + HasReturnvaluesIF::makeReturnCode(gpioRetvalId, 4); + + LinuxLibgpioIF(object_id_t objectId); + virtual ~LinuxLibgpioIF(); + + ReturnValue_t addGpios(GpioCookie* gpioCookie) override; + ReturnValue_t pullHigh(gpioId_t gpioId) override; + ReturnValue_t pullLow(gpioId_t gpioId) override; + ReturnValue_t readGpio(gpioId_t gpioId, int* gpioState) override; + + private: + /* Holds the information and configuration of all used GPIOs */ + GpioMap gpioMap; + GpioMapIter gpioMapIter; + + /** + * @brief This functions drives line of a GPIO specified by the GPIO ID. + * + * @param gpioId The GPIO ID of the GPIO to drive. + * @param logiclevel The logic level to set. O or 1. + */ + ReturnValue_t driveGpio(gpioId_t gpioId, GpiodRegularBase& regularGpio, unsigned int logiclevel); + + ReturnValue_t configureRegularGpio(gpioId_t gpioId, GpiodRegularBase& regularGpio); + + /** + * @brief This function checks if GPIOs are already registered and whether + * there exists a conflict in the GPIO configuration. E.g. the + * direction. + * + * @param mapToAdd The GPIOs which shall be added to the gpioMap. + * + * @return RETURN_OK if successful, otherwise RETURN_FAILED + */ + ReturnValue_t checkForConflicts(GpioMap& mapToAdd); + + ReturnValue_t checkForConflictsRegularGpio(gpioId_t gpiodId, GpiodRegular* regularGpio, + GpioMap& mapToAdd); + ReturnValue_t checkForConflictsCallbackGpio(gpioId_t gpiodId, GpioCallback* regularGpio, + GpioMap& mapToAdd); + + /** + * @brief Performs the initial configuration of all GPIOs specified in the GpioMap mapToAdd. + */ + ReturnValue_t configureGpios(GpioMap& mapToAdd); +}; + +#endif /* LINUX_GPIO_LINUXLIBGPIOIF_H_ */ diff --git a/archive/gpio/gpioDefinitions.h b/archive/gpio/gpioDefinitions.h new file mode 100644 index 0000000..46050d1 --- /dev/null +++ b/archive/gpio/gpioDefinitions.h @@ -0,0 +1,83 @@ +#ifndef LINUX_GPIO_GPIODEFINITIONS_H_ +#define LINUX_GPIO_GPIODEFINITIONS_H_ + +#include +#include + +using gpioId_t = uint16_t; + +namespace gpio { + +enum class Levels : uint8_t { LOW = 0, HIGH = 1 }; + +enum class Direction : uint8_t { IN = 0, OUT = 1 }; + +enum class GpioOperation { READ, WRITE }; + +enum class GpioTypes { NONE, GPIOD_REGULAR, CALLBACK }; + +static constexpr gpioId_t NO_GPIO = -1; +} // namespace gpio + +/** + * @brief Struct containing information about the GPIO to use. This is + * required by the libgpiod to access and drive a GPIO. + * @param chipname String of the chipname specifying the group which contains the GPIO to + * access. E.g. gpiochip0. To detect names of GPIO groups run gpiodetect on + * the linux command line. + * @param lineNum The offset of the GPIO within the GPIO group. + * @param consumer Name of the consumer. Simply a description of the GPIO configuration. + * @param direction Specifies whether the GPIO should be used as in- or output. + * @param initValue Defines the initial state of the GPIO when configured as output. + * Only required for output GPIOs. + * @param lineHandle The handle returned by gpiod_chip_get_line will be later written to this + * pointer. + */ +class GpioBase { + public: + GpioBase() = default; + + GpioBase(gpio::GpioTypes gpioType, std::string consumer, gpio::Direction direction, int initValue) + : gpioType(gpioType), consumer(consumer), direction(direction), initValue(initValue) {} + + virtual ~GpioBase(){}; + + /* Can be used to cast GpioBase to a concrete child implementation */ + gpio::GpioTypes gpioType = gpio::GpioTypes::NONE; + std::string consumer; + gpio::Direction direction = gpio::Direction::IN; + int initValue = 0; +}; + +class GpiodRegular : public GpioBase { + public: + GpiodRegular() + : GpioBase(gpio::GpioTypes::GPIOD_REGULAR, std::string(), gpio::Direction::IN, 0){}; + + GpiodRegular(std::string chipname_, int lineNum_, std::string consumer_, + gpio::Direction direction_, int initValue_) + : GpioBase(gpio::GpioTypes::GPIOD_REGULAR, consumer_, direction_, initValue_), + chipname(chipname_), + lineNum(lineNum_) {} + std::string chipname; + int lineNum = 0; + struct gpiod_line* lineHandle = nullptr; +}; + +class GpioCallback : public GpioBase { + public: + GpioCallback(std::string consumer, gpio::Direction direction_, int initValue_, + void (*callback)(gpioId_t gpioId, gpio::GpioOperation gpioOp, int value, void* args), + void* callbackArgs) + : GpioBase(gpio::GpioTypes::CALLBACK, consumer, direction_, initValue_), + callback(callback), + callbackArgs(callbackArgs) {} + + void (*callback)(gpioId_t gpioId, gpio::GpioOperation gpioOp, int value, void* args) = nullptr; + void* callbackArgs = nullptr; +}; + +using GpioMap = std::unordered_map; +using GpioMapIter = GpioMap::iterator; + +#endif /* LINUX_GPIO_GPIODEFINITIONS_H_ */ diff --git a/archive/tmtc/CCSDSIPCoreBridge.cpp b/archive/tmtc/CCSDSIPCoreBridge.cpp new file mode 100644 index 0000000..607cceb --- /dev/null +++ b/archive/tmtc/CCSDSIPCoreBridge.cpp @@ -0,0 +1,128 @@ +#include +#include +#include + +CCSDSIPCoreBridge::CCSDSIPCoreBridge(object_id_t objectId, object_id_t tcDestination, + object_id_t tmStoreId, object_id_t tcStoreId, + LinuxLibgpioIF* gpioComIF, std::string uioPtme, + gpioId_t papbBusyId, gpioId_t papbEmptyId) + : TmTcBridge(objectId, tcDestination, tmStoreId, tcStoreId), + gpioComIF(gpioComIF), + uioPtme(uioPtme), + papbBusyId(papbBusyId), + papbEmptyId(papbEmptyId) {} + +CCSDSIPCoreBridge::~CCSDSIPCoreBridge() {} + +ReturnValue_t CCSDSIPCoreBridge::initialize() { + ReturnValue_t result = TmTcBridge::initialize(); + + fd = open("/dev/uio0", O_RDWR); + if (fd < 1) { + sif::debug << "CCSDSIPCoreBridge::initialize: Invalid UIO device file" << std::endl; + return RETURN_FAILED; + } + + /** + * Map uio device in virtual address space + * PROT_WRITE: Map uio device in writable only mode + */ + ptmeBaseAddress = + static_cast(mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); + + if (ptmeBaseAddress == MAP_FAILED) { + sif::error << "CCSDSIPCoreBridge::initialize: Failed to map uio address" << std::endl; + return RETURN_FAILED; + } + + return result; +} + +ReturnValue_t CCSDSIPCoreBridge::handleTm() { +#if OBSW_TEST_CCSDS_PTME == 1 + return sendTestFrame(); +#else + return TmTcBridge::handleTm(); +#endif +} + +ReturnValue_t CCSDSIPCoreBridge::sendTm(const uint8_t* data, size_t dataLen) { + if (pollPapbBusySignal() == RETURN_OK) { + startPacketTransfer(); + } + + for (size_t idx = 0; idx < dataLen; idx++) { + if (pollPapbBusySignal() == RETURN_OK) { + *(ptmeBaseAddress + PTME_DATA_REG_OFFSET) = static_cast(*(data + idx)); + } else { + sif::debug << "CCSDSIPCoreBridge::sendTm: Only written " << idx - 1 << " of " << dataLen + << " data" << std::endl; + return RETURN_FAILED; + } + } + + if (pollPapbBusySignal() == RETURN_OK) { + endPacketTransfer(); + } + return RETURN_OK; +} + +void CCSDSIPCoreBridge::startPacketTransfer() { *ptmeBaseAddress = PTME_CONFIG_START; } + +void CCSDSIPCoreBridge::endPacketTransfer() { *ptmeBaseAddress = PTME_CONFIG_END; } + +ReturnValue_t CCSDSIPCoreBridge::pollPapbBusySignal() { + int papbBusyState = 0; + ReturnValue_t result = RETURN_OK; + + /** Check if PAPB interface is ready to receive data */ + result = gpioComIF->readGpio(papbBusyId, &papbBusyState); + if (result != RETURN_OK) { + sif::debug << "CCSDSIPCoreBridge::pollPapbBusySignal: Failed to read papb busy signal" + << std::endl; + return RETURN_FAILED; + } + if (!papbBusyState) { + sif::debug << "CCSDSIPCoreBridge::pollPapbBusySignal: PAPB busy" << std::endl; + return PAPB_BUSY; + } + + return RETURN_OK; +} + +void CCSDSIPCoreBridge::isPtmeBufferEmpty() { + ReturnValue_t result = RETURN_OK; + int papbEmptyState = 1; + + result = gpioComIF->readGpio(papbEmptyId, &papbEmptyState); + + if (result != RETURN_OK) { + sif::debug << "CCSDSIPCoreBridge::isPtmeBufferEmpty: Failed to read papb empty signal" + << std::endl; + return; + } + + if (papbEmptyState == 1) { + sif::debug << "CCSDSIPCoreBridge::isPtmeBufferEmpty: Buffer is empty" << std::endl; + } else { + sif::debug << "CCSDSIPCoreBridge::isPtmeBufferEmpty: Buffer is not empty" << std::endl; + } + return; +} + +ReturnValue_t CCSDSIPCoreBridge::sendTestFrame() { + /** Size of one complete transfer frame data field amounts to 1105 bytes */ + uint8_t testPacket[1105]; + + /** Fill one test packet */ + for (int idx = 0; idx < 1105; idx++) { + testPacket[idx] = static_cast(idx & 0xFF); + } + + ReturnValue_t result = sendTm(testPacket, 1105); + if (result != RETURN_OK) { + return result; + } + + return RETURN_OK; +} diff --git a/archive/tmtc/CCSDSIPCoreBridge.h b/archive/tmtc/CCSDSIPCoreBridge.h new file mode 100644 index 0000000..074769c --- /dev/null +++ b/archive/tmtc/CCSDSIPCoreBridge.h @@ -0,0 +1,129 @@ +#ifndef MISSION_OBC_CCSDSIPCOREBRIDGE_H_ +#define MISSION_OBC_CCSDSIPCOREBRIDGE_H_ + +#include +#include +#include + +#include + +#include "OBSWConfig.h" + +/** + * @brief This class handles the interfacing to the telemetry (PTME) and telecommand (PDEC) IP + * cores responsible for the CCSDS encoding and decoding. The IP cores are implemented + * on the programmable logic and are accessible through the linux UIO driver. + */ +class CCSDSIPCoreBridge : public TmTcBridge { + public: + /** + * @brief Constructor + * + * @param objectId + * @param tcDestination + * @param tmStoreId + * @param tcStoreId + * @param uioPtme Name of the uio device file which provides access to the PTME IP Core. + * @param papbBusyId The ID of the GPIO which is connected to the PAPBBusy_N signal of the + * PTME IP Core. A low logic level indicates the PTME is not ready to + * receive more data. + * @param papbEmptyId The ID of the GPIO which is connected to the PAPBEmpty signal of the + * PTME IP Core. The signal is high when there are no packets in the + * external buffer memory (BRAM). + */ + CCSDSIPCoreBridge(object_id_t objectId, object_id_t tcDestination, object_id_t tmStoreId, + object_id_t tcStoreId, LinuxLibgpioIF* gpioComIF, std::string uioPtme, + gpioId_t papbBusyId, gpioId_t papbEmptyId); + virtual ~CCSDSIPCoreBridge(); + + ReturnValue_t initialize() override; + + protected: + /** + * Overwriting this function to provide the capability of testing the PTME IP Core + * implementation. + */ + virtual ReturnValue_t handleTm() override; + + virtual ReturnValue_t sendTm(const uint8_t* data, size_t dataLen) override; + + private: + static const uint8_t INTERFACE_ID = CLASS_ID::CCSDS_IP_CORE_BRIDGE; + + static const ReturnValue_t PAPB_BUSY = MAKE_RETURN_CODE(0xA0); + + /** Size of mapped address space. 4k (minimal size of pl device) */ + // static const int MAP_SIZE = 0xFA0; + static const int MAP_SIZE = 0x1000; + + /** + * Configuration bits: + * bit[1:0]: Size of data (1,2,3 or 4 bytes). 1 Byte <=> b00 + * bit[2]: Set this bit to 1 to abort a transfered packet + * bit[3]: Signals to PTME the start of a new telemetry packet + */ + static const uint32_t PTME_CONFIG_START = 0x8; + + /** + * Writing this word to the ptme base address signals to the PTME that a complete tm packet has + * been transferred. + */ + static const uint32_t PTME_CONFIG_END = 0x0; + + /** + * Writing to this offset within the PTME memory space will insert data for encoding to the + * PTME IP core. + * The address offset is 0x400 (= 4 * 256) + */ + static const int PTME_DATA_REG_OFFSET = 256; + + LinuxLibgpioIF* gpioComIF = nullptr; + + /** The uio device file related to the PTME IP Core */ + std::string uioPtme; + + /** Pulled to low when PTME not ready to receive data */ + gpioId_t papbBusyId = gpio::NO_GPIO; + + /** High when externally buffer memory of PTME is empty */ + gpioId_t papbEmptyId = gpio::NO_GPIO; + + /** The file descriptor of the UIO driver */ + int fd; + + uint32_t* ptmeBaseAddress = nullptr; + + /** + * @brief This function sends the config byte to the PTME IP Core to initiate a packet + * transfer. + */ + void startPacketTransfer(); + + /** + * @brief This function sends the config byte to the PTME IP Core to signal the end of a + * packet transfer. + */ + void endPacketTransfer(); + + /** + * @brief This function reads the papb busy signal indicating whether the PAPB interface is + * ready to receive more data or not. PAPB is ready when PAPB_Busy_N == '1'. + * + * @return RETURN_OK when ready to receive data else PAPB_BUSY. + */ + ReturnValue_t pollPapbBusySignal(); + + /** + * @brief This function can be used for debugging to check wheter there are packets in + * the packet buffer of the PTME or not. + */ + void isPtmeBufferEmpty(); + + /** + * @brief This function sends a complete telemetry transfer frame data field (1105 bytes) + * to the input of the PTME IP Core. Can be used to test the implementation. + */ + ReturnValue_t sendTestFrame(); +}; + +#endif /* MISSION_OBC_CCSDSIPCOREBRIDGE_H_ */ diff --git a/arduino b/arduino new file mode 160000 index 0000000..3ea528d --- /dev/null +++ b/arduino @@ -0,0 +1 @@ +Subproject commit 3ea528dc5f890985f7d889b6e6496fc252a770ce diff --git a/automation/Dockerfile b/automation/Dockerfile new file mode 100644 index 0000000..73275ff --- /dev/null +++ b/automation/Dockerfile @@ -0,0 +1,27 @@ +FROM ubuntu:focal + +RUN apt-get update +RUN apt-get --yes upgrade +#tzdata is a dependency, won't install otherwise +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get --yes install cmake libgpiod-dev xz-utils nano curl git gcc g++ lcov valgrind libgps-dev python3 + +ARG XIPHOS_SDK_NAME=sdk-xiphos-eive-v0.2.0 +# Install Xiphos ARK SDK, which also installs Q7S root filesystem, required for cross-compilation. +RUN curl https://buggy.irs.uni-stuttgart.de/eive/tools/${XIPHOS_SDK_NAME}.tar | tar -x && \ + cd ${XIPHOS_SDK_NAME} && \ + ./ark-glibc-x86_64-eive-image-cortexa9hf-neon-toolchain-nodistro.0.sh -y + +# Cross compiler +RUN mkdir -p /usr/tools; \ +curl https://buggy.irs.uni-stuttgart.de/eive/tools/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf.tar.gz \ + | tar -xz -C /usr/tools + +RUN git clone https://github.com/catchorg/Catch2.git && \ + cd Catch2 && \ + git checkout v3.0.0-preview5 && \ + cmake -Bbuild -H. -DBUILD_TESTING=OFF && \ + cmake --build build/ --target install + +ENV ZYNQ_7020_SYSROOT="/opt/xiphos/sdk/ark/sysroots/cortexa9hf-neon-xiphos-linux-gnueabi" +ENV PATH=$PATH:"/usr/tools/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin" diff --git a/automation/Jenkinsfile b/automation/Jenkinsfile new file mode 100644 index 0000000..12c4bdf --- /dev/null +++ b/automation/Jenkinsfile @@ -0,0 +1,47 @@ +pipeline { + environment { + BUILDDIR_Q7S = 'build_q7s_fm' + BUILDDIR_Q7S_EM = 'build_q7s_em' + BUILDDIR_LINUX = 'build_linux' + } + agent { + docker { + image 'eive-obsw-ci:d5' + args '--sysctl fs.mqueue.msg_max=100' + } + } + stages { + stage('Clean') { + steps { + sh 'rm -rf $BUILDDIR_Q7S' + sh 'rm -rf $BUILDDIR_Q7S_EM' + sh 'rm -rf $BUILDDIR_LINUX' + } + } + stage('Build Q7S') { + steps { + dir(BUILDDIR_Q7S) { + sh 'cmake -DTGT_BSP="arm/q7s" -DCMAKE_BUILD_TYPE=Debug ..' + sh 'cmake --build . -j8' + } + } + } + stage('Build Q7S EM') { + steps { + dir(BUILDDIR_Q7S_EM) { + sh 'cmake -DTGT_BSP="arm/q7s" -DEIVE_Q7S_EM=ON -DCMAKE_BUILD_TYPE=Debug ..' + sh 'cmake --build . -j8' + } + } + } + stage('Build Host and Tests') { + steps { + dir(BUILDDIR_LINUX) { + sh 'cmake ..' + sh 'cmake --build . -j8' + sh './eive-unittest' + } + } + } + } +} diff --git a/bsp_egse/CMakeLists.txt b/bsp_egse/CMakeLists.txt new file mode 100644 index 0000000..d104354 --- /dev/null +++ b/bsp_egse/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources(${OBSW_NAME} PUBLIC InitMission.cpp main.cpp ObjectFactory.cpp) + +add_subdirectory(boardconfig) diff --git a/bsp_egse/InitMission.cpp b/bsp_egse/InitMission.cpp new file mode 100644 index 0000000..5a72f53 --- /dev/null +++ b/bsp_egse/InitMission.cpp @@ -0,0 +1,192 @@ +#include "InitMission.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OBSWConfig.h" +#include "ObjectFactory.h" +#include "objects/systemObjectList.h" +#include "pollingsequence/pollingSequenceFactory.h" + +ServiceInterfaceStream sif::debug("DEBUG"); +ServiceInterfaceStream sif::info("INFO"); +ServiceInterfaceStream sif::warning("WARNING"); +ServiceInterfaceStream sif::error("ERROR"); + +ObjectManagerIF* objectManager = nullptr; + +void initmission::initMission() { + sif::info << "Make sure the systemd service ser2net on the egse has been stopped " + << "(alias stop-ser2net)" << std::endl; + sif::info << "Make sure the power lines of the star tracker have been enabled " + << "(alias enable-startracker)" << std::endl; + sif::info << "Building global objects.." << std::endl; + /* Instantiate global object manager and also create all objects */ + ObjectManager::instance()->setObjectFactoryFunction(ObjectFactory::produce, nullptr); + sif::info << "Initializing all objects.." << std::endl; + ObjectManager::instance()->initialize(); + + /* This function creates and starts all tasks */ + initTasks(); +} + +void initmission::initTasks() { + TaskFactory* factory = TaskFactory::instance(); + ReturnValue_t result = returnvalue::OK; + if (factory == nullptr) { + /* Should never happen ! */ + return; + } +#if OBSW_PRINT_MISSED_DEADLINES == 1 + void (*missedDeadlineFunc)(void) = TaskFactory::printMissedDeadline; +#else + void (*missedDeadlineFunc)(void) = nullptr; +#endif + + /* TMTC Distribution */ + PeriodicTaskIF* tmtcDistributor = factory->createPeriodicTask( + "DIST", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmtcDistributor->addComponent(objects::CCSDS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::PUS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::TM_FUNNEL); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + + PeriodicTaskIF* tmtcBridgeTask = factory->createPeriodicTask( + "TMTC_BRIDGE", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmtcBridgeTask->addComponent(objects::TMTC_BRIDGE); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Bridge failed" << std::endl; + } + PeriodicTaskIF* tmtcPollingTask = factory->createPeriodicTask( + "TMTC_POLLING", 80, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = tmtcPollingTask->addComponent(objects::TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Polling failed" << std::endl; + } + + /* PUS Services */ + std::vector pusTasks; + createPusTasks(*factory, missedDeadlineFunc, pusTasks); + + std::vector pstTasks; + FixedTimeslotTaskIF* pst = factory->createFixedTimeslotTask( + "STAR_TRACKER_PST", 70, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 0.5, missedDeadlineFunc); + result = pst::pstUart(pst); + if (result != returnvalue::OK) { + sif::error << "InitMission::initTasks: Creating PST failed!" << std::endl; + } + pstTasks.push_back(pst); + + PeriodicTaskIF* strHelperTask = factory->createPeriodicTask( + "STR_HELPER", 20, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = strHelperTask->addComponent(objects::STR_HELPER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("STR_HELPER", objects::STR_HELPER); + } + pstTasks.push_back(strHelperTask); + + auto taskStarter = [](std::vector& taskVector, std::string name) { + for (const auto& task : taskVector) { + if (task != nullptr) { + task->startTask(); + } else { + sif::error << "Task in vector " << name << " is invalid!" << std::endl; + } + } + }; + + sif::info << "Starting tasks.." << std::endl; + tmtcDistributor->startTask(); + tmtcBridgeTask->startTask(); + tmtcPollingTask->startTask(); + + taskStarter(pstTasks, "PST Tasks"); + taskStarter(pusTasks, "PUS Tasks"); + + sif::info << "Tasks started.." << std::endl; +} + +void initmission::createPusTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; + PeriodicTaskIF* pusVerification = factory.createPeriodicTask( + "PUS_VERIF", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusVerification->addComponent(objects::PUS_SERVICE_1_VERIFICATION); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + taskVec.push_back(pusVerification); + + PeriodicTaskIF* pusEvents = factory.createPeriodicTask( + "PUS_EVENTS", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusEvents->addComponent(objects::PUS_SERVICE_5_EVENT_REPORTING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_EVENTS", objects::PUS_SERVICE_5_EVENT_REPORTING); + } + result = pusEvents->addComponent(objects::EVENT_MANAGER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_MGMT", objects::EVENT_MANAGER); + } + taskVec.push_back(pusEvents); + + PeriodicTaskIF* pusHighPrio = factory.createPeriodicTask( + "PUS_HIGH_PRIO", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_2_DEVICE_ACCESS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS2", objects::PUS_SERVICE_2_DEVICE_ACCESS); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_9_TIME_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS9", objects::PUS_SERVICE_9_TIME_MGMT); + } + taskVec.push_back(pusHighPrio); + + PeriodicTaskIF* pusMedPrio = factory.createPeriodicTask( + "PUS_MED_PRIO", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, missedDeadlineFunc); + result = pusMedPrio->addComponent(objects::PUS_SERVICE_8_FUNCTION_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS8", objects::PUS_SERVICE_8_FUNCTION_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_200_MODE_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS200", objects::PUS_SERVICE_200_MODE_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_20_PARAMETERS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS20", objects::PUS_SERVICE_20_PARAMETERS); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_3_HOUSEKEEPING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS3", objects::PUS_SERVICE_3_HOUSEKEEPING); + } + taskVec.push_back(pusMedPrio); + + PeriodicTaskIF* pusLowPrio = factory.createPeriodicTask( + "PUS_LOW_PRIO", 30, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.6, missedDeadlineFunc); + result = pusLowPrio->addComponent(objects::PUS_SERVICE_17_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS17", objects::PUS_SERVICE_17_TEST); + } + result = pusLowPrio->addComponent(objects::INTERNAL_ERROR_REPORTER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("INT_ERR_RPRT", objects::INTERNAL_ERROR_REPORTER); + } + taskVec.push_back(pusLowPrio); +} diff --git a/bsp_egse/InitMission.h b/bsp_egse/InitMission.h new file mode 100644 index 0000000..c3ba58e --- /dev/null +++ b/bsp_egse/InitMission.h @@ -0,0 +1,21 @@ +#ifndef BSP_LINUX_INITMISSION_H_ +#define BSP_LINUX_INITMISSION_H_ + +#include + +#include "fsfw/tasks/Typedef.h" + +class PeriodicTaskIF; +class TaskFactory; + +namespace initmission { +void initMission(); +void initTasks(); + +void createPstTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +void createPusTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +}; // namespace initmission + +#endif /* BSP_LINUX_INITMISSION_H_ */ diff --git a/bsp_egse/ObjectFactory.cpp b/bsp_egse/ObjectFactory.cpp new file mode 100644 index 0000000..b13af72 --- /dev/null +++ b/bsp_egse/ObjectFactory.cpp @@ -0,0 +1,48 @@ +#include "ObjectFactory.h" + +#include +#include +#include + +#include "OBSWConfig.h" +#include "busConf.h" +#include "fsfw/datapoollocal/LocalDataPoolManager.h" +#include "fsfw/tmtcpacket/pus/tm.h" +#include "fsfw/tmtcservices/CommandingServiceBase.h" +#include "fsfw/tmtcservices/PusServiceBase.h" +#include "linux/devices/devicedefinitions/StarTrackerDefinitions.h" +#include "linux/devices/startracker/StarTrackerHandler.h" +#include "mission/core/GenericFactory.h" +#include "mission/utility/TmFunnel.h" +#include "objects/systemObjectList.h" +#include "tmtc/apid.h" +#include "tmtc/pusIds.h" + +void Factory::setStaticFrameworkObjectIds() { + PusServiceBase::packetSource = objects::PUS_PACKET_DISTRIBUTOR; + PusServiceBase::packetDestination = objects::TM_FUNNEL; + + CommandingServiceBase::defaultPacketSource = objects::PUS_PACKET_DISTRIBUTOR; + CommandingServiceBase::defaultPacketDestination = objects::TM_FUNNEL; + + TmFunnel::downlinkDestination = objects::TMTC_BRIDGE; + TmFunnel::storageDestination = objects::NO_OBJECT; + + VerificationReporter::messageReceiver = objects::PUS_SERVICE_1_VERIFICATION; + TmPacketBase::timeStamperId = objects::TIME_STAMPER; +} + +void ObjectFactory::produce(void* args) { + Factory::setStaticFrameworkObjectIds(); + ObjectFactory::produceGenericObjects(); + + UartCookie* starTrackerCookie = + new UartCookie(objects::STAR_TRACKER, egse::STAR_TRACKER_UART, UartModes::NON_CANONICAL, + uart::STAR_TRACKER_BAUD, startracker::MAX_FRAME_SIZE * 2 + 2); + newSerialComIF(objects::UART_COM_IF); + starTrackerCookie->setNoFixedSizeReply(); + StrHelper* strHelper = new StrHelper(objects::STR_HELPER); + StarTrackerHandler* starTrackerHandler = new StarTrackerHandler( + objects::STAR_TRACKER, objects::UART_COM_IF, starTrackerCookie, strHelper); + starTrackerHandler->setStartUpImmediately(); +} diff --git a/bsp_egse/ObjectFactory.h b/bsp_egse/ObjectFactory.h new file mode 100644 index 0000000..b24dd32 --- /dev/null +++ b/bsp_egse/ObjectFactory.h @@ -0,0 +1,8 @@ +#ifndef BSP_LINUX_OBJECTFACTORY_H_ +#define BSP_LINUX_OBJECTFACTORY_H_ + +namespace ObjectFactory { +void produce(void* args); +}; // namespace ObjectFactory + +#endif /* BSP_LINUX_OBJECTFACTORY_H_ */ diff --git a/bsp_egse/boardconfig/CMakeLists.txt b/bsp_egse/boardconfig/CMakeLists.txt new file mode 100644 index 0000000..f08670d --- /dev/null +++ b/bsp_egse/boardconfig/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources(${OBSW_NAME} PRIVATE print.c) + +target_include_directories(${OBSW_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/bsp_egse/boardconfig/busConf.h b/bsp_egse/boardconfig/busConf.h new file mode 100644 index 0000000..4df55ed --- /dev/null +++ b/bsp_egse/boardconfig/busConf.h @@ -0,0 +1,8 @@ +#ifndef BSP_EGSE_BOARDCONFIG_BUSCONF_H_ +#define BSP_EGSE_BOARDCONFIG_BUSCONF_H_ + +namespace egse { +static constexpr char STAR_TRACKER_UART[] = "/dev/serial0"; +} + +#endif /* BSP_EGSE_BOARDCONFIG_BUSCONF_H_ */ diff --git a/bsp_egse/boardconfig/etl_profile.h b/bsp_egse/boardconfig/etl_profile.h new file mode 100644 index 0000000..54aca34 --- /dev/null +++ b/bsp_egse/boardconfig/etl_profile.h @@ -0,0 +1,38 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2019 jwellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ +#ifndef __ETL_PROFILE_H__ +#define __ETL_PROFILE_H__ + +#define ETL_CHECK_PUSH_POP + +#define ETL_CPP11_SUPPORTED 1 +#define ETL_NO_NULLPTR_SUPPORT 0 + +#endif diff --git a/bsp_egse/boardconfig/gcov.h b/bsp_egse/boardconfig/gcov.h new file mode 100644 index 0000000..80acdd8 --- /dev/null +++ b/bsp_egse/boardconfig/gcov.h @@ -0,0 +1,15 @@ +#ifndef LINUX_GCOV_H_ +#define LINUX_GCOV_H_ +#include + +#ifdef GCOV +extern "C" void __gcov_flush(); +#else +void __gcov_flush() { + sif::info << "GCC GCOV: Please supply GCOV=1 in Makefile if " + "coverage information is desired.\n" + << std::flush; +} +#endif + +#endif /* LINUX_GCOV_H_ */ diff --git a/bsp_egse/boardconfig/print.c b/bsp_egse/boardconfig/print.c new file mode 100644 index 0000000..5b3a0f9 --- /dev/null +++ b/bsp_egse/boardconfig/print.c @@ -0,0 +1,10 @@ +#include +#include + +void printChar(const char* character, bool errStream) { + if (errStream) { + putc(*character, stderr); + return; + } + putc(*character, stdout); +} diff --git a/bsp_egse/boardconfig/print.h b/bsp_egse/boardconfig/print.h new file mode 100644 index 0000000..8e7e2e5 --- /dev/null +++ b/bsp_egse/boardconfig/print.h @@ -0,0 +1,8 @@ +#ifndef HOSTED_BOARDCONFIG_PRINT_H_ +#define HOSTED_BOARDCONFIG_PRINT_H_ + +#include + +void printChar(const char* character, bool errStream); + +#endif /* HOSTED_BOARDCONFIG_PRINT_H_ */ diff --git a/bsp_egse/boardconfig/rpiConfig.h.in b/bsp_egse/boardconfig/rpiConfig.h.in new file mode 100644 index 0000000..af4f0dd --- /dev/null +++ b/bsp_egse/boardconfig/rpiConfig.h.in @@ -0,0 +1,6 @@ +#ifndef BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ +#define BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ + +#include + +#endif /* BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ */ diff --git a/bsp_egse/main.cpp b/bsp_egse/main.cpp new file mode 100644 index 0000000..75fb4f1 --- /dev/null +++ b/bsp_egse/main.cpp @@ -0,0 +1,28 @@ +#include + +#include "InitMission.h" +#include "OBSWConfig.h" +#include "OBSWVersion.h" +#include "fsfw/tasks/TaskFactory.h" +#include "fsfw/version.h" + +/** + * @brief This is the main program entry point for the egse (raspberry pi 4) + * @return + */ +int main(void) { + std::cout << "-- EIVE OBSW --" << std::endl; + std::cout << "-- Compiled for EGSE from Arcsec" + << " --" << std::endl; + std::cout << "-- OBSW " << SW_NAME << " v" << SW_VERSION << "." << SW_SUBVERSION << "." + << SW_REVISION << ", FSFW v" << FSFW_VERSION << "." << FSFW_SUBVERSION << FSFW_REVISION + << "--" << std::endl; + std::cout << "-- " << __DATE__ << " " << __TIME__ << " --" << std::endl; + + initmission::initMission(); + + for (;;) { + /* Suspend main thread by sleeping it. */ + TaskFactory::delayTask(5000); + } +} diff --git a/bsp_hosted/CMakeLists.txt b/bsp_hosted/CMakeLists.txt new file mode 100644 index 0000000..1300375 --- /dev/null +++ b/bsp_hosted/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources(${OBSW_NAME} PUBLIC scheduling.cpp main.cpp objectFactory.cpp) + +add_subdirectory(fsfwconfig) +add_subdirectory(boardconfig) diff --git a/bsp_hosted/Dockerfile b/bsp_hosted/Dockerfile new file mode 100644 index 0000000..4d89742 --- /dev/null +++ b/bsp_hosted/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:latest +# FROM alpine:latest + +ENV TZ=Europe/Berlin +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get install -y cmake g++ +# RUN apk add cmake make g++ + +WORKDIR /usr/src/app +COPY . . + +RUN set -ex; \ + rm -rf build-hosted; \ + mkdir build-hosted; \ + cd build-hosted; \ + cmake -DCMAKE_BUILD_TYPE=Release -DOSAL_FSFW=host ..; + +ENTRYPOINT ["cmake", "--build", "build-hosted"] +CMD ["-j"] +# CMD ["bash"] diff --git a/bsp_hosted/OBSWConfig.h.in b/bsp_hosted/OBSWConfig.h.in new file mode 100644 index 0000000..4151557 --- /dev/null +++ b/bsp_hosted/OBSWConfig.h.in @@ -0,0 +1,127 @@ +/** + * @brief This file can be used to add preprocessor define for conditional + * code inclusion exclusion or various other project constants and + * properties in one place. + */ +#ifndef FSFWCONFIG_OBSWCONFIG_H_ +#define FSFWCONFIG_OBSWCONFIG_H_ + +#include "commonConfig.h" + +/*******************************************************************/ +/** All of the following flags should be enabled for mission code */ +/*******************************************************************/ + +#define OBSW_ENABLE_TIMERS 1 +#define OBSW_ADD_STAR_TRACKER 0 +#define OBSW_ADD_PLOC_SUPERVISOR 0 +#define OBSW_ADD_PLOC_MPSOC 0 +#define OBSW_ADD_SUN_SENSORS 0 +#define OBSW_ADD_MGT 0 +#define OBSW_ADD_ACS_BOARD 0 +#define OBSW_ADD_ACS_HANDLERS 0 +#define OBSW_ADD_GPS_0 0 +#define OBSW_ADD_GPS_1 0 +#define OBSW_ADD_RW 0 +#define OBSW_DEBUG_TMP1075 0 +#define OBSW_ADD_BPX_BATTERY_HANDLER 0 +#define OBSW_ADD_RTD_DEVICES 0 +#define OBSW_ADD_PL_PCDU 0 +#define OBSW_ADD_TMP_DEVICES 0 +#define OBSW_ADD_RAD_SENSORS 0 +#define OBSW_ADD_SYRLINKS 0 +#define OBSW_STAR_TRACKER_GROUND_CONFIG 1 + +// This is a really tricky switch.. It initializes the PCDU switches to their default states +// at powerup. I think it would be better +// to leave it off for now. It makes testing a lot more difficult and it might mess with +// something the operators might want to do by giving the software too much intelligence +// at the wrong place. The system component might command all the Switches accordingly anyway +#define OBSW_INITIALIZE_SWITCHES 0 +#define OBSW_ENABLE_PERIODIC_HK 0 + +/*******************************************************************/ +/** All of the following flags should be disabled for mission code */ +/*******************************************************************/ + +// Can be used to switch device to NORMAL mode immediately +#define OBSW_SWITCH_TO_NORMAL_MODE_AFTER_STARTUP 1 +#define OBSW_PRINT_MISSED_DEADLINES 1 + +#define OBSW_MPSOC_JTAG_BOOT 0 +#define OBSW_SYRLINKS_SIMULATED 1 +#define OBSW_ADD_TEST_CODE 0 +#define OBSW_ADD_TEST_TASK 0 +#define OBSW_ADD_TEST_PST 0 +// If this is enabled, all other SPI code should be disabled +#define OBSW_ADD_SPI_TEST_CODE 0 +// If this is enabled, all other I2C code should be disabled +#define OBSW_ADD_I2C_TEST_CODE 0 +#define OBSW_ADD_UART_TEST_CODE 0 + +#define OBSW_TEST_ACS 0 +#define OBSW_DEBUG_ACS 0 +#define OBSW_TEST_SUS 0 +#define OBSW_DEBUG_SUS 0 +#define OBSW_TEST_RTD 0 +#define OBSW_DEBUG_RTD 0 +#define OBSW_TEST_RAD_SENSOR 0 +#define OBSW_DEBUG_RAD_SENSOR 0 +#define OBSW_TEST_PL_PCDU 0 +#define OBSW_DEBUG_PL_PCDU 0 +#define OBSW_TEST_BPX_BATT 0 +#define OBSW_DEBUG_BPX_BATT 0 +#define OBSW_TEST_IMTQ 0 +#define OBSW_DEBUG_IMTQ 0 +#define OBSW_TEST_RW 0 +#define OBSW_DEBUG_RW 0 + +#define OBSW_TEST_LIBGPIOD 0 +#define OBSW_TEST_PLOC_HANDLER 0 +#define OBSW_TEST_CCSDS_BRIDGE 0 +#define OBSW_TEST_CCSDS_PTME 0 +#define OBSW_TEST_TE7020_HEATER 0 +#define OBSW_TEST_GPIO_OPEN_BY_LABEL 0 +#define OBSW_TEST_GPIO_OPEN_BY_LINE_NAME 0 +#define OBSW_DEBUG_P60DOCK 0 + +#define OBSW_PRINT_CORE_HK 0 +#define OBSW_DEBUG_PDU1 0 +#define OBSW_DEBUG_PDU2 0 +#define OBSW_DEBUG_GPS 0 +#define OBSW_DEBUG_ACU 0 +#define OBSW_DEBUG_SYRLINKS 0 + +#define OBSW_DEBUG_PDEC_HANDLER 0 +#define OBSW_DEBUG_PLOC_SUPERVISOR 0 +#define OBSW_DEBUG_PLOC_MPSOC 0 +#define OBSW_DEBUG_STARTRACKER 0 +#define OBSW_TCP_SERVER_WIRETAPPING 0 + +/*******************************************************************/ +/** CMake Defines */ +/*******************************************************************/ + +#define OBSW_ADD_TMTC_UDP_SERVER 0 +#define OBSW_ADD_TMTC_TCP_SERVER 1 + +#cmakedefine EIVE_BUILD_GPSD_GPS_HANDLER + +#cmakedefine LIBGPS_VERSION_MAJOR @LIBGPS_VERSION_MAJOR@ +#cmakedefine LIBGPS_VERSION_MINOR @LIBGPS_VERSION_MINOR@ + +#ifdef RASPBERRY_PI +#include "rpiConfig.h" +#elif defined(XIPHOS_Q7S) +#include "q7sConfig.h" +#endif + +#ifdef __cplusplus + +#include "objects/systemObjectList.h" +#include "events/subsystemIdRanges.h" +#include "returnvalues/classIds.h" + +#endif + +#endif /* FSFWCONFIG_OBSWCONFIG_H_ */ diff --git a/bsp_hosted/boardconfig/CMakeLists.txt b/bsp_hosted/boardconfig/CMakeLists.txt new file mode 100644 index 0000000..f08670d --- /dev/null +++ b/bsp_hosted/boardconfig/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources(${OBSW_NAME} PRIVATE print.c) + +target_include_directories(${OBSW_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/bsp_hosted/boardconfig/etl_profile.h b/bsp_hosted/boardconfig/etl_profile.h new file mode 100644 index 0000000..54aca34 --- /dev/null +++ b/bsp_hosted/boardconfig/etl_profile.h @@ -0,0 +1,38 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2019 jwellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ +#ifndef __ETL_PROFILE_H__ +#define __ETL_PROFILE_H__ + +#define ETL_CHECK_PUSH_POP + +#define ETL_CPP11_SUPPORTED 1 +#define ETL_NO_NULLPTR_SUPPORT 0 + +#endif diff --git a/bsp_hosted/boardconfig/gcov.h b/bsp_hosted/boardconfig/gcov.h new file mode 100644 index 0000000..80acdd8 --- /dev/null +++ b/bsp_hosted/boardconfig/gcov.h @@ -0,0 +1,15 @@ +#ifndef LINUX_GCOV_H_ +#define LINUX_GCOV_H_ +#include + +#ifdef GCOV +extern "C" void __gcov_flush(); +#else +void __gcov_flush() { + sif::info << "GCC GCOV: Please supply GCOV=1 in Makefile if " + "coverage information is desired.\n" + << std::flush; +} +#endif + +#endif /* LINUX_GCOV_H_ */ diff --git a/bsp_hosted/boardconfig/print.c b/bsp_hosted/boardconfig/print.c new file mode 100644 index 0000000..9653fe5 --- /dev/null +++ b/bsp_hosted/boardconfig/print.c @@ -0,0 +1,11 @@ +#include "print.h" + +#include + +void printChar(const char* character, bool errStream) { + if (errStream) { + putc(*character, stderr); + return; + } + putc(*character, stdout); +} diff --git a/bsp_hosted/boardconfig/print.h b/bsp_hosted/boardconfig/print.h new file mode 100644 index 0000000..8479849 --- /dev/null +++ b/bsp_hosted/boardconfig/print.h @@ -0,0 +1,8 @@ +#ifndef BSP_HOSTED_BOARDCONFIG_PRINT_H_ +#define BSP_HOSTED_BOARDCONFIG_PRINT_H_ + +#include + +void printChar(const char* character, bool errStream); + +#endif /* BSP_HOSTED_BOARDCONFIG_PRINT_H_ */ diff --git a/bsp_hosted/comIF/ArduinoComIF.cpp b/bsp_hosted/comIF/ArduinoComIF.cpp new file mode 100644 index 0000000..f9206f7 --- /dev/null +++ b/bsp_hosted/comIF/ArduinoComIF.cpp @@ -0,0 +1,349 @@ +#include "ArduinoComIF.h" + +#include +#include +#include + +#include "ArduinoCookie.h" + +// This only works on Linux +#ifdef LINUX +#include +#include +#include +#elif WIN32 +#include +#include +#endif + +#include + +ArduinoComIF::ArduinoComIF(object_id_t setObjectId, bool promptComIF, const char *serialDevice) + : rxBuffer(MAX_PACKET_SIZE * MAX_NUMBER_OF_SPI_DEVICES * 10, true), SystemObject(setObjectId) { +#ifdef LINUX + initialized = false; + serialPort = ::open("/dev/ttyUSB0", O_RDWR); + + if (serialPort < 0) { + // configuration error + printf("Error %i from open: %s\n", errno, strerror(errno)); + return; + } + + struct termios tty; + memset(&tty, 0, sizeof tty); + + // Read in existing settings, and handle any error + if (tcgetattr(serialPort, &tty) != 0) { + printf("Error %i from tcgetattr: %s\n", errno, strerror(errno)); + return; + } + + tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity + tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication + tty.c_cflag |= CS8; // 8 bits per byte + tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control + tty.c_lflag &= ~ICANON; // Disable Canonical Mode + tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars) + tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed + tty.c_cc[VTIME] = 0; // Non Blocking + tty.c_cc[VMIN] = 0; + + cfsetispeed(&tty, B9600); // Baudrate + + if (tcsetattr(serialPort, TCSANOW, &tty) != 0) { + // printf("Error %i from tcsetattr: %s\n", errno, strerror(errno)); + return; + } + + initialized = true; +#elif WIN32 + DCB serialParams = {0}; + + // we need to ask the COM port from the user. + if (promptComIF) { + sif::info << "Please enter the COM port (c to cancel): " << std::flush; + std::string comPort; + while (hCom == INVALID_HANDLE_VALUE) { + std::getline(std::cin, comPort); + if (comPort[0] == 'c') { + break; + } + const TCHAR *pcCommPort = comPort.c_str(); + hCom = CreateFileA(pcCommPort, // port name + GENERIC_READ | GENERIC_WRITE, // Read/Write + 0, // No Sharing + NULL, // No Security + OPEN_EXISTING, // Open existing port only + 0, // Non Overlapped I/O + NULL); // Null for Comm Devices + + if (hCom == INVALID_HANDLE_VALUE) { + if (GetLastError() == 2) { + sif::error << "COM Port does not found!" << std::endl; + } else { + TCHAR err[128]; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), err, sizeof(err), NULL); + // Handle the error. + sif::info << "CreateFileA Error code: " << GetLastError() << std::endl; + sif::error << err << std::flush; + } + sif::info << "Please enter a valid COM port: " << std::flush; + } + } + } + + serialParams.DCBlength = sizeof(serialParams); + if (baudRate == 9600) { + serialParams.BaudRate = CBR_9600; + } + if (baudRate == 115200) { + serialParams.BaudRate = CBR_115200; + } else { + serialParams.BaudRate = baudRate; + } + + serialParams.ByteSize = 8; + serialParams.Parity = NOPARITY; + serialParams.StopBits = ONESTOPBIT; + SetCommState(hCom, &serialParams); + + COMMTIMEOUTS timeout = {0}; + // This will set the read operation to be blocking until data is received + // and then read continuously until there is a gap of one millisecond. + timeout.ReadIntervalTimeout = 1; + timeout.ReadTotalTimeoutConstant = 0; + timeout.ReadTotalTimeoutMultiplier = 0; + timeout.WriteTotalTimeoutConstant = 0; + timeout.WriteTotalTimeoutMultiplier = 0; + SetCommTimeouts(hCom, &timeout); + // Serial port should now be read for operations. +#endif +} + +ArduinoComIF::~ArduinoComIF() { +#ifdef LINUX + ::close(serialPort); +#elif WIN32 + CloseHandle(hCom); +#endif +} +ReturnValue_t ArduinoComIF::initializeInterface(CookieIF *cookie) { return returnvalue::OK; } + +ReturnValue_t ArduinoComIF::sendMessage(CookieIF *cookie, const uint8_t *data, size_t len) { + ArduinoCookie *arduinoCookie = dynamic_cast(cookie); + if (arduinoCookie == nullptr) { + return INVALID_COOKIE_TYPE; + } + + return sendMessage(arduinoCookie->command, arduinoCookie->address, data, len); +} + +ReturnValue_t ArduinoComIF::getSendSuccess(CookieIF *cookie) { return returnvalue::OK; } + +ReturnValue_t ArduinoComIF::requestReceiveMessage(CookieIF *cookie, size_t requestLen) { + return returnvalue::OK; +} + +ReturnValue_t ArduinoComIF::readReceivedMessage(CookieIF *cookie, uint8_t **buffer, size_t *size) { + handleSerialPortRx(); + + ArduinoCookie *arduinoCookie = dynamic_cast(cookie); + if (arduinoCookie == nullptr) { + return INVALID_COOKIE_TYPE; + } + + *buffer = arduinoCookie->replyBuffer.data(); + *size = arduinoCookie->receivedDataLen; + return returnvalue::OK; +} + +ReturnValue_t ArduinoComIF::sendMessage(uint8_t command, uint8_t address, const uint8_t *data, + size_t dataLen) { + if (dataLen > UINT16_MAX) { + return TOO_MUCH_DATA; + } + + // being conservative here + uint8_t sendBuffer[(dataLen + 6) * 2 + 2]; + + sendBuffer[0] = DleEncoder::STX_CHAR; + + uint8_t *currentPosition = sendBuffer + 1; + size_t remainingLen = sizeof(sendBuffer) - 1; + size_t encodedLen = 0; + + ReturnValue_t result = + DleEncoder::encode(&command, 1, currentPosition, remainingLen, &encodedLen, false); + if (result != returnvalue::OK) { + return result; + } + currentPosition += encodedLen; + remainingLen -= encodedLen; // DleEncoder will never return encodedLen > remainingLen + + result = DleEncoder::encode(&address, 1, currentPosition, remainingLen, &encodedLen, false); + if (result != returnvalue::OK) { + return result; + } + currentPosition += encodedLen; + remainingLen -= encodedLen; // DleEncoder will never return encodedLen > remainingLen + + uint8_t temporaryBuffer[2]; + + // note to Lukas: yes we _could_ use Serialize here, but for 16 bit it is a bit too much... + temporaryBuffer[0] = dataLen >> 8; // we checked dataLen above + temporaryBuffer[1] = dataLen; + + result = + DleEncoder::encode(temporaryBuffer, 2, currentPosition, remainingLen, &encodedLen, false); + if (result != returnvalue::OK) { + return result; + } + currentPosition += encodedLen; + remainingLen -= encodedLen; // DleEncoder will never return encodedLen > remainingLen + + // encoding the actual data + result = DleEncoder::encode(data, dataLen, currentPosition, remainingLen, &encodedLen, false); + if (result != returnvalue::OK) { + return result; + } + currentPosition += encodedLen; + remainingLen -= encodedLen; // DleEncoder will never return encodedLen > remainingLen + + uint16_t crc = CRC::crc16ccitt(&command, 1); + crc = CRC::crc16ccitt(&address, 1, crc); + // fortunately the length is still there + crc = CRC::crc16ccitt(temporaryBuffer, 2, crc); + crc = CRC::crc16ccitt(data, dataLen, crc); + + temporaryBuffer[0] = crc >> 8; + temporaryBuffer[1] = crc; + + result = + DleEncoder::encode(temporaryBuffer, 2, currentPosition, remainingLen, &encodedLen, false); + if (result != returnvalue::OK) { + return result; + } + currentPosition += encodedLen; + remainingLen -= encodedLen; // DleEncoder will never return encodedLen > remainingLen + + if (remainingLen > 0) { + *currentPosition = DleEncoder::ETX_CHAR; + } + remainingLen -= 1; + + encodedLen = sizeof(sendBuffer) - remainingLen; + +#ifdef LINUX + ssize_t writtenlen = ::write(serialPort, sendBuffer, encodedLen); + if (writtenlen < 0) { + // we could try to find out what happened... + return returnvalue::FAILED; + } + if (writtenlen != encodedLen) { + // the OS failed us, we do not try to block until everything is written, as + // we can not block the whole system here + return returnvalue::FAILED; + } + return returnvalue::OK; +#elif WIN32 + return returnvalue::OK; +#endif +} + +void ArduinoComIF::handleSerialPortRx() { +#ifdef LINUX + uint32_t availableSpace = rxBuffer.availableWriteSpace(); + + uint8_t dataFromSerial[availableSpace]; + + ssize_t bytesRead = read(serialPort, dataFromSerial, sizeof(dataFromSerial)); + + if (bytesRead < 0) { + return; + } + + rxBuffer.writeData(dataFromSerial, bytesRead); + + uint8_t dataReceivedSoFar[rxBuffer.getMaxSize()]; + + uint32_t dataLenReceivedSoFar = 0; + + rxBuffer.readData(dataReceivedSoFar, sizeof(dataReceivedSoFar), true, &dataLenReceivedSoFar); + + // look for STX + size_t firstSTXinRawData = 0; + while ((firstSTXinRawData < dataLenReceivedSoFar) && + (dataReceivedSoFar[firstSTXinRawData] != DleEncoder::STX_CHAR)) { + firstSTXinRawData++; + } + + if (dataReceivedSoFar[firstSTXinRawData] != DleEncoder::STX_CHAR) { + // there is no STX in our data, throw it away... + rxBuffer.deleteData(dataLenReceivedSoFar); + return; + } + + uint8_t packet[MAX_PACKET_SIZE]; + size_t packetLen = 0; + + size_t readSize = 0; + + ReturnValue_t result = DleEncoder::decode(dataReceivedSoFar + firstSTXinRawData, + dataLenReceivedSoFar - firstSTXinRawData, &readSize, + packet, sizeof(packet), &packetLen); + + size_t toDelete = firstSTXinRawData; + if (result == returnvalue::OK) { + handlePacket(packet, packetLen); + + // after handling the packet, we can delete it from the raw stream, + // it has been copied to packet + toDelete += readSize; + } + + // remove Data which was processed + rxBuffer.deleteData(toDelete); +#elif WIN32 +#endif +} + +void ArduinoComIF::setBaudrate(uint32_t baudRate) { this->baudRate = baudRate; } + +void ArduinoComIF::handlePacket(uint8_t *packet, size_t packetLen) { + uint16_t crc = CRC::crc16ccitt(packet, packetLen); + if (crc != 0) { + // CRC error + return; + } + + uint8_t command = packet[0]; + uint8_t address = packet[1]; + + uint16_t size = (packet[2] << 8) + packet[3]; + + if (size != packetLen - 6) { + // Invalid Length + return; + } + + switch (command) { + case ArduinoCookie::SPI: { + // ArduinoCookie **itsComplicated; + auto findIter = spiMap.find(address); + if (findIter == spiMap.end()) { + // we do no know this address + return; + } + ArduinoCookie &cookie = findIter->second; + if (packetLen > cookie.maxReplySize + 6) { + packetLen = cookie.maxReplySize + 6; + } + std::memcpy(cookie.replyBuffer.data(), packet + 4, packetLen - 6); + cookie.receivedDataLen = packetLen - 6; + } break; + default: + return; + } +} diff --git a/bsp_hosted/comIF/ArduinoComIF.h b/bsp_hosted/comIF/ArduinoComIF.h new file mode 100644 index 0000000..af84974 --- /dev/null +++ b/bsp_hosted/comIF/ArduinoComIF.h @@ -0,0 +1,66 @@ +#ifndef MISSION_ARDUINOCOMMINTERFACE_H_ +#define MISSION_ARDUINOCOMMINTERFACE_H_ + +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WIN32 +#include +#endif + +// Forward declaration, so users don't peek +class ArduinoCookie; + +class ArduinoComIF : public SystemObject, public DeviceCommunicationIF { + public: + static const uint8_t MAX_NUMBER_OF_SPI_DEVICES = 8; + static const uint8_t MAX_PACKET_SIZE = 64; + + static const uint8_t COMMAND_INVALID = -1; + static const uint8_t COMMAND_SPI = 1; + + ArduinoComIF(object_id_t setObjectId, bool promptComIF = false, + const char *serialDevice = nullptr); + void setBaudrate(uint32_t baudRate); + + virtual ~ArduinoComIF(); + + /** DeviceCommunicationIF overrides */ + virtual ReturnValue_t initializeInterface(CookieIF *cookie) override; + virtual ReturnValue_t sendMessage(CookieIF *cookie, const uint8_t *sendData, + size_t sendLen) override; + virtual ReturnValue_t getSendSuccess(CookieIF *cookie) override; + virtual ReturnValue_t requestReceiveMessage(CookieIF *cookie, size_t requestLen) override; + virtual ReturnValue_t readReceivedMessage(CookieIF *cookie, uint8_t **buffer, + size_t *size) override; + + private: +#ifdef LINUX +#elif WIN32 + HANDLE hCom = INVALID_HANDLE_VALUE; +#endif + // remembering if the initialization in the ctor worked + // if not, all calls are disabled + bool initialized = false; + int serialPort = 0; + // Default baud rate is 9600 for now. + uint32_t baudRate = 9600; + + // used to know where to put the data if a reply is received + std::map spiMap; + + SimpleRingBuffer rxBuffer; + + ReturnValue_t sendMessage(uint8_t command, uint8_t address, const uint8_t *data, size_t dataLen); + void handleSerialPortRx(); + + void handlePacket(uint8_t *packet, size_t packetLen); +}; + +#endif /* MISSION_ARDUINOCOMMINTERFACE_H_ */ diff --git a/bsp_hosted/comIF/ArduinoCookie.cpp b/bsp_hosted/comIF/ArduinoCookie.cpp new file mode 100644 index 0000000..89cb156 --- /dev/null +++ b/bsp_hosted/comIF/ArduinoCookie.cpp @@ -0,0 +1,8 @@ +#include + +ArduinoCookie::ArduinoCookie(Protocol_t protocol, uint8_t address, const size_t maxReplySize) + : protocol(protocol), + command(protocol), + address(address), + maxReplySize(maxReplySize), + replyBuffer(maxReplySize) {} diff --git a/bsp_hosted/comIF/ArduinoCookie.h b/bsp_hosted/comIF/ArduinoCookie.h new file mode 100644 index 0000000..04d4bd8 --- /dev/null +++ b/bsp_hosted/comIF/ArduinoCookie.h @@ -0,0 +1,22 @@ +#ifndef MISSION_ARDUINO_ARDUINOCOOKIE_H_ +#define MISSION_ARDUINO_ARDUINOCOOKIE_H_ + +#include + +#include + +class ArduinoCookie : public CookieIF { + public: + enum Protocol_t : uint8_t { INVALID, SPI, I2C }; + + ArduinoCookie(Protocol_t protocol, uint8_t address, const size_t maxReplySize); + + Protocol_t protocol; + uint8_t command; + uint8_t address; + std::vector replyBuffer; + size_t receivedDataLen = 0; + size_t maxReplySize; +}; + +#endif /* MISSION_ARDUINO_ARDUINOCOOKIE_H_ */ diff --git a/bsp_hosted/comIF/CMakeLists.txt b/bsp_hosted/comIF/CMakeLists.txt new file mode 100644 index 0000000..568cf56 --- /dev/null +++ b/bsp_hosted/comIF/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PUBLIC ArduinoComIF.cpp ArduinoCookie.cpp) diff --git a/bsp_hosted/fsfwconfig/CMakeLists.txt b/bsp_hosted/fsfwconfig/CMakeLists.txt new file mode 100644 index 0000000..95e43c2 --- /dev/null +++ b/bsp_hosted/fsfwconfig/CMakeLists.txt @@ -0,0 +1,17 @@ +target_sources(${OBSW_NAME} PRIVATE ipc/MissionMessageTypes.cpp) + +target_include_directories(${OBSW_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# If a special translation file for object IDs exists, compile it. +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/objects/translateObjects.cpp") + target_sources(${OBSW_NAME} PRIVATE objects/translateObjects.cpp) + target_sources(${UNITTEST_NAME} PRIVATE objects/translateObjects.cpp) +endif() + +# If a special translation file for events exists, compile it. +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/objects/translateObjects.cpp") + target_sources(${OBSW_NAME} PRIVATE events/translateEvents.cpp) + target_sources(${UNITTEST_NAME} PRIVATE events/translateEvents.cpp) +endif() + +add_subdirectory(pollingsequence) diff --git a/bsp_hosted/fsfwconfig/FSFWConfig.h.in b/bsp_hosted/fsfwconfig/FSFWConfig.h.in new file mode 100644 index 0000000..cf4b941 --- /dev/null +++ b/bsp_hosted/fsfwconfig/FSFWConfig.h.in @@ -0,0 +1,77 @@ +#ifndef CONFIG_FSFWCONFIG_H_ +#define CONFIG_FSFWCONFIG_H_ + +#include +#include + +//! Used to determine whether C++ ostreams are used which can increase +//! the binary size significantly. If this is disabled, +//! the C stdio functions can be used alternatively +#define FSFW_CPP_OSTREAM_ENABLED 1 + +//! More FSFW related printouts depending on level. Useful for development. +#define FSFW_VERBOSE_LEVEL 1 + +//! Can be used to completely disable printouts, even the C stdio ones. +#if FSFW_CPP_OSTREAM_ENABLED == 0 && FSFW_VERBOSE_LEVEL == 0 +#define FSFW_DISABLE_PRINTOUT 0 +#endif + +#define FSFW_USE_PUS_C_TELEMETRY 1 +#define FSFW_USE_PUS_C_TELECOMMANDS 1 + +//! Can be used to disable the ANSI color sequences for C stdio. +#define FSFW_COLORED_OUTPUT 1 + +//! If FSFW_OBJ_EVENT_TRANSLATION is set to one, +//! additional output which requires the translation files translateObjects +//! and translateEvents (and their compiled source files) +#define FSFW_OBJ_EVENT_TRANSLATION 1 + +#if FSFW_OBJ_EVENT_TRANSLATION == 1 +//! Specify whether info events are printed too. +#define FSFW_DEBUG_INFO 1 +#include "events/translateEvents.h" +#include "objects/translateObjects.h" +#else +#endif + +//! When using the newlib nano library, C99 support for stdio facilities +//! will not be provided. This define should be set to 1 if this is the case. +#define FSFW_NO_C99_IO 1 + +//! Specify whether a special mode store is used for Subsystem components. +#define FSFW_USE_MODESTORE 0 + +//! Defines if the real time scheduler for linux should be used. +//! If set to 0, this will also disable priority settings for linux +//! as most systems will not allow to set nice values without privileges +//! For embedded linux system set this to 1. +//! If set to 1 the binary needs "cap_sys_nice=eip" privileges to run +#define FSFW_USE_REALTIME_FOR_LINUX 0 + +#define FSFW_UDP_SEND_WIRETAPPING_ENABLED 0 + +namespace fsfwconfig { + +//! Default timestamp size. The default timestamp will be an seven byte CDC short timestamp. +static constexpr uint8_t FSFW_MISSION_TIMESTAMP_SIZE = 7; + +//! Configure the allocated pool sizes for the event manager. +static constexpr size_t FSFW_EVENTMGMR_MATCHTREE_NODES = 240; +static constexpr size_t FSFW_EVENTMGMT_EVENTIDMATCHERS = 120; +static constexpr size_t FSFW_EVENTMGMR_RANGEMATCHERS = 120; + +//! Defines the FIFO depth of each commanding service base which +//! also determines how many commands a CSB service can handle in one cycle +//! simultaneously. This will increase the required RAM for +//! each CSB service ! +static constexpr uint8_t FSFW_CSB_FIFO_DEPTH = 6; + +static constexpr size_t FSFW_PRINT_BUFFER_SIZE = 124; + +static constexpr size_t FSFW_MAX_TM_PACKET_SIZE = 2048; + +} // namespace fsfwconfig + +#endif /* CONFIG_FSFWCONFIG_H_ */ diff --git a/bsp_hosted/fsfwconfig/OBSWConfig.h.in b/bsp_hosted/fsfwconfig/OBSWConfig.h.in new file mode 100644 index 0000000..b7de30f --- /dev/null +++ b/bsp_hosted/fsfwconfig/OBSWConfig.h.in @@ -0,0 +1,46 @@ +/** + * @brief This file can be used to add preprocessor define for conditional + * code inclusion exclusion or various other project constants and + * properties in one place. + */ +#ifndef CONFIG_OBSWCONFIG_H_ +#define CONFIG_OBSWCONFIG_H_ + +#include "commonConfig.h" + +#define OBSW_PRINT_MISSED_DEADLINES 1 + +#define OBSW_ADD_TEST_CODE 1 + +/* These defines should be disabled for mission code but are useful for +debugging. */ +#define OBSW_VEBOSE_LEVEL 1 + +#define OBSW_ADD_CCSDS_IP_CORES 0 +// Set to 1 if all telemetry should be sent to the PTME IP Core +#define OBSW_TM_TO_PTME 0 +// Set to 1 if telecommands are received via the PDEC IP Core +#define OBSW_TC_FROM_PDEC 0 + +#define OBSW_SYRLINKS_SIMULATED 0 + +#define OBSW_INITIALIZE_SWITCHES 0 + +#define OBSW_TCP_SERVER_WIRETAPPING 0 + +#ifdef __cplusplus + +#include "objects/systemObjectList.h" +#include "events/subsystemIdRanges.h" +#include "returnvalues/classIds.h" + +namespace config { +#endif + +/* Add mission configuration flags here */ + +#ifdef __cplusplus +} +#endif + +#endif /* CONFIG_OBSWCONFIG_H_ */ diff --git a/bsp_hosted/fsfwconfig/events/subsystemIdRanges.h b/bsp_hosted/fsfwconfig/events/subsystemIdRanges.h new file mode 100644 index 0000000..b99164d --- /dev/null +++ b/bsp_hosted/fsfwconfig/events/subsystemIdRanges.h @@ -0,0 +1,16 @@ +#ifndef CONFIG_EVENTS_SUBSYSTEMIDRANGES_H_ +#define CONFIG_EVENTS_SUBSYSTEMIDRANGES_H_ + +#include + +#include "eive/eventSubsystemIds.h" + +/** + * These IDs are part of the ID for an event thrown by a subsystem. + * Numbers 0-80 are reserved for FSFW Subsystem IDs (framework/events/) + */ +namespace SUBSYSTEM_ID { +enum : uint8_t { SUBSYSTEM_ID_START = COMMON_SUBSYSTEM_ID_END }; +} + +#endif /* CONFIG_EVENTS_SUBSYSTEMIDRANGES_H_ */ diff --git a/bsp_hosted/fsfwconfig/events/translateEvents.cpp b/bsp_hosted/fsfwconfig/events/translateEvents.cpp new file mode 100644 index 0000000..7a8da30 --- /dev/null +++ b/bsp_hosted/fsfwconfig/events/translateEvents.cpp @@ -0,0 +1,990 @@ +/** + * @brief Auto-generated event translation file. Contains 325 translations. + * @details + * Generated on: 2024-05-06 13:47:38 + */ +#include "translateEvents.h" + +const char *STORE_SEND_WRITE_FAILED_STRING = "STORE_SEND_WRITE_FAILED"; +const char *STORE_WRITE_FAILED_STRING = "STORE_WRITE_FAILED"; +const char *STORE_SEND_READ_FAILED_STRING = "STORE_SEND_READ_FAILED"; +const char *STORE_READ_FAILED_STRING = "STORE_READ_FAILED"; +const char *UNEXPECTED_MSG_STRING = "UNEXPECTED_MSG"; +const char *STORING_FAILED_STRING = "STORING_FAILED"; +const char *TM_DUMP_FAILED_STRING = "TM_DUMP_FAILED"; +const char *STORE_INIT_FAILED_STRING = "STORE_INIT_FAILED"; +const char *STORE_INIT_EMPTY_STRING = "STORE_INIT_EMPTY"; +const char *STORE_CONTENT_CORRUPTED_STRING = "STORE_CONTENT_CORRUPTED"; +const char *STORE_INITIALIZE_STRING = "STORE_INITIALIZE"; +const char *INIT_DONE_STRING = "INIT_DONE"; +const char *DUMP_FINISHED_STRING = "DUMP_FINISHED"; +const char *DELETION_FINISHED_STRING = "DELETION_FINISHED"; +const char *DELETION_FAILED_STRING = "DELETION_FAILED"; +const char *AUTO_CATALOGS_SENDING_FAILED_STRING = "AUTO_CATALOGS_SENDING_FAILED"; +const char *GET_DATA_FAILED_STRING = "GET_DATA_FAILED"; +const char *STORE_DATA_FAILED_STRING = "STORE_DATA_FAILED"; +const char *DEVICE_BUILDING_COMMAND_FAILED_STRING = "DEVICE_BUILDING_COMMAND_FAILED"; +const char *DEVICE_SENDING_COMMAND_FAILED_STRING = "DEVICE_SENDING_COMMAND_FAILED"; +const char *DEVICE_REQUESTING_REPLY_FAILED_STRING = "DEVICE_REQUESTING_REPLY_FAILED"; +const char *DEVICE_READING_REPLY_FAILED_STRING = "DEVICE_READING_REPLY_FAILED"; +const char *DEVICE_INTERPRETING_REPLY_FAILED_STRING = "DEVICE_INTERPRETING_REPLY_FAILED"; +const char *DEVICE_MISSED_REPLY_STRING = "DEVICE_MISSED_REPLY"; +const char *DEVICE_UNKNOWN_REPLY_STRING = "DEVICE_UNKNOWN_REPLY"; +const char *DEVICE_UNREQUESTED_REPLY_STRING = "DEVICE_UNREQUESTED_REPLY"; +const char *INVALID_DEVICE_COMMAND_STRING = "INVALID_DEVICE_COMMAND"; +const char *MONITORING_LIMIT_EXCEEDED_STRING = "MONITORING_LIMIT_EXCEEDED"; +const char *MONITORING_AMBIGUOUS_STRING = "MONITORING_AMBIGUOUS"; +const char *DEVICE_WANTS_HARD_REBOOT_STRING = "DEVICE_WANTS_HARD_REBOOT"; +const char *SWITCH_WENT_OFF_STRING = "SWITCH_WENT_OFF"; +const char *FUSE_CURRENT_HIGH_STRING = "FUSE_CURRENT_HIGH"; +const char *FUSE_WENT_OFF_STRING = "FUSE_WENT_OFF"; +const char *POWER_ABOVE_HIGH_LIMIT_STRING = "POWER_ABOVE_HIGH_LIMIT"; +const char *POWER_BELOW_LOW_LIMIT_STRING = "POWER_BELOW_LOW_LIMIT"; +const char *HEATER_ON_STRING = "HEATER_ON"; +const char *HEATER_OFF_STRING = "HEATER_OFF"; +const char *HEATER_TIMEOUT_STRING = "HEATER_TIMEOUT"; +const char *HEATER_STAYED_ON_STRING = "HEATER_STAYED_ON"; +const char *HEATER_STAYED_OFF_STRING = "HEATER_STAYED_OFF"; +const char *TEMP_SENSOR_HIGH_STRING = "TEMP_SENSOR_HIGH"; +const char *TEMP_SENSOR_LOW_STRING = "TEMP_SENSOR_LOW"; +const char *TEMP_SENSOR_GRADIENT_STRING = "TEMP_SENSOR_GRADIENT"; +const char *COMPONENT_TEMP_LOW_STRING = "COMPONENT_TEMP_LOW"; +const char *COMPONENT_TEMP_HIGH_STRING = "COMPONENT_TEMP_HIGH"; +const char *COMPONENT_TEMP_OOL_LOW_STRING = "COMPONENT_TEMP_OOL_LOW"; +const char *COMPONENT_TEMP_OOL_HIGH_STRING = "COMPONENT_TEMP_OOL_HIGH"; +const char *TEMP_NOT_IN_OP_RANGE_STRING = "TEMP_NOT_IN_OP_RANGE"; +const char *FDIR_CHANGED_STATE_STRING = "FDIR_CHANGED_STATE"; +const char *FDIR_STARTS_RECOVERY_STRING = "FDIR_STARTS_RECOVERY"; +const char *FDIR_TURNS_OFF_DEVICE_STRING = "FDIR_TURNS_OFF_DEVICE"; +const char *MONITOR_CHANGED_STATE_STRING = "MONITOR_CHANGED_STATE"; +const char *VALUE_BELOW_LOW_LIMIT_STRING = "VALUE_BELOW_LOW_LIMIT"; +const char *VALUE_ABOVE_HIGH_LIMIT_STRING = "VALUE_ABOVE_HIGH_LIMIT"; +const char *VALUE_OUT_OF_RANGE_STRING = "VALUE_OUT_OF_RANGE"; +const char *CHANGING_MODE_STRING = "CHANGING_MODE"; +const char *MODE_INFO_STRING = "MODE_INFO"; +const char *FALLBACK_FAILED_STRING = "FALLBACK_FAILED"; +const char *MODE_TRANSITION_FAILED_STRING = "MODE_TRANSITION_FAILED"; +const char *CANT_KEEP_MODE_STRING = "CANT_KEEP_MODE"; +const char *OBJECT_IN_INVALID_MODE_STRING = "OBJECT_IN_INVALID_MODE"; +const char *FORCING_MODE_STRING = "FORCING_MODE"; +const char *MODE_CMD_REJECTED_STRING = "MODE_CMD_REJECTED"; +const char *HEALTH_INFO_STRING = "HEALTH_INFO"; +const char *CHILD_CHANGED_HEALTH_STRING = "CHILD_CHANGED_HEALTH"; +const char *CHILD_PROBLEMS_STRING = "CHILD_PROBLEMS"; +const char *OVERWRITING_HEALTH_STRING = "OVERWRITING_HEALTH"; +const char *TRYING_RECOVERY_STRING = "TRYING_RECOVERY"; +const char *RECOVERY_STEP_STRING = "RECOVERY_STEP"; +const char *RECOVERY_DONE_STRING = "RECOVERY_DONE"; +const char *HANDLE_PACKET_FAILED_STRING = "HANDLE_PACKET_FAILED"; +const char *RF_AVAILABLE_STRING = "RF_AVAILABLE"; +const char *RF_LOST_STRING = "RF_LOST"; +const char *BIT_LOCK_STRING = "BIT_LOCK"; +const char *BIT_LOCK_LOST_STRING = "BIT_LOCK_LOST"; +const char *FRAME_PROCESSING_FAILED_STRING = "FRAME_PROCESSING_FAILED"; +const char *CLOCK_SET_STRING = "CLOCK_SET"; +const char *CLOCK_DUMP_LEGACY_STRING = "CLOCK_DUMP_LEGACY"; +const char *CLOCK_SET_FAILURE_STRING = "CLOCK_SET_FAILURE"; +const char *CLOCK_DUMP_STRING = "CLOCK_DUMP"; +const char *CLOCK_DUMP_BEFORE_SETTING_TIME_STRING = "CLOCK_DUMP_BEFORE_SETTING_TIME"; +const char *CLOCK_DUMP_AFTER_SETTING_TIME_STRING = "CLOCK_DUMP_AFTER_SETTING_TIME"; +const char *TC_DELETION_FAILED_STRING = "TC_DELETION_FAILED"; +const char *TEST_STRING = "TEST"; +const char *CHANGE_OF_SETUP_PARAMETER_STRING = "CHANGE_OF_SETUP_PARAMETER"; +const char *STORE_ERROR_STRING = "STORE_ERROR"; +const char *MSG_QUEUE_ERROR_STRING = "MSG_QUEUE_ERROR"; +const char *SERIALIZATION_ERROR_STRING = "SERIALIZATION_ERROR"; +const char *FILESTORE_ERROR_STRING = "FILESTORE_ERROR"; +const char *FILENAME_TOO_LARGE_ERROR_STRING = "FILENAME_TOO_LARGE_ERROR"; +const char *HANDLING_CFDP_REQUEST_FAILED_STRING = "HANDLING_CFDP_REQUEST_FAILED"; +const char *SAFE_RATE_VIOLATION_STRING = "SAFE_RATE_VIOLATION"; +const char *RATE_RECOVERY_STRING = "RATE_RECOVERY"; +const char *MULTIPLE_RW_INVALID_STRING = "MULTIPLE_RW_INVALID"; +const char *MEKF_INVALID_INFO_STRING = "MEKF_INVALID_INFO"; +const char *MEKF_RECOVERY_STRING = "MEKF_RECOVERY"; +const char *MEKF_AUTOMATIC_RESET_STRING = "MEKF_AUTOMATIC_RESET"; +const char *PTG_CTRL_NO_ATTITUDE_INFORMATION_STRING = "PTG_CTRL_NO_ATTITUDE_INFORMATION"; +const char *SAFE_MODE_CONTROLLER_FAILURE_STRING = "SAFE_MODE_CONTROLLER_FAILURE"; +const char *TLE_TOO_OLD_STRING = "TLE_TOO_OLD"; +const char *TLE_FILE_READ_FAILED_STRING = "TLE_FILE_READ_FAILED"; +const char *PTG_RATE_VIOLATION_STRING = "PTG_RATE_VIOLATION"; +const char *DETUMBLE_TRANSITION_FAILED_STRING = "DETUMBLE_TRANSITION_FAILED"; +const char *SWITCH_CMD_SENT_STRING = "SWITCH_CMD_SENT"; +const char *SWITCH_HAS_CHANGED_STRING = "SWITCH_HAS_CHANGED"; +const char *SWITCHING_Q7S_DENIED_STRING = "SWITCHING_Q7S_DENIED"; +const char *FDIR_REACTION_IGNORED_STRING = "FDIR_REACTION_IGNORED"; +const char *DATASET_READ_FAILED_STRING = "DATASET_READ_FAILED"; +const char *VOLTAGE_OUT_OF_BOUNDS_STRING = "VOLTAGE_OUT_OF_BOUNDS"; +const char *TIMEDELTA_OUT_OF_BOUNDS_STRING = "TIMEDELTA_OUT_OF_BOUNDS"; +const char *POWER_LEVEL_LOW_STRING = "POWER_LEVEL_LOW"; +const char *POWER_LEVEL_CRITICAL_STRING = "POWER_LEVEL_CRITICAL"; +const char *GPIO_PULL_HIGH_FAILED_STRING = "GPIO_PULL_HIGH_FAILED"; +const char *GPIO_PULL_LOW_FAILED_STRING = "GPIO_PULL_LOW_FAILED"; +const char *HEATER_WENT_ON_STRING = "HEATER_WENT_ON"; +const char *HEATER_WENT_OFF_STRING = "HEATER_WENT_OFF"; +const char *SWITCH_ALREADY_ON_STRING = "SWITCH_ALREADY_ON"; +const char *SWITCH_ALREADY_OFF_STRING = "SWITCH_ALREADY_OFF"; +const char *MAIN_SWITCH_TIMEOUT_STRING = "MAIN_SWITCH_TIMEOUT"; +const char *FAULTY_HEATER_WAS_ON_STRING = "FAULTY_HEATER_WAS_ON"; +const char *BURN_PHASE_START_STRING = "BURN_PHASE_START"; +const char *BURN_PHASE_DONE_STRING = "BURN_PHASE_DONE"; +const char *MAIN_SWITCH_ON_TIMEOUT_STRING = "MAIN_SWITCH_ON_TIMEOUT"; +const char *MAIN_SWITCH_OFF_TIMEOUT_STRING = "MAIN_SWITCH_OFF_TIMEOUT"; +const char *DEPL_SA1_GPIO_SWTICH_ON_FAILED_STRING = "DEPL_SA1_GPIO_SWTICH_ON_FAILED"; +const char *DEPL_SA2_GPIO_SWTICH_ON_FAILED_STRING = "DEPL_SA2_GPIO_SWTICH_ON_FAILED"; +const char *DEPL_SA1_GPIO_SWTICH_OFF_FAILED_STRING = "DEPL_SA1_GPIO_SWTICH_OFF_FAILED"; +const char *DEPL_SA2_GPIO_SWTICH_OFF_FAILED_STRING = "DEPL_SA2_GPIO_SWTICH_OFF_FAILED"; +const char *AUTONOMOUS_DEPLOYMENT_COMPLETED_STRING = "AUTONOMOUS_DEPLOYMENT_COMPLETED"; +const char *MEMORY_READ_RPT_CRC_FAILURE_STRING = "MEMORY_READ_RPT_CRC_FAILURE"; +const char *ACK_FAILURE_STRING = "ACK_FAILURE"; +const char *EXE_FAILURE_STRING = "EXE_FAILURE"; +const char *MPSOC_HANDLER_CRC_FAILURE_STRING = "MPSOC_HANDLER_CRC_FAILURE"; +const char *MPSOC_HANDLER_SEQUENCE_COUNT_MISMATCH_STRING = "MPSOC_HANDLER_SEQUENCE_COUNT_MISMATCH"; +const char *MPSOC_SHUTDOWN_FAILED_STRING = "MPSOC_SHUTDOWN_FAILED"; +const char *SUPV_NOT_ON_STRING = "SUPV_NOT_ON"; +const char *SUPV_REPLY_TIMEOUT_STRING = "SUPV_REPLY_TIMEOUT"; +const char *CAM_MUST_BE_ON_FOR_SNAPSHOT_MODE_STRING = "CAM_MUST_BE_ON_FOR_SNAPSHOT_MODE"; +const char *SELF_TEST_I2C_FAILURE_STRING = "SELF_TEST_I2C_FAILURE"; +const char *SELF_TEST_SPI_FAILURE_STRING = "SELF_TEST_SPI_FAILURE"; +const char *SELF_TEST_ADC_FAILURE_STRING = "SELF_TEST_ADC_FAILURE"; +const char *SELF_TEST_PWM_FAILURE_STRING = "SELF_TEST_PWM_FAILURE"; +const char *SELF_TEST_TC_FAILURE_STRING = "SELF_TEST_TC_FAILURE"; +const char *SELF_TEST_MTM_RANGE_FAILURE_STRING = "SELF_TEST_MTM_RANGE_FAILURE"; +const char *SELF_TEST_COIL_CURRENT_FAILURE_STRING = "SELF_TEST_COIL_CURRENT_FAILURE"; +const char *INVALID_ERROR_BYTE_STRING = "INVALID_ERROR_BYTE"; +const char *ERROR_STATE_STRING = "ERROR_STATE"; +const char *RESET_OCCURED_STRING = "RESET_OCCURED"; +const char *BOOTING_FIRMWARE_FAILED_EVENT_STRING = "BOOTING_FIRMWARE_FAILED_EVENT"; +const char *BOOTING_BOOTLOADER_FAILED_EVENT_STRING = "BOOTING_BOOTLOADER_FAILED_EVENT"; +const char *COM_ERROR_REPLY_RECEIVED_STRING = "COM_ERROR_REPLY_RECEIVED"; +const char *SUPV_MEMORY_READ_RPT_CRC_FAILURE_STRING = "SUPV_MEMORY_READ_RPT_CRC_FAILURE"; +const char *SUPV_UNKNOWN_TM_STRING = "SUPV_UNKNOWN_TM"; +const char *SUPV_UNINIMPLEMENTED_TM_STRING = "SUPV_UNINIMPLEMENTED_TM"; +const char *SUPV_ACK_FAILURE_STRING = "SUPV_ACK_FAILURE"; +const char *SUPV_EXE_FAILURE_STRING = "SUPV_EXE_FAILURE"; +const char *SUPV_CRC_FAILURE_EVENT_STRING = "SUPV_CRC_FAILURE_EVENT"; +const char *SUPV_HELPER_EXECUTING_STRING = "SUPV_HELPER_EXECUTING"; +const char *SUPV_MPSOC_SHUTDOWN_BUILD_FAILED_STRING = "SUPV_MPSOC_SHUTDOWN_BUILD_FAILED"; +const char *SUPV_ACK_UNKNOWN_COMMAND_STRING = "SUPV_ACK_UNKNOWN_COMMAND"; +const char *SUPV_EXE_ACK_UNKNOWN_COMMAND_STRING = "SUPV_EXE_ACK_UNKNOWN_COMMAND"; +const char *SANITIZATION_FAILED_STRING = "SANITIZATION_FAILED"; +const char *MOUNTED_SD_CARD_STRING = "MOUNTED_SD_CARD"; +const char *SEND_MRAM_DUMP_FAILED_STRING = "SEND_MRAM_DUMP_FAILED"; +const char *MRAM_DUMP_FAILED_STRING = "MRAM_DUMP_FAILED"; +const char *MRAM_DUMP_FINISHED_STRING = "MRAM_DUMP_FINISHED"; +const char *INVALID_TC_FRAME_STRING = "INVALID_TC_FRAME"; +const char *INVALID_FAR_STRING = "INVALID_FAR"; +const char *CARRIER_LOCK_STRING = "CARRIER_LOCK"; +const char *BIT_LOCK_PDEC_STRING = "BIT_LOCK_PDEC"; +const char *LOST_CARRIER_LOCK_PDEC_STRING = "LOST_CARRIER_LOCK_PDEC"; +const char *LOST_BIT_LOCK_PDEC_STRING = "LOST_BIT_LOCK_PDEC"; +const char *TOO_MANY_IRQS_STRING = "TOO_MANY_IRQS"; +const char *POLL_SYSCALL_ERROR_PDEC_STRING = "POLL_SYSCALL_ERROR_PDEC"; +const char *WRITE_SYSCALL_ERROR_PDEC_STRING = "WRITE_SYSCALL_ERROR_PDEC"; +const char *PDEC_TRYING_RESET_WITH_INIT_STRING = "PDEC_TRYING_RESET_WITH_INIT"; +const char *PDEC_TRYING_RESET_NO_INIT_STRING = "PDEC_TRYING_RESET_NO_INIT"; +const char *PDEC_RESET_FAILED_STRING = "PDEC_RESET_FAILED"; +const char *OPEN_IRQ_FILE_FAILED_STRING = "OPEN_IRQ_FILE_FAILED"; +const char *PDEC_INIT_FAILED_STRING = "PDEC_INIT_FAILED"; +const char *PDEC_CONFIG_CORRUPTED_STRING = "PDEC_CONFIG_CORRUPTED"; +const char *IMAGE_UPLOAD_FAILED_STRING = "IMAGE_UPLOAD_FAILED"; +const char *IMAGE_DOWNLOAD_FAILED_STRING = "IMAGE_DOWNLOAD_FAILED"; +const char *IMAGE_UPLOAD_SUCCESSFUL_STRING = "IMAGE_UPLOAD_SUCCESSFUL"; +const char *IMAGE_DOWNLOAD_SUCCESSFUL_STRING = "IMAGE_DOWNLOAD_SUCCESSFUL"; +const char *FLASH_WRITE_SUCCESSFUL_STRING = "FLASH_WRITE_SUCCESSFUL"; +const char *FLASH_READ_SUCCESSFUL_STRING = "FLASH_READ_SUCCESSFUL"; +const char *FLASH_READ_FAILED_STRING = "FLASH_READ_FAILED"; +const char *FIRMWARE_UPDATE_SUCCESSFUL_STRING = "FIRMWARE_UPDATE_SUCCESSFUL"; +const char *FIRMWARE_UPDATE_FAILED_STRING = "FIRMWARE_UPDATE_FAILED"; +const char *STR_HELPER_READING_REPLY_FAILED_STRING = "STR_HELPER_READING_REPLY_FAILED"; +const char *STR_HELPER_COM_ERROR_STRING = "STR_HELPER_COM_ERROR"; +const char *STR_COM_REPLY_TIMEOUT_STRING = "STR_COM_REPLY_TIMEOUT"; +const char *STR_HELPER_DEC_ERROR_STRING = "STR_HELPER_DEC_ERROR"; +const char *POSITION_MISMATCH_STRING = "POSITION_MISMATCH"; +const char *STR_HELPER_FILE_NOT_EXISTS_STRING = "STR_HELPER_FILE_NOT_EXISTS"; +const char *STR_HELPER_SENDING_PACKET_FAILED_STRING = "STR_HELPER_SENDING_PACKET_FAILED"; +const char *STR_HELPER_REQUESTING_MSG_FAILED_STRING = "STR_HELPER_REQUESTING_MSG_FAILED"; +const char *MPSOC_FLASH_WRITE_FAILED_STRING = "MPSOC_FLASH_WRITE_FAILED"; +const char *MPSOC_FLASH_WRITE_SUCCESSFUL_STRING = "MPSOC_FLASH_WRITE_SUCCESSFUL"; +const char *MPSOC_SENDING_COMMAND_FAILED_STRING = "MPSOC_SENDING_COMMAND_FAILED"; +const char *MPSOC_HELPER_REQUESTING_REPLY_FAILED_STRING = "MPSOC_HELPER_REQUESTING_REPLY_FAILED"; +const char *MPSOC_HELPER_READING_REPLY_FAILED_STRING = "MPSOC_HELPER_READING_REPLY_FAILED"; +const char *MPSOC_MISSING_ACK_STRING = "MPSOC_MISSING_ACK"; +const char *MPSOC_MISSING_EXE_STRING = "MPSOC_MISSING_EXE"; +const char *MPSOC_ACK_FAILURE_REPORT_STRING = "MPSOC_ACK_FAILURE_REPORT"; +const char *MPSOC_EXE_FAILURE_REPORT_STRING = "MPSOC_EXE_FAILURE_REPORT"; +const char *MPSOC_ACK_INVALID_APID_STRING = "MPSOC_ACK_INVALID_APID"; +const char *MPSOC_EXE_INVALID_APID_STRING = "MPSOC_EXE_INVALID_APID"; +const char *MPSOC_HELPER_SEQ_CNT_MISMATCH_STRING = "MPSOC_HELPER_SEQ_CNT_MISMATCH"; +const char *MPSOC_TM_SIZE_ERROR_STRING = "MPSOC_TM_SIZE_ERROR"; +const char *MPSOC_TM_CRC_MISSMATCH_STRING = "MPSOC_TM_CRC_MISSMATCH"; +const char *MPSOC_FLASH_READ_PACKET_ERROR_STRING = "MPSOC_FLASH_READ_PACKET_ERROR"; +const char *MPSOC_FLASH_READ_FAILED_STRING = "MPSOC_FLASH_READ_FAILED"; +const char *MPSOC_FLASH_READ_SUCCESSFUL_STRING = "MPSOC_FLASH_READ_SUCCESSFUL"; +const char *MPSOC_READ_TIMEOUT_STRING = "MPSOC_READ_TIMEOUT"; +const char *TRANSITION_BACK_TO_OFF_STRING = "TRANSITION_BACK_TO_OFF"; +const char *NEG_V_OUT_OF_BOUNDS_STRING = "NEG_V_OUT_OF_BOUNDS"; +const char *U_DRO_OUT_OF_BOUNDS_STRING = "U_DRO_OUT_OF_BOUNDS"; +const char *I_DRO_OUT_OF_BOUNDS_STRING = "I_DRO_OUT_OF_BOUNDS"; +const char *U_X8_OUT_OF_BOUNDS_STRING = "U_X8_OUT_OF_BOUNDS"; +const char *I_X8_OUT_OF_BOUNDS_STRING = "I_X8_OUT_OF_BOUNDS"; +const char *U_TX_OUT_OF_BOUNDS_STRING = "U_TX_OUT_OF_BOUNDS"; +const char *I_TX_OUT_OF_BOUNDS_STRING = "I_TX_OUT_OF_BOUNDS"; +const char *U_MPA_OUT_OF_BOUNDS_STRING = "U_MPA_OUT_OF_BOUNDS"; +const char *I_MPA_OUT_OF_BOUNDS_STRING = "I_MPA_OUT_OF_BOUNDS"; +const char *U_HPA_OUT_OF_BOUNDS_STRING = "U_HPA_OUT_OF_BOUNDS"; +const char *I_HPA_OUT_OF_BOUNDS_STRING = "I_HPA_OUT_OF_BOUNDS"; +const char *TRANSITION_OTHER_SIDE_FAILED_STRING = "TRANSITION_OTHER_SIDE_FAILED"; +const char *NOT_ENOUGH_DEVICES_DUAL_MODE_STRING = "NOT_ENOUGH_DEVICES_DUAL_MODE"; +const char *POWER_STATE_MACHINE_TIMEOUT_STRING = "POWER_STATE_MACHINE_TIMEOUT"; +const char *SIDE_SWITCH_TRANSITION_NOT_ALLOWED_STRING = "SIDE_SWITCH_TRANSITION_NOT_ALLOWED"; +const char *DIRECT_TRANSITION_TO_DUAL_OTHER_GPS_FAULTY_STRING = "DIRECT_TRANSITION_TO_DUAL_OTHER_GPS_FAULTY"; +const char *TRANSITION_OTHER_SIDE_FAILED_12900_STRING = "TRANSITION_OTHER_SIDE_FAILED_12900"; +const char *NOT_ENOUGH_DEVICES_DUAL_MODE_12901_STRING = "NOT_ENOUGH_DEVICES_DUAL_MODE_12901"; +const char *POWER_STATE_MACHINE_TIMEOUT_12902_STRING = "POWER_STATE_MACHINE_TIMEOUT_12902"; +const char *SIDE_SWITCH_TRANSITION_NOT_ALLOWED_12903_STRING = "SIDE_SWITCH_TRANSITION_NOT_ALLOWED_12903"; +const char *CHILDREN_LOST_MODE_STRING = "CHILDREN_LOST_MODE"; +const char *GPS_FIX_CHANGE_STRING = "GPS_FIX_CHANGE"; +const char *CANT_GET_FIX_STRING = "CANT_GET_FIX"; +const char *RESET_FAIL_STRING = "RESET_FAIL"; +const char *P60_BOOT_COUNT_STRING = "P60_BOOT_COUNT"; +const char *BATT_MODE_STRING = "BATT_MODE"; +const char *BATT_MODE_CHANGED_STRING = "BATT_MODE_CHANGED"; +const char *SUPV_UPDATE_FAILED_STRING = "SUPV_UPDATE_FAILED"; +const char *SUPV_UPDATE_SUCCESSFUL_STRING = "SUPV_UPDATE_SUCCESSFUL"; +const char *SUPV_CONTINUE_UPDATE_FAILED_STRING = "SUPV_CONTINUE_UPDATE_FAILED"; +const char *SUPV_CONTINUE_UPDATE_SUCCESSFUL_STRING = "SUPV_CONTINUE_UPDATE_SUCCESSFUL"; +const char *TERMINATED_UPDATE_PROCEDURE_STRING = "TERMINATED_UPDATE_PROCEDURE"; +const char *SUPV_EVENT_BUFFER_REQUEST_SUCCESSFUL_STRING = "SUPV_EVENT_BUFFER_REQUEST_SUCCESSFUL"; +const char *SUPV_EVENT_BUFFER_REQUEST_FAILED_STRING = "SUPV_EVENT_BUFFER_REQUEST_FAILED"; +const char *SUPV_EVENT_BUFFER_REQUEST_TERMINATED_STRING = "SUPV_EVENT_BUFFER_REQUEST_TERMINATED"; +const char *SUPV_MEM_CHECK_OK_STRING = "SUPV_MEM_CHECK_OK"; +const char *SUPV_MEM_CHECK_FAIL_STRING = "SUPV_MEM_CHECK_FAIL"; +const char *SUPV_SENDING_COMMAND_FAILED_STRING = "SUPV_SENDING_COMMAND_FAILED"; +const char *SUPV_HELPER_REQUESTING_REPLY_FAILED_STRING = "SUPV_HELPER_REQUESTING_REPLY_FAILED"; +const char *SUPV_HELPER_READING_REPLY_FAILED_STRING = "SUPV_HELPER_READING_REPLY_FAILED"; +const char *SUPV_MISSING_ACK_STRING = "SUPV_MISSING_ACK"; +const char *SUPV_MISSING_EXE_STRING = "SUPV_MISSING_EXE"; +const char *SUPV_ACK_FAILURE_REPORT_STRING = "SUPV_ACK_FAILURE_REPORT"; +const char *SUPV_EXE_FAILURE_REPORT_STRING = "SUPV_EXE_FAILURE_REPORT"; +const char *SUPV_ACK_INVALID_APID_STRING = "SUPV_ACK_INVALID_APID"; +const char *SUPV_EXE_INVALID_APID_STRING = "SUPV_EXE_INVALID_APID"; +const char *ACK_RECEPTION_FAILURE_STRING = "ACK_RECEPTION_FAILURE"; +const char *EXE_RECEPTION_FAILURE_STRING = "EXE_RECEPTION_FAILURE"; +const char *WRITE_MEMORY_FAILED_STRING = "WRITE_MEMORY_FAILED"; +const char *SUPV_REPLY_SIZE_MISSMATCH_STRING = "SUPV_REPLY_SIZE_MISSMATCH"; +const char *SUPV_REPLY_CRC_MISSMATCH_STRING = "SUPV_REPLY_CRC_MISSMATCH"; +const char *SUPV_UPDATE_PROGRESS_STRING = "SUPV_UPDATE_PROGRESS"; +const char *HDLC_FRAME_REMOVAL_ERROR_STRING = "HDLC_FRAME_REMOVAL_ERROR"; +const char *HDLC_CRC_ERROR_STRING = "HDLC_CRC_ERROR"; +const char *TX_ON_STRING = "TX_ON"; +const char *TX_OFF_STRING = "TX_OFF"; +const char *MISSING_PACKET_STRING = "MISSING_PACKET"; +const char *EXPERIMENT_TIMEDOUT_STRING = "EXPERIMENT_TIMEDOUT"; +const char *MULTI_PACKET_COMMAND_DONE_STRING = "MULTI_PACKET_COMMAND_DONE"; +const char *FS_UNUSABLE_STRING = "FS_UNUSABLE"; +const char *SET_CONFIGFILEVALUE_FAILED_STRING = "SET_CONFIGFILEVALUE_FAILED"; +const char *GET_CONFIGFILEVALUE_FAILED_STRING = "GET_CONFIGFILEVALUE_FAILED"; +const char *INSERT_CONFIGFILEVALUE_FAILED_STRING = "INSERT_CONFIGFILEVALUE_FAILED"; +const char *WRITE_CONFIGFILE_FAILED_STRING = "WRITE_CONFIGFILE_FAILED"; +const char *READ_CONFIGFILE_FAILED_STRING = "READ_CONFIGFILE_FAILED"; +const char *ALLOC_FAILURE_STRING = "ALLOC_FAILURE"; +const char *REBOOT_SW_STRING = "REBOOT_SW"; +const char *REBOOT_MECHANISM_TRIGGERED_STRING = "REBOOT_MECHANISM_TRIGGERED"; +const char *REBOOT_HW_STRING = "REBOOT_HW"; +const char *NO_SD_CARD_ACTIVE_STRING = "NO_SD_CARD_ACTIVE"; +const char *VERSION_INFO_STRING = "VERSION_INFO"; +const char *CURRENT_IMAGE_INFO_STRING = "CURRENT_IMAGE_INFO"; +const char *REBOOT_COUNTER_STRING = "REBOOT_COUNTER"; +const char *INDIVIDUAL_BOOT_COUNTS_STRING = "INDIVIDUAL_BOOT_COUNTS"; +const char *TRYING_I2C_RECOVERY_STRING = "TRYING_I2C_RECOVERY"; +const char *I2C_REBOOT_STRING = "I2C_REBOOT"; +const char *PDEC_REBOOT_STRING = "PDEC_REBOOT"; +const char *FIRMWARE_INFO_STRING = "FIRMWARE_INFO"; +const char *ACTIVE_SD_INFO_STRING = "ACTIVE_SD_INFO"; +const char *NO_VALID_SENSOR_TEMPERATURE_STRING = "NO_VALID_SENSOR_TEMPERATURE"; +const char *NO_HEALTHY_HEATER_AVAILABLE_STRING = "NO_HEALTHY_HEATER_AVAILABLE"; +const char *SYRLINKS_OVERHEATING_STRING = "SYRLINKS_OVERHEATING"; +const char *OBC_OVERHEATING_STRING = "OBC_OVERHEATING"; +const char *CAMERA_OVERHEATING_STRING = "CAMERA_OVERHEATING"; +const char *PCDU_SYSTEM_OVERHEATING_STRING = "PCDU_SYSTEM_OVERHEATING"; +const char *HEATER_NOT_OFF_FOR_OFF_MODE_STRING = "HEATER_NOT_OFF_FOR_OFF_MODE"; +const char *MGT_OVERHEATING_STRING = "MGT_OVERHEATING"; +const char *TCS_SWITCHING_HEATER_ON_STRING = "TCS_SWITCHING_HEATER_ON"; +const char *TCS_SWITCHING_HEATER_OFF_STRING = "TCS_SWITCHING_HEATER_OFF"; +const char *TCS_HEATER_MAX_BURN_TIME_REACHED_STRING = "TCS_HEATER_MAX_BURN_TIME_REACHED"; +const char *TX_TIMER_EXPIRED_STRING = "TX_TIMER_EXPIRED"; +const char *BIT_LOCK_TX_ON_STRING = "BIT_LOCK_TX_ON"; +const char *POSSIBLE_FILE_CORRUPTION_STRING = "POSSIBLE_FILE_CORRUPTION"; +const char *FILE_TOO_LARGE_STRING = "FILE_TOO_LARGE"; +const char *BUSY_DUMPING_EVENT_STRING = "BUSY_DUMPING_EVENT"; +const char *DUMP_OK_STORE_DONE_STRING = "DUMP_OK_STORE_DONE"; +const char *DUMP_NOK_STORE_DONE_STRING = "DUMP_NOK_STORE_DONE"; +const char *DUMP_MISC_STORE_DONE_STRING = "DUMP_MISC_STORE_DONE"; +const char *DUMP_HK_STORE_DONE_STRING = "DUMP_HK_STORE_DONE"; +const char *DUMP_CFDP_STORE_DONE_STRING = "DUMP_CFDP_STORE_DONE"; +const char *DUMP_OK_CANCELLED_STRING = "DUMP_OK_CANCELLED"; +const char *DUMP_NOK_CANCELLED_STRING = "DUMP_NOK_CANCELLED"; +const char *DUMP_MISC_CANCELLED_STRING = "DUMP_MISC_CANCELLED"; +const char *DUMP_HK_CANCELLED_STRING = "DUMP_HK_CANCELLED"; +const char *DUMP_CFDP_CANCELLED_STRING = "DUMP_CFDP_CANCELLED"; +const char *TEMPERATURE_ALL_ONES_START_STRING = "TEMPERATURE_ALL_ONES_START"; +const char *TEMPERATURE_ALL_ONES_RECOVERY_STRING = "TEMPERATURE_ALL_ONES_RECOVERY"; +const char *FAULT_HANDLER_TRIGGERED_STRING = "FAULT_HANDLER_TRIGGERED"; + +const char *translateEvents(Event event) { + switch ((event & 0xFFFF)) { + case (2200): + return STORE_SEND_WRITE_FAILED_STRING; + case (2201): + return STORE_WRITE_FAILED_STRING; + case (2202): + return STORE_SEND_READ_FAILED_STRING; + case (2203): + return STORE_READ_FAILED_STRING; + case (2204): + return UNEXPECTED_MSG_STRING; + case (2205): + return STORING_FAILED_STRING; + case (2206): + return TM_DUMP_FAILED_STRING; + case (2207): + return STORE_INIT_FAILED_STRING; + case (2208): + return STORE_INIT_EMPTY_STRING; + case (2209): + return STORE_CONTENT_CORRUPTED_STRING; + case (2210): + return STORE_INITIALIZE_STRING; + case (2211): + return INIT_DONE_STRING; + case (2212): + return DUMP_FINISHED_STRING; + case (2213): + return DELETION_FINISHED_STRING; + case (2214): + return DELETION_FAILED_STRING; + case (2215): + return AUTO_CATALOGS_SENDING_FAILED_STRING; + case (2600): + return GET_DATA_FAILED_STRING; + case (2601): + return STORE_DATA_FAILED_STRING; + case (2800): + return DEVICE_BUILDING_COMMAND_FAILED_STRING; + case (2801): + return DEVICE_SENDING_COMMAND_FAILED_STRING; + case (2802): + return DEVICE_REQUESTING_REPLY_FAILED_STRING; + case (2803): + return DEVICE_READING_REPLY_FAILED_STRING; + case (2804): + return DEVICE_INTERPRETING_REPLY_FAILED_STRING; + case (2805): + return DEVICE_MISSED_REPLY_STRING; + case (2806): + return DEVICE_UNKNOWN_REPLY_STRING; + case (2807): + return DEVICE_UNREQUESTED_REPLY_STRING; + case (2808): + return INVALID_DEVICE_COMMAND_STRING; + case (2809): + return MONITORING_LIMIT_EXCEEDED_STRING; + case (2810): + return MONITORING_AMBIGUOUS_STRING; + case (2811): + return DEVICE_WANTS_HARD_REBOOT_STRING; + case (4300): + return SWITCH_WENT_OFF_STRING; + case (4301): + return FUSE_CURRENT_HIGH_STRING; + case (4302): + return FUSE_WENT_OFF_STRING; + case (4304): + return POWER_ABOVE_HIGH_LIMIT_STRING; + case (4305): + return POWER_BELOW_LOW_LIMIT_STRING; + case (5000): + return HEATER_ON_STRING; + case (5001): + return HEATER_OFF_STRING; + case (5002): + return HEATER_TIMEOUT_STRING; + case (5003): + return HEATER_STAYED_ON_STRING; + case (5004): + return HEATER_STAYED_OFF_STRING; + case (5200): + return TEMP_SENSOR_HIGH_STRING; + case (5201): + return TEMP_SENSOR_LOW_STRING; + case (5202): + return TEMP_SENSOR_GRADIENT_STRING; + case (5901): + return COMPONENT_TEMP_LOW_STRING; + case (5902): + return COMPONENT_TEMP_HIGH_STRING; + case (5903): + return COMPONENT_TEMP_OOL_LOW_STRING; + case (5904): + return COMPONENT_TEMP_OOL_HIGH_STRING; + case (5905): + return TEMP_NOT_IN_OP_RANGE_STRING; + case (7101): + return FDIR_CHANGED_STATE_STRING; + case (7102): + return FDIR_STARTS_RECOVERY_STRING; + case (7103): + return FDIR_TURNS_OFF_DEVICE_STRING; + case (7201): + return MONITOR_CHANGED_STATE_STRING; + case (7202): + return VALUE_BELOW_LOW_LIMIT_STRING; + case (7203): + return VALUE_ABOVE_HIGH_LIMIT_STRING; + case (7204): + return VALUE_OUT_OF_RANGE_STRING; + case (7400): + return CHANGING_MODE_STRING; + case (7401): + return MODE_INFO_STRING; + case (7402): + return FALLBACK_FAILED_STRING; + case (7403): + return MODE_TRANSITION_FAILED_STRING; + case (7404): + return CANT_KEEP_MODE_STRING; + case (7405): + return OBJECT_IN_INVALID_MODE_STRING; + case (7406): + return FORCING_MODE_STRING; + case (7407): + return MODE_CMD_REJECTED_STRING; + case (7506): + return HEALTH_INFO_STRING; + case (7507): + return CHILD_CHANGED_HEALTH_STRING; + case (7508): + return CHILD_PROBLEMS_STRING; + case (7509): + return OVERWRITING_HEALTH_STRING; + case (7510): + return TRYING_RECOVERY_STRING; + case (7511): + return RECOVERY_STEP_STRING; + case (7512): + return RECOVERY_DONE_STRING; + case (7600): + return HANDLE_PACKET_FAILED_STRING; + case (7900): + return RF_AVAILABLE_STRING; + case (7901): + return RF_LOST_STRING; + case (7902): + return BIT_LOCK_STRING; + case (7903): + return BIT_LOCK_LOST_STRING; + case (7905): + return FRAME_PROCESSING_FAILED_STRING; + case (8900): + return CLOCK_SET_STRING; + case (8901): + return CLOCK_DUMP_LEGACY_STRING; + case (8902): + return CLOCK_SET_FAILURE_STRING; + case (8903): + return CLOCK_DUMP_STRING; + case (8904): + return CLOCK_DUMP_BEFORE_SETTING_TIME_STRING; + case (8905): + return CLOCK_DUMP_AFTER_SETTING_TIME_STRING; + case (9100): + return TC_DELETION_FAILED_STRING; + case (9700): + return TEST_STRING; + case (10600): + return CHANGE_OF_SETUP_PARAMETER_STRING; + case (10800): + return STORE_ERROR_STRING; + case (10801): + return MSG_QUEUE_ERROR_STRING; + case (10802): + return SERIALIZATION_ERROR_STRING; + case (10803): + return FILESTORE_ERROR_STRING; + case (10804): + return FILENAME_TOO_LARGE_ERROR_STRING; + case (10805): + return HANDLING_CFDP_REQUEST_FAILED_STRING; + case (11200): + return SAFE_RATE_VIOLATION_STRING; + case (11201): + return RATE_RECOVERY_STRING; + case (11202): + return MULTIPLE_RW_INVALID_STRING; + case (11203): + return MEKF_INVALID_INFO_STRING; + case (11204): + return MEKF_RECOVERY_STRING; + case (11205): + return MEKF_AUTOMATIC_RESET_STRING; + case (11206): + return PTG_CTRL_NO_ATTITUDE_INFORMATION_STRING; + case (11207): + return SAFE_MODE_CONTROLLER_FAILURE_STRING; + case (11208): + return TLE_TOO_OLD_STRING; + case (11209): + return TLE_FILE_READ_FAILED_STRING; + case (11210): + return PTG_RATE_VIOLATION_STRING; + case (11211): + return DETUMBLE_TRANSITION_FAILED_STRING; + case (11300): + return SWITCH_CMD_SENT_STRING; + case (11301): + return SWITCH_HAS_CHANGED_STRING; + case (11302): + return SWITCHING_Q7S_DENIED_STRING; + case (11303): + return FDIR_REACTION_IGNORED_STRING; + case (11304): + return DATASET_READ_FAILED_STRING; + case (11305): + return VOLTAGE_OUT_OF_BOUNDS_STRING; + case (11306): + return TIMEDELTA_OUT_OF_BOUNDS_STRING; + case (11307): + return POWER_LEVEL_LOW_STRING; + case (11308): + return POWER_LEVEL_CRITICAL_STRING; + case (11400): + return GPIO_PULL_HIGH_FAILED_STRING; + case (11401): + return GPIO_PULL_LOW_FAILED_STRING; + case (11402): + return HEATER_WENT_ON_STRING; + case (11403): + return HEATER_WENT_OFF_STRING; + case (11404): + return SWITCH_ALREADY_ON_STRING; + case (11405): + return SWITCH_ALREADY_OFF_STRING; + case (11406): + return MAIN_SWITCH_TIMEOUT_STRING; + case (11407): + return FAULTY_HEATER_WAS_ON_STRING; + case (11500): + return BURN_PHASE_START_STRING; + case (11501): + return BURN_PHASE_DONE_STRING; + case (11502): + return MAIN_SWITCH_ON_TIMEOUT_STRING; + case (11503): + return MAIN_SWITCH_OFF_TIMEOUT_STRING; + case (11504): + return DEPL_SA1_GPIO_SWTICH_ON_FAILED_STRING; + case (11505): + return DEPL_SA2_GPIO_SWTICH_ON_FAILED_STRING; + case (11506): + return DEPL_SA1_GPIO_SWTICH_OFF_FAILED_STRING; + case (11507): + return DEPL_SA2_GPIO_SWTICH_OFF_FAILED_STRING; + case (11508): + return AUTONOMOUS_DEPLOYMENT_COMPLETED_STRING; + case (11601): + return MEMORY_READ_RPT_CRC_FAILURE_STRING; + case (11602): + return ACK_FAILURE_STRING; + case (11603): + return EXE_FAILURE_STRING; + case (11604): + return MPSOC_HANDLER_CRC_FAILURE_STRING; + case (11605): + return MPSOC_HANDLER_SEQUENCE_COUNT_MISMATCH_STRING; + case (11606): + return MPSOC_SHUTDOWN_FAILED_STRING; + case (11607): + return SUPV_NOT_ON_STRING; + case (11608): + return SUPV_REPLY_TIMEOUT_STRING; + case (11609): + return CAM_MUST_BE_ON_FOR_SNAPSHOT_MODE_STRING; + case (11701): + return SELF_TEST_I2C_FAILURE_STRING; + case (11702): + return SELF_TEST_SPI_FAILURE_STRING; + case (11703): + return SELF_TEST_ADC_FAILURE_STRING; + case (11704): + return SELF_TEST_PWM_FAILURE_STRING; + case (11705): + return SELF_TEST_TC_FAILURE_STRING; + case (11706): + return SELF_TEST_MTM_RANGE_FAILURE_STRING; + case (11707): + return SELF_TEST_COIL_CURRENT_FAILURE_STRING; + case (11708): + return INVALID_ERROR_BYTE_STRING; + case (11801): + return ERROR_STATE_STRING; + case (11802): + return RESET_OCCURED_STRING; + case (11901): + return BOOTING_FIRMWARE_FAILED_EVENT_STRING; + case (11902): + return BOOTING_BOOTLOADER_FAILED_EVENT_STRING; + case (11903): + return COM_ERROR_REPLY_RECEIVED_STRING; + case (12001): + return SUPV_MEMORY_READ_RPT_CRC_FAILURE_STRING; + case (12002): + return SUPV_UNKNOWN_TM_STRING; + case (12003): + return SUPV_UNINIMPLEMENTED_TM_STRING; + case (12004): + return SUPV_ACK_FAILURE_STRING; + case (12005): + return SUPV_EXE_FAILURE_STRING; + case (12006): + return SUPV_CRC_FAILURE_EVENT_STRING; + case (12007): + return SUPV_HELPER_EXECUTING_STRING; + case (12008): + return SUPV_MPSOC_SHUTDOWN_BUILD_FAILED_STRING; + case (12009): + return SUPV_ACK_UNKNOWN_COMMAND_STRING; + case (12010): + return SUPV_EXE_ACK_UNKNOWN_COMMAND_STRING; + case (12100): + return SANITIZATION_FAILED_STRING; + case (12101): + return MOUNTED_SD_CARD_STRING; + case (12300): + return SEND_MRAM_DUMP_FAILED_STRING; + case (12301): + return MRAM_DUMP_FAILED_STRING; + case (12302): + return MRAM_DUMP_FINISHED_STRING; + case (12401): + return INVALID_TC_FRAME_STRING; + case (12402): + return INVALID_FAR_STRING; + case (12403): + return CARRIER_LOCK_STRING; + case (12404): + return BIT_LOCK_PDEC_STRING; + case (12405): + return LOST_CARRIER_LOCK_PDEC_STRING; + case (12406): + return LOST_BIT_LOCK_PDEC_STRING; + case (12407): + return TOO_MANY_IRQS_STRING; + case (12408): + return POLL_SYSCALL_ERROR_PDEC_STRING; + case (12409): + return WRITE_SYSCALL_ERROR_PDEC_STRING; + case (12410): + return PDEC_TRYING_RESET_WITH_INIT_STRING; + case (12411): + return PDEC_TRYING_RESET_NO_INIT_STRING; + case (12412): + return PDEC_RESET_FAILED_STRING; + case (12413): + return OPEN_IRQ_FILE_FAILED_STRING; + case (12414): + return PDEC_INIT_FAILED_STRING; + case (12415): + return PDEC_CONFIG_CORRUPTED_STRING; + case (12500): + return IMAGE_UPLOAD_FAILED_STRING; + case (12501): + return IMAGE_DOWNLOAD_FAILED_STRING; + case (12502): + return IMAGE_UPLOAD_SUCCESSFUL_STRING; + case (12503): + return IMAGE_DOWNLOAD_SUCCESSFUL_STRING; + case (12504): + return FLASH_WRITE_SUCCESSFUL_STRING; + case (12505): + return FLASH_READ_SUCCESSFUL_STRING; + case (12506): + return FLASH_READ_FAILED_STRING; + case (12507): + return FIRMWARE_UPDATE_SUCCESSFUL_STRING; + case (12508): + return FIRMWARE_UPDATE_FAILED_STRING; + case (12509): + return STR_HELPER_READING_REPLY_FAILED_STRING; + case (12510): + return STR_HELPER_COM_ERROR_STRING; + case (12511): + return STR_COM_REPLY_TIMEOUT_STRING; + case (12513): + return STR_HELPER_DEC_ERROR_STRING; + case (12514): + return POSITION_MISMATCH_STRING; + case (12515): + return STR_HELPER_FILE_NOT_EXISTS_STRING; + case (12516): + return STR_HELPER_SENDING_PACKET_FAILED_STRING; + case (12517): + return STR_HELPER_REQUESTING_MSG_FAILED_STRING; + case (12600): + return MPSOC_FLASH_WRITE_FAILED_STRING; + case (12601): + return MPSOC_FLASH_WRITE_SUCCESSFUL_STRING; + case (12602): + return MPSOC_SENDING_COMMAND_FAILED_STRING; + case (12603): + return MPSOC_HELPER_REQUESTING_REPLY_FAILED_STRING; + case (12604): + return MPSOC_HELPER_READING_REPLY_FAILED_STRING; + case (12605): + return MPSOC_MISSING_ACK_STRING; + case (12606): + return MPSOC_MISSING_EXE_STRING; + case (12607): + return MPSOC_ACK_FAILURE_REPORT_STRING; + case (12608): + return MPSOC_EXE_FAILURE_REPORT_STRING; + case (12609): + return MPSOC_ACK_INVALID_APID_STRING; + case (12610): + return MPSOC_EXE_INVALID_APID_STRING; + case (12611): + return MPSOC_HELPER_SEQ_CNT_MISMATCH_STRING; + case (12612): + return MPSOC_TM_SIZE_ERROR_STRING; + case (12613): + return MPSOC_TM_CRC_MISSMATCH_STRING; + case (12614): + return MPSOC_FLASH_READ_PACKET_ERROR_STRING; + case (12615): + return MPSOC_FLASH_READ_FAILED_STRING; + case (12616): + return MPSOC_FLASH_READ_SUCCESSFUL_STRING; + case (12617): + return MPSOC_READ_TIMEOUT_STRING; + case (12700): + return TRANSITION_BACK_TO_OFF_STRING; + case (12701): + return NEG_V_OUT_OF_BOUNDS_STRING; + case (12702): + return U_DRO_OUT_OF_BOUNDS_STRING; + case (12703): + return I_DRO_OUT_OF_BOUNDS_STRING; + case (12704): + return U_X8_OUT_OF_BOUNDS_STRING; + case (12705): + return I_X8_OUT_OF_BOUNDS_STRING; + case (12706): + return U_TX_OUT_OF_BOUNDS_STRING; + case (12707): + return I_TX_OUT_OF_BOUNDS_STRING; + case (12708): + return U_MPA_OUT_OF_BOUNDS_STRING; + case (12709): + return I_MPA_OUT_OF_BOUNDS_STRING; + case (12710): + return U_HPA_OUT_OF_BOUNDS_STRING; + case (12711): + return I_HPA_OUT_OF_BOUNDS_STRING; + case (12800): + return TRANSITION_OTHER_SIDE_FAILED_STRING; + case (12801): + return NOT_ENOUGH_DEVICES_DUAL_MODE_STRING; + case (12802): + return POWER_STATE_MACHINE_TIMEOUT_STRING; + case (12803): + return SIDE_SWITCH_TRANSITION_NOT_ALLOWED_STRING; + case (12804): + return DIRECT_TRANSITION_TO_DUAL_OTHER_GPS_FAULTY_STRING; + case (12900): + return TRANSITION_OTHER_SIDE_FAILED_12900_STRING; + case (12901): + return NOT_ENOUGH_DEVICES_DUAL_MODE_12901_STRING; + case (12902): + return POWER_STATE_MACHINE_TIMEOUT_12902_STRING; + case (12903): + return SIDE_SWITCH_TRANSITION_NOT_ALLOWED_12903_STRING; + case (13000): + return CHILDREN_LOST_MODE_STRING; + case (13100): + return GPS_FIX_CHANGE_STRING; + case (13101): + return CANT_GET_FIX_STRING; + case (13102): + return RESET_FAIL_STRING; + case (13200): + return P60_BOOT_COUNT_STRING; + case (13201): + return BATT_MODE_STRING; + case (13202): + return BATT_MODE_CHANGED_STRING; + case (13600): + return SUPV_UPDATE_FAILED_STRING; + case (13601): + return SUPV_UPDATE_SUCCESSFUL_STRING; + case (13602): + return SUPV_CONTINUE_UPDATE_FAILED_STRING; + case (13603): + return SUPV_CONTINUE_UPDATE_SUCCESSFUL_STRING; + case (13604): + return TERMINATED_UPDATE_PROCEDURE_STRING; + case (13605): + return SUPV_EVENT_BUFFER_REQUEST_SUCCESSFUL_STRING; + case (13606): + return SUPV_EVENT_BUFFER_REQUEST_FAILED_STRING; + case (13607): + return SUPV_EVENT_BUFFER_REQUEST_TERMINATED_STRING; + case (13608): + return SUPV_MEM_CHECK_OK_STRING; + case (13609): + return SUPV_MEM_CHECK_FAIL_STRING; + case (13616): + return SUPV_SENDING_COMMAND_FAILED_STRING; + case (13617): + return SUPV_HELPER_REQUESTING_REPLY_FAILED_STRING; + case (13618): + return SUPV_HELPER_READING_REPLY_FAILED_STRING; + case (13619): + return SUPV_MISSING_ACK_STRING; + case (13620): + return SUPV_MISSING_EXE_STRING; + case (13621): + return SUPV_ACK_FAILURE_REPORT_STRING; + case (13622): + return SUPV_EXE_FAILURE_REPORT_STRING; + case (13623): + return SUPV_ACK_INVALID_APID_STRING; + case (13624): + return SUPV_EXE_INVALID_APID_STRING; + case (13625): + return ACK_RECEPTION_FAILURE_STRING; + case (13626): + return EXE_RECEPTION_FAILURE_STRING; + case (13627): + return WRITE_MEMORY_FAILED_STRING; + case (13628): + return SUPV_REPLY_SIZE_MISSMATCH_STRING; + case (13629): + return SUPV_REPLY_CRC_MISSMATCH_STRING; + case (13630): + return SUPV_UPDATE_PROGRESS_STRING; + case (13631): + return HDLC_FRAME_REMOVAL_ERROR_STRING; + case (13632): + return HDLC_CRC_ERROR_STRING; + case (13701): + return TX_ON_STRING; + case (13702): + return TX_OFF_STRING; + case (13800): + return MISSING_PACKET_STRING; + case (13801): + return EXPERIMENT_TIMEDOUT_STRING; + case (13802): + return MULTI_PACKET_COMMAND_DONE_STRING; + case (13803): + return FS_UNUSABLE_STRING; + case (13901): + return SET_CONFIGFILEVALUE_FAILED_STRING; + case (13902): + return GET_CONFIGFILEVALUE_FAILED_STRING; + case (13903): + return INSERT_CONFIGFILEVALUE_FAILED_STRING; + case (13904): + return WRITE_CONFIGFILE_FAILED_STRING; + case (13905): + return READ_CONFIGFILE_FAILED_STRING; + case (14000): + return ALLOC_FAILURE_STRING; + case (14001): + return REBOOT_SW_STRING; + case (14002): + return REBOOT_MECHANISM_TRIGGERED_STRING; + case (14003): + return REBOOT_HW_STRING; + case (14004): + return NO_SD_CARD_ACTIVE_STRING; + case (14005): + return VERSION_INFO_STRING; + case (14006): + return CURRENT_IMAGE_INFO_STRING; + case (14007): + return REBOOT_COUNTER_STRING; + case (14008): + return INDIVIDUAL_BOOT_COUNTS_STRING; + case (14010): + return TRYING_I2C_RECOVERY_STRING; + case (14011): + return I2C_REBOOT_STRING; + case (14012): + return PDEC_REBOOT_STRING; + case (14013): + return FIRMWARE_INFO_STRING; + case (14014): + return ACTIVE_SD_INFO_STRING; + case (14100): + return NO_VALID_SENSOR_TEMPERATURE_STRING; + case (14101): + return NO_HEALTHY_HEATER_AVAILABLE_STRING; + case (14102): + return SYRLINKS_OVERHEATING_STRING; + case (14104): + return OBC_OVERHEATING_STRING; + case (14105): + return CAMERA_OVERHEATING_STRING; + case (14106): + return PCDU_SYSTEM_OVERHEATING_STRING; + case (14107): + return HEATER_NOT_OFF_FOR_OFF_MODE_STRING; + case (14108): + return MGT_OVERHEATING_STRING; + case (14109): + return TCS_SWITCHING_HEATER_ON_STRING; + case (14110): + return TCS_SWITCHING_HEATER_OFF_STRING; + case (14111): + return TCS_HEATER_MAX_BURN_TIME_REACHED_STRING; + case (14201): + return TX_TIMER_EXPIRED_STRING; + case (14202): + return BIT_LOCK_TX_ON_STRING; + case (14300): + return POSSIBLE_FILE_CORRUPTION_STRING; + case (14301): + return FILE_TOO_LARGE_STRING; + case (14302): + return BUSY_DUMPING_EVENT_STRING; + case (14305): + return DUMP_OK_STORE_DONE_STRING; + case (14306): + return DUMP_NOK_STORE_DONE_STRING; + case (14307): + return DUMP_MISC_STORE_DONE_STRING; + case (14308): + return DUMP_HK_STORE_DONE_STRING; + case (14309): + return DUMP_CFDP_STORE_DONE_STRING; + case (14310): + return DUMP_OK_CANCELLED_STRING; + case (14311): + return DUMP_NOK_CANCELLED_STRING; + case (14312): + return DUMP_MISC_CANCELLED_STRING; + case (14313): + return DUMP_HK_CANCELLED_STRING; + case (14314): + return DUMP_CFDP_CANCELLED_STRING; + case (14500): + return TEMPERATURE_ALL_ONES_START_STRING; + case (14501): + return TEMPERATURE_ALL_ONES_RECOVERY_STRING; + case (14600): + return FAULT_HANDLER_TRIGGERED_STRING; + default: + return "UNKNOWN_EVENT"; + } + return 0; +} diff --git a/bsp_hosted/fsfwconfig/events/translateEvents.h b/bsp_hosted/fsfwconfig/events/translateEvents.h new file mode 100644 index 0000000..9955431 --- /dev/null +++ b/bsp_hosted/fsfwconfig/events/translateEvents.h @@ -0,0 +1,8 @@ +#ifndef FSFWCONFIG_EVENTS_TRANSLATEEVENTS_H_ +#define FSFWCONFIG_EVENTS_TRANSLATEEVENTS_H_ + +#include "fsfw/events/Event.h" + +const char *translateEvents(Event event); + +#endif /* FSFWCONFIG_EVENTS_TRANSLATEEVENTS_H_ */ diff --git a/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.cpp b/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.cpp new file mode 100644 index 0000000..fa1c487 --- /dev/null +++ b/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.cpp @@ -0,0 +1,10 @@ +#include "MissionMessageTypes.h" + +#include + +void messagetypes::clearMissionMessage(CommandMessage* message) { + switch (message->getMessageType()) { + default: + break; + } +} diff --git a/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.h b/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.h new file mode 100644 index 0000000..1d93ba2 --- /dev/null +++ b/bsp_hosted/fsfwconfig/ipc/MissionMessageTypes.h @@ -0,0 +1,22 @@ +#ifndef CONFIG_IPC_MISSIONMESSAGETYPES_H_ +#define CONFIG_IPC_MISSIONMESSAGETYPES_H_ + +#include + +class CommandMessage; + +/** + * Custom command messages are specified here. + * Most messages needed to use FSFW are already located in + * + * @param message Generic Command Message + */ +namespace messagetypes { +enum MESSAGE_TYPE { + MISSION_MESSAGE_TYPE_START = FW_MESSAGES_COUNT, +}; + +void clearMissionMessage(CommandMessage* message); +} // namespace messagetypes + +#endif /* CONFIG_IPC_MISSIONMESSAGETYPES_H_ */ diff --git a/bsp_hosted/fsfwconfig/objects/systemObjectList.h b/bsp_hosted/fsfwconfig/objects/systemObjectList.h new file mode 100644 index 0000000..539ef0d --- /dev/null +++ b/bsp_hosted/fsfwconfig/objects/systemObjectList.h @@ -0,0 +1,31 @@ +#ifndef HOSTED_CONFIG_OBJECTS_SYSTEMOBJECTLIST_H_ +#define HOSTED_CONFIG_OBJECTS_SYSTEMOBJECTLIST_H_ + +#include + +#include "eive/objects.h" + +// The objects will be instantiated in the ID order +namespace objects { +enum sourceObjects : uint32_t { + + PUS_SERVICE_3 = 0x51000300, + PUS_SERVICE_5 = 0x51000400, + PUS_SERVICE_6 = 0x51000500, + PUS_SERVICE_8 = 0x51000800, + PUS_SERVICE_23 = 0x51002300, + PUS_SERVICE_201 = 0x51020100, + + /* Test Task */ + + TEST_TASK = 0x42694269, + DUMMY_INTERFACE = 0xCAFECAFE, + DUMMY_HANDLER = 0x44000001, + /* 0x49 ('I') for Communication Interfaces **/ + ARDUINO_COM_IF = 0x49000001, + + DUMMY_COM_IF = 0x49000002, +}; +} + +#endif /* BSP_CONFIG_OBJECTS_SYSTEMOBJECTLIST_H_ */ diff --git a/bsp_hosted/fsfwconfig/objects/translateObjects.cpp b/bsp_hosted/fsfwconfig/objects/translateObjects.cpp new file mode 100644 index 0000000..53332c1 --- /dev/null +++ b/bsp_hosted/fsfwconfig/objects/translateObjects.cpp @@ -0,0 +1,544 @@ +/** + * @brief Auto-generated object translation file. + * @details + * Contains 176 translations. + * Generated on: 2024-05-06 13:47:38 + */ +#include "translateObjects.h" + +const char *TEST_TASK_STRING = "TEST_TASK"; +const char *ACS_CONTROLLER_STRING = "ACS_CONTROLLER"; +const char *CORE_CONTROLLER_STRING = "CORE_CONTROLLER"; +const char *POWER_CONTROLLER_STRING = "POWER_CONTROLLER"; +const char *GLOBAL_JSON_CFG_STRING = "GLOBAL_JSON_CFG"; +const char *XIPHOS_WDT_STRING = "XIPHOS_WDT"; +const char *THERMAL_CONTROLLER_STRING = "THERMAL_CONTROLLER"; +const char *DUMMY_HANDLER_STRING = "DUMMY_HANDLER"; +const char *MGM_0_LIS3_HANDLER_STRING = "MGM_0_LIS3_HANDLER"; +const char *GYRO_0_ADIS_HANDLER_STRING = "GYRO_0_ADIS_HANDLER"; +const char *SUS_0_N_LOC_XFYFZM_PT_XF_STRING = "SUS_0_N_LOC_XFYFZM_PT_XF"; +const char *SUS_1_N_LOC_XBYFZM_PT_XB_STRING = "SUS_1_N_LOC_XBYFZM_PT_XB"; +const char *SUS_2_N_LOC_XFYBZB_PT_YB_STRING = "SUS_2_N_LOC_XFYBZB_PT_YB"; +const char *SUS_3_N_LOC_XFYBZF_PT_YF_STRING = "SUS_3_N_LOC_XFYBZF_PT_YF"; +const char *SUS_4_N_LOC_XMYFZF_PT_ZF_STRING = "SUS_4_N_LOC_XMYFZF_PT_ZF"; +const char *SUS_5_N_LOC_XFYMZB_PT_ZB_STRING = "SUS_5_N_LOC_XFYMZB_PT_ZB"; +const char *SUS_6_R_LOC_XFYBZM_PT_XF_STRING = "SUS_6_R_LOC_XFYBZM_PT_XF"; +const char *SUS_7_R_LOC_XBYBZM_PT_XB_STRING = "SUS_7_R_LOC_XBYBZM_PT_XB"; +const char *SUS_8_R_LOC_XBYBZB_PT_YB_STRING = "SUS_8_R_LOC_XBYBZB_PT_YB"; +const char *SUS_9_R_LOC_XBYBZB_PT_YF_STRING = "SUS_9_R_LOC_XBYBZB_PT_YF"; +const char *SUS_10_N_LOC_XMYBZF_PT_ZF_STRING = "SUS_10_N_LOC_XMYBZF_PT_ZF"; +const char *SUS_11_R_LOC_XBYMZB_PT_ZB_STRING = "SUS_11_R_LOC_XBYMZB_PT_ZB"; +const char *RW1_STRING = "RW1"; +const char *MGM_1_RM3100_HANDLER_STRING = "MGM_1_RM3100_HANDLER"; +const char *GYRO_1_L3G_HANDLER_STRING = "GYRO_1_L3G_HANDLER"; +const char *RW2_STRING = "RW2"; +const char *MGM_2_LIS3_HANDLER_STRING = "MGM_2_LIS3_HANDLER"; +const char *GYRO_2_ADIS_HANDLER_STRING = "GYRO_2_ADIS_HANDLER"; +const char *RW3_STRING = "RW3"; +const char *MGM_3_RM3100_HANDLER_STRING = "MGM_3_RM3100_HANDLER"; +const char *GYRO_3_L3G_HANDLER_STRING = "GYRO_3_L3G_HANDLER"; +const char *RW4_STRING = "RW4"; +const char *STAR_TRACKER_STRING = "STAR_TRACKER"; +const char *GPS_CONTROLLER_STRING = "GPS_CONTROLLER"; +const char *GPS_0_HEALTH_DEV_STRING = "GPS_0_HEALTH_DEV"; +const char *GPS_1_HEALTH_DEV_STRING = "GPS_1_HEALTH_DEV"; +const char *IMTQ_POLLING_STRING = "IMTQ_POLLING"; +const char *IMTQ_HANDLER_STRING = "IMTQ_HANDLER"; +const char *PCDU_HANDLER_STRING = "PCDU_HANDLER"; +const char *P60DOCK_HANDLER_STRING = "P60DOCK_HANDLER"; +const char *PDU1_HANDLER_STRING = "PDU1_HANDLER"; +const char *PDU2_HANDLER_STRING = "PDU2_HANDLER"; +const char *ACU_HANDLER_STRING = "ACU_HANDLER"; +const char *BPX_BATT_HANDLER_STRING = "BPX_BATT_HANDLER"; +const char *PLPCDU_HANDLER_STRING = "PLPCDU_HANDLER"; +const char *RAD_SENSOR_STRING = "RAD_SENSOR"; +const char *PLOC_UPDATER_STRING = "PLOC_UPDATER"; +const char *PLOC_MEMORY_DUMPER_STRING = "PLOC_MEMORY_DUMPER"; +const char *STR_COM_IF_STRING = "STR_COM_IF"; +const char *PLOC_MPSOC_HELPER_STRING = "PLOC_MPSOC_HELPER"; +const char *AXI_PTME_CONFIG_STRING = "AXI_PTME_CONFIG"; +const char *PTME_CONFIG_STRING = "PTME_CONFIG"; +const char *PTME_VC0_LIVE_TM_STRING = "PTME_VC0_LIVE_TM"; +const char *PTME_VC1_LOG_TM_STRING = "PTME_VC1_LOG_TM"; +const char *PTME_VC2_HK_TM_STRING = "PTME_VC2_HK_TM"; +const char *PTME_VC3_CFDP_TM_STRING = "PTME_VC3_CFDP_TM"; +const char *PLOC_MPSOC_HANDLER_STRING = "PLOC_MPSOC_HANDLER"; +const char *PLOC_SUPERVISOR_HANDLER_STRING = "PLOC_SUPERVISOR_HANDLER"; +const char *PLOC_SUPERVISOR_HELPER_STRING = "PLOC_SUPERVISOR_HELPER"; +const char *PLOC_MPSOC_COMMUNICATION_STRING = "PLOC_MPSOC_COMMUNICATION"; +const char *SCEX_STRING = "SCEX"; +const char *SOLAR_ARRAY_DEPL_HANDLER_STRING = "SOLAR_ARRAY_DEPL_HANDLER"; +const char *HEATER_HANDLER_STRING = "HEATER_HANDLER"; +const char *TMP1075_HANDLER_TCS_0_STRING = "TMP1075_HANDLER_TCS_0"; +const char *TMP1075_HANDLER_TCS_1_STRING = "TMP1075_HANDLER_TCS_1"; +const char *TMP1075_HANDLER_PLPCDU_0_STRING = "TMP1075_HANDLER_PLPCDU_0"; +const char *TMP1075_HANDLER_PLPCDU_1_STRING = "TMP1075_HANDLER_PLPCDU_1"; +const char *TMP1075_HANDLER_IF_BOARD_STRING = "TMP1075_HANDLER_IF_BOARD"; +const char *RTD_0_IC3_PLOC_HEATSPREADER_STRING = "RTD_0_IC3_PLOC_HEATSPREADER"; +const char *RTD_1_IC4_PLOC_MISSIONBOARD_STRING = "RTD_1_IC4_PLOC_MISSIONBOARD"; +const char *RTD_2_IC5_4K_CAMERA_STRING = "RTD_2_IC5_4K_CAMERA"; +const char *RTD_3_IC6_DAC_HEATSPREADER_STRING = "RTD_3_IC6_DAC_HEATSPREADER"; +const char *RTD_4_IC7_STARTRACKER_STRING = "RTD_4_IC7_STARTRACKER"; +const char *RTD_5_IC8_RW1_MX_MY_STRING = "RTD_5_IC8_RW1_MX_MY"; +const char *RTD_6_IC9_DRO_STRING = "RTD_6_IC9_DRO"; +const char *RTD_7_IC10_SCEX_STRING = "RTD_7_IC10_SCEX"; +const char *RTD_8_IC11_X8_STRING = "RTD_8_IC11_X8"; +const char *RTD_9_IC12_HPA_STRING = "RTD_9_IC12_HPA"; +const char *RTD_10_IC13_PL_TX_STRING = "RTD_10_IC13_PL_TX"; +const char *RTD_11_IC14_MPA_STRING = "RTD_11_IC14_MPA"; +const char *RTD_12_IC15_ACU_STRING = "RTD_12_IC15_ACU"; +const char *RTD_13_IC16_PLPCDU_HEATSPREADER_STRING = "RTD_13_IC16_PLPCDU_HEATSPREADER"; +const char *RTD_14_IC17_TCS_BOARD_STRING = "RTD_14_IC17_TCS_BOARD"; +const char *RTD_15_IC18_IMTQ_STRING = "RTD_15_IC18_IMTQ"; +const char *SYRLINKS_HANDLER_STRING = "SYRLINKS_HANDLER"; +const char *SYRLINKS_COM_HANDLER_STRING = "SYRLINKS_COM_HANDLER"; +const char *ARDUINO_COM_IF_STRING = "ARDUINO_COM_IF"; +const char *DUMMY_COM_IF_STRING = "DUMMY_COM_IF"; +const char *SCEX_UART_READER_STRING = "SCEX_UART_READER"; +const char *UART_COM_IF_STRING = "UART_COM_IF"; +const char *ACS_BOARD_POLLING_TASK_STRING = "ACS_BOARD_POLLING_TASK"; +const char *RW_POLLING_TASK_STRING = "RW_POLLING_TASK"; +const char *SPI_RTD_COM_IF_STRING = "SPI_RTD_COM_IF"; +const char *SUS_POLLING_TASK_STRING = "SUS_POLLING_TASK"; +const char *CCSDS_PACKET_DISTRIBUTOR_STRING = "CCSDS_PACKET_DISTRIBUTOR"; +const char *PUS_PACKET_DISTRIBUTOR_STRING = "PUS_PACKET_DISTRIBUTOR"; +const char *TCP_TMTC_SERVER_STRING = "TCP_TMTC_SERVER"; +const char *UDP_TMTC_SERVER_STRING = "UDP_TMTC_SERVER"; +const char *TCP_TMTC_POLLING_TASK_STRING = "TCP_TMTC_POLLING_TASK"; +const char *UDP_TMTC_POLLING_TASK_STRING = "UDP_TMTC_POLLING_TASK"; +const char *FILE_SYSTEM_HANDLER_STRING = "FILE_SYSTEM_HANDLER"; +const char *SDC_MANAGER_STRING = "SDC_MANAGER"; +const char *PTME_STRING = "PTME"; +const char *PDEC_HANDLER_STRING = "PDEC_HANDLER"; +const char *CCSDS_HANDLER_STRING = "CCSDS_HANDLER"; +const char *PUS_SERVICE_3_STRING = "PUS_SERVICE_3"; +const char *PUS_SERVICE_5_STRING = "PUS_SERVICE_5"; +const char *PUS_SERVICE_6_STRING = "PUS_SERVICE_6"; +const char *PUS_SERVICE_8_STRING = "PUS_SERVICE_8"; +const char *PUS_SERVICE_23_STRING = "PUS_SERVICE_23"; +const char *PUS_SERVICE_201_STRING = "PUS_SERVICE_201"; +const char *FSFW_OBJECTS_START_STRING = "FSFW_OBJECTS_START"; +const char *PUS_SERVICE_1_VERIFICATION_STRING = "PUS_SERVICE_1_VERIFICATION"; +const char *PUS_SERVICE_2_DEVICE_ACCESS_STRING = "PUS_SERVICE_2_DEVICE_ACCESS"; +const char *PUS_SERVICE_3_HOUSEKEEPING_STRING = "PUS_SERVICE_3_HOUSEKEEPING"; +const char *PUS_SERVICE_5_EVENT_REPORTING_STRING = "PUS_SERVICE_5_EVENT_REPORTING"; +const char *PUS_SERVICE_8_FUNCTION_MGMT_STRING = "PUS_SERVICE_8_FUNCTION_MGMT"; +const char *PUS_SERVICE_9_TIME_MGMT_STRING = "PUS_SERVICE_9_TIME_MGMT"; +const char *PUS_SERVICE_11_TC_SCHEDULER_STRING = "PUS_SERVICE_11_TC_SCHEDULER"; +const char *PUS_SERVICE_15_TM_STORAGE_STRING = "PUS_SERVICE_15_TM_STORAGE"; +const char *PUS_SERVICE_17_TEST_STRING = "PUS_SERVICE_17_TEST"; +const char *PUS_SERVICE_20_PARAMETERS_STRING = "PUS_SERVICE_20_PARAMETERS"; +const char *PUS_SERVICE_200_MODE_MGMT_STRING = "PUS_SERVICE_200_MODE_MGMT"; +const char *PUS_SERVICE_201_HEALTH_STRING = "PUS_SERVICE_201_HEALTH"; +const char *CFDP_PACKET_DISTRIBUTOR_STRING = "CFDP_PACKET_DISTRIBUTOR"; +const char *HEALTH_TABLE_STRING = "HEALTH_TABLE"; +const char *MODE_STORE_STRING = "MODE_STORE"; +const char *EVENT_MANAGER_STRING = "EVENT_MANAGER"; +const char *INTERNAL_ERROR_REPORTER_STRING = "INTERNAL_ERROR_REPORTER"; +const char *TC_STORE_STRING = "TC_STORE"; +const char *TM_STORE_STRING = "TM_STORE"; +const char *IPC_STORE_STRING = "IPC_STORE"; +const char *TIME_STAMPER_STRING = "TIME_STAMPER"; +const char *VERIFICATION_REPORTER_STRING = "VERIFICATION_REPORTER"; +const char *FSFW_OBJECTS_END_STRING = "FSFW_OBJECTS_END"; +const char *HEATER_0_PLOC_PROC_BRD_STRING = "HEATER_0_PLOC_PROC_BRD"; +const char *HEATER_1_PCDU_BRD_STRING = "HEATER_1_PCDU_BRD"; +const char *HEATER_2_ACS_BRD_STRING = "HEATER_2_ACS_BRD"; +const char *HEATER_3_OBC_BRD_STRING = "HEATER_3_OBC_BRD"; +const char *HEATER_4_CAMERA_STRING = "HEATER_4_CAMERA"; +const char *HEATER_5_STR_STRING = "HEATER_5_STR"; +const char *HEATER_6_DRO_STRING = "HEATER_6_DRO"; +const char *HEATER_7_SYRLINKS_STRING = "HEATER_7_SYRLINKS"; +const char *ACS_BOARD_ASS_STRING = "ACS_BOARD_ASS"; +const char *SUS_BOARD_ASS_STRING = "SUS_BOARD_ASS"; +const char *TCS_BOARD_ASS_STRING = "TCS_BOARD_ASS"; +const char *RW_ASSY_STRING = "RW_ASSY"; +const char *CAM_SWITCHER_STRING = "CAM_SWITCHER"; +const char *SYRLINKS_ASSY_STRING = "SYRLINKS_ASSY"; +const char *IMTQ_ASSY_STRING = "IMTQ_ASSY"; +const char *STR_ASSY_STRING = "STR_ASSY"; +const char *TM_FUNNEL_STRING = "TM_FUNNEL"; +const char *PUS_TM_FUNNEL_STRING = "PUS_TM_FUNNEL"; +const char *CFDP_TM_FUNNEL_STRING = "CFDP_TM_FUNNEL"; +const char *CFDP_HANDLER_STRING = "CFDP_HANDLER"; +const char *CFDP_DISTRIBUTOR_STRING = "CFDP_DISTRIBUTOR"; +const char *CFDP_FAULT_HANDLER_STRING = "CFDP_FAULT_HANDLER"; +const char *EIVE_SYSTEM_STRING = "EIVE_SYSTEM"; +const char *ACS_SUBSYSTEM_STRING = "ACS_SUBSYSTEM"; +const char *PL_SUBSYSTEM_STRING = "PL_SUBSYSTEM"; +const char *TCS_SUBSYSTEM_STRING = "TCS_SUBSYSTEM"; +const char *COM_SUBSYSTEM_STRING = "COM_SUBSYSTEM"; +const char *EPS_SUBSYSTEM_STRING = "EPS_SUBSYSTEM"; +const char *MISC_TM_STORE_STRING = "MISC_TM_STORE"; +const char *OK_TM_STORE_STRING = "OK_TM_STORE"; +const char *NOT_OK_TM_STORE_STRING = "NOT_OK_TM_STORE"; +const char *HK_TM_STORE_STRING = "HK_TM_STORE"; +const char *CFDP_TM_STORE_STRING = "CFDP_TM_STORE"; +const char *LIVE_TM_TASK_STRING = "LIVE_TM_TASK"; +const char *LOG_STORE_AND_TM_TASK_STRING = "LOG_STORE_AND_TM_TASK"; +const char *HK_STORE_AND_TM_TASK_STRING = "HK_STORE_AND_TM_TASK"; +const char *CFDP_STORE_AND_TM_TASK_STRING = "CFDP_STORE_AND_TM_TASK"; +const char *DOWNLINK_RAM_STORE_STRING = "DOWNLINK_RAM_STORE"; +const char *THERMAL_TEMP_INSERTER_STRING = "THERMAL_TEMP_INSERTER"; +const char *DUMMY_INTERFACE_STRING = "DUMMY_INTERFACE"; +const char *NO_OBJECT_STRING = "NO_OBJECT"; + +const char *translateObject(object_id_t object) { + switch ((object & 0xFFFFFFFF)) { + case 0x42694269: + return TEST_TASK_STRING; + case 0x43000002: + return ACS_CONTROLLER_STRING; + case 0x43000003: + return CORE_CONTROLLER_STRING; + case 0x43000004: + return POWER_CONTROLLER_STRING; + case 0x43000006: + return GLOBAL_JSON_CFG_STRING; + case 0x43000007: + return XIPHOS_WDT_STRING; + case 0x43400001: + return THERMAL_CONTROLLER_STRING; + case 0x44000001: + return DUMMY_HANDLER_STRING; + case 0x44120006: + return MGM_0_LIS3_HANDLER_STRING; + case 0x44120010: + return GYRO_0_ADIS_HANDLER_STRING; + case 0x44120032: + return SUS_0_N_LOC_XFYFZM_PT_XF_STRING; + case 0x44120033: + return SUS_1_N_LOC_XBYFZM_PT_XB_STRING; + case 0x44120034: + return SUS_2_N_LOC_XFYBZB_PT_YB_STRING; + case 0x44120035: + return SUS_3_N_LOC_XFYBZF_PT_YF_STRING; + case 0x44120036: + return SUS_4_N_LOC_XMYFZF_PT_ZF_STRING; + case 0x44120037: + return SUS_5_N_LOC_XFYMZB_PT_ZB_STRING; + case 0x44120038: + return SUS_6_R_LOC_XFYBZM_PT_XF_STRING; + case 0x44120039: + return SUS_7_R_LOC_XBYBZM_PT_XB_STRING; + case 0x44120040: + return SUS_8_R_LOC_XBYBZB_PT_YB_STRING; + case 0x44120041: + return SUS_9_R_LOC_XBYBZB_PT_YF_STRING; + case 0x44120042: + return SUS_10_N_LOC_XMYBZF_PT_ZF_STRING; + case 0x44120043: + return SUS_11_R_LOC_XBYMZB_PT_ZB_STRING; + case 0x44120047: + return RW1_STRING; + case 0x44120107: + return MGM_1_RM3100_HANDLER_STRING; + case 0x44120111: + return GYRO_1_L3G_HANDLER_STRING; + case 0x44120148: + return RW2_STRING; + case 0x44120208: + return MGM_2_LIS3_HANDLER_STRING; + case 0x44120212: + return GYRO_2_ADIS_HANDLER_STRING; + case 0x44120249: + return RW3_STRING; + case 0x44120309: + return MGM_3_RM3100_HANDLER_STRING; + case 0x44120313: + return GYRO_3_L3G_HANDLER_STRING; + case 0x44120350: + return RW4_STRING; + case 0x44130001: + return STAR_TRACKER_STRING; + case 0x44130045: + return GPS_CONTROLLER_STRING; + case 0x44130046: + return GPS_0_HEALTH_DEV_STRING; + case 0x44130047: + return GPS_1_HEALTH_DEV_STRING; + case 0x44140013: + return IMTQ_POLLING_STRING; + case 0x44140014: + return IMTQ_HANDLER_STRING; + case 0x442000A1: + return PCDU_HANDLER_STRING; + case 0x44250000: + return P60DOCK_HANDLER_STRING; + case 0x44250001: + return PDU1_HANDLER_STRING; + case 0x44250002: + return PDU2_HANDLER_STRING; + case 0x44250003: + return ACU_HANDLER_STRING; + case 0x44260000: + return BPX_BATT_HANDLER_STRING; + case 0x44300000: + return PLPCDU_HANDLER_STRING; + case 0x443200A5: + return RAD_SENSOR_STRING; + case 0x44330000: + return PLOC_UPDATER_STRING; + case 0x44330001: + return PLOC_MEMORY_DUMPER_STRING; + case 0x44330002: + return STR_COM_IF_STRING; + case 0x44330003: + return PLOC_MPSOC_HELPER_STRING; + case 0x44330004: + return AXI_PTME_CONFIG_STRING; + case 0x44330005: + return PTME_CONFIG_STRING; + case 0x44330006: + return PTME_VC0_LIVE_TM_STRING; + case 0x44330007: + return PTME_VC1_LOG_TM_STRING; + case 0x44330008: + return PTME_VC2_HK_TM_STRING; + case 0x44330009: + return PTME_VC3_CFDP_TM_STRING; + case 0x44330015: + return PLOC_MPSOC_HANDLER_STRING; + case 0x44330016: + return PLOC_SUPERVISOR_HANDLER_STRING; + case 0x44330017: + return PLOC_SUPERVISOR_HELPER_STRING; + case 0x44330018: + return PLOC_MPSOC_COMMUNICATION_STRING; + case 0x44330032: + return SCEX_STRING; + case 0x444100A2: + return SOLAR_ARRAY_DEPL_HANDLER_STRING; + case 0x444100A4: + return HEATER_HANDLER_STRING; + case 0x44420004: + return TMP1075_HANDLER_TCS_0_STRING; + case 0x44420005: + return TMP1075_HANDLER_TCS_1_STRING; + case 0x44420006: + return TMP1075_HANDLER_PLPCDU_0_STRING; + case 0x44420007: + return TMP1075_HANDLER_PLPCDU_1_STRING; + case 0x44420008: + return TMP1075_HANDLER_IF_BOARD_STRING; + case 0x44420016: + return RTD_0_IC3_PLOC_HEATSPREADER_STRING; + case 0x44420017: + return RTD_1_IC4_PLOC_MISSIONBOARD_STRING; + case 0x44420018: + return RTD_2_IC5_4K_CAMERA_STRING; + case 0x44420019: + return RTD_3_IC6_DAC_HEATSPREADER_STRING; + case 0x44420020: + return RTD_4_IC7_STARTRACKER_STRING; + case 0x44420021: + return RTD_5_IC8_RW1_MX_MY_STRING; + case 0x44420022: + return RTD_6_IC9_DRO_STRING; + case 0x44420023: + return RTD_7_IC10_SCEX_STRING; + case 0x44420024: + return RTD_8_IC11_X8_STRING; + case 0x44420025: + return RTD_9_IC12_HPA_STRING; + case 0x44420026: + return RTD_10_IC13_PL_TX_STRING; + case 0x44420027: + return RTD_11_IC14_MPA_STRING; + case 0x44420028: + return RTD_12_IC15_ACU_STRING; + case 0x44420029: + return RTD_13_IC16_PLPCDU_HEATSPREADER_STRING; + case 0x44420030: + return RTD_14_IC17_TCS_BOARD_STRING; + case 0x44420031: + return RTD_15_IC18_IMTQ_STRING; + case 0x445300A3: + return SYRLINKS_HANDLER_STRING; + case 0x445300A4: + return SYRLINKS_COM_HANDLER_STRING; + case 0x49000001: + return ARDUINO_COM_IF_STRING; + case 0x49000002: + return DUMMY_COM_IF_STRING; + case 0x49010006: + return SCEX_UART_READER_STRING; + case 0x49030003: + return UART_COM_IF_STRING; + case 0x49060004: + return ACS_BOARD_POLLING_TASK_STRING; + case 0x49060005: + return RW_POLLING_TASK_STRING; + case 0x49060006: + return SPI_RTD_COM_IF_STRING; + case 0x49060007: + return SUS_POLLING_TASK_STRING; + case 0x50000100: + return CCSDS_PACKET_DISTRIBUTOR_STRING; + case 0x50000200: + return PUS_PACKET_DISTRIBUTOR_STRING; + case 0x50000300: + return TCP_TMTC_SERVER_STRING; + case 0x50000301: + return UDP_TMTC_SERVER_STRING; + case 0x50000400: + return TCP_TMTC_POLLING_TASK_STRING; + case 0x50000401: + return UDP_TMTC_POLLING_TASK_STRING; + case 0x50000500: + return FILE_SYSTEM_HANDLER_STRING; + case 0x50000550: + return SDC_MANAGER_STRING; + case 0x50000600: + return PTME_STRING; + case 0x50000700: + return PDEC_HANDLER_STRING; + case 0x50000800: + return CCSDS_HANDLER_STRING; + case 0x51000300: + return PUS_SERVICE_3_STRING; + case 0x51000400: + return PUS_SERVICE_5_STRING; + case 0x51000500: + return PUS_SERVICE_6_STRING; + case 0x51000800: + return PUS_SERVICE_8_STRING; + case 0x51002300: + return PUS_SERVICE_23_STRING; + case 0x51020100: + return PUS_SERVICE_201_STRING; + case 0x53000000: + return FSFW_OBJECTS_START_STRING; + case 0x53000001: + return PUS_SERVICE_1_VERIFICATION_STRING; + case 0x53000002: + return PUS_SERVICE_2_DEVICE_ACCESS_STRING; + case 0x53000003: + return PUS_SERVICE_3_HOUSEKEEPING_STRING; + case 0x53000005: + return PUS_SERVICE_5_EVENT_REPORTING_STRING; + case 0x53000008: + return PUS_SERVICE_8_FUNCTION_MGMT_STRING; + case 0x53000009: + return PUS_SERVICE_9_TIME_MGMT_STRING; + case 0x53000011: + return PUS_SERVICE_11_TC_SCHEDULER_STRING; + case 0x53000015: + return PUS_SERVICE_15_TM_STORAGE_STRING; + case 0x53000017: + return PUS_SERVICE_17_TEST_STRING; + case 0x53000020: + return PUS_SERVICE_20_PARAMETERS_STRING; + case 0x53000200: + return PUS_SERVICE_200_MODE_MGMT_STRING; + case 0x53000201: + return PUS_SERVICE_201_HEALTH_STRING; + case 0x53001000: + return CFDP_PACKET_DISTRIBUTOR_STRING; + case 0x53010000: + return HEALTH_TABLE_STRING; + case 0x53010100: + return MODE_STORE_STRING; + case 0x53030000: + return EVENT_MANAGER_STRING; + case 0x53040000: + return INTERNAL_ERROR_REPORTER_STRING; + case 0x534f0100: + return TC_STORE_STRING; + case 0x534f0200: + return TM_STORE_STRING; + case 0x534f0300: + return IPC_STORE_STRING; + case 0x53500010: + return TIME_STAMPER_STRING; + case 0x53500020: + return VERIFICATION_REPORTER_STRING; + case 0x53ffffff: + return FSFW_OBJECTS_END_STRING; + case 0x60000000: + return HEATER_0_PLOC_PROC_BRD_STRING; + case 0x60000001: + return HEATER_1_PCDU_BRD_STRING; + case 0x60000002: + return HEATER_2_ACS_BRD_STRING; + case 0x60000003: + return HEATER_3_OBC_BRD_STRING; + case 0x60000004: + return HEATER_4_CAMERA_STRING; + case 0x60000005: + return HEATER_5_STR_STRING; + case 0x60000006: + return HEATER_6_DRO_STRING; + case 0x60000007: + return HEATER_7_SYRLINKS_STRING; + case 0x73000001: + return ACS_BOARD_ASS_STRING; + case 0x73000002: + return SUS_BOARD_ASS_STRING; + case 0x73000003: + return TCS_BOARD_ASS_STRING; + case 0x73000004: + return RW_ASSY_STRING; + case 0x73000006: + return CAM_SWITCHER_STRING; + case 0x73000007: + return SYRLINKS_ASSY_STRING; + case 0x73000008: + return IMTQ_ASSY_STRING; + case 0x73000009: + return STR_ASSY_STRING; + case 0x73000100: + return TM_FUNNEL_STRING; + case 0x73000101: + return PUS_TM_FUNNEL_STRING; + case 0x73000102: + return CFDP_TM_FUNNEL_STRING; + case 0x73000205: + return CFDP_HANDLER_STRING; + case 0x73000206: + return CFDP_DISTRIBUTOR_STRING; + case 0x73000207: + return CFDP_FAULT_HANDLER_STRING; + case 0x73010000: + return EIVE_SYSTEM_STRING; + case 0x73010001: + return ACS_SUBSYSTEM_STRING; + case 0x73010002: + return PL_SUBSYSTEM_STRING; + case 0x73010003: + return TCS_SUBSYSTEM_STRING; + case 0x73010004: + return COM_SUBSYSTEM_STRING; + case 0x73010005: + return EPS_SUBSYSTEM_STRING; + case 0x73020001: + return MISC_TM_STORE_STRING; + case 0x73020002: + return OK_TM_STORE_STRING; + case 0x73020003: + return NOT_OK_TM_STORE_STRING; + case 0x73020004: + return HK_TM_STORE_STRING; + case 0x73030000: + return CFDP_TM_STORE_STRING; + case 0x73040000: + return LIVE_TM_TASK_STRING; + case 0x73040001: + return LOG_STORE_AND_TM_TASK_STRING; + case 0x73040002: + return HK_STORE_AND_TM_TASK_STRING; + case 0x73040003: + return CFDP_STORE_AND_TM_TASK_STRING; + case 0x73040004: + return DOWNLINK_RAM_STORE_STRING; + case 0x90000003: + return THERMAL_TEMP_INSERTER_STRING; + case 0xCAFECAFE: + return DUMMY_INTERFACE_STRING; + case 0xFFFFFFFF: + return NO_OBJECT_STRING; + default: + return "UNKNOWN_OBJECT"; + } + return 0; +} diff --git a/bsp_hosted/fsfwconfig/objects/translateObjects.h b/bsp_hosted/fsfwconfig/objects/translateObjects.h new file mode 100644 index 0000000..257912f --- /dev/null +++ b/bsp_hosted/fsfwconfig/objects/translateObjects.h @@ -0,0 +1,8 @@ +#ifndef FSFWCONFIG_OBJECTS_TRANSLATEOBJECTS_H_ +#define FSFWCONFIG_OBJECTS_TRANSLATEOBJECTS_H_ + +#include + +const char *translateObject(object_id_t object); + +#endif /* FSFWCONFIG_OBJECTS_TRANSLATEOBJECTS_H_ */ diff --git a/bsp_hosted/fsfwconfig/pollingsequence/CMakeLists.txt b/bsp_hosted/fsfwconfig/pollingsequence/CMakeLists.txt new file mode 100644 index 0000000..f92d0c3 --- /dev/null +++ b/bsp_hosted/fsfwconfig/pollingsequence/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PRIVATE DummyPst.cpp) diff --git a/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.cpp b/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.cpp new file mode 100644 index 0000000..de1efa0 --- /dev/null +++ b/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.cpp @@ -0,0 +1,139 @@ +#include "DummyPst.h" + +#include +#include +#include +#include + +ReturnValue_t dummy_pst::pst(FixedTimeslotTaskIF *thisSequence) { + uint32_t length = thisSequence->getPeriodMs(); + + thisSequence->addSlot(objects::BPX_BATT_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::BPX_BATT_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::BPX_BATT_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::BPX_BATT_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::BPX_BATT_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::RW1, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::RW1, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::RW1, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::RW1, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::RW1, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::RW2, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::RW2, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::RW2, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::RW2, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::RW2, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::RW3, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::RW3, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::RW3, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::RW3, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::RW3, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::RW4, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::RW4, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::RW4, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::RW4, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::RW4, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::STAR_TRACKER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::STAR_TRACKER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::STAR_TRACKER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::STAR_TRACKER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::STAR_TRACKER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::SYRLINKS_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::SYRLINKS_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::SYRLINKS_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::SYRLINKS_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::SYRLINKS_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::IMTQ_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::IMTQ_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::IMTQ_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::IMTQ_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::IMTQ_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::ACU_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::ACU_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::ACU_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::ACU_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::ACU_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::PDU1_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::PDU1_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::PDU1_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::PDU1_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::PDU1_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::PDU2_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::PDU2_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::PDU2_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::PDU2_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::PDU2_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::P60DOCK_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::P60DOCK_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::P60DOCK_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::P60DOCK_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::P60DOCK_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::GYRO_0_ADIS_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::GYRO_0_ADIS_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::GYRO_0_ADIS_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::GYRO_0_ADIS_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::GYRO_0_ADIS_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::GYRO_1_L3G_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::GYRO_1_L3G_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::GYRO_1_L3G_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::GYRO_1_L3G_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::GYRO_1_L3G_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::GYRO_2_ADIS_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::GYRO_2_ADIS_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::GYRO_2_ADIS_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::GYRO_2_ADIS_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::GYRO_2_ADIS_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::GYRO_3_L3G_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::GYRO_3_L3G_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::GYRO_3_L3G_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::GYRO_3_L3G_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::GYRO_3_L3G_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::MGM_0_LIS3_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::MGM_0_LIS3_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::MGM_0_LIS3_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::MGM_0_LIS3_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::MGM_0_LIS3_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::MGM_2_LIS3_HANDLER, length * 0, + DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::MGM_2_LIS3_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::MGM_2_LIS3_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::MGM_2_LIS3_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::MGM_2_LIS3_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + thisSequence->addSlot(objects::PLPCDU_HANDLER, length * 0, DeviceHandlerIF::PERFORM_OPERATION); + thisSequence->addSlot(objects::PLPCDU_HANDLER, length * 0, DeviceHandlerIF::SEND_WRITE); + thisSequence->addSlot(objects::PLPCDU_HANDLER, length * 0, DeviceHandlerIF::GET_WRITE); + thisSequence->addSlot(objects::PLPCDU_HANDLER, length * 0, DeviceHandlerIF::SEND_READ); + thisSequence->addSlot(objects::PLPCDU_HANDLER, length * 0, DeviceHandlerIF::GET_READ); + + if (thisSequence->checkSequence() == returnvalue::OK) { + return returnvalue::OK; + } else { +#if FSFW_CPP_OSTREAM_ENABLED == 1 + sif::error << "pst::pollingSequenceInitDefault: Sequence invalid!" << std::endl; +#endif + return returnvalue::FAILED; + } +} diff --git a/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.h b/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.h new file mode 100644 index 0000000..08bf3ca --- /dev/null +++ b/bsp_hosted/fsfwconfig/pollingsequence/DummyPst.h @@ -0,0 +1,14 @@ +#ifndef POLLINGSEQUENCEFACTORY_H_ +#define POLLINGSEQUENCEFACTORY_H_ + +#include + +class FixedTimeslotTaskIF; + +namespace dummy_pst { + +ReturnValue_t pst(FixedTimeslotTaskIF *thisSequence); + +} + +#endif /* POLLINGSEQUENCEINIT_H_ */ diff --git a/bsp_hosted/fsfwconfig/returnvalues/classIds.h b/bsp_hosted/fsfwconfig/returnvalues/classIds.h new file mode 100644 index 0000000..16e6eab --- /dev/null +++ b/bsp_hosted/fsfwconfig/returnvalues/classIds.h @@ -0,0 +1,20 @@ +#ifndef CONFIG_RETURNVALUES_CLASSIDS_H_ +#define CONFIG_RETURNVALUES_CLASSIDS_H_ + +#include + +#include "eive/resultClassIds.h" + +/** + * Source IDs starts at 73 for now + * Framework IDs for ReturnValues run from 0 to 56 + * and are located inside + */ +namespace CLASS_ID { +enum { + CLASS_ID_START = COMMON_CLASS_ID_END, + CLASS_ID_END // [EXPORT] : [END] +}; +} + +#endif /* CONFIG_RETURNVALUES_CLASSIDS_H_ */ diff --git a/bsp_hosted/main.cpp b/bsp_hosted/main.cpp new file mode 100644 index 0000000..83f6777 --- /dev/null +++ b/bsp_hosted/main.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include "commonConfig.h" +#include "fsfw/FSFWVersion.h" +#include "fsfw/controller/ControllerBase.h" +#include "fsfw/ipc/QueueFactory.h" +#include "fsfw/modes/HasModesIF.h" +#include "fsfw/modes/ModeMessage.h" +#include "fsfw/objectmanager/ObjectManager.h" +#include "fsfw/tasks/TaskFactory.h" +#include "scheduling.h" + +#ifdef WIN32 +static const char* COMPILE_PRINTOUT = "Windows"; +#elif LINUX +static const char* COMPILE_PRINTOUT = "Linux"; +#else +static const char* COMPILE_PRINTOUT = "unknown OS"; +#endif +/** + * @brief This is the main program for the hosted build. It can be run for + * Linux and Windows. + * @return + */ +int main(void) { + std::cout << "-- EIVE OBSW --" << std::endl; + std::cout << "-- Compiled for " << COMPILE_PRINTOUT << " --" << std::endl; + std::cout << "-- OBSW " + << "v" << common::OBSW_VERSION << " | FSFW v" << fsfw::FSFW_VERSION << " --" + << std::endl; + std::cout << "-- " << __DATE__ << " " << __TIME__ << " --" << std::endl; + std::cout << "-- " + << " BSP HOSTED" + << " --" << std::endl; + + scheduling::initMission(); + + for (;;) { + // suspend main thread by sleeping it. + TaskFactory::delayTask(5000); + } +} diff --git a/bsp_hosted/objectFactory.cpp b/bsp_hosted/objectFactory.cpp new file mode 100644 index 0000000..1ffc662 --- /dev/null +++ b/bsp_hosted/objectFactory.cpp @@ -0,0 +1,124 @@ +#include "objectFactory.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../mission/utility/DummySdCardManager.h" +#include "OBSWConfig.h" +#include "fsfw/platform.h" +#include "fsfw/power/PowerSwitchIF.h" +#include "fsfw_tests/integration/task/TestTask.h" + +#if OBSW_ADD_TMTC_UDP_SERVER == 1 +#include "fsfw/osal/common/UdpTcPollingTask.h" +#include "fsfw/osal/common/UdpTmTcBridge.h" +#endif +#if OBSW_ADD_TMTC_TCP_SERVER == 1 +#include "fsfw/osal/common/TcpTmTcBridge.h" +#include "fsfw/osal/common/TcpTmTcServer.h" +#endif + +#if OBSW_ADD_TEST_CODE == 1 +#include +#endif + +#include +#include + +#include "dummies/helperFactory.h" + +#ifdef PLATFORM_UNIX +#include +#include + +#include "devices/gpioIds.h" +#include "fsfw_hal/linux/gpio/Gpio.h" +#include "linux/payload/FreshSupvHandler.h" +#include "linux/payload/PlocSupvUartMan.h" +#include "test/gpio/DummyGpioIF.h" +#endif + +void Factory::setStaticFrameworkObjectIds() { + PusServiceBase::PUS_DISTRIBUTOR = objects::PUS_PACKET_DISTRIBUTOR; + PusServiceBase::PACKET_DESTINATION = objects::PUS_TM_FUNNEL; + + CommandingServiceBase::defaultPacketSource = objects::PUS_PACKET_DISTRIBUTOR; + CommandingServiceBase::defaultPacketDestination = objects::PUS_TM_FUNNEL; + + VerificationReporter::DEFAULT_RECEIVER = objects::PUS_SERVICE_1_VERIFICATION; +} + +void ObjectFactory::produce(void* args) { + Factory::setStaticFrameworkObjectIds(); + PusTmFunnel* pusFunnel; + CfdpTmFunnel* cfdpFunnel; + StorageManagerIF* tmStore; + StorageManagerIF* ipcStore; + PersistentTmStores persistentStores{}; + bool enableHkSets = false; +#if OBSW_ENABLE_PERIODIC_HK == 1 + enableHkSets = true; +#endif + auto sdcMan = new DummySdCardManager("/tmp"); + ObjectFactory::produceGenericObjects(nullptr, &pusFunnel, &cfdpFunnel, *sdcMan, &ipcStore, + &tmStore, persistentStores, 120, enableHkSets, false); + + new TmFunnelHandler(objects::LIVE_TM_TASK, *pusFunnel, *cfdpFunnel); + auto* dummyGpioIF = new DummyGpioIF(); + auto* dummySwitcher = new DummyPowerSwitcher(objects::PCDU_HANDLER, 18, 0); + std::vector switcherList; + auto initVal = PowerSwitchIF::SWITCH_OFF; + for (unsigned i = 0; i < 18; i++) { + switcherList.emplace_back(initVal); + } + dummySwitcher->setInitialSwitcherList(switcherList); + +#ifdef PLATFORM_UNIX + // Obsolete dev handler.. + /* + new SerialComIF(objects::UART_COM_IF); +#if OBSW_ADD_PLOC_MPSOC == 1 + std::string mpscoDev = ""; + auto mpsocCookie = new UartCookie(objects::PLOC_MPSOC_HANDLER, mpscoDev, uart::PLOC_MPSOC_BAUD, + mpsoc::MAX_REPLY_SIZE, UartModes::NON_CANONICAL); + mpsocCookie->setNoFixedSizeReply(); + auto plocMpsocHelper = new PlocMpsocSpecialComHelper(objects::PLOC_MPSOC_HELPER); + new PlocMpsocHandler(objects::PLOC_MPSOC_HANDLER, objects::UART_COM_IF, mpsocCookie, + plocMpsocHelper, Gpio(gpioIds::ENABLE_MPSOC_UART, dummyGpioIF), + objects::PLOC_SUPERVISOR_HANDLER); +#endif // OBSW_ADD_PLOC_MPSOC == 1 + */ +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + std::string plocSupvString = "/dev/ploc_supv"; + auto supervisorCookie = + new SerialCookie(objects::PLOC_SUPERVISOR_HANDLER, plocSupvString, uart::PLOC_SUPV_BAUD, + supv::MAX_PACKET_SIZE * 20, UartModes::NON_CANONICAL); + supervisorCookie->setNoFixedSizeReply(); + new PlocSupvUartManager(objects::PLOC_SUPERVISOR_HELPER); + DhbConfig dhbConf(objects::PLOC_SUPERVISOR_HANDLER); + auto* supvHandler = + new FreshSupvHandler(dhbConf, supervisorCookie, Gpio(gpioIds::ENABLE_SUPV_UART, dummyGpioIF), + dummySwitcher, power::PDU1_CH6_PLOC_12V); +#endif /* OBSW_ADD_PLOC_SUPERVISOR == 1 */ +#endif + + dummy::DummyCfg cfg; + cfg.addPlPcduDummy = true; + cfg.addCamSwitcherDummy = true; + dummy::createDummies(cfg, *dummySwitcher, dummyGpioIF, enableHkSets); + + HeaterHandler* heaterHandler = nullptr; + // new ThermalController(objects::THERMAL_CONTROLLER); + ObjectFactory::createGenericHeaterComponents(*dummyGpioIF, *dummySwitcher, heaterHandler); + if (heaterHandler == nullptr) { + sif::error << "HeaterHandler could not be created" << std::endl; + } else { + ObjectFactory::createThermalController(*heaterHandler, true); + } + new TestTask(objects::TEST_TASK); +} diff --git a/bsp_hosted/objectFactory.h b/bsp_hosted/objectFactory.h new file mode 100644 index 0000000..b042f9d --- /dev/null +++ b/bsp_hosted/objectFactory.h @@ -0,0 +1,9 @@ +#ifndef BSP_LINUX_OBJECTFACTORY_H_ +#define BSP_LINUX_OBJECTFACTORY_H_ + +namespace ObjectFactory { +void setStatics(); +void produce(void* args); +}; // namespace ObjectFactory + +#endif /* BSP_LINUX_OBJECTFACTORY_H_ */ diff --git a/bsp_hosted/scheduling.cpp b/bsp_hosted/scheduling.cpp new file mode 100644 index 0000000..c771608 --- /dev/null +++ b/bsp_hosted/scheduling.cpp @@ -0,0 +1,280 @@ +#include "linux/scheduling.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OBSWConfig.h" +#include "mission/scheduling.h" +#include "objectFactory.h" +#include "scheduling.h" + +#ifdef LINUX +ServiceInterfaceStream sif::debug("DEBUG"); +ServiceInterfaceStream sif::info("INFO"); +ServiceInterfaceStream sif::warning("WARNING"); +ServiceInterfaceStream sif::error("ERROR", false, false, true); +#else +ServiceInterfaceStream sif::debug("DEBUG", true); +ServiceInterfaceStream sif::info("INFO", true); +ServiceInterfaceStream sif::warning("WARNING", true); +ServiceInterfaceStream sif::error("ERROR", true, false, true); +#endif + +ObjectManagerIF* objectManager = nullptr; + +void scheduling::initMission() { + sif::info << "Building global objects.." << std::endl; + /* Instantiate global object manager and also create all objects */ + ObjectManager::instance()->setObjectFactoryFunction(ObjectFactory::produce, nullptr); + sif::info << "Initializing all objects.." << std::endl; + ObjectManager::instance()->initialize(); + + /* This function creates and starts all tasks */ + initTasks(); +} + +void scheduling::initTasks() { + TaskFactory* factory = TaskFactory::instance(); + if (factory == nullptr) { + /* Should never happen ! */ + return; + } +#if OBSW_PRINT_MISSED_DEADLINES == 1 + void (*missedDeadlineFunc)(void) = TaskFactory::printMissedDeadline; +#else + void (*missedDeadlineFunc)(void) = nullptr; +#endif + + /* TMTC Distribution */ + PeriodicTaskIF* tmtcDistributor = factory->createPeriodicTask( + "DIST", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + ReturnValue_t result = tmtcDistributor->addComponent(objects::CCSDS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Adding CCSDS distributor failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::PUS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Adding PUS distributor failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::CFDP_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Adding CFDP distributor failed" << std::endl; + } +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + result = tmtcDistributor->addComponent(objects::UDP_TMTC_SERVER); + if (result != returnvalue::OK) { + sif::error << "adding UDP server failed" << std::endl; + } +#endif + result = tmtcDistributor->addComponent(objects::TCP_TMTC_SERVER); + if (result != returnvalue::OK) { + sif::error << "adding TCP server failed" << std::endl; + } + +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + PeriodicTaskIF* udpPollingTask = factory->createPeriodicTask( + "UDP_POLLING", 70, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = udpPollingTask->addComponent(objects::UDP_TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + sif::error << "Add component UDP Polling failed" << std::endl; + } +#endif + PeriodicTaskIF* tcpPollingTask = factory->createPeriodicTask( + "TCP_POLLING", 70, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = tcpPollingTask->addComponent(objects::TCP_TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + sif::error << "Add component UDP Polling failed" << std::endl; + } + + PeriodicTaskIF* liveTmTask = factory->createPeriodicTask( + "LIVE_TM", 55, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, nullptr, &RR_SCHEDULING); + result = liveTmTask->addComponent(objects::LIVE_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("LIVE_TM", objects::LIVE_TM_TASK); + } + + PeriodicTaskIF* pusHighPrio = factory->createPeriodicTask( + "PUS_HIGH_PRIO", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_1_VERIFICATION); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = pusHighPrio->addComponent(objects::EVENT_MANAGER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("EVENT_MGMT", objects::EVENT_MANAGER); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_5_EVENT_REPORTING); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS5", objects::PUS_SERVICE_5_EVENT_REPORTING); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_9_TIME_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS9", objects::PUS_SERVICE_9_TIME_MGMT); + } + + PeriodicTaskIF* pusMedPrio = factory->createPeriodicTask( + "PUS_MED_PRIO", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, missedDeadlineFunc); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_2_DEVICE_ACCESS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS2", objects::PUS_SERVICE_2_DEVICE_ACCESS); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_3_HOUSEKEEPING); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS3", objects::PUS_SERVICE_3_HOUSEKEEPING); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_8_FUNCTION_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS8", objects::PUS_SERVICE_8_FUNCTION_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_15_TM_STORAGE); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS15", objects::PUS_SERVICE_15_TM_STORAGE); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_200_MODE_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS200", objects::PUS_SERVICE_200_MODE_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_20_PARAMETERS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS20", objects::PUS_SERVICE_20_PARAMETERS); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_17_TEST); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS17", objects::PUS_SERVICE_17_TEST); + } + + PeriodicTaskIF* thermalTask = factory->createPeriodicTask( + "THERMAL_CTL_TASK", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + result = thermalTask->addComponent(objects::CORE_CONTROLLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CORE_CTRL", objects::CORE_CONTROLLER); + } + result = thermalTask->addComponent(objects::THERMAL_CONTROLLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("THERMAL_CONTROLLER", objects::THERMAL_CONTROLLER); + } + result = thermalTask->addComponent(objects::HEATER_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("HEATER_HANDLER", objects::HEATER_HANDLER); + } + + FixedTimeslotTaskIF* pstTask = factory->createFixedTimeslotTask( + "DUMMY_PST", 75, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 0.5, missedDeadlineFunc); + result = dummy_pst::pst(pstTask); + if (result != returnvalue::OK) { + sif::error << "Failed to add dummy pst to fixed timeslot task" << std::endl; + } + +#if OBSW_ADD_CFDP_COMPONENTS == 1 + PeriodicTaskIF* cfdpTask = factory->createPeriodicTask( + "CFDP Handler", 45, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.4, missedDeadlineFunc); + result = cfdpTask->addComponent(objects::CFDP_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CFDP Handler", objects::CFDP_HANDLER); + } +#endif + + // If those are added at a later stage.. + /* + PeriodicTaskIF* logTmTask = factory->createPeriodicTask( + "LOG_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = logTmTask->addComponent(objects::LOG_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("LOG_STORE_AND_TM", objects::LOG_STORE_AND_TM_TASK); + } + PeriodicTaskIF* hkTmTask = + factory->createPeriodicTask("HK_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = hkTmTask->addComponent(objects::HK_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("HK_STORE_AND_TM", objects::HK_STORE_AND_TM_TASK); + } + PeriodicTaskIF* cfdpTmTask = factory->createPeriodicTask( + "CFDP_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = cfdpTmTask->addComponent(objects::CFDP_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CFDP_STORE_AND_TM", objects::CFDP_STORE_AND_TM_TASK); + } + */ + +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + PeriodicTaskIF* supvHelperTask = factory->createPeriodicTask( + "PLOC_SUPV_HELPER", 20, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + result = supvHelperTask->addComponent(objects::PLOC_SUPERVISOR_HELPER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PLOC_SUPV_HELPER", objects::PLOC_SUPERVISOR_HELPER); + } +#endif /* OBSW_ADD_PLOC_SUPERVISOR */ + + PeriodicTaskIF* plTask = factory->createPeriodicTask( + "PL_TASK", 25, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + scheduling::addMpsocSupvHandlers(plTask); +#if OBSW_ADD_TEST_CODE == 1 + result = testTask->addComponent(objects::TEST_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("TEST_TASK", objects::TEST_TASK); + } +#endif /* OBSW_ADD_TEST_CODE == 1 */ + + PeriodicTaskIF* dummyTask = factory->createPeriodicTask( + "DUMMY_TASK", 35, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, missedDeadlineFunc); + dummyTask->addComponent(objects::THERMAL_TEMP_INSERTER); + scheduling::scheduleTmpTempSensors(dummyTask, true); + scheduling::scheduleRtdSensors(dummyTask); + dummyTask->addComponent(objects::SUS_0_N_LOC_XFYFZM_PT_XF); + dummyTask->addComponent(objects::SUS_1_N_LOC_XBYFZM_PT_XB); + dummyTask->addComponent(objects::SUS_2_N_LOC_XFYBZB_PT_YB); + dummyTask->addComponent(objects::SUS_3_N_LOC_XFYBZF_PT_YF); + dummyTask->addComponent(objects::SUS_4_N_LOC_XMYFZF_PT_ZF); + dummyTask->addComponent(objects::SUS_5_N_LOC_XFYMZB_PT_ZB); + dummyTask->addComponent(objects::SUS_6_R_LOC_XFYBZM_PT_XF); + dummyTask->addComponent(objects::SUS_7_R_LOC_XBYBZM_PT_XB); + dummyTask->addComponent(objects::SUS_8_R_LOC_XBYBZB_PT_YB); + dummyTask->addComponent(objects::SUS_9_R_LOC_XBYBZB_PT_YF); + dummyTask->addComponent(objects::SUS_10_N_LOC_XMYBZF_PT_ZF); + dummyTask->addComponent(objects::SUS_11_R_LOC_XBYMZB_PT_ZB); + + sif::info << "Starting tasks.." << std::endl; + tmtcDistributor->startTask(); +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + udpPollingTask->startTask(); +#endif + tcpPollingTask->startTask(); + liveTmTask->startTask(); + + pusHighPrio->startTask(); + pusMedPrio->startTask(); + + pstTask->startTask(); + thermalTask->startTask(); + dummyTask->startTask(); + + // If those are added at a later stage.. + // logTmTask->startTask(); + // cfdpTmTask->startTask(); + // hkTmTask->startTask(); + +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + supvHelperTask->startTask(); +#endif +#if OBSW_ADD_PLOC_SUPERVISOR == 1 || OBSW_ADD_PLOC_MPSOC == 1 + plTask->startTask(); +#endif +#if OBSW_ADD_CFDP_COMPONENTS == 1 + cfdpTask->startTask(); +#endif + +#if OBSW_ADD_TEST_CODE == 1 + testTask->startTask(); +#endif /* OBSW_ADD_TEST_CODE == 1 */ + + sif::info << "Tasks started.." << std::endl; +} diff --git a/bsp_hosted/scheduling.h b/bsp_hosted/scheduling.h new file mode 100644 index 0000000..2a2f32a --- /dev/null +++ b/bsp_hosted/scheduling.h @@ -0,0 +1,6 @@ +#pragma once + +namespace scheduling { +void initMission(); +void initTasks(); +}; // namespace scheduling diff --git a/bsp_linux_board/CMakeLists.txt b/bsp_linux_board/CMakeLists.txt new file mode 100644 index 0000000..9e3ec02 --- /dev/null +++ b/bsp_linux_board/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(${OBSW_NAME} PUBLIC InitMission.cpp main.cpp gpioInit.cpp + ObjectFactory.cpp) + +add_subdirectory(boardconfig) +add_subdirectory(boardtest) +add_subdirectory(fsfwconfig) diff --git a/bsp_linux_board/Dockerfile b/bsp_linux_board/Dockerfile new file mode 100644 index 0000000..970b44d --- /dev/null +++ b/bsp_linux_board/Dockerfile @@ -0,0 +1,37 @@ +FROM ubuntu:latest +# FROM alpine:latest + +RUN apt-get update && apt-get install -y curl wget cmake g++ + +# Raspberry Pi rootfs +RUN mkdir -p /usr/rootfs; \ + curl https://eive-cloud.irs.uni-stuttgart.de/index.php/s/kJe3nCnGPRGKFCz/download/rpi-rootfs.tar.gz /usr/rootfs \ + | tar xvz -C /usr/rootfs +# Raspberry Pi toolchain +RUN mkdir -p /opt; \ + cd /opt; \ + wget https://github.com/Pro/raspi-toolchain/releases/latest/download/raspi-toolchain.tar.gz; \ + tar xfz raspi-toolchain.tar.gz --strip-components=1 -C .; \ + rm -rf raspi-toolchain.tar.gz + +# RUN apk add cmake make g++ + +# Required for cmake build +ENV RASPBERRY_VERSION="4" +ENV RASPBIAN_ROOTFS="/usr/rootfs/rootfs" +ENV PATH=$PATH:"/opt/cross-pi-gcc/bin" +ENV CROSS_COMPILE="arm-linux-gnueabihf" + +WORKDIR /usr/src/app +COPY . . + +RUN set -ex; \ + rm -rf build-rpi; \ + mkdir build-rpi; \ + cd build-rpi; \ + cmake -DCMAKE_BUILD_TYPE=Release -DOS_FSFW=linux -DTGT_BSP="arm/raspberrypi" ..; + +ENTRYPOINT ["cmake", "--build", "build-rpi"] +CMD ["-j"] +# CMD ["bash"] + diff --git a/bsp_linux_board/InitMission.cpp b/bsp_linux_board/InitMission.cpp new file mode 100644 index 0000000..21cc5c2 --- /dev/null +++ b/bsp_linux_board/InitMission.cpp @@ -0,0 +1,267 @@ +#include "InitMission.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OBSWConfig.h" +#include "ObjectFactory.h" +#include "objects/systemObjectList.h" +#include "pollingsequence/pollingSequenceFactory.h" + +ServiceInterfaceStream sif::debug("DEBUG"); +ServiceInterfaceStream sif::info("INFO"); +ServiceInterfaceStream sif::warning("WARNING"); +ServiceInterfaceStream sif::error("ERROR"); + +ObjectManagerIF* objectManager = nullptr; + +void initmission::initMission() { + sif::info << "Building global objects.." << std::endl; + /* Instantiate global object manager and also create all objects */ + ObjectManager::instance()->setObjectFactoryFunction(ObjectFactory::produce, nullptr); + sif::info << "Initializing all objects.." << std::endl; + ObjectManager::instance()->initialize(); + + /* This function creates and starts all tasks */ + initTasks(); +} + +void initmission::initTasks() { + TaskFactory* factory = TaskFactory::instance(); + ReturnValue_t result = returnvalue::OK; + if (factory == nullptr) { + /* Should never happen ! */ + return; + } +#if OBSW_PRINT_MISSED_DEADLINES == 1 + void (*missedDeadlineFunc)(void) = TaskFactory::printMissedDeadline; +#else + void (*missedDeadlineFunc)(void) = nullptr; +#endif + + /* TMTC Distribution */ + PeriodicTaskIF* tmTcDistributor = factory->createPeriodicTask( + "DIST", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmTcDistributor->addComponent(objects::CCSDS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmTcDistributor->addComponent(objects::PUS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmTcDistributor->addComponent(objects::TM_FUNNEL); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + + /* UDP bridge */ + PeriodicTaskIF* tmtcBridgeTask = factory->createPeriodicTask( + "TMTC_BRIDGE", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmtcBridgeTask->addComponent(objects::TMTC_BRIDGE); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Bridge failed" << std::endl; + } + PeriodicTaskIF* tmtcPollingTask = factory->createPeriodicTask( + "TMTC_POLLING", 80, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = tmtcPollingTask->addComponent(objects::TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Polling failed" << std::endl; + } + +#if OBSW_ADD_SCEX_DEVICE == 1 + PeriodicTaskIF* scexDevHandler; + PeriodicTaskIF* scexReaderTask; + scheduling::schedulingScex(*factory, scexDevHandler, scexReaderTask); +#endif + + /* PUS Services */ + std::vector pusTasks; + createPusTasks(*factory, missedDeadlineFunc, pusTasks); + + std::vector pstTasks; + createPstTasks(*factory, missedDeadlineFunc, pstTasks); +#if OBSW_ADD_TEST_CODE == 1 + std::vector testTasks; + createTestTasks(*factory, missedDeadlineFunc, pstTasks); +#endif /* OBSW_ADD_TEST_CODE == 1 */ + + auto taskStarter = [](std::vector& taskVector, std::string name) { + for (const auto& task : taskVector) { + if (task != nullptr) { + task->startTask(); + } else { + sif::error << "Task in vector " << name << " is invalid!" << std::endl; + } + } + }; + + sif::info << "Starting tasks.." << std::endl; + tmTcDistributor->startTask(); + tmtcBridgeTask->startTask(); + tmtcPollingTask->startTask(); + + taskStarter(pusTasks, "PUS Tasks"); +#if OBSW_ADD_TEST_CODE == 1 + taskStarter(testTasks, "Test Tasks"); +#endif /* OBSW_ADD_TEST_CODE == 1 */ + taskStarter(pstTasks, "PST Tasks"); + +#if OBSW_ADD_SCEX_DEVICE == 1 + scexDevHandler->startTask(); + scexReaderTask->startTask(); +#endif +#if OBSW_ADD_TEST_PST == 1 + if (startTestPst) { + pstTestTask->startTask(); + } +#endif /* RPI_TEST_ACS_BOARD == 1 */ + sif::info << "Tasks started.." << std::endl; +} + +void initmission::createPusTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; + PeriodicTaskIF* pusVerification = factory.createPeriodicTask( + "PUS_VERIF", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusVerification->addComponent(objects::PUS_SERVICE_1_VERIFICATION); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + taskVec.push_back(pusVerification); + + PeriodicTaskIF* pusEvents = factory.createPeriodicTask( + "PUS_EVENTS", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusEvents->addComponent(objects::PUS_SERVICE_5_EVENT_REPORTING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_EVENTS", objects::PUS_SERVICE_5_EVENT_REPORTING); + } + result = pusEvents->addComponent(objects::EVENT_MANAGER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_MGMT", objects::EVENT_MANAGER); + } + taskVec.push_back(pusEvents); + + PeriodicTaskIF* pusHighPrio = factory.createPeriodicTask( + "PUS_HIGH_PRIO", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_2_DEVICE_ACCESS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS2", objects::PUS_SERVICE_2_DEVICE_ACCESS); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_9_TIME_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS9", objects::PUS_SERVICE_9_TIME_MGMT); + } + taskVec.push_back(pusHighPrio); + + PeriodicTaskIF* pusMedPrio = factory.createPeriodicTask( + "PUS_MED_PRIO", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, missedDeadlineFunc); + result = pusMedPrio->addComponent(objects::PUS_SERVICE_8_FUNCTION_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS8", objects::PUS_SERVICE_8_FUNCTION_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_200_MODE_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS200", objects::PUS_SERVICE_200_MODE_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_20_PARAMETERS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS20", objects::PUS_SERVICE_20_PARAMETERS); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_3_HOUSEKEEPING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS3", objects::PUS_SERVICE_3_HOUSEKEEPING); + } + taskVec.push_back(pusMedPrio); + + PeriodicTaskIF* pusLowPrio = factory.createPeriodicTask( + "PUS_LOW_PRIO", 30, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.6, missedDeadlineFunc); + result = pusLowPrio->addComponent(objects::PUS_SERVICE_17_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS17", objects::PUS_SERVICE_17_TEST); + } + result = pusLowPrio->addComponent(objects::INTERNAL_ERROR_REPORTER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("INT_ERR_RPRT", objects::INTERNAL_ERROR_REPORTER); + } + taskVec.push_back(pusLowPrio); +} + +void initmission::createPstTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; +#if OBSW_ADD_SPI_TEST_CODE == 0 + FixedTimeslotTaskIF* spiPst = factory.createFixedTimeslotTask( + "SPI_PST", 70, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 1.0, missedDeadlineFunc); + result = pst::pstSpi(spiPst); + if (result != returnvalue::OK) { + if (result != FixedTimeslotTaskIF::SLOT_LIST_EMPTY) { + sif::error << "InitMission::createPstTasks: Creating PST failed!" << std::endl; + } + } else { + taskVec.push_back(spiPst); + } +#endif +} + +void initmission::createTestTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; + PeriodicTaskIF* testTask = factory.createPeriodicTask( + "TEST_TASK", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = testTask->addComponent(objects::TEST_TASK); + if (result != returnvalue::OK) { + initmission::printAddObjectError("TEST_TASK", objects::TEST_TASK); + } +#if OBSW_ADD_SPI_TEST_CODE == 1 + result = testTask->addComponent(objects::SPI_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("SPI_TEST", objects::SPI_TEST); + } +#endif /* RPI_ADD_SPI_TEST == 1 */ +#if RPI_ADD_GPIO_TEST == 1 + result = testTask->addComponent(objects::LIBGPIOD_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("GPIOD_TEST", objects::LIBGPIOD_TEST); + } +#endif /* RPI_ADD_GPIO_TEST == 1 */ +#if OBSW_ADD_UART_TEST_CODE == 1 + result = testTask->addComponent(objects::UART_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("UART_TEST", objects::UART_TEST); + } + PeriodicTaskIF* scexReaderTask = factory.createPeriodicTask( + "SCEX_UART_READER", 20, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = scexReaderTask->addComponent(objects::SCEX_UART_READER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("SCEX_UART_READER", objects::SCEX_UART_READER); + } + taskVec.push_back(scexReaderTask); +#endif /* RPI_ADD_GPIO_TEST == 1 */ + taskVec.push_back(testTask); + + bool startTestPst = true; + static_cast(startTestPst); +#if OBSW_ADD_TEST_PST == 1 + FixedTimeslotTaskIF* pstTestTask = factory->createFixedTimeslotTask( + "TEST_PST", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, 2.0, missedDeadlineFunc); + result = pst::pstTest(pstTestTask); + if (result != returnvalue::OK) { + sif::info << "initmission::initTasks: ACS PST empty or invalid" << std::endl; + startTestPst = false; + } +#endif /* RPI_TEST_ACS_BOARD == 1 */ +} diff --git a/bsp_linux_board/InitMission.h b/bsp_linux_board/InitMission.h new file mode 100644 index 0000000..6e38fc9 --- /dev/null +++ b/bsp_linux_board/InitMission.h @@ -0,0 +1,23 @@ +#ifndef BSP_LINUX_INITMISSION_H_ +#define BSP_LINUX_INITMISSION_H_ + +#include + +#include "fsfw/tasks/definitions.h" + +class PeriodicTaskIF; +class TaskFactory; + +namespace initmission { +void initMission(); +void initTasks(); + +void createPstTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +void createTestTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +void createPusTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +}; // namespace initmission + +#endif /* BSP_LINUX_INITMISSION_H_ */ diff --git a/bsp_linux_board/OBSWConfig.h.in b/bsp_linux_board/OBSWConfig.h.in new file mode 100644 index 0000000..04b90ed --- /dev/null +++ b/bsp_linux_board/OBSWConfig.h.in @@ -0,0 +1,129 @@ +/** + * @brief This file can be used to add preprocessor define for conditional + * code inclusion exclusion or various other project constants and + * properties in one place. + */ +#ifndef FSFWCONFIG_OBSWCONFIG_H_ +#define FSFWCONFIG_OBSWCONFIG_H_ + +#include "commonConfig.h" +#include "OBSWVersion.h" + +/*******************************************************************/ +/** All of the following flags should be enabled for mission code */ +/*******************************************************************/ + +#define OBSW_ENABLE_TIMERS 1 +#define OBSW_ADD_STAR_TRACKER 0 +#define OBSW_ADD_PLOC_SUPERVISOR 0 +#define OBSW_ADD_PLOC_MPSOC 0 +#define OBSW_ADD_SUN_SENSORS 0 +#define OBSW_ADD_MGT 0 +#define OBSW_ADD_ACS_BOARD 0 +#define OBSW_ADD_ACS_HANDLERS 0 +#define OBSW_ADD_GPS_0 0 +#define OBSW_ADD_GPS_1 0 +#define OBSW_ADD_RW 0 +#define OBSW_ADD_BPX_BATTERY_HANDLER 0 +#define OBSW_ADD_RTD_DEVICES 0 +#define OBSW_ADD_PL_PCDU 0 +#define OBSW_ADD_TMP_DEVICES 0 +#define OBSW_ADD_SCEX_DEVICE 1 +#define OBSW_ADD_RAD_SENSORS 0 +#define OBSW_ADD_SYRLINKS 0 +#define OBSW_STAR_TRACKER_GROUND_CONFIG 1 + +// This is a really tricky switch.. It initializes the PCDU switches to their default states +// at powerup. I think it would be better +// to leave it off for now. It makes testing a lot more difficult and it might mess with +// something the operators might want to do by giving the software too much intelligence +// at the wrong place. The system component might command all the Switches accordingly anyway +#define OBSW_INITIALIZE_SWITCHES 0 +#define OBSW_ENABLE_PERIODIC_HK 0 + +/*******************************************************************/ +/** All of the following flags should be disabled for mission code */ +/*******************************************************************/ + +// Can be used to switch device to NORMAL mode immediately +#define OBSW_SWITCH_TO_NORMAL_MODE_AFTER_STARTUP 1 +#define OBSW_PRINT_MISSED_DEADLINES 1 + +#define OBSW_SYRLINKS_SIMULATED 1 +#define OBSW_ADD_TEST_CODE 0 +#define OBSW_ADD_TEST_TASK 0 +#define OBSW_ADD_TEST_PST 0 +// If this is enabled, all other SPI code should be disabled +#define OBSW_ADD_SPI_TEST_CODE 0 +// If this is enabled, all other I2C code should be disabled +#define OBSW_ADD_I2C_TEST_CODE 0 +#define OBSW_ADD_UART_TEST_CODE 0 + +#define OBSW_TEST_ACS 0 +#define OBSW_DEBUG_ACS 0 +#define OBSW_TEST_SUS 0 +#define OBSW_DEBUG_SUS 0 +#define OBSW_TEST_RTD 0 +#define OBSW_DEBUG_RTD 0 +#define OBSW_TEST_RAD_SENSOR 0 +#define OBSW_DEBUG_RAD_SENSOR 0 +#define OBSW_TEST_PL_PCDU 0 +#define OBSW_DEBUG_PL_PCDU 0 +#define OBSW_TEST_BPX_BATT 0 +#define OBSW_DEBUG_BPX_BATT 0 +#define OBSW_TEST_IMTQ 0 +#define OBSW_DEBUG_IMTQ 0 +#define OBSW_TEST_RW 0 +#define OBSW_DEBUG_RW 0 + +#define OBSW_TEST_LIBGPIOD 0 +#define OBSW_TEST_PLOC_HANDLER 0 +#define OBSW_TEST_CCSDS_BRIDGE 0 +#define OBSW_TEST_CCSDS_PTME 0 +#define OBSW_TEST_TE7020_HEATER 0 +#define OBSW_TEST_GPIO_OPEN_BY_LABEL 0 +#define OBSW_TEST_GPIO_OPEN_BY_LINE_NAME 0 +#define OBSW_DEBUG_P60DOCK 0 + +#define OBSW_PRINT_CORE_HK 0 +#define OBSW_DEBUG_PDU1 0 +#define OBSW_DEBUG_PDU2 0 +#define OBSW_DEBUG_GPS 0 +#define OBSW_DEBUG_ACU 0 +#define OBSW_DEBUG_SYRLINKS 0 + +#define OBSW_DEBUG_PDEC_HANDLER 0 +#define OBSW_DEBUG_PLOC_SUPERVISOR 0 +#define OBSW_DEBUG_PLOC_MPSOC 0 +#define OBSW_DEBUG_STARTRACKER 0 +#define OBSW_TCP_SERVER_WIRETAPPING 0 + +/*******************************************************************/ +/** CMake Defines */ +/*******************************************************************/ +#cmakedefine EIVE_BUILD_GPSD_GPS_HANDLER + +#define OBSW_ADD_CCSDS_IP_CORES 0 +// Set to 1 if all telemetry should be sent to the PTME IP Core +#define OBSW_TM_TO_PTME 0 +// Set to 1 if telecommands are received via the PDEC IP Core +#define OBSW_TC_FROM_PDEC 0 + +#cmakedefine LIBGPS_VERSION_MAJOR @LIBGPS_VERSION_MAJOR@ +#cmakedefine LIBGPS_VERSION_MINOR @LIBGPS_VERSION_MINOR@ + +#ifdef RASPBERRY_PI +#include "rpiConfig.h" +#elif defined(XIPHOS_Q7S) +#include "q7sConfig.h" +#endif + +#ifdef __cplusplus + +#include "objects/systemObjectList.h" +#include "events/subsystemIdRanges.h" +#include "returnvalues/classIds.h" + +#endif + +#endif /* FSFWCONFIG_OBSWCONFIG_H_ */ diff --git a/bsp_linux_board/ObjectFactory.cpp b/bsp_linux_board/ObjectFactory.cpp new file mode 100644 index 0000000..95ea87b --- /dev/null +++ b/bsp_linux_board/ObjectFactory.cpp @@ -0,0 +1,248 @@ +#include "ObjectFactory.h" + +#include +#include + +#include "OBSWConfig.h" +#include "devConf.h" +#include "devices/addresses.h" +#include "devices/gpioIds.h" +#include "fsfw/datapoollocal/LocalDataPoolManager.h" +#include "fsfw/power/DummyPowerSwitcher.h" +#include "fsfw/tasks/TaskFactory.h" +#include "fsfw/tmtcpacket/pus/tm.h" +#include "fsfw/tmtcservices/CommandingServiceBase.h" +#include "fsfw/tmtcservices/PusServiceBase.h" +#include "gpioInit.h" +#include "linux/ObjectFactory.h" +#include "linux/boardtest/LibgpiodTest.h" +#include "linux/boardtest/SpiTestClass.h" +#include "linux/boardtest/UartTestClass.h" +#include "mission/core/GenericFactory.h" +#include "mission/devices/GPSHyperionHandler.h" +#include "mission/devices/GyroADIS1650XHandler.h" +#include "mission/tmtc/TmFunnel.h" +#include "objects/systemObjectList.h" +#include "tmtc/pusIds.h" + +/* UDP server includes */ +#if OBSW_USE_TMTC_TCP_BRIDGE == 1 +#include +#include +#else +#include "fsfw/osal/common/UdpTcPollingTask.h" +#include "fsfw/osal/common/UdpTmTcBridge.h" +#endif + +#include +#include + +#include "fsfw_hal/common/gpio/GpioCookie.h" +#include "fsfw_hal/devicehandlers/GyroL3GD20Handler.h" +#include "fsfw_hal/devicehandlers/MgmLIS3MDLHandler.h" +#include "fsfw_hal/devicehandlers/MgmRM3100Handler.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" +#include "fsfw_hal/linux/rpi/GpioRPi.h" +#include "fsfw_hal/linux/spi/SpiComIF.h" +#include "fsfw_hal/linux/spi/SpiCookie.h" + +void Factory::setStaticFrameworkObjectIds() { + PusServiceBase::PUS_DISTRIBUTOR = objects::PUS_PACKET_DISTRIBUTOR; + PusServiceBase::PACKET_DESTINATION = objects::TM_FUNNEL; + + CommandingServiceBase::defaultPacketSource = objects::PUS_PACKET_DISTRIBUTOR; + CommandingServiceBase::defaultPacketDestination = objects::TM_FUNNEL; + + TmFunnel::downlinkDestination = objects::TMTC_BRIDGE; + // No storage object for now. + TmFunnel::storageDestination = objects::NO_OBJECT; +} + +void ObjectFactory::produce(void* args) { + Factory::setStaticFrameworkObjectIds(); + ObjectFactory::produceGenericObjects(); + + GpioIF* gpioIF = new LinuxLibgpioIF(objects::GPIO_IF); + GpioCookie* gpioCookie = nullptr; + static_cast(gpioCookie); + + SpiComIF* spiComIF = new SpiComIF(objects::SPI_MAIN_COM_IF, spi::DEV, gpioIF); + static_cast(spiComIF); + auto pwrSwitcher = new DummyPowerSwitcher(objects::PCDU_HANDLER, 18, 0); + static_cast(pwrSwitcher); + +#if OBSW_ADD_ACS_BOARD == 1 && defined(RASPBERRY_PI) + createRpiAcsBoard(gpioIF, spiDev); +#endif + +#if OBSW_ADD_SUN_SENSORS == 1 || OBSW_ADD_RTD_DEVICES == 1 +#ifdef RASPBERRY_PI + rpi::gpio::initSpiCsDecoder(gpioIF); +#endif +#endif + +#if OBSW_ADD_SCEX_DEVICE == 1 + auto* sdcMan = new DummySdCardManager("/tmp"); + createScexComponents(uart::DEV, pwrSwitcher, *sdcMan, true, std::nullopt); +#endif + +#if OBSW_ADD_SUN_SENSORS == 1 + createSunSensorComponents(gpioIF, spiComIF, pwrSwitcher, spi::DEV); +#endif + +#if OBSW_ADD_RTD_DEVICES == 1 + createRtdComponents(spi::DEV, gpioIF, pwrSwitcher); +#endif + +#if OBSW_ADD_TEST_CODE == 1 + createTestTasks(); +#endif /* OBSW_ADD_TEST_CODE == 1 */ +} + +void ObjectFactory::createRpiAcsBoard(GpioIF* gpioIF, std::string spiDev) { + GpioCookie* gpioCookie = new GpioCookie(); + // TODO: Missing pin for Gyro 2 + gpio::createRpiGpioConfig(gpioCookie, gpioIds::MGM_0_LIS3_CS, gpio::MGM_0_BCM_PIN, "MGM_0_LIS3", + gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::MGM_1_RM3100_CS, gpio::MGM_1_BCM_PIN, + "MGM_1_RM3100", gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::MGM_2_LIS3_CS, gpio::MGM_2_BCM_PIN, "MGM_2_LIS3", + gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::MGM_3_RM3100_CS, gpio::MGM_3_BCM_PIN, + "MGM_3_RM3100", gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::GYRO_0_ADIS_CS, gpio::GYRO_0_BCM_PIN, + "GYRO_0_ADIS", gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::GYRO_1_L3G_CS, gpio::GYRO_1_BCM_PIN, "GYRO_1_L3G", + gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::GYRO_2_ADIS_CS, gpio::GYRO_2_BCM_PIN, + "GYRO_2_ADIS", gpio::Direction::OUT, gpio::Levels::HIGH); + gpio::createRpiGpioConfig(gpioCookie, gpioIds::GYRO_3_L3G_CS, gpio::GYRO_3_BCM_PIN, "GYRO_3_L3G", + gpio::Direction::OUT, gpio::Levels::HIGH); + gpioIF->addGpios(gpioCookie); + SpiCookie* spiCookie = + new SpiCookie(addresses::MGM_0_LIS3, gpioIds::MGM_0_LIS3_CS, MGMLIS3MDL::MAX_BUFFER_SIZE, + spi::DEFAULT_LIS3_MODE, spi::DEFAULT_LIS3_SPEED); + auto mgmLis3Handler = + new MgmLIS3MDLHandler(objects::MGM_0_LIS3_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + mgmLis3Handler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + mgmLis3Handler->setToGoToNormalMode(true); +#endif + + spiCookie = + new SpiCookie(addresses::MGM_1_RM3100, gpioIds::MGM_1_RM3100_CS, RM3100::MAX_BUFFER_SIZE, + spi::DEFAULT_RM3100_MODE, spi::DEFAULT_RM3100_SPEED); + auto mgmRm3100Handler = + new MgmRM3100Handler(objects::MGM_1_RM3100_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + mgmRm3100Handler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + mgmRm3100Handler->setToGoToNormalMode(true); +#endif + + spiCookie = + new SpiCookie(addresses::MGM_2_LIS3, gpioIds::MGM_2_LIS3_CS, MGMLIS3MDL::MAX_BUFFER_SIZE, + spi::DEFAULT_LIS3_MODE, spi::DEFAULT_LIS3_SPEED); + mgmLis3Handler = + new MgmLIS3MDLHandler(objects::MGM_2_LIS3_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + mgmLis3Handler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + mgmLis3Handler->setToGoToNormalMode(true); +#endif + + spiCookie = + new SpiCookie(addresses::MGM_3_RM3100, gpioIds::MGM_3_RM3100_CS, RM3100::MAX_BUFFER_SIZE, + spi::DEFAULT_RM3100_MODE, spi::DEFAULT_RM3100_SPEED); + mgmRm3100Handler = + new MgmRM3100Handler(objects::MGM_3_RM3100_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + mgmRm3100Handler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + mgmRm3100Handler->setToGoToNormalMode(true); +#endif + + spiCookie = + new SpiCookie(addresses::GYRO_0_ADIS, gpioIds::GYRO_0_ADIS_CS, ADIS1650X::MAXIMUM_REPLY_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + auto adisHandler = + new GyroADIS1650XHandler(objects::GYRO_0_ADIS_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, + ADIS1650X::Type::ADIS16505); + adisHandler->setStartUpImmediately(); + spiCookie = new SpiCookie(addresses::GYRO_1_L3G, gpioIds::GYRO_1_L3G_CS, L3GD20H::MAX_BUFFER_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + auto gyroL3gHandler = + new GyroHandlerL3GD20H(objects::GYRO_1_L3G_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + gyroL3gHandler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + gyroL3gHandler->setToGoToNormalMode(true); +#endif + + spiCookie = + new SpiCookie(addresses::GYRO_2_ADIS, gpioIds::GYRO_2_ADIS_CS, ADIS1650X::MAXIMUM_REPLY_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + adisHandler = new GyroADIS1650XHandler(objects::GYRO_2_ADIS_HANDLER, objects::SPI_MAIN_COM_IF, + spiCookie, ADIS1650X::Type::ADIS16505); + adisHandler->setStartUpImmediately(); + + spiCookie = new SpiCookie(addresses::GYRO_3_L3G, gpioIds::GYRO_3_L3G_CS, L3GD20H::MAX_BUFFER_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + gyroL3gHandler = + new GyroHandlerL3GD20H(objects::GYRO_3_L3G_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, 0); + gyroL3gHandler->setStartUpImmediately(); +#if OBSW_TEST_ACS == 1 + gyroL3gHandler->setToGoToNormalMode(true); +#endif +} + +void ObjectFactory::createTestTasks() { + new TestTask(objects::TEST_TASK); + +#if OBSW_ADD_SPI_TEST_CODE == 1 + new SpiTestClass(objects::SPI_TEST, gpioIF); +#endif + +#if OBSW_ADD_UART_TEST_CODE == 1 + new UartTestClass(objects::UART_TEST); +#else + newSerialComIF(objects::UART_COM_IF); +#endif + +#if RPI_LOOPBACK_TEST_GPIO == 1 + GpioCookie* gpioCookieLoopback = new GpioCookie(); + /* Loopback pins. Adapt according to setup */ + gpioId_t gpioIdSender = gpioIds::TEST_ID_0; + int bcmPinSender = 26; + gpioId_t gpioIdReader = gpioIds::TEST_ID_1; + int bcmPinReader = 16; + gpio::createRpiGpioConfig(gpioCookieLoopback, gpioIdSender, bcmPinSender, "GPIO_LB_SENDER", + gpio::Direction::OUT, 0); + gpio::createRpiGpioConfig(gpioCookieLoopback, gpioIdReader, bcmPinReader, "GPIO_LB_READER", + gpio::Direction::IN, 0); + new LibgpiodTest(objects::LIBGPIOD_TEST, objects::GPIO_IF, gpioCookieLoopback); +#endif /* RPI_LOOPBACK_TEST_GPIO == 1 */ + +#if RPI_TEST_ADIS16507 == 1 + if (gpioCookie == nullptr) { + gpioCookie = new GpioCookie(); + } + gpio::createRpiGpioConfig(gpioCookie, gpioIds::GYRO_0_ADIS_CS, gpio::GYRO_0_BCM_PIN, + "GYRO_0_ADIS", gpio::Direction::OUT, 1); + gpioIF->addGpios(gpioCookie); + + spiDev = "/dev/spidev0.1"; + spiCookie = new SpiCookie(addresses::GYRO_0_ADIS, gpioIds::GYRO_0_ADIS_CS, spiDev, + ADIS16507::MAXIMUM_REPLY_SIZE, spi::DEFAULT_ADIS16507_MODE, + spi::DEFAULT_ADIS16507_SPEED, nullptr, nullptr); + auto adisGyroHandler = + new GyroADIS16507Handler(objects::GYRO_0_ADIS_HANDLER, objects::SPI_COM_IF, spiCookie); + adisGyroHandler->setStartUpImmediately(); +#endif /* RPI_TEST_ADIS16507 == 1 */ + +#if RPI_TEST_GPS_HANDLER == 1 + UartCookie* uartCookie = + new UartCookie(objects::GPS0_HANDLER, "/dev/serial0", UartModes::CANONICAL, 9600, 1024); + uartCookie->setToFlushInput(true); + uartCookie->setReadCycles(6); + GPSHyperionHandler* gpsHandler = + new GPSHyperionHandler(objects::GPS0_HANDLER, objects::UART_COM_IF, uartCookie, false); + gpsHandler->setStartUpImmediately(); +#endif +} diff --git a/bsp_linux_board/ObjectFactory.h b/bsp_linux_board/ObjectFactory.h new file mode 100644 index 0000000..7365fb8 --- /dev/null +++ b/bsp_linux_board/ObjectFactory.h @@ -0,0 +1,16 @@ +#ifndef BSP_LINUX_OBJECTFACTORY_H_ +#define BSP_LINUX_OBJECTFACTORY_H_ + +#include + +class GpioIF; + +namespace ObjectFactory { +void setStatics(); +void produce(void* args); + +void createRpiAcsBoard(GpioIF* gpioIF, std::string spiDev); +void createTestTasks(); +}; // namespace ObjectFactory + +#endif /* BSP_LINUX_OBJECTFACTORY_H_ */ diff --git a/bsp_linux_board/boardconfig/CMakeLists.txt b/bsp_linux_board/boardconfig/CMakeLists.txt new file mode 100644 index 0000000..f08670d --- /dev/null +++ b/bsp_linux_board/boardconfig/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources(${OBSW_NAME} PRIVATE print.c) + +target_include_directories(${OBSW_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/bsp_linux_board/boardconfig/etl_profile.h b/bsp_linux_board/boardconfig/etl_profile.h new file mode 100644 index 0000000..54aca34 --- /dev/null +++ b/bsp_linux_board/boardconfig/etl_profile.h @@ -0,0 +1,38 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2019 jwellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ +#ifndef __ETL_PROFILE_H__ +#define __ETL_PROFILE_H__ + +#define ETL_CHECK_PUSH_POP + +#define ETL_CPP11_SUPPORTED 1 +#define ETL_NO_NULLPTR_SUPPORT 0 + +#endif diff --git a/bsp_linux_board/boardconfig/gcov.h b/bsp_linux_board/boardconfig/gcov.h new file mode 100644 index 0000000..80acdd8 --- /dev/null +++ b/bsp_linux_board/boardconfig/gcov.h @@ -0,0 +1,15 @@ +#ifndef LINUX_GCOV_H_ +#define LINUX_GCOV_H_ +#include + +#ifdef GCOV +extern "C" void __gcov_flush(); +#else +void __gcov_flush() { + sif::info << "GCC GCOV: Please supply GCOV=1 in Makefile if " + "coverage information is desired.\n" + << std::flush; +} +#endif + +#endif /* LINUX_GCOV_H_ */ diff --git a/bsp_linux_board/boardconfig/print.c b/bsp_linux_board/boardconfig/print.c new file mode 100644 index 0000000..c2b2e15 --- /dev/null +++ b/bsp_linux_board/boardconfig/print.c @@ -0,0 +1,10 @@ +#include +#include + +void printChar(const char* character, bool errStream) { + if (errStream) { + putc(*character, stderr); + return; + } + putc(*character, stdout); +} diff --git a/bsp_linux_board/boardconfig/print.h b/bsp_linux_board/boardconfig/print.h new file mode 100644 index 0000000..8e7e2e5 --- /dev/null +++ b/bsp_linux_board/boardconfig/print.h @@ -0,0 +1,8 @@ +#ifndef HOSTED_BOARDCONFIG_PRINT_H_ +#define HOSTED_BOARDCONFIG_PRINT_H_ + +#include + +void printChar(const char* character, bool errStream); + +#endif /* HOSTED_BOARDCONFIG_PRINT_H_ */ diff --git a/bsp_linux_board/boardconfig/rpiConfig.h.in b/bsp_linux_board/boardconfig/rpiConfig.h.in new file mode 100644 index 0000000..b58a103 --- /dev/null +++ b/bsp_linux_board/boardconfig/rpiConfig.h.in @@ -0,0 +1,20 @@ +#ifndef BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ +#define BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ + +#include + +#define RPI_ADD_GPIO_TEST 0 +#define RPI_LOOPBACK_TEST_GPIO 0 + +#define RPI_TEST_ADIS16507 0 +#define RPI_TEST_GPS_HANDLER 0 + +// Only one of those 2 should be enabled! +#define RPI_ADD_SPI_TEST 0 +#if RPI_ADD_SPI_TEST == 0 +#define RPI_TEST_ACS_BOARD 0 +#endif + +#define RPI_ADD_UART_TEST 0 + +#endif /* BSP_RPI_BOARDCONFIG_RPI_CONFIG_H_ */ diff --git a/bsp_linux_board/boardtest/CMakeLists.txt b/bsp_linux_board/boardtest/CMakeLists.txt new file mode 100644 index 0000000..431972e --- /dev/null +++ b/bsp_linux_board/boardtest/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PRIVATE) diff --git a/bsp_linux_board/definitions.h b/bsp_linux_board/definitions.h new file mode 100644 index 0000000..83aeb84 --- /dev/null +++ b/bsp_linux_board/definitions.h @@ -0,0 +1,43 @@ +#ifndef BSP_LINUX_BOARD_DEFINITIONS_H_ +#define BSP_LINUX_BOARD_DEFINITIONS_H_ + +#include + +#include "OBSWConfig.h" + +#ifdef RASPBERRY_PI + +namespace spi { + +static constexpr char DEV[] = "/dev/spidev0.1"; + +} + +namespace uart { + +static constexpr char DEV[] = "/dev/serial0"; + +} + +/* Adapt these values accordingly */ +namespace gpio { +static constexpr uint8_t MGM_0_BCM_PIN = 17; +static constexpr uint8_t MGM_1_BCM_PIN = 27; +static constexpr uint8_t MGM_2_BCM_PIN = 22; +static constexpr uint8_t MGM_3_BCM_PIN = 23; +static constexpr uint8_t GYRO_0_BCM_PIN = 5; +static constexpr uint8_t GYRO_1_BCM_PIN = 6; +static constexpr uint8_t GYRO_2_BCM_PIN = 13; +static constexpr uint8_t GYRO_3_BCM_PIN = 19; + +static constexpr uint8_t SPI_MUX_0_BCM = 17; +static constexpr uint8_t SPI_MUX_1_BCM = 27; +static constexpr uint8_t SPI_MUX_2_BCM = 22; +static constexpr uint8_t SPI_MUX_3_BCM = 23; +static constexpr uint8_t SPI_MUX_4_BCM = 5; +static constexpr uint8_t SPI_MUX_5_BCM = 6; +} // namespace gpio + +#endif + +#endif /* BSP_LINUX_BOARD_DEFINITIONS_H_ */ diff --git a/bsp_linux_board/gpioInit.cpp b/bsp_linux_board/gpioInit.cpp new file mode 100644 index 0000000..b922961 --- /dev/null +++ b/bsp_linux_board/gpioInit.cpp @@ -0,0 +1,56 @@ +#include "gpioInit.h" + +#include +#include +#include +#include + +#include "definitions.h" +#include "fsfw_hal/linux/rpi/GpioRPi.h" + +#ifdef RASPBERRY_PI + +struct MuxInfo { + MuxInfo(gpioId_t gpioId, int bcmNum, std::string consumer) + : gpioId(gpioId), bcmNum(bcmNum), consumer(consumer) {} + gpioId_t gpioId; + int bcmNum; + std::string consumer; +}; + +void rpi::gpio::initSpiCsDecoder(GpioIF* gpioComIF) { + using namespace ::gpio; + ReturnValue_t result; + + if (gpioComIF == nullptr) { + sif::debug << "initSpiCsDecoder: Invalid gpioComIF" << std::endl; + return; + } + + std::array<::MuxInfo, 6> muxInfo{ + MuxInfo(gpioIds::SPI_MUX_BIT_0, SPI_MUX_0_BCM, "SPI_MUX_0"), + MuxInfo(gpioIds::SPI_MUX_BIT_1, SPI_MUX_1_BCM, "SPI_MUX_1"), + MuxInfo(gpioIds::SPI_MUX_BIT_2, SPI_MUX_2_BCM, "SPI_MUX_2"), + MuxInfo(gpioIds::SPI_MUX_BIT_3, SPI_MUX_3_BCM, "SPI_MUX_3"), + MuxInfo(gpioIds::SPI_MUX_BIT_4, SPI_MUX_4_BCM, "SPI_MUX_4"), + MuxInfo(gpioIds::SPI_MUX_BIT_5, SPI_MUX_5_BCM, "SPI_MUX_5"), + }; + GpioCookie* spiMuxGpios = new GpioCookie; + + for (const auto& info : muxInfo) { + result = createRpiGpioConfig(spiMuxGpios, info.gpioId, info.bcmNum, info.consumer, + Direction::OUT, Levels::LOW); + if (result != returnvalue::OK) { + sif::error << "Creating Raspberry Pi SPI Mux GPIO failed with code " << result << std::endl; + return; + } + } + + result = gpioComIF->addGpios(spiMuxGpios); + if (result != returnvalue::OK) { + sif::error << "initSpiCsDecoder: Failed to add mux bit gpios to gpioComIF" << std::endl; + return; + } +} + +#endif diff --git a/bsp_linux_board/gpioInit.h b/bsp_linux_board/gpioInit.h new file mode 100644 index 0000000..0ca6641 --- /dev/null +++ b/bsp_linux_board/gpioInit.h @@ -0,0 +1,20 @@ +#pragma once + +#include "OBSWConfig.h" + +class GpioIF; + +#ifdef RASPBERRY_PI +namespace rpi { +namespace gpio { + +/** + * @brief This function initializes the GPIOs used to control the SN74LVC138APWR decoders on + * the TCS Board and the interface board. + */ +void initSpiCsDecoder(GpioIF* gpioComIF); + +} // namespace gpio +} // namespace rpi + +#endif diff --git a/bsp_linux_board/main.cpp b/bsp_linux_board/main.cpp new file mode 100644 index 0000000..98c4d79 --- /dev/null +++ b/bsp_linux_board/main.cpp @@ -0,0 +1,34 @@ +#include + +#include "InitMission.h" +#include "OBSWConfig.h" +#include "OBSWVersion.h" +#include "fsfw/tasks/TaskFactory.h" +#include "fsfw/version.h" + +#ifdef RASPBERRY_PI +static const char* const BOARD_NAME = "Raspberry Pi"; +#elif defined(BEAGLEBONEBLACK) +static const char* const BOARD_NAME = "Beaglebone Black"; +#else +static const char* const BOARD_NAME = "Unknown Board"; +#endif + +/** + * @brief This is the main program and entry point for the Raspberry Pi. + * @return + */ +int main(void) { + std::cout << "-- EIVE OBSW --" << std::endl; + std::cout << "-- Compiled for Linux board " << BOARD_NAME << " --" << std::endl; + std::cout << "-- OBSW " << SW_NAME << " v" << SW_VERSION << "." << SW_SUBVERSION << "." + << SW_REVISION << ", FSFW v" << fsfw::FSFW_VERSION << " --" << std::endl; + std::cout << "-- " << __DATE__ << " " << __TIME__ << " --" << std::endl; + + initmission::initMission(); + + for (;;) { + /* Suspend main thread by sleeping it. */ + TaskFactory::delayTask(5000); + } +} diff --git a/bsp_q7s/CMakeLists.txt b/bsp_q7s/CMakeLists.txt new file mode 100644 index 0000000..0a1a943 --- /dev/null +++ b/bsp_q7s/CMakeLists.txt @@ -0,0 +1,28 @@ +# simple mode +add_executable(${SIMPLE_OBSW_NAME} EXCLUDE_FROM_ALL) +target_compile_definitions(${SIMPLE_OBSW_NAME} PRIVATE "Q7S_SIMPLE_MODE") +target_sources(${SIMPLE_OBSW_NAME} PUBLIC main.cpp) +# I think this is unintentional? (produces linker errors for stuff in /linux) +target_link_libraries(${SIMPLE_OBSW_NAME} PUBLIC ${LIB_FSFW_NAME}) +target_compile_definitions(${SIMPLE_OBSW_NAME} PRIVATE "Q7S_SIMPLE_MODE") +add_subdirectory(simple) + +target_sources(${OBSW_NAME} PUBLIC main.cpp obsw.cpp scheduling.cpp + objectFactory.cpp) + +add_subdirectory(boardtest) + +add_subdirectory(boardconfig) +add_subdirectory(core) + +if(EIVE_Q7S_EM) + add_subdirectory(em) +else() + target_sources(${OBSW_NAME} PUBLIC fmObjectFactory.cpp) +endif() + +add_subdirectory(memory) +add_subdirectory(callbacks) +add_subdirectory(xadc) +add_subdirectory(fs) +add_subdirectory(acs) diff --git a/bsp_q7s/OBSWConfig.h.in b/bsp_q7s/OBSWConfig.h.in new file mode 100644 index 0000000..2555d7c --- /dev/null +++ b/bsp_q7s/OBSWConfig.h.in @@ -0,0 +1,147 @@ +/** + * @brief This file can be used to add preprocessor define for conditional + * code inclusion exclusion or various other project constants and + * properties in one place. + */ +#ifndef FSFWCONFIG_OBSWCONFIG_H_ +#define FSFWCONFIG_OBSWCONFIG_H_ + +#include "commonConfig.h" +#include "q7sConfig.h" + +/*******************************************************************/ +/** All of the following flags should be enabled for mission code */ +/*******************************************************************/ + +// This enables a lot of periodically generated telemetry, so it can make sense to +// disable this for debugging purposes. +#define OBSW_ENABLE_PERIODIC_HK @OBSW_ENABLE_PERIODIC_HK@ + +// This switch will cause the SW to command the EIVE system object to safe mode. This will +// trigger a lot of events, so it can make sense to disable this for debugging purposes. +#define OBSW_COMMAND_SAFE_MODE_AT_STARTUP 1 + +#define OBSW_ADD_GOMSPACE_PCDU @OBSW_ADD_GOMSPACE_PCDU@ +// This define is necessary because the EM setup has the P60 dock module, but no ACU on the P60 +// module because it broke. +#define OBSW_ADD_GOMSPACE_ACU @OBSW_ADD_GOMSPACE_ACU@ +#define OBSW_ADD_MGT @OBSW_ADD_MGT@ +#define OBSW_ADD_BPX_BATTERY_HANDLER @OBSW_ADD_BPX_BATTERY_HANDLER@ +#define OBSW_ADD_STAR_TRACKER @OBSW_ADD_STAR_TRACKER@ +#define OBSW_ADD_PLOC_SUPERVISOR @OBSW_ADD_PLOC_SUPERVISOR@ +#define OBSW_ADD_PLOC_MPSOC @OBSW_ADD_PLOC_MPSOC@ +#define OBSW_ADD_SUN_SENSORS @OBSW_ADD_SUN_SENSORS@ +#define OBSW_ADD_SUS_BOARD_ASS @OBSW_ADD_SUS_BOARD_ASS@ +#define OBSW_ADD_ACS_BOARD @OBSW_ADD_ACS_BOARD@ +#define OBSW_ADD_ACS_CTRL 1 +#define OBSW_ADD_TCS_CTRL 1 +#define OBSW_ADD_GPS_CTRL @OBSW_ADD_GPS_CTRL@ +#define OBSW_ADD_RW @OBSW_ADD_RW@ +#define OBSW_ADD_RTD_DEVICES @OBSW_ADD_RTD_DEVICES@ +#define OBSW_ADD_SA_DEPL @OBSW_ADD_SA_DEPL@ +#define OBSW_ADD_SCEX_DEVICE @OBSW_ADD_SCEX_DEVICE@ +#define OBSW_ADD_HEATERS @OBSW_ADD_HEATERS@ +#define OBSW_ADD_TMP_DEVICES @OBSW_ADD_TMP_DEVICES@ +#define OBSW_ADD_RAD_SENSORS @OBSW_ADD_RAD_SENSORS@ +#define OBSW_ADD_PL_PCDU @OBSW_ADD_PL_PCDU@ +#define OBSW_ADD_SYRLINKS @OBSW_ADD_SYRLINKS@ +#define OBSW_ADD_CCSDS_IP_CORES @OBSW_ADD_CCSDS_IP_CORES@ +// Only relevant for EM for TCS tests. +#define OBSW_ADD_THERMAL_TEMP_INSERTER @OBSW_ADD_THERMAL_TEMP_INSERTER@ + +// Set to 1 if all telemetry should be sent to the PTME IP Core +#define OBSW_TM_TO_PTME @OBSW_TM_TO_PTME@ +// Set to 1 if telecommands are received via the PDEC IP Core +#define OBSW_TC_FROM_PDEC @OBSW_TC_FROM_PDEC@ + +// Configuration parameter which causes the core controller to try to keep at least one SD card +// working +#define OBSW_SD_CARD_MUST_BE_ON 1 +#define OBSW_ENABLE_TIMERS 1 + +/*******************************************************************/ +/** All of the following flags should be disabled for mission code */ +/*******************************************************************/ + +// Use TCP instead of UDP for the TMTC bridge. This allows using the TMTC client locally +// because UDP packets are not allowed in the VPN +// This will cause the OBSW to initialize the TMTC bridge responsible for exchanging data with the +// CCSDS IP Cores. +#define OBSW_ADD_TMTC_TCP_SERVER @OBSW_ADD_TMTC_TCP_SERVER@ +#define OBSW_ADD_TMTC_UDP_SERVER @OBSW_ADD_TMTC_UDP_SERVER@ + +// Can be used to switch device to NORMAL mode immediately +#define OBSW_SWITCH_TO_NORMAL_MODE_AFTER_STARTUP 0 +#define OBSW_PRINT_MISSED_DEADLINES 0 + +#define OBSW_MPSOC_JTAG_BOOT 0 +#define OBSW_STAR_TRACKER_GROUND_CONFIG @OBSW_STAR_TRACKER_GROUND_CONFIG@ +#define OBSW_SYRLINKS_SIMULATED @OBSW_SYRLINKS_SIMULATED@ +#define OBSW_ADD_TEST_CODE 0 +#define OBSW_ADD_TEST_TASK 0 +#define OBSW_ADD_TEST_PST 0 +// If this is enabled, all other SPI code should be disabled +#define OBSW_ADD_SPI_TEST_CODE 0 +// If this is enabled, all other I2C code should be disabled +#define OBSW_ADD_I2C_TEST_CODE 0 +#define OBSW_ADD_UART_TEST_CODE 0 + +#define OBSW_TEST_ACS 0 +#define OBSW_DEBUG_ACS 0 +#define OBSW_TEST_SUS 0 +#define OBSW_DEBUG_SUS 0 +#define OBSW_TEST_RTD 0 +#define OBSW_DEBUG_RTD 0 +#define OBSW_TEST_RAD_SENSOR 0 +#define OBSW_DEBUG_RAD_SENSOR 0 +#define OBSW_TEST_PL_PCDU 0 +#define OBSW_DEBUG_PL_PCDU 0 +#define OBSW_TEST_BPX_BATT 0 +#define OBSW_DEBUG_BPX_BATT 0 +#define OBSW_TEST_IMTQ 0 +#define OBSW_DEBUG_IMTQ 0 +#define OBSW_TEST_RW 0 +#define OBSW_DEBUG_RW 0 + +#define OBSW_TEST_LIBGPIOD 0 +#define OBSW_TEST_PLOC_HANDLER 0 +#define OBSW_TEST_CCSDS_BRIDGE 0 +#define OBSW_TEST_CCSDS_PTME 0 +#define OBSW_TEST_TE7020_HEATER 0 +#define OBSW_TEST_GPIO_OPEN_BY_LABEL 0 +#define OBSW_TEST_GPIO_OPEN_BY_LINE_NAME 0 +#define OBSW_DEBUG_P60DOCK 0 + +#define OBSW_PRINT_CORE_HK 0 +#define OBSW_DEBUG_PDU1 0 +#define OBSW_DEBUG_PDU2 0 +#define OBSW_DEBUG_TMP1075 0 +#define OBSW_DEBUG_GPS 0 +#define OBSW_DEBUG_ACU 0 +#define OBSW_DEBUG_SYRLINKS 0 + +#define OBSW_DEBUG_PDEC_HANDLER 0 +#define OBSW_DEBUG_PLOC_SUPERVISOR 0 +#define OBSW_DEBUG_PLOC_MPSOC 0 +#define OBSW_DEBUG_STARTRACKER 0 + +#define OBSW_TCP_SERVER_WIRETAPPING 0 + +/*******************************************************************/ +/** CMake Defines */ +/*******************************************************************/ + +#cmakedefine EIVE_BUILD_GPSD_GPS_HANDLER + +#cmakedefine LIBGPS_VERSION_MAJOR @LIBGPS_VERSION_MAJOR@ +#cmakedefine LIBGPS_VERSION_MINOR @LIBGPS_VERSION_MINOR@ + +#ifdef __cplusplus + +#include "objects/systemObjectList.h" +#include "events/subsystemIdRanges.h" +#include "returnvalues/classIds.h" + +#endif + +#endif /* FSFWCONFIG_OBSWCONFIG_H_ */ diff --git a/bsp_q7s/acs/CMakeLists.txt b/bsp_q7s/acs/CMakeLists.txt new file mode 100644 index 0000000..87bf46f --- /dev/null +++ b/bsp_q7s/acs/CMakeLists.txt @@ -0,0 +1 @@ +# target_sources(${OBSW_NAME} PUBLIC ) diff --git a/bsp_q7s/acs/StrConfigPathGetter.h b/bsp_q7s/acs/StrConfigPathGetter.h new file mode 100644 index 0000000..58d6964 --- /dev/null +++ b/bsp_q7s/acs/StrConfigPathGetter.h @@ -0,0 +1,23 @@ +#include + +#include "bsp_q7s/fs/SdCardManager.h" +#include "mission/acs/str/strHelpers.h" + +class StrConfigPathGetter : public startracker::SdCardConfigPathGetter { + public: + StrConfigPathGetter(SdCardManager& sdcMan) : sdcMan(sdcMan) {} + + std::optional getCfgPath() override { + if (!sdcMan.isSdCardUsable(std::nullopt)) { + return std::nullopt; + } + if (sdcMan.getActiveSdCard() == sd::SdCard::SLOT_1) { + return std::string("/mnt/sd1/startracker/flight-config.json"); + } else { + return std::string("/mnt/sd0/startracker/flight-config.json"); + } + } + + private: + SdCardManager& sdcMan; +}; diff --git a/bsp_q7s/boardconfig/CMakeLists.txt b/bsp_q7s/boardconfig/CMakeLists.txt new file mode 100644 index 0000000..3224d75 --- /dev/null +++ b/bsp_q7s/boardconfig/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources(${OBSW_NAME} PRIVATE print.c) + +target_sources(${SIMPLE_OBSW_NAME} PRIVATE print.c) + +target_include_directories(${OBSW_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/bsp_q7s/boardconfig/busConf.h b/bsp_q7s/boardconfig/busConf.h new file mode 100644 index 0000000..aab8ce4 --- /dev/null +++ b/bsp_q7s/boardconfig/busConf.h @@ -0,0 +1,121 @@ +#ifndef BSP_Q7S_BOARDCONFIG_BUSCONF_H_ +#define BSP_Q7S_BOARDCONFIG_BUSCONF_H_ + +namespace q7s { + +static constexpr char SPI_DEFAULT_DEV[] = "/dev/spi_main"; +static constexpr uint32_t SPI_MAIN_BUS_LOCK_TIMEOUT = 50; + +static constexpr char SPI_RW_DEV[] = "/dev/spi_rw"; + +//! I2C bus using an I2C IP core in the programmable logic (PL) +static constexpr char I2C_PL_EIVE[] = "/dev/i2c_pl"; +//! I2C bus using the I2C peripheral of the ARM processing system (PS) +static constexpr char I2C_PS_EIVE[] = "/dev/i2c_ps"; +//! I2C bus using the first I2C peripheral of the ARM processing system (PS). +//! Named like this because it is used by default for the Q7 devices. +static constexpr char I2C_Q7_EIVE[] = "/dev/i2c_q7"; + +static constexpr char UART_GNSS_DEV[] = "/dev/gps0"; +static constexpr char UART_PLOC_MPSOC_DEV[] = "/dev/ul_plmpsoc"; +static constexpr char UART_PLOC_SUPERVISOR_DEV_FALLBACK[] = "/dev/ttyUL4"; +static constexpr char UART_PLOC_SUPERVISOR_DEV[] = "/dev/ploc_supv"; +static constexpr char UART_SYRLINKS_DEV[] = "/dev/ul_syrlinks"; +static constexpr char UART_STAR_TRACKER_DEV[] = "/dev/ul_str"; +static constexpr char UART_SCEX_DEV[] = "/dev/scex"; + +static constexpr char UIO_PDEC_REGISTERS[] = "/dev/uio_pdec_regs"; +static constexpr char UIO_PTME[] = "/dev/uio_ptme"; +static constexpr char UIO_PDEC_CONFIG_MEMORY[] = "/dev/uio_pdec_cfg_mem"; +static constexpr char UIO_SYS_ROM[] = "/dev/uio_sys_rom"; +static constexpr char UIO_PDEC_RAM[] = "/dev/uio_pdec_ram"; +static constexpr char UIO_PDEC_IRQ[] = "/dev/uio_pdec_irq"; +static constexpr int MAP_ID_PTME_CONFIG = 3; + +namespace uiomapids { +// Live TM +static const int PTME_VC0 = 0; +// OK/NOK/MISC Store +static const int PTME_VC1 = 1; +// HK store +static const int PTME_VC2 = 2; +// CFDP +static const int PTME_VC3 = 3; +static const int PTME_CONFIG = 4; +} // namespace uiomapids + +namespace gpioNames { + +static constexpr char GYRO_0_ADIS_CS[] = "gyro_0_adis_chip_select"; +static constexpr char GYRO_1_L3G_CS[] = "gyro_1_l3g_chip_select"; +static constexpr char GYRO_2_ADIS_CS[] = "gyro_2_adis_chip_select"; +static constexpr char GYRO_3_L3G_CS[] = "gyro_3_l3g_chip_select"; +static constexpr char MGM_0_CS[] = "mgm_0_lis3_chip_select"; +static constexpr char MGM_1_CS[] = "mgm_1_rm3100_chip_select"; +static constexpr char MGM_2_CS[] = "mgm_2_lis3_chip_select"; +static constexpr char MGM_3_CS[] = "mgm_3_rm3100_chip_select"; +static constexpr char RESET_GNSS_0[] = "reset_gnss_0"; +static constexpr char RESET_GNSS_1[] = "reset_gnss_1"; +static constexpr char GNSS_0_ENABLE[] = "enable_gnss_0"; +static constexpr char GNSS_1_ENABLE[] = "enable_gnss_1"; +static constexpr char GYRO_0_ENABLE[] = "enable_gyro_0"; +static constexpr char GYRO_2_ENABLE[] = "enable_gyro_2"; +static constexpr char GNSS_SELECT[] = "gnss_mux_select"; +static constexpr char GNSS_MUX_SELECT[] = "gnss_mux_select"; +static constexpr char PL_I2C_ARESETN[] = "pl_i2c_aresetn"; + +static constexpr char HEATER_0[] = "heater0"; +static constexpr char HEATER_1[] = "heater1"; +static constexpr char HEATER_2[] = "heater2"; +static constexpr char HEATER_3[] = "heater3"; +static constexpr char HEATER_4[] = "heater4"; +static constexpr char HEATER_5[] = "heater5"; +static constexpr char HEATER_6[] = "heater6"; +static constexpr char HEATER_7[] = "heater7"; +static constexpr char SA_DPL_PIN_0[] = "sa_dpl_0"; +static constexpr char SA_DPL_PIN_1[] = "sa_dpl_1"; +static constexpr char SPI_MUX_BIT_0_PIN[] = "spi_mux_bit_0"; +static constexpr char SPI_MUX_BIT_1_PIN[] = "spi_mux_bit_1"; +static constexpr char SPI_MUX_BIT_2_PIN[] = "spi_mux_bit_2"; +static constexpr char SPI_MUX_BIT_3_PIN[] = "spi_mux_bit_3"; +static constexpr char SPI_MUX_BIT_4_PIN[] = "spi_mux_bit_4"; +static constexpr char SPI_MUX_BIT_5_PIN[] = "spi_mux_bit_5"; +static constexpr char EN_RW_CS[] = "en_rw_cs"; +static constexpr char EN_RW_1[] = "enable_rw_1"; +static constexpr char EN_RW_2[] = "enable_rw_2"; +static constexpr char EN_RW_3[] = "enable_rw_3"; +static constexpr char EN_RW_4[] = "enable_rw_4"; + +static constexpr char RAD_SENSOR_CHIP_SELECT[] = "rad_sensor_chip_select"; +static constexpr char ENABLE_RADFET[] = "enable_radfet"; + +static constexpr char PAPB_EMPTY_SIGNAL_VC0[] = "papb_empty_signal_vc0"; +static constexpr char PAPB_EMPTY_SIGNAL_VC1[] = "papb_empty_signal_vc1"; +static constexpr char PAPB_EMPTY_SIGNAL_VC2[] = "papb_empty_signal_vc2"; +static constexpr char PAPB_EMPTY_SIGNAL_VC3[] = "papb_empty_signal_vc3"; + +static constexpr char PTME_RESETN[] = "ptme_resetn"; + +static constexpr char RS485_EN_TX_CLOCK[] = "tx_clock_enable_ltc2872"; +static constexpr char RS485_EN_TX_DATA[] = "tx_data_enable_ltc2872"; +static constexpr char RS485_EN_RX_CLOCK[] = "rx_clock_enable_ltc2872"; +static constexpr char RS485_EN_RX_DATA[] = "rx_data_enable_ltc2872"; +static constexpr char PDEC_RESET[] = "pdec_reset"; +static constexpr char SYRLINKS_FAULT[] = "syrlinks_fault"; + +static constexpr char PL_PCDU_ENABLE_VBAT0[] = "enable_plpcdu_vbat0"; +static constexpr char PL_PCDU_ENABLE_VBAT1[] = "enable_plpcdu_vbat1"; +static constexpr char PL_PCDU_ENABLE_DRO[] = "enable_plpcdu_dro"; +static constexpr char PL_PCDU_ENABLE_X8[] = "enable_plpcdu_x8"; +static constexpr char PL_PCDU_ENABLE_TX[] = "enable_plpcdu_tx"; +static constexpr char PL_PCDU_ENABLE_HPA[] = "enable_plpcdu_hpa"; +static constexpr char PL_PCDU_ENABLE_MPA[] = "enable_plpcdu_mpa"; +static constexpr char PL_PCDU_ADC_CS[] = "plpcdu_adc_chip_select"; + +static constexpr char ENABLE_SUPV_UART[] = "enable_supv_uart"; +static constexpr char ENABLE_MPSOC_UART[] = "enable_mpsoc_uart"; + +} // namespace gpioNames +} // namespace q7s + +#endif /* BSP_Q7S_BOARDCONFIG_BUSCONF_H_ */ diff --git a/bsp_q7s/boardconfig/etl_profile.h b/bsp_q7s/boardconfig/etl_profile.h new file mode 100644 index 0000000..86534d1 --- /dev/null +++ b/bsp_q7s/boardconfig/etl_profile.h @@ -0,0 +1,39 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2019 jwellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ +#ifndef __ETL_PROFILE_H__ +#define __ETL_PROFILE_H__ + +#define ETL_CHECK_PUSH_POP + +#define ETL_CPP11_SUPPORTED 1 +#define ETL_NO_NULLPTR_SUPPORT 0 +#define ETL_HAS_ERROR_ON_STRING_TRUNCATION 1 + +#endif diff --git a/bsp_q7s/boardconfig/gcov.h b/bsp_q7s/boardconfig/gcov.h new file mode 100644 index 0000000..80acdd8 --- /dev/null +++ b/bsp_q7s/boardconfig/gcov.h @@ -0,0 +1,15 @@ +#ifndef LINUX_GCOV_H_ +#define LINUX_GCOV_H_ +#include + +#ifdef GCOV +extern "C" void __gcov_flush(); +#else +void __gcov_flush() { + sif::info << "GCC GCOV: Please supply GCOV=1 in Makefile if " + "coverage information is desired.\n" + << std::flush; +} +#endif + +#endif /* LINUX_GCOV_H_ */ diff --git a/bsp_q7s/boardconfig/print.c b/bsp_q7s/boardconfig/print.c new file mode 100644 index 0000000..3aba2d7 --- /dev/null +++ b/bsp_q7s/boardconfig/print.c @@ -0,0 +1,10 @@ +#include +#include + +void printChar(const char* character, bool errStream) { + if (errStream) { + putc(*character, stderr); + return; + } + putc(*character, stdout); +} diff --git a/bsp_q7s/boardconfig/print.h b/bsp_q7s/boardconfig/print.h new file mode 100644 index 0000000..8e7e2e5 --- /dev/null +++ b/bsp_q7s/boardconfig/print.h @@ -0,0 +1,8 @@ +#ifndef HOSTED_BOARDCONFIG_PRINT_H_ +#define HOSTED_BOARDCONFIG_PRINT_H_ + +#include + +void printChar(const char* character, bool errStream); + +#endif /* HOSTED_BOARDCONFIG_PRINT_H_ */ diff --git a/bsp_q7s/boardconfig/q7sConfig.h.in b/bsp_q7s/boardconfig/q7sConfig.h.in new file mode 100644 index 0000000..8fe0f65 --- /dev/null +++ b/bsp_q7s/boardconfig/q7sConfig.h.in @@ -0,0 +1,34 @@ +#ifndef BSP_Q7S_BOARDCONFIG_Q7S_CONFIG_H_ +#define BSP_Q7S_BOARDCONFIG_Q7S_CONFIG_H_ + +#include + +#define OBSW_Q7S_EM @OBSW_Q7S_EM@ + +/*******************************************************************/ +/** All of the following flags should be enabled for mission code */ +/*******************************************************************/ + +//! Timers can mess up the code when debugging +//! All of this should be enabled for mission code! + +/*******************************************************************/ +/** Other flags */ +/*******************************************************************/ + +// Probably better if this is disabled for mission code. Convenient for development +#define Q7S_CHECK_FOR_ALREADY_RUNNING_IMG @Q7S_CHECK_FOR_ALREADY_RUNNING_IMG@ + +#define Q7S_SIMPLE_ADD_FILE_SYSTEM_TEST 0 + +#ifndef Q7S_SIMPLE_MODE +#define Q7S_SIMPLE_MODE 0 +#endif + +namespace config { + +static const uint32_t SD_CARD_ACCESS_MUTEX_TIMEOUT = 50; + +} + +#endif /* BSP_Q7S_BOARDCONFIG_Q7S_CONFIG_H_ */ diff --git a/bsp_q7s/boardtest/CMakeLists.txt b/bsp_q7s/boardtest/CMakeLists.txt new file mode 100644 index 0000000..9520a62 --- /dev/null +++ b/bsp_q7s/boardtest/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources(${OBSW_NAME} PRIVATE FileSystemTest.cpp Q7STestTask.cpp) + +if(EIVE_BUILD_Q7S_SIMPLE_MODE) + target_sources(${SIMPLE_OBSW_NAME} PRIVATE FileSystemTest.cpp) +endif() diff --git a/bsp_q7s/boardtest/FileSystemTest.cpp b/bsp_q7s/boardtest/FileSystemTest.cpp new file mode 100644 index 0000000..e1dd564 --- /dev/null +++ b/bsp_q7s/boardtest/FileSystemTest.cpp @@ -0,0 +1,23 @@ +#include "FileSystemTest.h" + +#include +#include + +#include "fsfw/timemanager/Stopwatch.h" + +enum SdCard { SDC0, SDC1 }; + +FileSystemTest::FileSystemTest() { + using namespace std; + SdCard sdCard = SdCard::SDC0; + cout << "SD Card Test for SD card " << static_cast(sdCard) << std::endl; + // Stopwatch stopwatch; + std::system("q7hw sd info all > /tmp/sd_status.txt"); + // stopwatch.stop(true); + std::system("q7hw sd set 0 on > /tmp/sd_set.txt"); + // stopwatch.stop(true); + std::system("q7hw sd set 0 off > /tmp/sd_set.txt"); + // stopwatch.stop(true); +} + +FileSystemTest::~FileSystemTest() {} diff --git a/bsp_q7s/boardtest/FileSystemTest.h b/bsp_q7s/boardtest/FileSystemTest.h new file mode 100644 index 0000000..bdb7989 --- /dev/null +++ b/bsp_q7s/boardtest/FileSystemTest.h @@ -0,0 +1,12 @@ +#ifndef BSP_Q7S_BOARDTEST_FILESYSTEMTEST_H_ +#define BSP_Q7S_BOARDTEST_FILESYSTEMTEST_H_ + +class FileSystemTest { + public: + FileSystemTest(); + virtual ~FileSystemTest(); + + private: +}; + +#endif /* BSP_Q7S_BOARDTEST_FILESYSTEMTEST_H_ */ diff --git a/bsp_q7s/boardtest/Q7STestTask.cpp b/bsp_q7s/boardtest/Q7STestTask.cpp new file mode 100644 index 0000000..03805fd --- /dev/null +++ b/bsp_q7s/boardtest/Q7STestTask.cpp @@ -0,0 +1,458 @@ +#include "Q7STestTask.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/fs/SdCardManager.h" +#include "bsp_q7s/fs/helpers.h" +#include "bsp_q7s/memory/scratchApi.h" +#include "fsfw/tasks/TaskFactory.h" +#include "fsfw/timemanager/Stopwatch.h" +#include "p60pdu.h" +#include "test/DummyParameter.h" + +using namespace returnvalue; + +Q7STestTask::Q7STestTask(object_id_t objectId) : TestTask(objectId) { + doTestSdCard = false; + doTestScratchApi = false; + doTestGpsShm = false; + doTestGpsSocket = false; + doTestXadc = false; +} + +ReturnValue_t Q7STestTask::performOneShotAction() { + if (doTestSdCard) { + testSdCard(); + } + if (doTestScratchApi) { + testScratchApi(); + } + if (DO_TEST_GOMSPACE_API) { + uint8_t p60pdu_node = 3; + uint8_t hk_mem[P60PDU_HK_SIZE]; + param_index_t p60pdu_hk{}; + p60pdu_hk.physaddr = hk_mem; + if (!p60pdu_get_hk(&p60pdu_hk, p60pdu_node, 1000)) { + printf("Error getting p60pdu hk\n"); + } else { + param_list(&p60pdu_hk, 1); + } + } + + if (DO_TEST_GOMSPACE_GET_CONFIG) { + uint8_t p60pdu_node = 3; + param_index_t requestStruct{}; + requestStruct.table = p60pdu_config; + requestStruct.mem_id = P60PDU_PARAM; + uint8_t hk_mem[P60PDU_PARAM_SIZE]; + requestStruct.count = p60pdu_config_count; + requestStruct.size = P60PDU_PARAM_SIZE; + requestStruct.physaddr = hk_mem; + int result = rparam_get_full_table(&requestStruct, p60pdu_node, P60_PORT_RPARAM, + requestStruct.mem_id, 1000); + param_list(&requestStruct, 1); + return (result == 0); + } + + // testJsonLibDirect(); + // testDummyParams(); + if (doTestProtHandler) { + testProtHandler(); + } + if (DO_TEST_FS_HANDLER) { + FsOpCodes opCode = FsOpCodes::CREATE_EMPTY_FILE_IN_TMP; + testFileSystemHandlerDirect(opCode); + } + return TestTask::performOneShotAction(); +} + +ReturnValue_t Q7STestTask::performPeriodicAction() { + if (doTestGpsShm) { + testGpsDaemonShm(); + } + if (doTestGpsSocket) { + testGpsDaemonSocket(); + } + if (doTestXadc) { + xadcTest(); + } + return TestTask::performPeriodicAction(); +} + +void Q7STestTask::testSdCard() { + using namespace std; + Stopwatch stopwatch; + int result = std::system("q7hw sd info all > /tmp/sd_status.txt"); + if (result != 0) { + sif::debug << "system call failed with " << result << endl; + } + ifstream sdStatus("/tmp/sd_status.txt"); + string line; + uint8_t idx = 0; + while (std::getline(sdStatus, line)) { + std::istringstream iss(line); + string word; + while (iss >> word) { + if (word == "on") { + sif::info << "SD card " << static_cast(idx) << " is on" << endl; + } else if (word == "off") { + sif::info << "SD card " << static_cast(idx) << " is off" << endl; + } + } + idx++; + } + std::remove("/tmp/sd_status.txt"); +} + +void Q7STestTask::fileTests() { + using namespace std; + ofstream testFile("/tmp/test.txt"); + testFile << "Hallo Welt" << endl; + testFile.close(); + + system("echo \"Hallo Welt\" > /tmp/test2.txt"); + system("echo \"Hallo Welt\""); +} + +void Q7STestTask::testScratchApi() { + ReturnValue_t result = scratch::writeNumber("TEST", 1); + if (result != returnvalue::OK) { + sif::debug << "Q7STestTask::scratchApiTest: Writing number failed" << std::endl; + } + int number = 0; + result = scratch::readNumber("TEST", number); + sif::info << "Q7STestTask::testScratchApi: Value for key \"TEST\": " << number << std::endl; + if (result != returnvalue::OK) { + sif::debug << "Q7STestTask::scratchApiTest: Reading number failed" << std::endl; + } + + result = scratch::writeString("TEST2", "halloWelt"); + if (result != returnvalue::OK) { + sif::debug << "Q7STestTask::scratchApiTest: Writing string failed" << std::endl; + } + std::string string; + result = scratch::readString("TEST2", string); + if (result != returnvalue::OK) { + sif::debug << "Q7STestTask::scratchApiTest: Reading number failed" << std::endl; + } + sif::info << "Q7STestTask::testScratchApi: Value for key \"TEST2\": " << string << std::endl; + + result = scratch::clearValue("TEST"); + result = scratch::clearValue("TEST2"); +} + +void Q7STestTask::testJsonLibDirect() { + Stopwatch stopwatch; + // for convenience + using json = nlohmann::json; + json helloTest; + // add a number that is stored as double (note the implicit conversion of j to an object) + helloTest["pi"] = 3.141; + std::string mntPrefix = SdCardManager::instance()->getCurrentMountPrefix(); + std::string fileName = mntPrefix + "/pretty.json"; + std::ofstream o(fileName); + o << std::setw(4) << helloTest << std::endl; +} + +void Q7STestTask::testDummyParams() { + std::string mntPrefix = SdCardManager::instance()->getCurrentMountPrefix(); + DummyParameter param(mntPrefix, "dummy_json.txt"); + param.printKeys(); + param.print(); + if (not param.getJsonFileExists()) { + param.writeJsonFile(); + } + + ReturnValue_t result = param.readJsonFile(); + if (result != returnvalue::OK) { + } + + param.setValue(DummyParameter::DUMMY_KEY_PARAM_1, 3); + param.setValue(DummyParameter::DUMMY_KEY_PARAM_2, "blirb"); + + param.writeJsonFile(); + param.print(); + + int test = 0; + result = param.getValue(DummyParameter::DUMMY_KEY_PARAM_1, test); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testDummyParams: Key " << DummyParameter::DUMMY_KEY_PARAM_1 + << " does not exist" << std::endl; + } + std::string test2; + result = param.getValue(DummyParameter::DUMMY_KEY_PARAM_2, test2); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testDummyParams: Key " << DummyParameter::DUMMY_KEY_PARAM_1 + << " does not exist" << std::endl; + } + sif::info << "Test value (3 expected): " << test << std::endl; + sif::info << "Test value 2 (\"blirb\" expected): " << test2 << std::endl; +} + +ReturnValue_t Q7STestTask::initialize() { + coreController = ObjectManager::instance()->get(objects::CORE_CONTROLLER); + if (coreController == nullptr) { + sif::warning << "Q7STestTask::initialize: Could not retrieve CORE_CONTROLLER object" + << std::endl; + } + return TestTask::initialize(); +} + +void Q7STestTask::testProtHandler() { + bool opPerformed = false; + ReturnValue_t result = returnvalue::OK; + // If any chips are unlocked, lock them here + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_0, xsc::Copy::COPY_0, + true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_0, xsc::Copy::COPY_1, + true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_1, xsc::Copy::COPY_0, + true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_1, xsc::Copy::COPY_1, + true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + + // unlock own copy + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::SELF_CHIP, + xsc::Copy::SELF_COPY, false); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + if (not opPerformed) { + sif::warning << "Q7STestTask::testProtHandler: No op performed" << std::endl; + } + int retval = std::system("print-chip-prot-status.sh"); + if (retval != 0) { + utility::handleSystemError(retval, "Q7STestTask::testProtHandler"); + } + + // lock own copy + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::SELF_CHIP, + xsc::Copy::SELF_COPY, true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + if (not opPerformed) { + sif::warning << "Q7STestTask::testProtHandler: No op performed" << std::endl; + } + retval = std::system("print-chip-prot-status.sh"); + if (retval != 0) { + utility::handleSystemError(retval, "Q7STestTask::testProtHandler"); + } + + // unlock specific copy + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_1, xsc::Copy::COPY_1, + false); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + if (not opPerformed) { + sif::warning << "Q7STestTask::testProtHandler: No op performed" << std::endl; + } + retval = std::system("print-chip-prot-status.sh"); + if (retval != 0) { + utility::handleSystemError(retval, "Q7STestTask::testProtHandler"); + } + + // lock specific copy + result = coreController->setBootCopyProtectionAndUpdateFile(xsc::Chip::CHIP_1, xsc::Copy::COPY_1, + true); + if (result != returnvalue::OK) { + sif::warning << "Q7STestTask::testProtHandler: Op failed" << std::endl; + } + if (not opPerformed) { + sif::warning << "Q7STestTask::testProtHandler: No op performed" << std::endl; + } + retval = std::system("print-chip-prot-status.sh"); + if (retval != 0) { + utility::handleSystemError(retval, "Q7STestTask::testProtHandler"); + } +} + +void Q7STestTask::testGpsDaemonShm() { + gpsmm gpsmm(GPSD_SHARED_MEMORY, ""); + gps_data_t* gps; + gps = gpsmm.read(); + if (gps == nullptr) { + sif::warning << "Q7STestTask: Reading GPS data failed" << std::endl; + } + sif::info << "-- Q7STestTask: GPS shared memory read test --" << std::endl; +#if LIBGPS_VERSION_MINOR <= 17 + time_t timeRaw = gps->fix.time; +#else + time_t timeRaw = gps->fix.time.tv_sec; +#endif + std::tm* time = gmtime(&timeRaw); + sif::info << "Time: " << std::put_time(time, "%c %Z") << std::endl; + sif::info << "Visible satellites: " << gps->satellites_visible << std::endl; + sif::info << "Satellites used: " << gps->satellites_used << std::endl; + sif::info << "Fix (0:Not Seen|1:No Fix|2:2D|3:3D): " << gps->fix.mode << std::endl; + sif::info << "Latitude: " << gps->fix.latitude << std::endl; + sif::info << "Longitude: " << gps->fix.longitude << std::endl; +#if LIBGPS_VERSION_MINOR <= 17 + sif::info << "Altitude(MSL): " << gps->fix.altitude << std::endl; +#else + sif::info << "Altitude(MSL): " << gps->fix.altMSL << std::endl; +#endif + sif::info << "Speed(m/s): " << gps->fix.speed << std::endl; +} + +void Q7STestTask::testGpsDaemonSocket() { + if (gpsmmShmPtr == nullptr) { + gpsmmShmPtr = new gpsmm("localhost", DEFAULT_GPSD_PORT); + } + // The data from the device will generally be read all at once. Therefore, we + // can set all field here + if (not gpsmmShmPtr->is_open()) { + if (gpsNotOpenSwitch) { + // Opening failed +#if FSFW_VERBOSE_LEVEL >= 1 + sif::warning << "Q7STestTask::testGpsDaemonSocket: Opening GPSMM failed | " + << "Error " << errno << " | " << gps_errstr(errno) << std::endl; +#endif + + gpsNotOpenSwitch = false; + } + return; + } + // Stopwatch watch; + gps_data_t* gps = nullptr; + gpsmmShmPtr->stream(WATCH_ENABLE | WATCH_JSON); + if (not gpsmmShmPtr->waiting(50000000)) { + return; + } + gps = gpsmmShmPtr->read(); + if (gps == nullptr) { + if (gpsReadFailedSwitch) { + gpsReadFailedSwitch = false; + sif::warning << "Q7STestTask::testGpsDaemonSocket: Reading GPS data failed" << std::endl; + } + return; + } + if (MODE_SET != (MODE_SET & gps->set)) { + if (noModeSetCntr >= 0) { + noModeSetCntr++; + } + if (noModeSetCntr == 10) { + // TODO: Trigger event here + sif::warning << "Q7STestTask::testGpsDaemonSocket: No mode could be " + "read for 10 consecutive reads" + << std::endl; + noModeSetCntr = -1; + } + return; + } else { + noModeSetCntr = 0; + } + sif::info << "-- Q7STestTask: GPS socket read test --" << std::endl; +#if LIBGPS_VERSION_MINOR <= 17 + time_t timeRaw = gps->fix.time; +#else + time_t timeRaw = gps->fix.time.tv_sec; +#endif + std::tm* time = gmtime(&timeRaw); + sif::info << "Time: " << std::put_time(time, "%c %Z") << std::endl; + sif::info << "Visible satellites: " << gps->satellites_visible << std::endl; + sif::info << "Satellites used: " << gps->satellites_used << std::endl; + sif::info << "Fix (0:Not Seen|1:No Fix|2:2D|3:3D): " << gps->fix.mode << std::endl; + sif::info << "Latitude: " << gps->fix.latitude << std::endl; + sif::info << "Longitude: " << gps->fix.longitude << std::endl; +} + +void Q7STestTask::testFileSystemHandlerDirect(FsOpCodes opCode) { + HostFilesystem hostFs; + auto* sdcMan = SdCardManager::instance(); + std::string mountPrefix = sdcMan->getCurrentMountPrefix(); + sif::info << "Current mount prefix: " << mountPrefix << std::endl; + auto prefixedPath = fshelpers::getPrefixedPath(*sdcMan, "conf/test.txt"); + sif::info << "Prefixed path: " << prefixedPath << std::endl; + if (opCode == FsOpCodes::CREATE_EMPTY_FILE_IN_TMP) { + FilesystemParams params("/tmp/hello.txt"); + auto res = hostFs.createFile(params); + if (res != OK) { + sif::warning << "Creating empty file in /tmp failed" << std::endl; + } + bool fileExists = std::filesystem::exists("/tmp/hello.txt"); + if (not fileExists) { + sif::warning << "File was not created!" << std::endl; + } + hostFs.removeFile("/tmp/hello.txt"); + } +} + +void Q7STestTask::xadcTest() { + ReturnValue_t result = returnvalue::OK; + float temperature = 0; + float vccPint = 0; + float vccPaux = 0; + float vccInt = 0; + float vccAux = 0; + float vccBram = 0; + float vccOddr = 0; + float vrefp = 0; + float vrefn = 0; + Xadc xadc; + result = xadc.getTemperature(temperature); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: Chip Temperature: " << temperature << " °C" << std::endl; + } + result = xadc.getVccPint(vccPint); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC PS internal: " << vccPint << " mV" << std::endl; + } + result = xadc.getVccPaux(vccPaux); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC PS auxilliary: " << vccPaux << " mV" << std::endl; + } + result = xadc.getVccInt(vccInt); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC PL internal: " << vccInt << " mV" << std::endl; + } + result = xadc.getVccAux(vccAux); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC PL auxilliary: " << vccAux << " mV" << std::endl; + } + result = xadc.getVccBram(vccBram); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC BRAM: " << vccBram << " mV" << std::endl; + } + result = xadc.getVccOddr(vccOddr); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: VCC PS I/O DDR : " << vccOddr << " mV" << std::endl; + } + result = xadc.getVrefp(vrefp); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: Vrefp : " << vrefp << " mV" << std::endl; + } + result = xadc.getVrefn(vrefn); + if (result == returnvalue::OK) { + sif::info << "Q7STestTask::xadcTest: Vrefn : " << vrefn << " mV" << std::endl; + } +} diff --git a/bsp_q7s/boardtest/Q7STestTask.h b/bsp_q7s/boardtest/Q7STestTask.h new file mode 100644 index 0000000..9ba00c0 --- /dev/null +++ b/bsp_q7s/boardtest/Q7STestTask.h @@ -0,0 +1,61 @@ +#ifndef BSP_Q7S_BOARDTEST_Q7STESTTASK_H_ +#define BSP_Q7S_BOARDTEST_Q7STESTTASK_H_ + +#include + +#include "test/TestTask.h" + +class CoreController; + +class Q7STestTask : public TestTask { + public: + Q7STestTask(object_id_t objectId); + + ReturnValue_t initialize() override; + + private: + bool doTestSdCard = false; + bool doTestScratchApi = false; + static constexpr bool DO_TEST_GOMSPACE_API = false; + static constexpr bool DO_TEST_GOMSPACE_GET_CONFIG = false; + static constexpr bool DO_TEST_FS_HANDLER = false; + bool doTestGpsShm = false; + bool doTestGpsSocket = false; + bool doTestProtHandler = false; + bool doTestXadc = false; + + bool gpsNotOpenSwitch = false; + bool gpsReadFailedSwitch = false; + int32_t noModeSetCntr = 0; + gpsmm* gpsmmShmPtr = nullptr; + + CoreController* coreController = nullptr; + ReturnValue_t performOneShotAction() override; + ReturnValue_t performPeriodicAction() override; + + void testGpsDaemonShm(); + void testGpsDaemonSocket(); + + void testSdCard(); + void fileTests(); + void xadcTest(); + + void testScratchApi(); + void testJsonLibDirect(); + void testDummyParams(); + void testProtHandler(); + + enum FsOpCodes { + CREATE_EMPTY_FILE_IN_TMP, + REMOVE_TMP_FILE, + CREATE_DIR_IN_TMP, + REMOVE_EMPTY_DIR_IN_TMP, + ATTEMPT_DIR_REMOVAL_NON_EMPTY, + REMOVE_FILLED_DIR_IN_TMP, + RENAME_FILE, + APPEND_TO_FILE, + }; + void testFileSystemHandlerDirect(FsOpCodes opCode); +}; + +#endif /* BSP_Q7S_BOARDTEST_Q7STESTTASK_H_ */ diff --git a/bsp_q7s/callbacks/CMakeLists.txt b/bsp_q7s/callbacks/CMakeLists.txt new file mode 100644 index 0000000..ed2e16f --- /dev/null +++ b/bsp_q7s/callbacks/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(${OBSW_NAME} PRIVATE rwSpiCallback.cpp gnssCallback.cpp + pcduSwitchCb.cpp q7sGpioCallbacks.cpp) diff --git a/bsp_q7s/callbacks/gnssCallback.cpp b/bsp_q7s/callbacks/gnssCallback.cpp new file mode 100644 index 0000000..22ceb3a --- /dev/null +++ b/bsp_q7s/callbacks/gnssCallback.cpp @@ -0,0 +1,29 @@ +#include "gnssCallback.h" + +#include "devices/gpioIds.h" +#include "fsfw/action/HasActionsIF.h" +#include "fsfw/tasks/TaskFactory.h" + +ReturnValue_t gps::triggerGpioResetPin(const uint8_t* actionData, size_t len, void* args) { + // At least one byte which denotes which GPS to reset is required + if (len < 1 or actionData == nullptr) { + return HasActionsIF::INVALID_PARAMETERS; + } + ResetArgs* resetArgs = reinterpret_cast(args); + if (args == nullptr) { + return returnvalue::FAILED; + } + if (resetArgs->gpioComIF == nullptr) { + return returnvalue::FAILED; + } + gpioId_t gpioId; + if (actionData[0] == 0) { + gpioId = gpioIds::GNSS_0_NRESET; + } else { + gpioId = gpioIds::GNSS_1_NRESET; + } + resetArgs->gpioComIF->pullLow(gpioId); + TaskFactory::delayTask(resetArgs->waitPeriodMs); + resetArgs->gpioComIF->pullHigh(gpioId); + return returnvalue::OK; +} diff --git a/bsp_q7s/callbacks/gnssCallback.h b/bsp_q7s/callbacks/gnssCallback.h new file mode 100644 index 0000000..331f97e --- /dev/null +++ b/bsp_q7s/callbacks/gnssCallback.h @@ -0,0 +1,18 @@ +#ifndef BSP_Q7S_CALLBACKS_GNSSCALLBACK_H_ +#define BSP_Q7S_CALLBACKS_GNSSCALLBACK_H_ + +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" + +struct ResetArgs { + LinuxLibgpioIF* gpioComIF = nullptr; + uint32_t waitPeriodMs = 100; +}; + +namespace gps { + +ReturnValue_t triggerGpioResetPin(const uint8_t* actionData, size_t len, void* args); + +} + +#endif /* BSP_Q7S_CALLBACKS_GNSSCALLBACK_H_ */ diff --git a/bsp_q7s/callbacks/pcduSwitchCb.cpp b/bsp_q7s/callbacks/pcduSwitchCb.cpp new file mode 100644 index 0000000..f9b6c76 --- /dev/null +++ b/bsp_q7s/callbacks/pcduSwitchCb.cpp @@ -0,0 +1,32 @@ +#include "pcduSwitchCb.h" + +#include + +#include "devices/gpioIds.h" + +void pcdu::switchCallback(GOMSPACE::Pdu pdu, uint8_t channel, bool state, void* args) { + LinuxLibgpioIF* gpioComIF = reinterpret_cast(args); + if (gpioComIF == nullptr) { + return; + } + if (pdu == GOMSPACE::Pdu::PDU1) { + PDU1::Channels typedChannel = static_cast(channel); + if (typedChannel == PDU1::Channels::ACS_A_SIDE) { + if (state) { + gpioComIF->pullHigh(gpioIds::GNSS_0_NRESET); + } else { + gpioComIF->pullLow(gpioIds::GNSS_0_NRESET); + } + } + + } else if (pdu == GOMSPACE::Pdu::PDU2) { + PDU2::Channels typedChannel = static_cast(channel); + if (typedChannel == PDU2::Channels::ACS_B_SIDE) { + if (state) { + gpioComIF->pullHigh(gpioIds::GNSS_1_NRESET); + } else { + gpioComIF->pullLow(gpioIds::GNSS_1_NRESET); + } + } + } +} diff --git a/bsp_q7s/callbacks/pcduSwitchCb.h b/bsp_q7s/callbacks/pcduSwitchCb.h new file mode 100644 index 0000000..4c00697 --- /dev/null +++ b/bsp_q7s/callbacks/pcduSwitchCb.h @@ -0,0 +1,14 @@ +#ifndef BSP_Q7S_CALLBACKS_PCDUSWITCHCB_H_ +#define BSP_Q7S_CALLBACKS_PCDUSWITCHCB_H_ + +#include + +#include + +namespace pcdu { + +void switchCallback(GOMSPACE::Pdu pdu, uint8_t channel, bool state, void* args); + +} + +#endif /* BSP_Q7S_CALLBACKS_PCDUSWITCHCB_H_ */ diff --git a/bsp_q7s/callbacks/q7sGpioCallbacks.cpp b/bsp_q7s/callbacks/q7sGpioCallbacks.cpp new file mode 100644 index 0000000..512050e --- /dev/null +++ b/bsp_q7s/callbacks/q7sGpioCallbacks.cpp @@ -0,0 +1,54 @@ +#include "q7sGpioCallbacks.h" + +#include +#include +#include +#include + +#include "busConf.h" + +void q7s::gpioCallbacks::initSpiCsDecoder(GpioIF* gpioComIF) { + using namespace gpio; + ReturnValue_t result; + + if (gpioComIF == nullptr) { + sif::debug << "initSpiCsDecoder: Invalid gpioComIF" << std::endl; + return; + } + + GpioCookie* spiMuxGpios = new GpioCookie; + + GpiodRegularByLineName* spiMuxBit = nullptr; + /** Setting mux bit 1 to low will disable IC21 on the interface board */ + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_0_PIN, "SPI Mux Bit 1", + Direction::OUT, Levels::HIGH); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_0, spiMuxBit); + /** Setting mux bit 2 to low disables IC1 on the TCS board */ + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_1_PIN, "SPI Mux Bit 2", + Direction::OUT, Levels::HIGH); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_1, spiMuxBit); + /** Setting mux bit 3 to low disables IC2 on the TCS board and IC22 on the interface board */ + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_2_PIN, "SPI Mux Bit 3", + Direction::OUT, Levels::LOW); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_2, spiMuxBit); + + /** The following gpios can take arbitrary initial values */ + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_3_PIN, "SPI Mux Bit 4", + Direction::OUT, Levels::LOW); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_3, spiMuxBit); + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_4_PIN, "SPI Mux Bit 5", + Direction::OUT, Levels::LOW); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_4, spiMuxBit); + spiMuxBit = new GpiodRegularByLineName(q7s::gpioNames::SPI_MUX_BIT_5_PIN, "SPI Mux Bit 6", + Direction::OUT, Levels::LOW); + spiMuxGpios->addGpio(gpioIds::SPI_MUX_BIT_5, spiMuxBit); + GpiodRegularByLineName* enRwDecoder = new GpiodRegularByLineName( + q7s::gpioNames::EN_RW_CS, "EN_RW_CS", Direction::OUT, Levels::HIGH); + spiMuxGpios->addGpio(gpioIds::EN_RW_CS, enRwDecoder); + + result = gpioComIF->addGpios(spiMuxGpios); + if (result != returnvalue::OK) { + sif::error << "initSpiCsDecoder: Failed to add SPI MUX bit GPIOs" << std::endl; + return; + } +} diff --git a/bsp_q7s/callbacks/q7sGpioCallbacks.h b/bsp_q7s/callbacks/q7sGpioCallbacks.h new file mode 100644 index 0000000..e330655 --- /dev/null +++ b/bsp_q7s/callbacks/q7sGpioCallbacks.h @@ -0,0 +1,15 @@ +#pragma once + +class GpioIF; + +namespace q7s { +namespace gpioCallbacks { + +/** + * @brief This function initializes the GPIOs used to control the SN74LVC138APWR decoders on + * the TCS Board and the interface board. + */ +void initSpiCsDecoder(GpioIF* gpioComIF); + +} // namespace gpioCallbacks +} // namespace q7s diff --git a/bsp_q7s/callbacks/rwSpiCallback.cpp b/bsp_q7s/callbacks/rwSpiCallback.cpp new file mode 100644 index 0000000..60ad566 --- /dev/null +++ b/bsp_q7s/callbacks/rwSpiCallback.cpp @@ -0,0 +1,283 @@ +#include "rwSpiCallback.h" + +#include + +#include "devices/gpioIds.h" +#include "fsfw/serviceinterface/ServiceInterface.h" +#include "fsfw_hal/linux/UnixFileGuard.h" +#include "fsfw_hal/linux/spi/SpiCookie.h" +#include "mission/acs/RwHandler.h" + +namespace rwSpiCallback { + +namespace { +static bool MODE_SET = false; + +ReturnValue_t openSpi(const std::string& devname, int flags, GpioIF* gpioIF, gpioId_t gpioId, + MutexIF* mutex, MutexIF::TimeoutType timeoutType, uint32_t timeoutMs, + int& fd); +/** + * @brief This function closes a spi session. Pulls the chip select to high an releases the + * mutex. + * @param gpioId Gpio ID of chip select + * @param gpioIF Pointer to gpio interface to drive the chip select + * @param mutex The spi mutex + */ +void closeSpi(int fd, gpioId_t gpioId, GpioIF* gpioIF, MutexIF* mutex); +} // namespace + +ReturnValue_t spiCallback(SpiComIF* comIf, SpiCookie* cookie, const uint8_t* sendData, + size_t sendLen, void* args) { + // Stopwatch watch; + ReturnValue_t result = returnvalue::OK; + + RwHandler* handler = reinterpret_cast(args); + if (handler == nullptr) { + sif::error << "rwSpiCallback::spiCallback: Pointer to handler is invalid" << std::endl; + return returnvalue::FAILED; + } + + uint8_t writeBuffer[2] = {}; + uint8_t writeSize = 0; + + gpioId_t gpioId = cookie->getChipSelectPin(); + GpioIF& gpioIF = comIf->getGpioInterface(); + MutexIF::TimeoutType timeoutType = MutexIF::TimeoutType::WAITING; + uint32_t timeoutMs = 0; + MutexIF* mutex = comIf->getCsMutex(); + cookie->getMutexParams(timeoutType, timeoutMs); + if (mutex == nullptr) { + sif::debug << "rwSpiCallback::spiCallback: Mutex or GPIO interface invalid" << std::endl; + return returnvalue::FAILED; + } + + int fileDescriptor = 0; + const std::string& dev = comIf->getSpiDev(); + result = openSpi(dev, O_RDWR, &gpioIF, gpioId, mutex, timeoutType, timeoutMs, fileDescriptor); + if (result != returnvalue::OK) { + return result; + } + + spi::SpiModes spiMode = spi::SpiModes::MODE_0; + uint32_t spiSpeed = 0; + cookie->getSpiParameters(spiMode, spiSpeed, nullptr); + // We are in protected section, so we can use the static variable here without issues. + // We don't need to set the speed because a SPI core is used, but the mode has to be set once + // correctly for all RWs + if (not MODE_SET) { + comIf->setSpiSpeedAndMode(fileDescriptor, spiMode, spiSpeed); + MODE_SET = true; + } + + /** Sending frame start sign */ + writeBuffer[0] = FLAG_BYTE; + writeSize = 1; + + if (write(fileDescriptor, writeBuffer, writeSize) != static_cast(writeSize)) { + sif::error << "rwSpiCallback::spiCallback: Write failed!" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::SPI_WRITE_FAILURE; + } + + /** Encoding and sending command */ + size_t idx = 0; + while (idx < sendLen) { + switch (*(sendData + idx)) { + case 0x7E: + writeBuffer[0] = 0x7D; + writeBuffer[1] = 0x5E; + writeSize = 2; + break; + case 0x7D: + writeBuffer[0] = 0x7D; + writeBuffer[1] = 0x5D; + writeSize = 2; + break; + default: + writeBuffer[0] = *(sendData + idx); + writeSize = 1; + break; + } + if (write(fileDescriptor, writeBuffer, writeSize) != static_cast(writeSize)) { + sif::error << "rwSpiCallback::spiCallback: Write failed!" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::SPI_WRITE_FAILURE; + } + idx++; + } + + /** Sending frame end sign */ + writeBuffer[0] = FLAG_BYTE; + writeSize = 1; + + if (write(fileDescriptor, writeBuffer, writeSize) != static_cast(writeSize)) { + sif::error << "rwSpiCallback::spiCallback: Write failed!" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::SPI_WRITE_FAILURE; + } + + uint8_t* rxBuf = nullptr; + result = comIf->getReadBuffer(cookie->getSpiAddress(), &rxBuf); + if (result != returnvalue::OK) { + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return result; + } + + size_t replyBufferSize = cookie->getMaxBufferSize(); + + // There must be a delay of at least 20 ms after sending the command. + // Delay for 70 ms here and release the SPI bus for that duration. + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + usleep(rws::SPI_REPLY_DELAY); + result = openSpi(dev, O_RDWR, &gpioIF, gpioId, mutex, timeoutType, timeoutMs, fileDescriptor); + if (result != returnvalue::OK) { + return result; + } + + /** + * The reaction wheel responds with empty frames while preparing the reply data. + * However, receiving more than 5 empty frames will be interpreted as an error. + */ + uint8_t byteRead = 0; + for (idx = 0; idx < 10; idx++) { + if (read(fileDescriptor, &byteRead, 1) != 1) { + sif::error << "rwSpiCallback::spiCallback: Read failed" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::SPI_READ_FAILURE; + } + if (idx == 0) { + if (byteRead != FLAG_BYTE) { + sif::error << "Invalid data, expected start marker" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::NO_START_MARKER; + } + } + + if (byteRead != FLAG_BYTE) { + break; + } + + if (idx == 9) { + sif::error << "rwSpiCallback::spiCallback: Empty frame timeout" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + return rws::NO_REPLY; + } + } + +#if FSFW_HAL_SPI_WIRETAPPING == 1 + sif::info << "RW start marker detected" << std::endl; +#endif + + size_t decodedFrameLen = 0; + while (decodedFrameLen < replyBufferSize) { + /** First byte already read in */ + if (decodedFrameLen != 0) { + byteRead = 0; + if (read(fileDescriptor, &byteRead, 1) != 1) { + sif::error << "rwSpiCallback::spiCallback: Read failed" << std::endl; + result = rws::SPI_READ_FAILURE; + break; + } + } + + if (byteRead == FLAG_BYTE) { + /** Reached end of frame */ + break; + } else if (byteRead == 0x7D) { + if (read(fileDescriptor, &byteRead, 1) != 1) { + sif::error << "rwSpiCallback::spiCallback: Read failed" << std::endl; + result = rws::SPI_READ_FAILURE; + break; + } + if (byteRead == 0x5E) { + *(rxBuf + decodedFrameLen) = 0x7E; + decodedFrameLen++; + continue; + } else if (byteRead == 0x5D) { + *(rxBuf + decodedFrameLen) = 0x7D; + decodedFrameLen++; + continue; + } else { + sif::error << "rwSpiCallback::spiCallback: Invalid substitute" << std::endl; + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + result = rws::INVALID_SUBSTITUTE; + break; + } + } else { + *(rxBuf + decodedFrameLen) = byteRead; + decodedFrameLen++; + continue; + } + + /** + * There might be the unlikely case that each byte in a get-telemetry reply has been + * replaced by its substitute. Than the next byte must correspond to the end sign 0x7E. + * Otherwise there might be something wrong. + */ + if (decodedFrameLen == replyBufferSize) { + if (read(fileDescriptor, &byteRead, 1) != 1) { + sif::error << "rwSpiCallback::spiCallback: Failed to read last byte" << std::endl; + result = rws::SPI_READ_FAILURE; + break; + } + if (byteRead != FLAG_BYTE) { + sif::error << "rwSpiCallback::spiCallback: Missing end sign " << static_cast(FLAG_BYTE) + << std::endl; + decodedFrameLen--; + result = rws::MISSING_END_SIGN; + break; + } + } + result = returnvalue::OK; + } + + cookie->setTransferSize(decodedFrameLen); + + closeSpi(fileDescriptor, gpioId, &gpioIF, mutex); + + return result; +} + +namespace { + +ReturnValue_t openSpi(const std::string& devname, int flags, GpioIF* gpioIF, gpioId_t gpioId, + MutexIF* mutex, MutexIF::TimeoutType timeoutType, uint32_t timeoutMs, + int& fd) { + ReturnValue_t result = mutex->lockMutex(timeoutType, timeoutMs); + if (result != returnvalue::OK) { + sif::debug << "rwSpiCallback::spiCallback: Failed to lock mutex" << std::endl; + return result; + } + + fd = open(devname.c_str(), flags); + if (fd < 0) { + sif::error << "rwSpiCallback::spiCallback: Failed to open device file" << std::endl; + return spi::OPENING_FILE_FAILED; + } + + // Pull SPI CS low. For now, no support for active high given + if (gpioId != gpio::NO_GPIO) { + result = gpioIF->pullLow(gpioId); + if (result != returnvalue::OK) { + sif::error << "rwSpiCallback::spiCallback: Failed to pull chip select low" << std::endl; + return result; + } + } + return returnvalue::OK; +} +void closeSpi(int fd, gpioId_t gpioId, GpioIF* gpioIF, MutexIF* mutex) { + close(fd); + if (gpioId != gpio::NO_GPIO) { + if (gpioIF->pullHigh(gpioId) != returnvalue::OK) { + sif::error << "closeSpi: Failed to pull chip select high" << std::endl; + } + } + if (mutex->unlockMutex() != returnvalue::OK) { + sif::error << "rwSpiCallback::closeSpi: Failed to unlock mutex" << std::endl; + ; + } +} + +} // namespace + +} // namespace rwSpiCallback diff --git a/bsp_q7s/callbacks/rwSpiCallback.h b/bsp_q7s/callbacks/rwSpiCallback.h new file mode 100644 index 0000000..7390db5 --- /dev/null +++ b/bsp_q7s/callbacks/rwSpiCallback.h @@ -0,0 +1,37 @@ +#ifndef BSP_Q7S_RW_SPI_CALLBACK_H_ +#define BSP_Q7S_RW_SPI_CALLBACK_H_ + +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw_hal/common/gpio/GpioCookie.h" +#include "fsfw_hal/linux/spi/SpiComIF.h" + +namespace rwSpiCallback { + +//! This is the end and start marker of the frame datalinklayer +static constexpr uint8_t FLAG_BYTE = 0x7E; + +/** + * @brief This is the callback function to send commands to the nano avionics reaction wheels and + * receive the replies. + * + * @details The data to sent are additionally encoded according to the HDLC framing defined in the + * datasheet of the reaction wheels: + * https://eive-cloud.irs.uni-stuttgart.de/index.php/apps/files/?dir=/EIVE_IRS/ + * Arbeitsdaten/08_Used%20Components/Nanoavionics_Reactionwheels&fileid=181622 + * Each command entails exactly one reply which will also be read in and decoded by this + * function. + * Because the reaction wheels require a spi clock frequency of maximum 300 kHZ and minimum + * 150 kHz which is not supported by the processing system SPI peripheral an AXI SPI core + * has been implemented in the programmable logic. This AXI SPI core works with a fixed + * frequency of 250 kHz. + * To allow the parallel usage of the same physical SPI bus, a VHDL module has been + * implemented which is able to disconnect the hard-wired SPI peripheral of the PS and + * route the AXI SPI to the SPI lines. + * To switch between the to SPI peripherals, an EMIO is used which will also be controlled + * by this function. + */ +ReturnValue_t spiCallback(SpiComIF* comIf, SpiCookie* cookie, const uint8_t* sendData, + size_t sendLen, void* args); + +} // namespace rwSpiCallback +#endif /* BSP_Q7S_RW_SPI_CALLBACK_H_ */ diff --git a/bsp_q7s/core/CMakeLists.txt b/bsp_q7s/core/CMakeLists.txt new file mode 100644 index 0000000..530b53e --- /dev/null +++ b/bsp_q7s/core/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(${OBSW_NAME} PRIVATE CoreController.cpp WatchdogHandler.cpp + XiphosWdtHandler.cpp) diff --git a/bsp_q7s/core/CoreController.cpp b/bsp_q7s/core/CoreController.cpp new file mode 100644 index 0000000..30eff4e --- /dev/null +++ b/bsp_q7s/core/CoreController.cpp @@ -0,0 +1,2633 @@ +#include "CoreController.h" + +#include +#include +#include +#include +#include + +#include "commonConfig.h" +#include "fsfw/serviceinterface/ServiceInterface.h" +#include "fsfw/timemanager/Stopwatch.h" +#include "fsfw/version.h" +#include "watchdog/definitions.h" +#if OBSW_ADD_TMTC_UDP_SERVER == 1 +#include "fsfw/osal/common/UdpTmTcBridge.h" +#endif +#if OBSW_ADD_TMTC_TCP_SERVER == 1 +#include "fsfw/osal/common/TcpTmTcServer.h" +#endif +#include +#include + +#include +#include + +#include "bsp_q7s/boardconfig/busConf.h" +#include "bsp_q7s/fs/SdCardManager.h" +#include "bsp_q7s/memory/scratchApi.h" +#include "bsp_q7s/xadc/Xadc.h" +#include "eive/definitions.h" +#include "linux/utility/utility.h" + +xsc::Chip CoreController::CURRENT_CHIP = xsc::Chip::NO_CHIP; +xsc::Copy CoreController::CURRENT_COPY = xsc::Copy::NO_COPY; + +CoreController::CoreController(object_id_t objectId, bool enableHkSet) + : ExtendedControllerBase(objectId, 5), + enableHkSet(enableHkSet), + cmdExecutor(4096), + cmdReplyBuf(4096, true), + cmdRepliesSizes(128), + opDivider5(5), + opDivider10(10), + hkSet(this), + paramHelper(this) { + cmdExecutor.setRingBuffer(&cmdReplyBuf, &cmdRepliesSizes); + try { + sdcMan = SdCardManager::instance(); + if (sdcMan == nullptr) { + sif::error << "CoreController::CoreController: SD card manager invalid!" << std::endl; + } + + if (not BLOCKING_SD_INIT) { + sdcMan->setBlocking(false); + } + // Set up state of SD card manager and own initial state. + // Stopwatch watch; + sdcMan->updateSdCardStateFile(); + SdCardManager::SdStatePair sdStates; + sdcMan->getSdCardsStatus(sdStates); + auto sdCard = sdcMan->getPreferredSdCard(); + if (not sdCard.has_value()) { + sif::error << "CoreController::initializeAfterTaskCreation: " + "Issues getting preferred SD card, setting to 0" + << std::endl; + sdCard = sd::SdCard::SLOT_0; + } + sdInfo.active = sdCard.value(); + if (sdStates.first == sd::SdState::MOUNTED) { + sdcMan->setActiveSdCard(sd::SdCard::SLOT_0); + } else if (sdStates.second == sd::SdState::MOUNTED) { + sdcMan->setActiveSdCard(sd::SdCard::SLOT_1); + } + currMntPrefix = sdcMan->getCurrentMountPrefix(); + + getCurrentBootCopy(CURRENT_CHIP, CURRENT_COPY); + + initClockFromTimeFile(); + } catch (const std::filesystem::filesystem_error &e) { + sif::error << "CoreController::CoreController: Failed with exception " << e.what() << std::endl; + } + // Add script folder to path + char *currentEnvPath = getenv("PATH"); + std::string updatedEnvPath = std::string(currentEnvPath) + ":/home/root/scripts:/usr/local/bin"; + setenv("PATH", updatedEnvPath.c_str(), true); + sdCardCheckCd.timeOut(); + eventQueue = QueueFactory::instance()->createMessageQueue(5, EventMessage::MAX_MESSAGE_SIZE); +} + +CoreController::~CoreController() {} + +ReturnValue_t CoreController::handleCommandMessage(CommandMessage *message) { + ReturnValue_t result = paramHelper.handleParameterMessage(message); + if (result == returnvalue::OK) { + return result; + } + return ExtendedControllerBase::handleCommandMessage(message); +} + +void CoreController::performControlOperation() { +#if OBSW_THREAD_TRACING == 1 + trace::threadTrace(opCounter, "CORE CTRL"); +#endif + EventMessage event; + for (ReturnValue_t result = eventQueue->receiveMessage(&event); result == returnvalue::OK; + result = eventQueue->receiveMessage(&event)) { + switch (event.getEvent()) { + case (GpsHyperion::GPS_FIX_CHANGE): { + gpsFix = static_cast(event.getParameter2()); + break; + } + } + } + sdStateMachine(); + performMountedSdCardOperations(); + readHkData(); + if (dumpContext.active) { + dirListingDumpHandler(); + } + + if (shellCmdIsExecuting) { + bool replyReceived = false; + // TODO: We could read the data in the ring buffer and send it as an action data reply. + if (cmdExecutor.check(replyReceived) == CommandExecutor::EXECUTION_FINISHED) { + actionHelper.finish(true, successRecipient, core::EXECUTE_SHELL_CMD_BLOCKING); + shellCmdIsExecuting = false; + cmdReplyBuf.clear(); + while (not cmdRepliesSizes.empty()) { + cmdRepliesSizes.pop(); + } + successRecipient = MessageQueueIF::NO_QUEUE; + } + } + opDivider5.checkAndIncrement(); + opDivider10.checkAndIncrement(); +} + +ReturnValue_t CoreController::initializeLocalDataPool(localpool::DataPool &localDataPoolMap, + LocalDataPoolManager &poolManager) { + localDataPoolMap.emplace(core::TEMPERATURE, &tempPoolEntry); + localDataPoolMap.emplace(core::PS_VOLTAGE, &psVoltageEntry); + localDataPoolMap.emplace(core::PL_VOLTAGE, &plVoltageEntry); + poolManager.subscribeForRegularPeriodicPacket({hkSet.getSid(), enableHkSet, 60.0}); + return returnvalue::OK; +} + +LocalPoolDataSetBase *CoreController::getDataSetHandle(sid_t sid) { + if (sid.ownerSetId == core::HK_SET_ID) { + return &hkSet; + } + return nullptr; +} + +ReturnValue_t CoreController::initialize() { + ReturnValue_t result = ExtendedControllerBase::initialize(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::initialize: Base init failed" << std::endl; + } + + result = paramHelper.initialize(); + if (result != returnvalue::OK) { + return result; + } + + EventManagerIF *eventManager = + ObjectManager::instance()->get(objects::EVENT_MANAGER); + if (eventManager == nullptr or eventQueue == nullptr) { + sif::warning << "CoreController::initialize: No valid event manager found or " + "queue invalid" + << std::endl; + } + result = eventManager->registerListener(eventQueue->getId()); + if (result != returnvalue::OK) { + sif::warning << "CoreController::initialize: Registering as event listener failed" << std::endl; + } + result = eventManager->subscribeToEvent(eventQueue->getId(), + event::getEventId(GpsHyperion::GPS_FIX_CHANGE)); + if (result != returnvalue::OK) { + sif::warning << "Subscribing for GPS GPS_FIX_CHANGE event failed" << std::endl; + } + triggerEvent(core::REBOOT_SW, CURRENT_CHIP, CURRENT_COPY); + announceCurrentImageInfo(); + announceVersionInfo(); + SdCardManager::SdStatePair sdStates; + sdcMan->getSdCardsStatus(sdStates); + announceSdInfo(sdStates); + sdStateMachine(); + result = scratch::writeNumber(scratch::ALLOC_FAILURE_COUNT, 0); + if (result != returnvalue::OK) { + sif::warning << "CoreController::initialize: Setting up alloc failure " + "count failed" + << std::endl; + } + return result; +} + +ReturnValue_t CoreController::initializeAfterTaskCreation() { + ReturnValue_t result = returnvalue::OK; + if (BLOCKING_SD_INIT) { + result = initSdCardBlocking(); + if (result != returnvalue::OK and result != SdCardManager::ALREADY_MOUNTED) { + sif::warning << "CoreController::CoreController: SD card init failed" << std::endl; + } + } + sdStateMachine(); + performMountedSdCardOperations(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::initialize: Version initialization failed" << std::endl; + } + updateProtInfo(); + return ExtendedControllerBase::initializeAfterTaskCreation(); +} + +ReturnValue_t CoreController::executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t *data, size_t size) { + using namespace core; + switch (actionId) { + case (ANNOUNCE_VERSION): { + announceVersionInfo(); + return HasActionsIF::EXECUTION_FINISHED; + } + case (ANNOUNCE_BOOT_COUNTS): { + announceBootCounts(); + return HasActionsIF::EXECUTION_FINISHED; + } + case (ANNOUNCE_CURRENT_IMAGE): { + announceCurrentImageInfo(); + return HasActionsIF::EXECUTION_FINISHED; + } + case (LIST_DIRECTORY_INTO_FILE): { + return actionListDirectoryIntoFile(actionId, commandedBy, data, size); + } + case (LIST_DIRECTORY_DUMP_DIRECTLY): { + return actionListDirectoryDumpDirectly(actionId, commandedBy, data, size); + } + case (CP_HELPER): { + CpHelperParser parser(data, size); + ReturnValue_t result = parser.parse(); + if (result != returnvalue::OK) { + return result; + } + std::ostringstream oss("cp ", std::ostringstream::ate); + if (parser.isForceOptSet()) { + oss << "-f "; + } + if (parser.isRecursiveOptSet()) { + oss << "-r "; + } + auto &sourceTgt = parser.destTgtPair(); + oss << sourceTgt.sourceName << " " << sourceTgt.targetName; + sif::info << "CoreController: Performing copy command: " << oss.str() << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (MV_HELPER): { + MvHelperParser parser(data, size); + ReturnValue_t result = parser.parse(); + if (result != returnvalue::OK) { + return result; + } + std::ostringstream oss("mv ", std::ostringstream::ate); + auto &sourceTgt = parser.destTgtPair(); + oss << sourceTgt.sourceName << " " << sourceTgt.targetName; + sif::info << "CoreController: Performing move command: " << oss.str() << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (RM_HELPER): { + RmHelperParser parser(data, size); + ReturnValue_t result = parser.parse(); + if (result != returnvalue::OK) { + return result; + } + std::ostringstream oss("rm ", std::ostringstream::ate); + if (parser.isRecursiveOptSet() or parser.isForceOptSet()) { + oss << "-"; + } + if (parser.isRecursiveOptSet()) { + oss << "r"; + } + if (parser.isForceOptSet()) { + oss << "f"; + } + size_t removeTargetSize = 0; + const char *removeTgt = parser.getRemoveTarget(removeTargetSize); + oss << " " << removeTgt; + sif::info << "CoreController: Performing remove command: " << oss.str() << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (MKDIR_HELPER): { + if (size < 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + std::string createdDir = std::string(reinterpret_cast(data), size); + std::ostringstream oss("mkdir ", std::ostringstream::ate); + oss << createdDir; + sif::info << "CoreController: Performing directory creation: " << oss.str() << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (SWITCH_REBOOT_FILE_HANDLING): { + if (size < 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + std::string path = sdcMan->getCurrentMountPrefix() + REBOOT_WATCHDOG_FILE; + parseRebootWatchdogFile(path, rebootWatchdogFile); + if (data[0] == 0) { + rebootWatchdogFile.enabled = false; + rewriteRebootWatchdogFile(rebootWatchdogFile); + } else if (data[0] == 1) { + rebootWatchdogFile.enabled = true; + rewriteRebootWatchdogFile(rebootWatchdogFile); + } else { + return HasActionsIF::INVALID_PARAMETERS; + } + return HasActionsIF::EXECUTION_FINISHED; + } + case (READ_REBOOT_MECHANISM_INFO): { + std::string path = sdcMan->getCurrentMountPrefix() + REBOOT_WATCHDOG_FILE; + parseRebootWatchdogFile(path, rebootWatchdogFile); + RebootWatchdogPacket packet(rebootWatchdogFile); + ReturnValue_t result = actionHelper.reportData(commandedBy, actionId, &packet); + if (result != returnvalue::OK) { + return result; + } + return HasActionsIF::EXECUTION_FINISHED; + } + case (RESET_REBOOT_COUNTERS): { + if (size == 0) { + resetRebootWatchdogCounters(xsc::ALL_CHIP, xsc::ALL_COPY); + } else if (size == 2) { + if (data[0] > 1 or data[1] > 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + resetRebootWatchdogCounters(static_cast(data[0]), + static_cast(data[1])); + } + return HasActionsIF::EXECUTION_FINISHED; + } + case (OBSW_UPDATE_FROM_SD_0): { + return executeSwUpdate(SwUpdateSources::SD_0, data, size); + } + case (OBSW_UPDATE_FROM_SD_1): { + return executeSwUpdate(SwUpdateSources::SD_1, data, size); + } + case (OBSW_UPDATE_FROM_TMP): { + return executeSwUpdate(SwUpdateSources::TMP_DIR, data, size); + } + case (SWITCH_TO_SD_0): { + if (not startSdStateMachine(sd::SdCard::SLOT_0, SdCfgMode::COLD_REDUNDANT, commandedBy, + actionId)) { + return HasActionsIF::IS_BUSY; + } + // Completion will be reported by SD card state machine + return returnvalue::OK; + } + case (SWITCH_TO_SD_1): { + if (not startSdStateMachine(sd::SdCard::SLOT_1, SdCfgMode::COLD_REDUNDANT, commandedBy, + actionId)) { + return HasActionsIF::IS_BUSY; + } + // Completion will be reported by SD card state machine + return returnvalue::OK; + } + case (SWITCH_TO_BOTH_SD_CARDS): { + // An active SD still needs to be specified because the system needs to know which SD + // card to use for regular operations like telemetry storage. + if (size != 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + if (data[0] != 0 and data[0] != 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + auto active = static_cast(data[0]); + if (not startSdStateMachine(active, SdCfgMode::HOT_REDUNDANT, commandedBy, actionId)) { + return HasActionsIF::IS_BUSY; + } + // Completion will be reported by SD card state machine + return returnvalue::OK; + } + case (SYSTEMCTL_CMD_EXECUTOR): { + // Expect one byte systemctl command type and a unit name with at least one byte as minimum. + if (size < 2) { + return HasActionsIF::INVALID_PARAMETERS; + } + if (data[0] >= core::SystemctlCmd::NUM_CMDS) { + return HasActionsIF::INVALID_PARAMETERS; + } + core::SystemctlCmd cmdType = static_cast(data[0]); + std::string unitName = std::string(reinterpret_cast(data + 1), size - 1); + std::ostringstream oss("systemctl ", std::ostringstream::ate); + switch (cmdType) { + case (core::SystemctlCmd::START): { + oss << "start "; + break; + } + case (core::SystemctlCmd::STOP): { + oss << "stop "; + break; + } + case (core::SystemctlCmd::RESTART): { + oss << "restart "; + break; + } + default: { + return HasActionsIF::INVALID_PARAMETERS; + } + } + oss << unitName; + int result = std::system(oss.str().c_str()); + if (result != 0) { + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (SWITCH_IMG_LOCK): { + if (size != 3) { + return HasActionsIF::INVALID_PARAMETERS; + } + if (data[1] > 1 or data[2] > 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + setRebootMechanismLock(data[0], static_cast(data[1]), + static_cast(data[2])); + return HasActionsIF::EXECUTION_FINISHED; + } + case (SET_MAX_REBOOT_CNT): { + if (size < 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + std::string path = sdcMan->getCurrentMountPrefix() + REBOOT_WATCHDOG_FILE; + // Disable the reboot file mechanism + parseRebootWatchdogFile(path, rebootWatchdogFile); + rebootWatchdogFile.maxCount = data[0]; + rewriteRebootWatchdogFile(rebootWatchdogFile); + return HasActionsIF::EXECUTION_FINISHED; + } + case (XSC_REBOOT_OBC): { + // Warning: This function will never return, because it reboots the system + return actionXscReboot(data, size); + } + case (REBOOT_OBC): { + // Warning: This function will never return, because it reboots the system + return actionReboot(data, size); + } + case (EXECUTE_SHELL_CMD_BLOCKING): { + std::string cmdToExecute = std::string(reinterpret_cast(data), size); + int result = std::system(cmdToExecute.c_str()); + if (result != 0) { + // TODO: Data reply with returnalue maybe? + return returnvalue::FAILED; + } + return EXECUTION_FINISHED; + } + case (EXECUTE_SHELL_CMD_NON_BLOCKING): { + std::string cmdToExecute = std::string(reinterpret_cast(data), size); + if (cmdExecutor.getCurrentState() == CommandExecutor::States::PENDING or + shellCmdIsExecuting) { + return HasActionsIF::IS_BUSY; + } + cmdExecutor.load(cmdToExecute, false, false); + ReturnValue_t result = cmdExecutor.execute(); + if (result != returnvalue::OK) { + return result; + } + shellCmdIsExecuting = true; + successRecipient = commandedBy; + return returnvalue::OK; + } + case (UPDATE_LEAP_SECONDS): { + if (size != sizeof(uint16_t)) { + return HasActionsIF::INVALID_PARAMETERS; + } + ReturnValue_t result = actionUpdateLeapSeconds(data); + if (result != returnvalue::OK) { + return result; + } + return HasActionsIF::EXECUTION_FINISHED; + } + default: { + return HasActionsIF::INVALID_ACTION_ID; + } + } +} + +ReturnValue_t CoreController::checkModeCommand(Mode_t mode, Submode_t submode, + uint32_t *msToReachTheMode) { + return returnvalue::OK; +} + +ReturnValue_t CoreController::initSdCardBlocking() { + // Create update status file + ReturnValue_t result = sdcMan->updateSdCardStateFile(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::initialize: Updating SD card state file failed" << std::endl; + } + if (sdInfo.cfgMode == SdCfgMode::PASSIVE) { + sif::info << "No SD card initialization will be performed" << std::endl; + return returnvalue::OK; + } + + result = sdcMan->getSdCardsStatus(sdInfo.currentState); + if (result != returnvalue::OK) { + sif::warning << "Getting SD card activity status failed" << std::endl; + } + + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + updateInternalSdInfo(); + sif::info << "Cold redundant SD card configuration, preferred SD card: " + << static_cast(sdInfo.active) << std::endl; + result = sdColdRedundantBlockingInit(); + // Update status file + sdcMan->updateSdCardStateFile(); + return result; + } + if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT) { + sif::info << "Hot redundant SD card configuration" << std::endl; + sdCardSetup(sd::SdCard::SLOT_0, sd::SdState::MOUNTED, "0", false); + sdCardSetup(sd::SdCard::SLOT_1, sd::SdState::MOUNTED, "1", false); + // Update status file + sdcMan->updateSdCardStateFile(); + } + return returnvalue::OK; +} + +ReturnValue_t CoreController::sdStateMachine() { + ReturnValue_t result = returnvalue::OK; + SdCardManager::Operations operation; + + if (sdFsmState == SdStates::IDLE) { + // Nothing to do + return result; + } + + if (sdFsmState == SdStates::START) { + // Init will be performed by separate function + if (BLOCKING_SD_INIT) { + sdFsmState = SdStates::IDLE; + sdInfo.initFinished = true; + return result; + } else { + // Still update SD state file + if (sdInfo.cfgMode == SdCfgMode::PASSIVE) { + sdFsmState = SdStates::UPDATE_SD_INFO_END; + } else { + sdInfo.cycleCount = 0; + sdInfo.commandPending = false; + sdFsmState = SdStates::UPDATE_SD_INFO_START; + } + } + } + + // This lambda checks the non-blocking operation of the SD card manager and assigns the new + // state on success. It returns 0 for an operation success, -1 for failed operations, and 1 + // for pending operations + auto nonBlockingSdcOpChecking = [&](SdStates newStateOnSuccess, uint16_t maxCycleCount, + std::string opPrintout) { + SdCardManager::OpStatus status = sdcMan->checkCurrentOp(operation); + if (status == SdCardManager::OpStatus::SUCCESS or sdInfo.cycleCount > maxCycleCount) { + sdFsmState = newStateOnSuccess; + sdInfo.commandPending = false; + if (sdInfo.cycleCount > maxCycleCount) { + sif::warning << "CoreController::sdStateMachine: " << opPrintout << " takes too long" + << std::endl; + sdInfo.cycleCount = 0; + return -1; + } + sdInfo.cycleCount = 0; + return 0; + }; + return 1; + }; + + if (sdFsmState == SdStates::UPDATE_SD_INFO_START) { + if (not sdInfo.commandPending) { + // Create updated status file + result = sdcMan->updateSdCardStateFile(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::sdStateMachine: Updating SD card state file failed" + << std::endl; + } + result = sdcMan->getSdCardsStatus(sdInfo.currentState); + updateInternalSdInfo(); + auto currentlyActiveSdc = sdcMan->getActiveSdCard(); + // Used/active SD card switches, so mark SD card unusable so other tasks have some time + // registering the unavailable SD card. + if (not currentlyActiveSdc.has_value() or + ((currentlyActiveSdc.value() == sd::SdCard::SLOT_0) and + (sdInfo.active == sd::SdCard::SLOT_1)) or + ((currentlyActiveSdc.value() == sd::SdCard::SLOT_1) and + (sdInfo.active == sd::SdCard::SLOT_0))) { + sdInfo.lockSdCardUsage = true; + } + if (sdInfo.lockSdCardUsage) { + sdcMan->markUnusable(); + } + if (sdInfo.active != sd::SdCard::SLOT_0 and sdInfo.active != sd::SdCard::SLOT_1) { + sif::warning << "Preferred SD card invalid. Setting to card 0.." << std::endl; + sdInfo.active = sd::SdCard::SLOT_0; + } + if (result != returnvalue::OK) { + sif::warning << "Getting SD card activity status failed" << std::endl; + } + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + sif::info << "Cold redundant SD card configuration, target SD card: " + << static_cast(sdInfo.active) << std::endl; + } + SdStates tgtState = SdStates::IDLE; + bool skipCycles = sdInfo.lockSdCardUsage; + // Need to do different things depending on state of SD card which will be active. + if (sdInfo.activeState == sd::SdState::MOUNTED) { + // Already mounted, so we can perform handling of the other side. +#if OBSW_VERBOSE_LEVEL >= 1 + std::string mountString; + if (sdInfo.active == sd::SdCard::SLOT_0) { + mountString = config::SD_0_MOUNT_POINT; + } else { + mountString = config::SD_1_MOUNT_POINT; + } + sif::info << "SD card " << sdInfo.activeChar << " already on and mounted at " << mountString + << std::endl; +#endif + sdcMan->setActiveSdCard(sdInfo.active); + currMntPrefix = sdcMan->getCurrentMountPrefix(); + tgtState = SdStates::DETERMINE_OTHER; + } else if (sdInfo.activeState == sd::SdState::OFF) { + // It's okay to do the delay after swichting active SD on, no one can use it anyway.. + sdCardSetup(sdInfo.active, sd::SdState::ON, sdInfo.activeChar, false); + sdInfo.commandPending = true; + // Do not skip cycles here, would mess up the state machine. We skip the cycles after + // the SD card was switched on. + skipCycles = false; + // Remain on the current state. + tgtState = sdFsmState; + } else if (sdInfo.activeState == sd::SdState::ON) { + // We can do the delay before mounting where applicable. + tgtState = SdStates::MOUNT_SELF; + } + if (skipCycles) { + sdFsmState = SdStates::SKIP_TWO_CYCLES_IF_SD_LOCKED; + fsmStateAfterDelay = tgtState; + sdInfo.skippedCyclesCount = 0; + } else { + sdFsmState = tgtState; + } + } else { + if (nonBlockingSdcOpChecking(SdStates::MOUNT_SELF, 10, "Setting SDC state") <= 0) { + sdInfo.activeState = sd::SdState::ON; + currentStateSetter(sdInfo.active, sd::SdState::ON); + // Skip the two cycles now. + if (sdInfo.lockSdCardUsage) { + sdFsmState = SdStates::SKIP_TWO_CYCLES_IF_SD_LOCKED; + fsmStateAfterDelay = SdStates::MOUNT_SELF; + sdInfo.skippedCyclesCount = 0; + } + } + } + } + + if (sdFsmState == SdStates::SKIP_TWO_CYCLES_IF_SD_LOCKED) { + sdInfo.skippedCyclesCount++; + // Count to three because this branch will run in the same FSM cycle. + if (sdInfo.skippedCyclesCount == 3) { + sdFsmState = fsmStateAfterDelay; + fsmStateAfterDelay = SdStates::IDLE; + sdInfo.skippedCyclesCount = 0; + } + } + + if (sdFsmState == SdStates::MOUNT_SELF) { + if (not sdInfo.commandPending) { + result = sdCardSetup(sdInfo.active, sd::SdState::MOUNTED, sdInfo.activeChar); + sdInfo.commandPending = true; + } else { + if (nonBlockingSdcOpChecking(SdStates::DETERMINE_OTHER, 5, "Mounting SD card") <= 0) { + sdcMan->setActiveSdCard(sdInfo.active); + currMntPrefix = sdcMan->getCurrentMountPrefix(); + sdInfo.activeState = sd::SdState::MOUNTED; + currentStateSetter(sdInfo.active, sd::SdState::MOUNTED); + } + } + } + + if (sdFsmState == SdStates::DETERMINE_OTHER) { + // Determine whether any additional operations have to be done for the other SD card + // 1. Cold redundant case: Other SD card needs to be unmounted and switched off + // 2. Hot redundant case: Other SD card needs to be mounted and switched on + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + if (sdInfo.otherState == sd::SdState::ON) { + sdFsmState = SdStates::SET_STATE_OTHER; + } else if (sdInfo.otherState == sd::SdState::MOUNTED) { + sdFsmState = SdStates::MOUNT_UNMOUNT_OTHER; + } else { + // Is already off, update info, but with a small delay + sdFsmState = SdStates::SKIP_CYCLE_BEFORE_INFO_UPDATE; + } + } else if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT) { + if (sdInfo.otherState == sd::SdState::OFF) { + sdFsmState = SdStates::SET_STATE_OTHER; + } else if (sdInfo.otherState == sd::SdState::ON) { + sdFsmState = SdStates::MOUNT_UNMOUNT_OTHER; + } else { + // Is already on and mounted, update info + sdFsmState = SdStates::SKIP_CYCLE_BEFORE_INFO_UPDATE; + } + } + } + + if (sdFsmState == SdStates::SET_STATE_OTHER) { + // Set state of other SD card to ON or OFF, depending on redundancy mode + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + if (not sdInfo.commandPending) { + result = sdCardSetup(sdInfo.other, sd::SdState::OFF, sdInfo.otherChar, false); + sdInfo.commandPending = true; + } else { + if (nonBlockingSdcOpChecking(SdStates::SKIP_CYCLE_BEFORE_INFO_UPDATE, 10, + "Switching off other SD card") <= 0) { + sdInfo.otherState = sd::SdState::OFF; + currentStateSetter(sdInfo.other, sd::SdState::OFF); + } + } + } else if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT) { + if (not sdInfo.commandPending) { + result = sdCardSetup(sdInfo.other, sd::SdState::ON, sdInfo.otherChar, false); + sdInfo.commandPending = true; + } else { + if (nonBlockingSdcOpChecking(SdStates::MOUNT_UNMOUNT_OTHER, 10, + "Switching on other SD card") <= 0) { + sdInfo.otherState = sd::SdState::ON; + currentStateSetter(sdInfo.other, sd::SdState::ON); + } + } + } + } + + if (sdFsmState == SdStates::MOUNT_UNMOUNT_OTHER) { + // Mount or unmount other SD card, depending on redundancy mode + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + if (not sdInfo.commandPending) { + result = sdCardSetup(sdInfo.other, sd::SdState::ON, sdInfo.otherChar); + sdInfo.commandPending = true; + } else { + if (nonBlockingSdcOpChecking(SdStates::SET_STATE_OTHER, 10, "Unmounting other SD card") <= + 0) { + sdInfo.otherState = sd::SdState::ON; + currentStateSetter(sdInfo.other, sd::SdState::ON); + } else { + sdInfo.otherState = sd::SdState::ON; + currentStateSetter(sdInfo.other, sd::SdState::ON); + sdFsmState = SdStates::SET_STATE_OTHER; + } + } + } else if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT) { + if (not sdInfo.commandPending) { + result = sdCardSetup(sdInfo.other, sd::SdState::MOUNTED, sdInfo.otherChar); + sdInfo.commandPending = true; + } else { + if (nonBlockingSdcOpChecking(SdStates::UPDATE_SD_INFO_END, 4, "Mounting other SD card") <= + 0) { + sdInfo.otherState = sd::SdState::MOUNTED; + currentStateSetter(sdInfo.other, sd::SdState::MOUNTED); + } + } + } + } + + if (sdFsmState == SdStates::SKIP_CYCLE_BEFORE_INFO_UPDATE) { + sdFsmState = SdStates::UPDATE_SD_INFO_END; + } else if (sdFsmState == SdStates::UPDATE_SD_INFO_END) { + // Update status file + result = sdcMan->updateSdCardStateFile(); + if (result != returnvalue::OK) { + sif::warning << "CoreController: Updating SD card state file failed" << std::endl; + } + updateInternalSdInfo(); + // Mark usable again in any case. + sdcMan->markUsable(); + sdInfo.commandPending = false; + sdFsmState = SdStates::IDLE; + sdInfo.cycleCount = 0; + sdcMan->setBlocking(false); + sdcMan->getSdCardsStatus(sdInfo.currentState); + if (sdCommandingInfo.cmdPending) { + sdCommandingInfo.cmdPending = false; + actionHelper.finish(true, sdCommandingInfo.commander, sdCommandingInfo.actionId, + returnvalue::OK); + } + const char *modeStr = "UNKNOWN"; + if (sdInfo.cfgMode == SdCfgMode::COLD_REDUNDANT) { + modeStr = "COLD REDUNDANT"; + } else if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT) { + modeStr = "HOT REDUNDANT"; + } + sif::info << "SD card update into " << modeStr + << " mode finished. Active SD: " << sdInfo.activeChar << std::endl; + announceSdInfo(sdInfo.currentState); + if (not sdInfo.initFinished) { + updateInternalSdInfo(); + sdInfo.initFinished = true; + sif::info << "SD card initialization finished" << std::endl; + } + } + + sdInfo.cycleCount++; + return returnvalue::OK; +} + +void CoreController::currentStateSetter(sd::SdCard sdCard, sd::SdState newState) { + if (sdCard == sd::SdCard::SLOT_0) { + sdInfo.currentState.first = newState; + } else { + sdInfo.currentState.second = newState; + } +} + +ReturnValue_t CoreController::sdCardSetup(sd::SdCard sdCard, sd::SdState targetState, + std::string sdChar, bool printOutput) { + std::string mountString; + sdcMan->setPrintCommandOutput(printOutput); + if (sdCard == sd::SdCard::SLOT_0) { + mountString = config::SD_0_MOUNT_POINT; + } else { + mountString = config::SD_1_MOUNT_POINT; + } + + sd::SdState state = sd::SdState::OFF; + if (sdCard == sd::SdCard::SLOT_0) { + state = sdInfo.currentState.first; + } else { + state = sdInfo.currentState.second; + } + if (state == sd::SdState::MOUNTED) { + if (targetState == sd::SdState::OFF) { + sif::info << "Switching off SD card " << sdChar << std::endl; + return sdcMan->switchOffSdCard(sdCard, sdInfo.currentState, true); + } else if (targetState == sd::SdState::ON) { + sif::info << "Unmounting SD card " << sdChar << std::endl; + return sdcMan->unmountSdCard(sdCard); + } else { + std::error_code e; + if (std::filesystem::exists(mountString, e)) { + sif::info << "SD card " << sdChar << " already on and mounted at " << mountString + << std::endl; + return SdCardManager::ALREADY_MOUNTED; + } + sif::error << "SD card mounted but expected mount point " << mountString << " not found!" + << std::endl; + return SdCardManager::MOUNT_ERROR; + } + } + + if (state == sd::SdState::OFF) { + if (targetState == sd::SdState::MOUNTED) { + sif::info << "Switching on and mounting SD card " << sdChar << " at " << mountString + << std::endl; + return sdcMan->switchOnSdCard(sdCard, true, &sdInfo.currentState); + } else if (targetState == sd::SdState::ON) { + sif::info << "Switching on SD card " << sdChar << std::endl; + return sdcMan->switchOnSdCard(sdCard, false, &sdInfo.currentState); + } + } + + else if (state == sd::SdState::ON) { + if (targetState == sd::SdState::MOUNTED) { + sif::info << "Mounting SD card " << sdChar << " at " << mountString << std::endl; + return sdcMan->mountSdCard(sdCard); + } else if (targetState == sd::SdState::OFF) { + sif::info << "Switching off SD card " << sdChar << std::endl; + return sdcMan->switchOffSdCard(sdCard, sdInfo.currentState, false); + } + } else { + sif::warning << "CoreController::sdCardSetup: Invalid state for this call" << std::endl; + } + return returnvalue::OK; +} + +ReturnValue_t CoreController::sdColdRedundantBlockingInit() { + ReturnValue_t result = returnvalue::OK; + + result = sdCardSetup(sdInfo.active, sd::SdState::MOUNTED, sdInfo.activeChar); + if (result != SdCardManager::ALREADY_MOUNTED and result != returnvalue::OK) { + sif::warning << "Setting up preferred card " << sdInfo.otherChar + << " in cold redundant mode failed" << std::endl; + // Try other SD card and mark set up operation as failed + sdCardSetup(sdInfo.active, sd::SdState::MOUNTED, sdInfo.activeChar); + result = returnvalue::FAILED; + } + + if (result != returnvalue::FAILED and sdInfo.otherState != sd::SdState::OFF) { + sif::info << "Switching off secondary SD card " << sdInfo.otherChar << std::endl; + // Switch off other SD card in cold redundant mode if setting up preferred one worked + // without issues + ReturnValue_t result2 = sdcMan->switchOffSdCard(sdInfo.other, sdInfo.currentState, true); + if (result2 != returnvalue::OK and result2 != SdCardManager::ALREADY_OFF) { + sif::warning << "Switching off secondary SD card " << sdInfo.otherChar + << " in cold redundant mode failed" << std::endl; + } + } + return result; +} + +ReturnValue_t CoreController::incrementAllocationFailureCount() { + uint32_t count = 0; + ReturnValue_t result = scratch::readNumber(scratch::ALLOC_FAILURE_COUNT, count); + if (result != returnvalue::OK) { + return result; + } + count++; + return scratch::writeNumber(scratch::ALLOC_FAILURE_COUNT, count); +} + +ReturnValue_t CoreController::initVersionFile() { + using namespace fsfw; + std::string unameFileName = "/tmp/uname_version.txt"; + // TODO: No -v flag for now. If the kernel version is used, need to cut off first few letters + std::string unameCmd = "uname -mnrso > " + unameFileName; + int result = std::system(unameCmd.c_str()); + if (result != 0) { + utility::handleSystemError(result, "CoreController::versionFileInit"); + } + std::ifstream unameFile(unameFileName); + std::string unameLine; + if (not std::getline(unameFile, unameLine)) { + sif::warning << "CoreController::versionFileInit: Retrieving uname line failed" << std::endl; + } + + std::string fullObswVersionString = "OBSW: v" + std::to_string(common::OBSW_VERSION_MAJOR) + "." + + std::to_string(common::OBSW_VERSION_MINOR) + "." + + std::to_string(common::OBSW_VERSION_REVISION); + char versionString[16] = {}; + fsfw::FSFW_VERSION.getVersion(versionString, sizeof(versionString)); + std::string fullFsfwVersionString = "FSFW: v" + std::string(versionString); + std::string systemString = "System: " + unameLine; + std::string versionFilePath = currMntPrefix + VERSION_FILE; + std::fstream versionFile; + + std::error_code e; + if (not std::filesystem::exists(versionFilePath, e)) { + sif::info << "Writing version file " << versionFilePath << ".." << std::endl; + versionFile.open(versionFilePath, std::ios_base::out); + versionFile << fullObswVersionString << std::endl; + versionFile << fullFsfwVersionString << std::endl; + versionFile << systemString << std::endl; + return returnvalue::OK; + } + + // Check whether any version has changed + bool createNewFile = false; + versionFile.open(versionFilePath); + std::string currentVersionString; + uint8_t idx = 0; + while (std::getline(versionFile, currentVersionString)) { + if (idx == 0) { + if (currentVersionString != fullObswVersionString) { + sif::info << "OBSW version changed" << std::endl; + sif::info << "From " << currentVersionString << " to " << fullObswVersionString + << std::endl; + createNewFile = true; + } + } else if (idx == 1) { + if (currentVersionString != fullFsfwVersionString) { + sif::info << "FSFW version changed" << std::endl; + sif::info << "From " << currentVersionString << " to " << fullFsfwVersionString + << std::endl; + createNewFile = true; + } + } else if (idx == 2) { + if (currentVersionString != systemString) { + sif::info << "System version changed" << std::endl; + sif::info << "Old: " << currentVersionString << std::endl; + sif::info << "New: " << systemString << std::endl; + createNewFile = true; + } + } else { + sif::warning << "Invalid version file! Rewriting it.." << std::endl; + createNewFile = true; + } + idx++; + } + + // Overwrite file if necessary + if (createNewFile) { + sif::info << "Rewriting version.txt file with updated versions.." << std::endl; + versionFile.close(); + versionFile.open(versionFilePath, std::ios_base::out | std::ios_base::trunc); + versionFile << fullObswVersionString << std::endl; + versionFile << fullFsfwVersionString << std::endl; + versionFile << systemString << std::endl; + } + + return returnvalue::OK; +} + +ReturnValue_t CoreController::actionListDirectoryDumpDirectly(ActionId_t actionId, + MessageQueueId_t commandedBy, + const uint8_t *data, size_t size) { + core::ListDirectoryCmdBase parser(data, size); + ReturnValue_t result = parser.parse(); + if (result != returnvalue::OK) { + return result; + } + + std::ostringstream oss("ls -l", std::ostringstream::ate); + if (parser.aFlagSet()) { + oss << "a"; + } + if (parser.rFlagSet()) { + oss << "R"; + } + + size_t repoNameLen = 0; + const char *repoName = parser.getRepoName(repoNameLen); + + oss << " " << repoName << " > " << LIST_DIR_DUMP_WORK_FILE; + sif::info << "Executing " << oss.str() << " for direct dump"; + if (parser.compressionOptionSet()) { + sif::info << " with compression"; + } + sif::info << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + utility::handleSystemError(result, "CoreController::actionListDirectoryDumpDirectly"); + return returnvalue::FAILED; + } + if (parser.compressionOptionSet()) { + std::string compressedName = LIST_DIR_DUMP_WORK_FILE + std::string(".gz"); + oss.str(""); + oss << "gzip " << LIST_DIR_DUMP_WORK_FILE; + ret = std::system(oss.str().c_str()); + if (ret != 0) { + utility::handleSystemError(result, "CoreController::actionListDirectoryDumpDirectly"); + return returnvalue::FAILED; + } + oss.str(""); + // Overwrite the work file with the compressed archive. + oss << "mv " << compressedName << " " << LIST_DIR_DUMP_WORK_FILE; + ret = std::system(oss.str().c_str()); + if (ret != 0) { + utility::handleSystemError(result, "CoreController::actionListDirectoryDumpDirectly"); + return returnvalue::FAILED; + } + } + dirListingBuf[8] = parser.compressionOptionSet(); + // First four bytes reserved for segment index. One byte for compression option information + std::strcpy(reinterpret_cast(dirListingBuf.data() + 2 * sizeof(uint32_t) + 1), repoName); + std::ifstream ifile(LIST_DIR_DUMP_WORK_FILE, std::ios::binary); + if (ifile.bad()) { + return returnvalue::FAILED; + } + std::error_code e; + dumpContext.totalFileSize = std::filesystem::file_size(LIST_DIR_DUMP_WORK_FILE, e); + dumpContext.segmentIdx = 0; + dumpContext.dumpedBytes = 0; + size_t nextDumpLen = 0; + size_t dummy = 0; + dumpContext.maxDumpLen = dirListingBuf.size() - 2 * sizeof(uint32_t) - 1 - repoNameLen - 1; + dumpContext.listingDataOffset = 2 * sizeof(uint32_t) + 1 + repoNameLen + 1; + uint32_t chunks = dumpContext.totalFileSize / dumpContext.maxDumpLen; + if (dumpContext.totalFileSize % dumpContext.maxDumpLen != 0) { + chunks++; + } + SerializeAdapter::serialize(&chunks, dirListingBuf.data() + sizeof(uint32_t), &dummy, + dirListingBuf.size() - sizeof(uint32_t), + SerializeIF::Endianness::NETWORK); + while (dumpContext.dumpedBytes < dumpContext.totalFileSize) { + ifile.seekg(dumpContext.dumpedBytes, std::ios::beg); + nextDumpLen = dumpContext.maxDumpLen; + if (dumpContext.totalFileSize - dumpContext.dumpedBytes < dumpContext.maxDumpLen) { + nextDumpLen = dumpContext.totalFileSize - dumpContext.dumpedBytes; + } + SerializeAdapter::serialize(&dumpContext.segmentIdx, dirListingBuf.data(), &dummy, + dirListingBuf.size(), SerializeIF::Endianness::NETWORK); + ifile.read(reinterpret_cast(dirListingBuf.data() + dumpContext.listingDataOffset), + nextDumpLen); + result = actionHelper.reportData(commandedBy, actionId, dirListingBuf.data(), + dumpContext.listingDataOffset + nextDumpLen); + if (result != returnvalue::OK) { + // Remove work file when we are done + std::filesystem::remove(LIST_DIR_DUMP_WORK_FILE, e); + return result; + } + dumpContext.segmentIdx++; + dumpContext.dumpedBytes += nextDumpLen; + // Dump takes multiple task cycles, so cache the dump state and continue dump the next cycles. + if (dumpContext.segmentIdx == 10) { + dumpContext.active = true; + dumpContext.firstDump = true; + dumpContext.commander = commandedBy; + dumpContext.actionId = actionId; + return returnvalue::OK; + } + } + // Remove work file when we are done + std::filesystem::remove(LIST_DIR_DUMP_WORK_FILE, e); + return EXECUTION_FINISHED; +} + +ReturnValue_t CoreController::actionListDirectoryIntoFile(ActionId_t actionId, + MessageQueueId_t commandedBy, + const uint8_t *data, size_t size) { + core::ListDirectoryIntoFile parser(data, size); + ReturnValue_t result = parser.parse(); + if (result != returnvalue::OK) { + return result; + } + + std::ostringstream oss("ls -l", std::ostringstream::ate); + if (parser.aFlagSet()) { + oss << "a"; + } + if (parser.rFlagSet()) { + oss << "R"; + } + + size_t repoNameLen = 0; + const char *repoName = parser.getRepoName(repoNameLen); + size_t targetFileNameLen = 0; + const char *targetFileName = parser.getTargetName(targetFileNameLen); + oss << " " << repoName << " > " << targetFileName; + sif::info << "Executing list directory request, command: " << oss.str() << std::endl; + int ret = std::system(oss.str().c_str()); + if (ret != 0) { + utility::handleSystemError(result, "CoreController::actionListDirectoryIntoFile"); + return returnvalue::FAILED; + } + + // Compression will add a .gz ending. I don't have any issue with this, it makes it explicit + // that this is a compressed file. + if (parser.compressionOptionSet()) { + oss.str(""); + oss << "gzip " << targetFileName; + sif::info << "Compressing directory listing: " << oss.str() << std::endl; + ret = std::system(oss.str().c_str()); + if (ret != 0) { + utility::handleSystemError(result, "CoreController::actionListDirectoryIntoFile"); + return returnvalue::FAILED; + } + } + return EXECUTION_FINISHED; +} + +ReturnValue_t CoreController::initBootCopyFile() { + std::error_code e; + if (not std::filesystem::exists(CURR_COPY_FILE, e)) { + // This file is created by the systemd service eive-early-config so this should + // not happen normally + std::string cmd = "xsc_boot_copy > " + std::string(CURR_COPY_FILE); + int result = std::system(cmd.c_str()); + if (result != 0) { + utility::handleSystemError(result, "CoreController::initBootCopy"); + } + } + return returnvalue::OK; +} + +void CoreController::getCurrentBootCopy(xsc::Chip &chip, xsc::Copy ©) { + xsc_libnor_chip_t xscChip; + xsc_libnor_copy_t xscCopy; + xsc_boot_get_chip_copy(&xscChip, &xscCopy); + // Not really thread-safe but it does not need to be + chip = static_cast(xscChip); + copy = static_cast(xscCopy); +} + +ReturnValue_t CoreController::actionXscReboot(const uint8_t *data, size_t size) { + if (size < 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + bool rebootSameBootCopy = data[0]; + bool protOpPerformed = false; + SdCardManager::instance()->setBlocking(true); + if (rebootSameBootCopy) { +#if OBSW_VERBOSE_LEVEL >= 1 + sif::info << "CoreController::actionPerformReboot: Rebooting on current image" << std::endl; +#endif + gracefulShutdownTasks(xsc::Chip::SELF_CHIP, xsc::Copy::SELF_COPY, protOpPerformed); + int result = std::system("xsc_boot_copy -r"); + if (result != 0) { + utility::handleSystemError(result, "CoreController::executeAction"); + return returnvalue::FAILED; + } + return HasActionsIF::EXECUTION_FINISHED; + } + if (size < 3 or (data[1] > 1 or data[2] > 1)) { + return HasActionsIF::INVALID_PARAMETERS; + } +#if OBSW_VERBOSE_LEVEL >= 1 + sif::info << "CoreController::actionPerformReboot: Rebooting on " << static_cast(data[1]) + << " " << static_cast(data[2]) << std::endl; +#endif + + // Check that the target chip and copy is writeprotected first + generateChipStateFile(); + // If any boot copies are unprotected, protect them here + auto tgtChip = static_cast(data[1]); + auto tgtCopy = static_cast(data[2]); + + performGracefulShutdown(tgtChip, tgtCopy); + return returnvalue::FAILED; +} + +ReturnValue_t CoreController::actionReboot(const uint8_t *data, size_t size) { + bool protOpPerformed = false; + gracefulShutdownTasks(xsc::Chip::CHIP_0, xsc::Copy::COPY_0, protOpPerformed); + std::system("reboot"); + return returnvalue::OK; +} + +ReturnValue_t CoreController::gracefulShutdownTasks(xsc::Chip chip, xsc::Copy copy, + bool &protOpPerformed) { + // Store both sequence counters persistently. + core::SAVE_CFDP_SEQUENCE_COUNT = true; + core::SAVE_PUS_SEQUENCE_COUNT = true; + + sdcMan->setBlocking(true); + sdcMan->markUnusable(); + // Wait two seconds to ensure no one uses the SD cards + TaskFactory::delayTask(2000); + + // Ensure that all writes/reads do finish. + sync(); + + // Unmount and switch off SD cards. This could possibly fix issues with the SD card and is + // the more graceful way to reboot the system. This function takes around 400 ms. + ReturnValue_t result = handleSwitchingSdCardsOffNonBlocking(); + if (result != returnvalue::OK) { + sif::error + << "CoreController::gracefulShutdownTasks: Issues unmounting or switching SD cards off" + << std::endl; + } + + // Ensure that the target chip is writeprotected in any case. + bool wasProtected = handleBootCopyProt(chip, copy, true); + if (wasProtected) { + // TODO: Would be nice to notify operator. But we can't use the filesystem anymore + // and a reboot is imminent. Use scratch buffer? + sif::info << "Running slot was writeprotected before reboot" << std::endl; + } + sif::info << "Graceful shutdown handling done" << std::endl; + // Ensure that all diagnostic prinouts arrive. + TaskFactory::delayTask(50); + return result; +} + +void CoreController::updateInternalSdInfo() { + if (sdInfo.active == sd::SdCard::SLOT_0) { + sdInfo.activeChar = "0"; + sdInfo.otherChar = "1"; + sdInfo.otherState = sdInfo.currentState.second; + sdInfo.activeState = sdInfo.currentState.first; + sdInfo.other = sd::SdCard::SLOT_1; + + } else if (sdInfo.active == sd::SdCard::SLOT_1) { + sdInfo.activeChar = "1"; + sdInfo.otherChar = "0"; + sdInfo.otherState = sdInfo.currentState.first; + sdInfo.activeState = sdInfo.currentState.second; + sdInfo.other = sd::SdCard::SLOT_0; + } else { + sif::warning << "CoreController::updateSdInfoOther: Invalid SD card passed" << std::endl; + } +} + +bool CoreController::sdInitFinished() const { return sdInfo.initFinished; } + +ReturnValue_t CoreController::generateChipStateFile() { + int result = std::system(CHIP_PROT_SCRIPT); + if (result != 0) { + utility::handleSystemError(result, "CoreController::generateChipStateFile"); + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +ReturnValue_t CoreController::setBootCopyProtectionAndUpdateFile(xsc::Chip targetChip, + xsc::Copy targetCopy, + bool protect) { + if (targetChip == xsc::Chip::ALL_CHIP or targetCopy == xsc::Copy::ALL_COPY) { + return returnvalue::FAILED; + } + + bool protOperationPerformed = handleBootCopyProt(targetChip, targetCopy, protect); + if (protOperationPerformed) { + updateProtInfo(); + } + return returnvalue::OK; +} + +bool CoreController::handleBootCopyProt(xsc::Chip targetChip, xsc::Copy targetCopy, bool protect) { + std::ostringstream oss; + oss << "writeprotect "; + if (targetChip == xsc::Chip::SELF_CHIP) { + targetChip = CURRENT_CHIP; + } + if (targetCopy == xsc::Copy::SELF_COPY) { + targetCopy = CURRENT_COPY; + } + if (targetChip == xsc::Chip::CHIP_0) { + oss << "0 "; + } else if (targetChip == xsc::Chip::CHIP_1) { + oss << "1 "; + } + if (targetCopy == xsc::Copy::COPY_0) { + oss << "0 "; + } else if (targetCopy == xsc::Copy::COPY_1) { + oss << "1 "; + } + if (protect) { + oss << "1"; + } else { + oss << "0"; + } + sif::info << "Executing command: " << oss.str() << std::endl; + int result = std::system(oss.str().c_str()); + if (result == 0) { + return true; + } + return false; +} + +ReturnValue_t CoreController::updateProtInfo(bool regenerateChipStateFile) { + using namespace std; + ReturnValue_t result = returnvalue::OK; + if (regenerateChipStateFile) { + result = generateChipStateFile(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::updateProtInfo: Generating chip state file failed" + << std::endl; + return result; + } + } + std::error_code e; + if (not filesystem::exists(CHIP_STATE_FILE, e)) { + return returnvalue::FAILED; + } + ifstream chipStateFile(CHIP_STATE_FILE); + if (not chipStateFile.good()) { + return returnvalue::FAILED; + } + string nextLine; + uint8_t lineCounter = 0; + string word; + while (getline(chipStateFile, nextLine)) { + result = handleProtInfoUpdateLine(nextLine); + if (result != returnvalue::OK) { + sif::warning << "CoreController::updateProtInfo: Protection info update failed!" << std::endl; + return result; + } + ++lineCounter; + if (lineCounter > 4) { + sif::warning << "CoreController::checkAndProtectBootCopy: " + "Line counter larger than 4" + << std::endl; + } + } + return returnvalue::OK; +} + +ReturnValue_t CoreController::handleProtInfoUpdateLine(std::string nextLine) { + using namespace std; + string word; + uint8_t wordIdx = 0; + istringstream iss(nextLine); + xsc::Chip currentChip = xsc::Chip::CHIP_0; + xsc::Copy currentCopy = xsc::Copy::COPY_0; + while (iss >> word) { + if (wordIdx == 1) { + currentChip = static_cast(stoi(word)); + } + if (wordIdx == 3) { + currentCopy = static_cast(stoi(word)); + } + + if (wordIdx == 5) { + if (word == "unlocked.") { + protArray[currentChip][currentCopy] = false; + } else { + protArray[currentChip][currentCopy] = true; + } + } + wordIdx++; + if (wordIdx >= 10) { + break; + } + } + return returnvalue::OK; +} + +void CoreController::performMountedSdCardOperations() { + auto mountedSdCardOp = [&](sd::SdCard sdCard, std::string mntPoint) { + if (not performOneShotSdCardOpsSwitch) { + std::ostringstream path; + path << mntPoint << "/" << core::CONF_FOLDER; + std::error_code e; + if (not std::filesystem::exists(path.str()), e) { + bool created = std::filesystem::create_directory(path.str(), e); + if (not created) { + sif::error << "Could not create CONF folder at " << path.str() << ": " << e.message() + << std::endl; + return; + } + } + initVersionFile(); + ReturnValue_t result = initBootCopyFile(); + if (result != returnvalue::OK) { + sif::warning << "CoreController::CoreController: Boot copy init" << std::endl; + } + if (not timeFileInitDone) { + initClockFromTimeFile(); + } + if (not leapSecondsInitDone) { + initLeapSeconds(); + } + performRebootWatchdogHandling(false); + performRebootCountersHandling(false); + } + backupTimeFileHandler(); + }; + bool someSdCardActive = false; + if (sdInfo.active == sd::SdCard::SLOT_0 and sdcMan->isSdCardUsable(sd::SdCard::SLOT_0)) { + mountedSdCardOp(sd::SdCard::SLOT_0, config::SD_0_MOUNT_POINT); + someSdCardActive = true; + } + if (sdInfo.active == sd::SdCard::SLOT_1 and sdcMan->isSdCardUsable(sd::SdCard::SLOT_1)) { + mountedSdCardOp(sd::SdCard::SLOT_1, config::SD_1_MOUNT_POINT); + someSdCardActive = true; + } + if (someSdCardActive) { + performOneShotSdCardOpsSwitch = true; + } +} + +ReturnValue_t CoreController::performSdCardCheck() { + bool mountedReadOnly = false; + SdCardManager::SdStatePair active; + sdcMan->getSdCardsStatus(active); + if (sdFsmState != SdStates::IDLE) { + return returnvalue::OK; + } + auto sdCardCheck = [&](sd::SdCard sdCard) { + ReturnValue_t result = sdcMan->isSdCardMountedReadOnly(sdCard, mountedReadOnly); + if (result != returnvalue::OK) { + sif::error << "CoreController::performSdCardCheck: Could not check " + "read-only mount state" + << std::endl; + } + if (mountedReadOnly) { + int linuxErrno = 0; + result = sdcMan->performFsck(sdCard, true, linuxErrno); + if (result != returnvalue::OK) { + sif::error << "CoreController::performSdCardCheck: fsck command on SD Card " + << static_cast(sdCard) << " failed with code " << linuxErrno << " | " + << strerror(linuxErrno); + } + result = sdcMan->remountReadWrite(sdCard); + if (result == returnvalue::OK) { + sif::warning << "CoreController::performSdCardCheck: Remounted SD Card " + << static_cast(sdCard) << " read-write"; + } else { + sif::error << "CoreController::performSdCardCheck: Remounting SD Card " + << static_cast(sdCard) << " read-write failed"; + } + } + }; + if (active.first == sd::SdState::MOUNTED) { + sdCardCheck(sd::SdCard::SLOT_0); + } + if (active.second == sd::SdState::MOUNTED) { + sdCardCheck(sd::SdCard::SLOT_1); + } +#if OBSW_SD_CARD_MUST_BE_ON == 1 + // This is FDIR. The core controller will attempt once to get some SD card working + bool someSdCardActive = false; + if ((sdInfo.active == sd::SdCard::SLOT_0 and sdcMan->isSdCardUsable(sd::SdCard::SLOT_0)) or + (sdInfo.active == sd::SdCard::SLOT_1 and sdcMan->isSdCardUsable(sd::SdCard::SLOT_1))) { + someSdCardActive = true; + } + if (not someSdCardActive and remountAttemptFlag) { + triggerEvent(core::NO_SD_CARD_ACTIVE); + initSdCardBlocking(); + remountAttemptFlag = false; + } +#endif + return returnvalue::OK; +} + +void CoreController::performRebootWatchdogHandling(bool recreateFile) { + using namespace std; + std::string path = currMntPrefix + REBOOT_WATCHDOG_FILE; + std::string legacyPath = currMntPrefix + LEGACY_REBOOT_WATCHDOG_FILE; + std::error_code e; + // TODO: Remove at some point in the future. + if (std::filesystem::exists(legacyPath, e)) { + // Old file might still exist, so copy it to new path + std::filesystem::copy(legacyPath, path, std::filesystem::copy_options::overwrite_existing, e); + if (e) { + sif::error << "File copy has failed: " << e.message() << std::endl; + } + } + if (not std::filesystem::exists(path, e) or recreateFile) { +#if OBSW_VERBOSE_LEVEL >= 1 + sif::info << "CoreController::performRebootFileHandling: Recreating reboot watchdog file" + << std::endl; +#endif + rebootWatchdogFile.enabled = false; + rebootWatchdogFile.img00Cnt = 0; + rebootWatchdogFile.img01Cnt = 0; + rebootWatchdogFile.img10Cnt = 0; + rebootWatchdogFile.img11Cnt = 0; + rebootWatchdogFile.lastChip = xsc::Chip::CHIP_0; + rebootWatchdogFile.lastCopy = xsc::Copy::COPY_0; + rebootWatchdogFile.img00Lock = false; + rebootWatchdogFile.img01Lock = false; + rebootWatchdogFile.img10Lock = false; + rebootWatchdogFile.img11Lock = false; + rebootWatchdogFile.mechanismNextChip = xsc::Chip::NO_CHIP; + rebootWatchdogFile.mechanismNextCopy = xsc::Copy::NO_COPY; + rebootWatchdogFile.bootFlag = false; + rewriteRebootWatchdogFile(rebootWatchdogFile); + } else { + if (not parseRebootWatchdogFile(path, rebootWatchdogFile)) { + performRebootWatchdogHandling(true); + return; + } + } + + if (CURRENT_CHIP == xsc::CHIP_0) { + if (CURRENT_COPY == xsc::COPY_0) { + rebootWatchdogFile.img00Cnt++; + } else { + rebootWatchdogFile.img01Cnt++; + } + } else { + if (CURRENT_COPY == xsc::COPY_0) { + rebootWatchdogFile.img10Cnt++; + } else { + rebootWatchdogFile.img11Cnt++; + } + } + + if (rebootWatchdogFile.bootFlag) { + // Trigger event to inform ground that a reboot was triggered + uint32_t p1 = rebootWatchdogFile.lastChip << 16 | rebootWatchdogFile.lastCopy; + triggerEvent(core::REBOOT_MECHANISM_TRIGGERED, p1, 0); + // Clear the boot flag + rebootWatchdogFile.bootFlag = false; + } + + if (rebootWatchdogFile.mechanismNextChip != xsc::NO_CHIP and + rebootWatchdogFile.mechanismNextCopy != xsc::NO_COPY) { + if (CURRENT_CHIP != rebootWatchdogFile.mechanismNextChip or + CURRENT_COPY != rebootWatchdogFile.mechanismNextCopy) { + std::string infoString = std::to_string(rebootWatchdogFile.mechanismNextChip) + " " + + std::to_string(rebootWatchdogFile.mechanismNextCopy); + sif::warning << "CoreController::performRebootFileHandling: Expected to be on image " + << infoString << " but currently on other image. Locking " << infoString + << std::endl; + // Firmware or other component might be corrupt and we are on another image then the target + // image specified by the mechanism. We can't really trust the target image anymore. + // Lock it for now + if (rebootWatchdogFile.mechanismNextChip == xsc::CHIP_0) { + if (rebootWatchdogFile.mechanismNextCopy == xsc::COPY_0) { + rebootWatchdogFile.img00Lock = true; + } else { + rebootWatchdogFile.img01Lock = true; + } + } else { + if (rebootWatchdogFile.mechanismNextCopy == xsc::COPY_0) { + rebootWatchdogFile.img10Lock = true; + } else { + rebootWatchdogFile.img11Lock = true; + } + } + } + } + + rebootWatchdogFile.lastChip = CURRENT_CHIP; + rebootWatchdogFile.lastCopy = CURRENT_COPY; + // Only reboot if the reboot functionality is enabled. + // The handler will still increment the boot counts + if (rebootWatchdogFile.enabled and + (*rebootWatchdogFile.relevantBootCnt >= rebootWatchdogFile.maxCount)) { + // Reboot to other image + bool doReboot = false; + xsc::Chip tgtChip = xsc::NO_CHIP; + xsc::Copy tgtCopy = xsc::NO_COPY; + rebootWatchdogAlgorithm(rebootWatchdogFile, doReboot, tgtChip, tgtCopy); + if (doReboot) { + rebootWatchdogFile.bootFlag = true; +#if OBSW_VERBOSE_LEVEL >= 1 + sif::info << "Boot counter for image " << CURRENT_CHIP << " " << CURRENT_COPY + << " too high. Rebooting to " << tgtChip << " " << tgtCopy << std::endl; +#endif + rebootWatchdogFile.mechanismNextChip = tgtChip; + rebootWatchdogFile.mechanismNextCopy = tgtCopy; + rewriteRebootWatchdogFile(rebootWatchdogFile); + performGracefulShutdown(tgtChip, tgtCopy); + } + } else { + rebootWatchdogFile.mechanismNextChip = xsc::NO_CHIP; + rebootWatchdogFile.mechanismNextCopy = xsc::NO_COPY; + } + rewriteRebootWatchdogFile(rebootWatchdogFile); +} + +void CoreController::rebootWatchdogAlgorithm(RebootWatchdogFile &rf, bool &needsReboot, + xsc::Chip &tgtChip, xsc::Copy &tgtCopy) { + tgtChip = xsc::CHIP_0; + tgtCopy = xsc::COPY_0; + needsReboot = false; + if ((CURRENT_CHIP == xsc::CHIP_0) and (CURRENT_COPY == xsc::COPY_0) and + (rf.img00Cnt >= rf.maxCount)) { + needsReboot = true; + if (rf.img01Cnt < rf.maxCount and not rf.img01Lock) { + tgtCopy = xsc::COPY_1; + return; + } + if (rf.img10Cnt < rf.maxCount and not rf.img10Lock) { + tgtChip = xsc::CHIP_1; + return; + } + if (rf.img11Cnt < rf.maxCount and not rf.img11Lock) { + tgtChip = xsc::CHIP_1; + tgtCopy = xsc::COPY_1; + return; + } + // Can't really do much here. Stay on image + sif::warning + << "All reboot counts too high or all fallback images locked, already on fallback image" + << std::endl; + needsReboot = false; + return; + } + if ((CURRENT_CHIP == xsc::CHIP_0) and (CURRENT_COPY == xsc::COPY_1) and + (rf.img01Cnt >= rf.maxCount)) { + needsReboot = true; + if (rf.img00Cnt < rf.maxCount and not rf.img00Lock) { + // Reboot on fallback image + return; + } + if (rf.img10Cnt < rf.maxCount and not rf.img10Lock) { + tgtChip = xsc::CHIP_1; + return; + } + if (rf.img11Cnt < rf.maxCount and not rf.img11Lock) { + tgtChip = xsc::CHIP_1; + tgtCopy = xsc::COPY_1; + } + if (rf.img00Lock) { + needsReboot = false; + } + // Reboot to fallback image + } + if ((CURRENT_CHIP == xsc::CHIP_1) and (CURRENT_COPY == xsc::COPY_0) and + (rf.img10Cnt >= rf.maxCount)) { + needsReboot = true; + if (rf.img11Cnt < rf.maxCount and not rf.img11Lock) { + tgtChip = xsc::CHIP_1; + tgtCopy = xsc::COPY_1; + return; + } + if (rf.img00Cnt < rf.maxCount and not rf.img00Lock) { + return; + } + if (rf.img01Cnt < rf.maxCount and not rf.img01Lock) { + tgtCopy = xsc::COPY_1; + return; + } + if (rf.img00Lock) { + needsReboot = false; + } + // Reboot to fallback image + } + if ((CURRENT_CHIP == xsc::CHIP_1) and (CURRENT_COPY == xsc::COPY_1) and + (rf.img11Cnt >= rf.maxCount)) { + needsReboot = true; + if (rf.img10Cnt < rf.maxCount and not rf.img10Lock) { + tgtChip = xsc::CHIP_1; + return; + } + if (rf.img00Cnt < rf.maxCount and not rf.img00Lock) { + return; + } + if (rf.img01Cnt < rf.maxCount and not rf.img01Lock) { + tgtCopy = xsc::COPY_1; + return; + } + if (rf.img00Lock) { + needsReboot = false; + } + // Reboot to fallback image + } +} + +bool CoreController::parseRebootWatchdogFile(std::string path, RebootWatchdogFile &rf) { + using namespace std; + std::string selfMatch; + if (CURRENT_CHIP == xsc::CHIP_0) { + if (CURRENT_COPY == xsc::COPY_0) { + selfMatch = "00"; + } else { + selfMatch = "01"; + } + } else { + if (CURRENT_COPY == xsc::COPY_0) { + selfMatch = "10"; + } else { + selfMatch = "11"; + } + } + ifstream file(path); + string word; + string line; + uint8_t lineIdx = 0; + while (std::getline(file, line)) { + istringstream iss(line); + switch (lineIdx) { + case 0: { + iss >> word; + if (word.find("on:") == string::npos) { + // invalid file + return false; + } + iss >> rf.enabled; + break; + } + case 1: { + iss >> word; + if (word.find("maxcnt:") == string::npos) { + return false; + } + iss >> rf.maxCount; + break; + } + case 2: { + iss >> word; + if (word.find("img00:") == string::npos) { + return false; + } + iss >> rf.img00Cnt; + if (word.find(selfMatch) != string::npos) { + rf.relevantBootCnt = &rf.img00Cnt; + } + break; + } + case 3: { + iss >> word; + if (word.find("img01:") == string::npos) { + return false; + } + iss >> rf.img01Cnt; + if (word.find(selfMatch) != string::npos) { + rf.relevantBootCnt = &rf.img01Cnt; + } + break; + } + case 4: { + iss >> word; + if (word.find("img10:") == string::npos) { + return false; + } + iss >> rf.img10Cnt; + if (word.find(selfMatch) != string::npos) { + rf.relevantBootCnt = &rf.img10Cnt; + } + break; + } + case 5: { + iss >> word; + if (word.find("img11:") == string::npos) { + return false; + } + iss >> rf.img11Cnt; + if (word.find(selfMatch) != string::npos) { + rf.relevantBootCnt = &rf.img11Cnt; + } + break; + } + case 6: { + iss >> word; + if (word.find("img00lock:") == string::npos) { + return false; + } + iss >> rf.img00Lock; + break; + } + case 7: { + iss >> word; + if (word.find("img01lock:") == string::npos) { + return false; + } + iss >> rf.img01Lock; + break; + } + case 8: { + iss >> word; + if (word.find("img10lock:") == string::npos) { + return false; + } + iss >> rf.img10Lock; + break; + } + case 9: { + iss >> word; + if (word.find("img11lock:") == string::npos) { + return false; + } + iss >> rf.img11Lock; + break; + } + case 10: { + iss >> word; + if (word.find("bootflag:") == string::npos) { + return false; + } + iss >> rf.bootFlag; + break; + } + case 11: { + iss >> word; + int copyRaw = 0; + int chipRaw = 0; + if (word.find("last:") == string::npos) { + return false; + } + iss >> chipRaw; + if (iss.fail()) { + return false; + } + iss >> copyRaw; + if (iss.fail()) { + return false; + } + + if (chipRaw > 1 or copyRaw > 1) { + return false; + } + rf.lastChip = static_cast(chipRaw); + rf.lastCopy = static_cast(copyRaw); + break; + } + case 12: { + iss >> word; + int copyRaw = 0; + int chipRaw = 0; + if (word.find("next:") == string::npos) { + return false; + } + iss >> chipRaw; + if (iss.fail()) { + return false; + } + iss >> copyRaw; + if (iss.fail()) { + return false; + } + + if (chipRaw > 2 or copyRaw > 2) { + return false; + } + rf.mechanismNextChip = static_cast(chipRaw); + rf.mechanismNextCopy = static_cast(copyRaw); + break; + } + } + if (iss.fail()) { + return false; + } + lineIdx++; + } + if (lineIdx < 12) { + return false; + } + return true; +} + +bool CoreController::parseRebootCountersFile(std::string path, RebootCountersFile &rf) { + using namespace std; + ifstream file(path); + string word; + string line; + uint8_t lineIdx = 0; + while (std::getline(file, line)) { + istringstream iss(line); + switch (lineIdx) { + case 0: { + iss >> word; + if (word.find("img00:") == string::npos) { + return false; + } + iss >> rf.img00Cnt; + + break; + } + case 1: { + iss >> word; + if (word.find("img01:") == string::npos) { + return false; + } + iss >> rf.img01Cnt; + + break; + } + case 2: { + iss >> word; + if (word.find("img10:") == string::npos) { + return false; + } + iss >> rf.img10Cnt; + + break; + } + case 3: { + iss >> word; + if (word.find("img11:") == string::npos) { + return false; + } + iss >> rf.img11Cnt; + break; + } + } + lineIdx++; + } + return true; +} + +void CoreController::resetRebootWatchdogCounters(xsc::Chip tgtChip, xsc::Copy tgtCopy) { + std::string path = currMntPrefix + REBOOT_WATCHDOG_FILE; + parseRebootWatchdogFile(path, rebootWatchdogFile); + if (tgtChip == xsc::ALL_CHIP and tgtCopy == xsc::ALL_COPY) { + rebootWatchdogFile.img00Cnt = 0; + rebootWatchdogFile.img01Cnt = 0; + rebootWatchdogFile.img10Cnt = 0; + rebootWatchdogFile.img11Cnt = 0; + } else { + if (tgtChip == xsc::CHIP_0) { + if (tgtCopy == xsc::COPY_0) { + rebootWatchdogFile.img00Cnt = 0; + } else { + rebootWatchdogFile.img01Cnt = 0; + } + } else { + if (tgtCopy == xsc::COPY_0) { + rebootWatchdogFile.img10Cnt = 0; + } else { + rebootWatchdogFile.img11Cnt = 0; + } + } + } + rewriteRebootWatchdogFile(rebootWatchdogFile); +} + +void CoreController::performRebootCountersHandling(bool recreateFile) { + std::string path = currMntPrefix + REBOOT_COUNTERS_FILE; + std::error_code e; + if (not std::filesystem::exists(path, e) or recreateFile) { +#if OBSW_VERBOSE_LEVEL >= 1 + sif::info << "CoreController::performRebootFileHandling: Recreating reboot counters file" + << std::endl; +#endif + rebootCountersFile.img00Cnt = 0; + rebootCountersFile.img01Cnt = 0; + rebootCountersFile.img10Cnt = 0; + rebootCountersFile.img11Cnt = 0; + rewriteRebootCountersFile(rebootCountersFile); + } else { + if (not parseRebootCountersFile(path, rebootCountersFile)) { + performRebootCountersHandling(true); + return; + } + } + + if (CURRENT_CHIP == xsc::CHIP_0) { + if (CURRENT_COPY == xsc::COPY_0) { + rebootCountersFile.img00Cnt++; + } else { + rebootCountersFile.img01Cnt++; + } + } else { + if (CURRENT_COPY == xsc::COPY_0) { + rebootCountersFile.img10Cnt++; + } else { + rebootCountersFile.img11Cnt++; + } + } + announceBootCounts(); + rewriteRebootCountersFile(rebootCountersFile); +} +void CoreController::rewriteRebootWatchdogFile(RebootWatchdogFile file) { + using namespace std::filesystem; + std::string path = currMntPrefix + REBOOT_WATCHDOG_FILE; + std::string legacyPath = currMntPrefix + LEGACY_REBOOT_WATCHDOG_FILE; + { + std::ofstream rebootFile(path); + if (rebootFile.is_open()) { + // Initiate reboot file first. Reboot handling will be on on initialization + rebootFile << "on: " << file.enabled << "\nmaxcnt: " << file.maxCount + << "\nimg00: " << file.img00Cnt << "\nimg01: " << file.img01Cnt + << "\nimg10: " << file.img10Cnt << "\nimg11: " << file.img11Cnt + << "\nimg00lock: " << file.img00Lock << "\nimg01lock: " << file.img01Lock + << "\nimg10lock: " << file.img10Lock << "\nimg11lock: " << file.img11Lock + << "\nbootflag: " << file.bootFlag << "\nlast: " << static_cast(file.lastChip) + << " " << static_cast(file.lastCopy) + << "\nnext: " << static_cast(file.mechanismNextChip) << " " + << static_cast(file.mechanismNextCopy) << "\n"; + } + } + std::error_code e; + // TODO: Remove at some point in the future when all images have been updated. + if (std::filesystem::exists(legacyPath)) { + // Keep those two files in sync + std::filesystem::copy(path, legacyPath, std::filesystem::copy_options::overwrite_existing, e); + if (e) { + sif::error << "File copy has failed: " << e.message() << std::endl; + } + } +} + +void CoreController::rewriteRebootCountersFile(RebootCountersFile file) { + std::string path = currMntPrefix + REBOOT_COUNTERS_FILE; + std::ofstream rebootFile(path); + if (rebootFile.is_open()) { + rebootFile << "img00: " << file.img00Cnt << "\nimg01: " << file.img01Cnt + << "\nimg10: " << file.img10Cnt << "\nimg11: " << file.img11Cnt << "\n"; + } +} + +void CoreController::setRebootMechanismLock(bool lock, xsc::Chip tgtChip, xsc::Copy tgtCopy) { + std::string path = currMntPrefix + REBOOT_WATCHDOG_FILE; + parseRebootWatchdogFile(path, rebootWatchdogFile); + if (tgtChip == xsc::CHIP_0) { + if (tgtCopy == xsc::COPY_0) { + rebootWatchdogFile.img00Lock = lock; + } else { + rebootWatchdogFile.img01Lock = lock; + } + } else { + if (tgtCopy == xsc::COPY_0) { + rebootWatchdogFile.img10Lock = lock; + } else { + rebootWatchdogFile.img11Lock = lock; + } + } + rewriteRebootWatchdogFile(rebootWatchdogFile); +} + +ReturnValue_t CoreController::backupTimeFileHandler() { + // Always set time. We could only set it if it is updated by GPS, but then the backup time would + // become obsolete on GPS problems. + if (opDivider10.check()) { + // It is assumed that the system time is set from the GPS time + timeval currentTime = {}; + ReturnValue_t result = Clock::getClock_timeval(¤tTime); + if (result != returnvalue::OK) { + return result; + } + std::string fileName = currMntPrefix + BACKUP_TIME_FILE; + std::ofstream timeFile(fileName); + if (not timeFile.good()) { + sif::error << "CoreController::timeFileHandler: Error opening time file: " << strerror(errno) + << std::endl; + return returnvalue::FAILED; + } + timeFile << "UNIX SECONDS: " << currentTime.tv_sec + BOOT_OFFSET_SECONDS << std::endl; + } + return returnvalue::OK; +} + +void CoreController::initLeapSeconds() { + ReturnValue_t result = initLeapSecondsFromFile(); + if (result != returnvalue::OK) { + Clock::setLeapSeconds(config::LEAP_SECONDS); + writeLeapSecondsToFile(config::LEAP_SECONDS); + } + leapSecondsInitDone = true; +} + +ReturnValue_t CoreController::initLeapSecondsFromFile() { + std::string fileName = currMntPrefix + LEAP_SECONDS_FILE; + std::error_code e; + if (sdcMan->isSdCardUsable(std::nullopt) and std::filesystem::exists(fileName, e)) { + std::ifstream leapSecondsFile(fileName); + std::string nextWord; + std::getline(leapSecondsFile, nextWord); + std::istringstream iss(nextWord); + iss >> nextWord; + if (iss.bad() or nextWord != "LEAP") { + return returnvalue::FAILED; + } + iss >> nextWord; + if (iss.bad() or nextWord != "SECONDS:") { + return returnvalue::FAILED; + } + iss >> nextWord; + uint16_t leapSeconds = 0; + leapSeconds = std::stoi(nextWord.c_str()); + if (iss.bad()) { + return returnvalue::FAILED; + } + Clock::setLeapSeconds(leapSeconds); + return returnvalue::OK; + } + sif::error + << "CoreController::leapSecondsFileHandler: Initalization of leap seconds from file failed" + << std::endl; + return returnvalue::FAILED; +}; + +ReturnValue_t CoreController::writeLeapSecondsToFile(const uint16_t leapSeconds) { + std::string fileName = currMntPrefix + LEAP_SECONDS_FILE; + if (not sdcMan->isSdCardUsable(std::nullopt)) { + return returnvalue::FAILED; + } + std::ofstream leapSecondsFile(fileName.c_str(), std::ofstream::out | std::ofstream::trunc); + if (not leapSecondsFile.good()) { + sif::error << "CoreController::leapSecondsFileHandler: Error opening leap seconds file: " + << strerror(errno) << std::endl; + return returnvalue::FAILED; + } + leapSecondsFile << "LEAP SECONDS: " << leapSeconds << std::endl; + return returnvalue::OK; +}; + +ReturnValue_t CoreController::actionUpdateLeapSeconds(const uint8_t *data) { + uint16_t leapSeconds = data[1] | (data[0] << 8); + ReturnValue_t result = writeLeapSecondsToFile(leapSeconds); + if (result != returnvalue::OK) { + return result; + } + Clock::setLeapSeconds(leapSeconds); + return returnvalue::OK; +} + +ReturnValue_t CoreController::initClockFromTimeFile() { + using namespace GpsHyperion; + using namespace std; + std::string fileName = currMntPrefix + BACKUP_TIME_FILE; + std::error_code e; + if (sdcMan->isSdCardUsable(std::nullopt) and std::filesystem::exists(fileName, e) and + ((gpsFix == FixMode::NOT_SEEN) or not utility::timeSanityCheck())) { + ifstream timeFile(fileName); + string nextWord; + getline(timeFile, nextWord); + istringstream iss(nextWord); + iss >> nextWord; + if (iss.bad() or nextWord != "UNIX") { + return returnvalue::FAILED; + } + iss >> nextWord; + if (iss.bad() or nextWord != "SECONDS:") { + return returnvalue::FAILED; + } + iss >> nextWord; + timeval currentTime = {}; + char *checkPtr; + currentTime.tv_sec = strtol(nextWord.c_str(), &checkPtr, 10); + if (iss.bad() or *checkPtr) { + return returnvalue::FAILED; + } +#if OBSW_VERBOSE_LEVEL >= 1 + time_t timeRaw = currentTime.tv_sec; + std::tm *time = std::gmtime(&timeRaw); + sif::info << "Setting system time from time files: " << std::put_time(time, "%c %Z") + << std::endl; +#endif + timeFileInitDone = true; + return Clock::setClock(¤tTime); + } + return returnvalue::OK; +} + +void CoreController::readHkData() { + ReturnValue_t result = returnvalue::OK; + result = hkSet.read(TIMEOUT_TYPE, MUTEX_TIMEOUT); + if (result != returnvalue::OK) { + return; + } + Xadc xadc; + result = xadc.getTemperature(hkSet.temperature.value); + if (result != returnvalue::OK) { + hkSet.temperature.setValid(false); + } else { + hkSet.temperature.setValid(true); + } + result = xadc.getVccPint(hkSet.psVoltage.value); + if (result != returnvalue::OK) { + hkSet.psVoltage.setValid(false); + } else { + hkSet.psVoltage.setValid(true); + } + result = xadc.getVccInt(hkSet.plVoltage.value); + if (result != returnvalue::OK) { + hkSet.plVoltage.setValid(false); + } else { + hkSet.plVoltage.setValid(true); + } +#if OBSW_PRINT_CORE_HK == 1 + hkSet.printSet(); +#endif /* OBSW_PRINT_CORE_HK == 1 */ + result = hkSet.commit(TIMEOUT_TYPE, MUTEX_TIMEOUT); + if (result != returnvalue::OK) { + return; + } +} + +const char *CoreController::getXscMountDir(xsc::Chip chip, xsc::Copy copy) { + if (chip == xsc::Chip::CHIP_0) { + if (copy == xsc::Copy::COPY_0) { + return CHIP_0_COPY_0_MOUNT_DIR; + } else if (copy == xsc::Copy::COPY_1) { + return CHIP_0_COPY_1_MOUNT_DIR; + } + } else if (chip == xsc::Chip::CHIP_1) { + if (copy == xsc::Copy::COPY_0) { + return CHIP_1_COPY_0_MOUNT_DIR; + } else if (copy == xsc::Copy::COPY_1) { + return CHIP_1_COPY_1_MOUNT_DIR; + } + } + sif::error << "Invalid chip or copy passed to CoreController::getXscMountDir" << std::endl; + return CHIP_0_COPY_0_MOUNT_DIR; +} + +ReturnValue_t CoreController::executeSwUpdate(SwUpdateSources sourceDir, const uint8_t *data, + size_t size) { + using namespace std; + using namespace std::filesystem; + // At the very least, chip and copy ID need to be included in the command + if (size < 2) { + return HasActionsIF::INVALID_PARAMETERS; + } + if (data[0] > 1 or data[1] > 1) { + return HasActionsIF::INVALID_PARAMETERS; + } + auto chip = static_cast(data[0]); + auto copy = static_cast(data[1]); + const char *sourceStr = "unknown"; + if (sourceDir == SwUpdateSources::SD_0) { + sourceStr = "SD 0"; + } else if (sourceDir == SwUpdateSources::SD_1) { + sourceStr = "SD 1"; + } else { + sourceStr = "tmp directory"; + } + bool sameChipAndCopy = false; + if (chip == CURRENT_CHIP and copy == CURRENT_COPY) { + // This is problematic if the OBSW is running as a systemd service. + // Do not allow for now. + return HasActionsIF::INVALID_PARAMETERS; + // sameChipAndCopy = true; + } + sif::info << "Executing SW update for Chip " << static_cast(data[0]) << " Copy " + << static_cast(data[1]) << " from " << sourceStr << std::endl; + path prefixPath; + if (sourceDir == SwUpdateSources::SD_0) { + prefixPath = path(config::SD_0_MOUNT_POINT); + } else if (sourceDir == SwUpdateSources::SD_1) { + prefixPath = path(config::SD_1_MOUNT_POINT); + } else if (sourceDir == SwUpdateSources::TMP_DIR) { + prefixPath = path("/tmp"); + } + path archivePath; + // It is optionally possible to supply the source file path + if (size > 2) { + archivePath = prefixPath / std::string(reinterpret_cast(data + 2), size - 2); + } else { + archivePath = prefixPath / path(config::OBSW_UPDATE_ARCHIVE_FILE_NAME); + } + sif::info << "Updating with archive path " << archivePath << std::endl; + std::error_code e; + if (not exists(archivePath, e)) { + return HasFileSystemIF::FILE_DOES_NOT_EXIST; + } + // TODO: Decompressing without limiting memory usage with xz is actually a bit risky.. + // But has not been an issue so far. + ostringstream cmd("tar -xJf", ios::app); + cmd << " " << archivePath << " -C " << prefixPath; + int result = system(cmd.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "CoreController::executeAction: SW Update Decompression"); + } + path strippedImagePath = prefixPath / path(config::STRIPPED_OBSW_BINARY_FILE_NAME); + if (!exists(strippedImagePath, e)) { + // TODO: Custom returnvalue? + return returnvalue::FAILED; + } + path obswVersionFilePath = prefixPath / path(config::OBSW_VERSION_FILE_NAME); + if (!exists(obswVersionFilePath, e)) { + // TODO: Custom returnvalue? + return returnvalue::FAILED; + } + cmd.str(""); + cmd.clear(); + path obswDestPath; + path obswVersionDestPath; + if (not sameChipAndCopy) { + cmd << "xsc_mount_copy " << std::to_string(data[0]) << " " << std::to_string(data[1]); + result = system(cmd.str().c_str()); + if (result != 0) { + std::string contextString = "CoreController::executeAction: SW Update Mounting " + + std::to_string(data[0]) + " " + std::to_string(data[1]); + utility::handleSystemError(result, contextString); + } + cmd.str(""); + cmd.clear(); + path xscMountDest(getXscMountDir(chip, copy)); + obswDestPath = xscMountDest / path(relative(config::OBSW_PATH, "/")); + obswVersionDestPath = xscMountDest / path(relative(config::OBSW_VERSION_FILE_PATH, "/")); + } else { + obswDestPath = path(config::OBSW_PATH); + obswVersionDestPath = path(config::OBSW_VERSION_FILE_PATH); + cmd << "writeprotect " << std::to_string(CURRENT_CHIP) << " " << std::to_string(CURRENT_COPY) + << " 0"; + result = system(cmd.str().c_str()); + if (result != 0) { + std::string contextString = "CoreController::executeAction: Unlocking current chip"; + utility::handleSystemError(result, contextString); + } + cmd.str(""); + cmd.clear(); + } + + cmd << "cp " << strippedImagePath << " " << obswDestPath; + result = system(cmd.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "CoreController::executeAction: Copying SW update"); + } + cmd.str(""); + cmd.clear(); + + cmd << "cp " << obswVersionFilePath << " " << obswVersionDestPath; + result = system(cmd.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "CoreController::executeAction: Copying SW version file"); + } + cmd.str(""); + cmd.clear(); + + // Set correct permission for both files + cmd << "chmod 0755 " << obswDestPath; + result = system(cmd.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, + "CoreController::executeAction: Setting SW permissions 0755"); + } + cmd.str(""); + cmd.clear(); + + cmd << "chmod 0644 " << obswVersionDestPath; + result = system(cmd.str().c_str()); + if (result != 0) { + utility::handleSystemError( + result, "CoreController::executeAction: Setting version file permission 0644"); + } + cmd.str(""); + cmd.clear(); + + // Remove the extracted files to keep directories clean. + std::filesystem::remove(strippedImagePath, e); + std::filesystem::remove(obswVersionFilePath, e); + + // TODO: This takes a long time and will block the core controller.. Maybe use command executor? + // For now dont care.. + cmd << "writeprotect " << std::to_string(data[0]) << " " << std::to_string(data[1]) << " 1"; + result = system(cmd.str().c_str()); + if (result != 0) { + std::string contextString = "CoreController::executeAction: Writeprotecting " + + std::to_string(data[0]) + " " + std::to_string(data[1]); + utility::handleSystemError(result, contextString); + } + sif::info << "SW update complete" << std::endl; + return HasActionsIF::EXECUTION_FINISHED; +} + +bool CoreController::startSdStateMachine(sd::SdCard targetActiveSd, SdCfgMode mode, + MessageQueueId_t commander, DeviceCommandId_t actionId) { + if (sdFsmState != SdStates::IDLE or sdCommandingInfo.cmdPending) { + return false; + } + sdFsmState = SdStates::START; + sdInfo.active = targetActiveSd; + // If we are going from 2 SD cards to one, lock SD card usage in any case because 1 SD card is + // going off. + if (sdInfo.cfgMode == SdCfgMode::HOT_REDUNDANT and mode == SdCfgMode::COLD_REDUNDANT) { + sdInfo.lockSdCardUsage = true; + } + sdInfo.cfgMode = mode; + sdCommandingInfo.actionId = actionId; + sdCommandingInfo.commander = commander; + sdCommandingInfo.cmdPending = true; + return true; +} + +void CoreController::announceBootCounts() { + uint64_t totalBootCount = rebootCountersFile.img00Cnt + rebootCountersFile.img01Cnt + + rebootCountersFile.img10Cnt + rebootCountersFile.img11Cnt; + uint32_t individualBootCountsP1 = + (rebootCountersFile.img00Cnt << 16) | rebootCountersFile.img01Cnt; + uint32_t individualBootCountsP2 = + (rebootCountersFile.img10Cnt << 16) | rebootCountersFile.img11Cnt; + triggerEvent(core::INDIVIDUAL_BOOT_COUNTS, individualBootCountsP1, individualBootCountsP2); + triggerEvent(core::REBOOT_COUNTER, (totalBootCount >> 32) & 0xffffffff, + totalBootCount & 0xffffffff); +} + +MessageQueueId_t CoreController::getCommandQueue() const { + return ExtendedControllerBase::getCommandQueue(); +} + +void CoreController::dirListingDumpHandler() { + if (dumpContext.firstDump) { + dumpContext.firstDump = false; + return; + } + size_t nextDumpLen = 0; + size_t dummy = 0; + ReturnValue_t result; + std::error_code e; + std::ifstream ifile(LIST_DIR_DUMP_WORK_FILE, std::ios::binary); + if (ifile.bad()) { + return; + } + while (dumpContext.dumpedBytes < dumpContext.totalFileSize) { + ifile.seekg(dumpContext.dumpedBytes, std::ios::beg); + nextDumpLen = dumpContext.maxDumpLen; + if (dumpContext.totalFileSize - dumpContext.dumpedBytes < dumpContext.maxDumpLen) { + nextDumpLen = dumpContext.totalFileSize - dumpContext.dumpedBytes; + } + SerializeAdapter::serialize(&dumpContext.segmentIdx, dirListingBuf.data(), &dummy, + dirListingBuf.size(), SerializeIF::Endianness::NETWORK); + ifile.read(reinterpret_cast(dirListingBuf.data() + dumpContext.listingDataOffset), + nextDumpLen); + result = + actionHelper.reportData(dumpContext.commander, dumpContext.actionId, dirListingBuf.data(), + dumpContext.listingDataOffset + nextDumpLen); + if (result != returnvalue::OK) { + // Remove work file when we are done + std::filesystem::remove(LIST_DIR_DUMP_WORK_FILE, e); + dumpContext.active = false; + actionHelper.finish(false, dumpContext.commander, dumpContext.actionId, result); + return; + } + dumpContext.segmentIdx++; + dumpContext.dumpedBytes += nextDumpLen; + // Dump takes multiple task cycles, so cache the dump state and continue dump the next cycles. + if (dumpContext.segmentIdx == 10) { + break; + } + } + if (dumpContext.dumpedBytes >= dumpContext.totalFileSize) { + actionHelper.finish(true, dumpContext.commander, dumpContext.actionId, result); + dumpContext.active = false; + // Remove work file when we are done + std::filesystem::remove(LIST_DIR_DUMP_WORK_FILE, e); + } +} + +void CoreController::announceVersionInfo() { + using namespace core; + uint32_t p1 = (common::OBSW_VERSION_MAJOR << 24) | (common::OBSW_VERSION_MINOR << 16) | + (common::OBSW_VERSION_REVISION << 8); + uint32_t p2 = 0; + if (strcmp("", common::OBSW_VERSION_CST_GIT_SHA1) != 0) { + p1 |= 1; + auto shaAsStr = std::string(common::OBSW_VERSION_CST_GIT_SHA1); + size_t posDash = shaAsStr.find("-"); + auto gitHash = shaAsStr.substr(posDash + 2, 4); + // Only copy first 4 letters of git hash + memcpy(&p2, gitHash.c_str(), 4); + } + + triggerEvent(VERSION_INFO, p1, p2); + p1 = (core::FW_VERSION_MAJOR << 24) | (core::FW_VERSION_MINOR << 16) | + (core::FW_VERSION_REVISION << 8) | (core::FW_VERSION_HAS_SHA); + std::memcpy(&p2, core::FW_VERSION_GIT_SHA, 4); + triggerEvent(FIRMWARE_INFO, p1, p2); +} + +void CoreController::announceCurrentImageInfo() { + using namespace core; + triggerEvent(CURRENT_IMAGE_INFO, CURRENT_CHIP, CURRENT_COPY); +} + +ReturnValue_t CoreController::performGracefulShutdown(xsc::Chip tgtChip, xsc::Copy tgtCopy) { + bool protOpPerformed = false; + // This function can not really fail + gracefulShutdownTasks(tgtChip, tgtCopy, protOpPerformed); + + switch (tgtChip) { + case (xsc::Chip::CHIP_0): { + switch (tgtCopy) { + case (xsc::Copy::COPY_0): { + xsc_boot_copy(XSC_LIBNOR_CHIP_0, XSC_LIBNOR_COPY_NOMINAL); + break; + } + case (xsc::Copy::COPY_1): { + xsc_boot_copy(XSC_LIBNOR_CHIP_0, XSC_LIBNOR_COPY_GOLD); + break; + } + default: { + break; + } + } + break; + } + case (xsc::Chip::CHIP_1): { + switch (tgtCopy) { + case (xsc::Copy::COPY_0): { + xsc_boot_copy(XSC_LIBNOR_CHIP_1, XSC_LIBNOR_COPY_NOMINAL); + break; + } + case (xsc::Copy::COPY_1): { + xsc_boot_copy(XSC_LIBNOR_CHIP_1, XSC_LIBNOR_COPY_GOLD); + break; + } + default: { + break; + } + } + break; + } + default: + break; + } + return returnvalue::OK; +} + +void CoreController::announceSdInfo(SdCardManager::SdStatePair sdStates) { + auto activeSdCard = sdcMan->getActiveSdCard(); + uint32_t p1 = sd::SdCard::NONE; + if (activeSdCard.has_value()) { + p1 = static_cast(activeSdCard.value()); + } + uint32_t p2 = + (static_cast(sdStates.first) << 16) | static_cast(sdStates.second); + triggerEvent(core::ACTIVE_SD_INFO, p1, p2); +} + +ReturnValue_t CoreController::handleSwitchingSdCardsOffNonBlocking() { + sdcMan->setBlocking(false); + SdCardManager::Operations op; + std::pair sdStatus; + ReturnValue_t result = sdcMan->getSdCardsStatus(sdStatus); + if (result != returnvalue::OK) { + return result; + } + Countdown maxWaitTimeCd(10000); + // Stopwatch watch; + auto waitingForFinish = [&]() { + auto currentState = sdcMan->checkCurrentOp(op); + if (currentState == SdCardManager::OpStatus::IDLE) { + return returnvalue::OK; + } + while (currentState == SdCardManager::OpStatus::ONGOING) { + if (maxWaitTimeCd.hasTimedOut()) { + return returnvalue::FAILED; + } + TaskFactory::delayTask(50); + currentState = sdcMan->checkCurrentOp(op); + } + return returnvalue::OK; + }; + if (sdStatus.first != sd::SdState::OFF) { + sdcMan->unmountSdCard(sd::SdCard::SLOT_0); + result = waitingForFinish(); + if (result != returnvalue::OK) { + return result; + } + sdcMan->switchOffSdCard(sd::SdCard::SLOT_0, sdStatus, false); + result = waitingForFinish(); + if (result != returnvalue::OK) { + return result; + } + } + if (sdStatus.second != sd::SdState::OFF) { + sdcMan->unmountSdCard(sd::SdCard::SLOT_1); + result = waitingForFinish(); + if (result != returnvalue::OK) { + return result; + } + sdcMan->switchOffSdCard(sd::SdCard::SLOT_1, sdStatus, false); + result = waitingForFinish(); + if (result != returnvalue::OK) { + return result; + } + } + return result; +} + +bool CoreController::isNumber(const std::string &s) { + return !s.empty() && std::find_if(s.begin(), s.end(), + [](unsigned char c) { return !std::isdigit(c); }) == s.end(); +} + +ReturnValue_t CoreController::getParameter(uint8_t domainId, uint8_t uniqueIdentifier, + ParameterWrapper *parameterWrapper, + const ParameterWrapper *newValues, + uint16_t startAtIndex) { + if (domainId != 0) { + return HasParametersIF::INVALID_DOMAIN_ID; + } + if (uniqueIdentifier >= ParamId::NUM_IDS) { + return HasParametersIF::INVALID_IDENTIFIER_ID; + } + uint8_t newPrefSd; + ReturnValue_t result = newValues->getElement(&newPrefSd); + if (result != returnvalue::OK) { + return result; + } + // Only SD card 0 (0) and 1 (1) are allowed values. + if (newPrefSd > 1) { + return HasParametersIF::INVALID_VALUE; + } + result = sdcMan->setPreferredSdCard(static_cast(newPrefSd)); + if (result != returnvalue::OK) { + return returnvalue::FAILED; + } + parameterWrapper->set(prefSdRaw); + return returnvalue::OK; +} diff --git a/bsp_q7s/core/CoreController.h b/bsp_q7s/core/CoreController.h new file mode 100644 index 0000000..6827a39 --- /dev/null +++ b/bsp_q7s/core/CoreController.h @@ -0,0 +1,401 @@ +#ifndef BSP_Q7S_CORE_CORECONTROLLER_H_ +#define BSP_Q7S_CORE_CORECONTROLLER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/fs/SdCardManager.h" +#include "events/subsystemIdRanges.h" +#include "fsfw/controller/ExtendedControllerBase.h" +#include "mission/sysDefs.h" + +class Timer; +class SdCardManager; + +struct RebootWatchdogFile { + static constexpr uint8_t DEFAULT_MAX_BOOT_CNT = 10; + + bool enabled = true; + size_t maxCount = DEFAULT_MAX_BOOT_CNT; + uint32_t img00Cnt = 0; + uint32_t img01Cnt = 0; + uint32_t img10Cnt = 0; + uint32_t img11Cnt = 0; + bool img00Lock = false; + bool img01Lock = false; + bool img10Lock = false; + bool img11Lock = false; + uint32_t* relevantBootCnt = &img00Cnt; + bool bootFlag = false; + xsc::Chip lastChip = xsc::Chip::CHIP_0; + xsc::Copy lastCopy = xsc::Copy::COPY_0; + xsc::Chip mechanismNextChip = xsc::Chip::NO_CHIP; + xsc::Copy mechanismNextCopy = xsc::Copy::NO_COPY; +}; + +class RebootWatchdogPacket : public SerialLinkedListAdapter { + public: + RebootWatchdogPacket(RebootWatchdogFile& rf) { + enabled = rf.enabled; + maxCount = rf.maxCount; + img00Count = rf.img00Cnt; + img01Count = rf.img01Cnt; + img10Count = rf.img10Cnt; + img11Count = rf.img11Cnt; + img00Lock = rf.img00Lock; + img01Lock = rf.img01Lock; + img10Lock = rf.img10Lock; + img11Lock = rf.img11Lock; + lastChip = static_cast(rf.lastChip); + lastCopy = static_cast(rf.lastCopy); + nextChip = static_cast(rf.mechanismNextChip); + nextCopy = static_cast(rf.mechanismNextCopy); + setLinks(); + } + + private: + void setLinks() { + setStart(&enabled); + enabled.setNext(&maxCount); + maxCount.setNext(&img00Count); + img00Count.setNext(&img01Count); + img01Count.setNext(&img10Count); + img10Count.setNext(&img11Count); + img11Count.setNext(&img00Lock); + img00Lock.setNext(&img01Lock); + img01Lock.setNext(&img10Lock); + img10Lock.setNext(&img11Lock); + img11Lock.setNext(&lastChip); + lastChip.setNext(&lastCopy); + lastCopy.setNext(&nextChip); + nextChip.setNext(&nextCopy); + setLast(&nextCopy); + } + + SerializeElement enabled = false; + SerializeElement maxCount = 0; + SerializeElement img00Count = 0; + SerializeElement img01Count = 0; + SerializeElement img10Count = 0; + SerializeElement img11Count = 0; + SerializeElement img00Lock = false; + SerializeElement img01Lock = false; + SerializeElement img10Lock = false; + SerializeElement img11Lock = false; + SerializeElement lastChip = 0; + SerializeElement lastCopy = 0; + SerializeElement nextChip = 0; + SerializeElement nextCopy = 0; +}; + +struct RebootCountersFile { + // 16 bit values so all boot counters fit into one event. + uint16_t img00Cnt = 0; + uint16_t img01Cnt = 0; + uint16_t img10Cnt = 0; + uint16_t img11Cnt = 0; +}; + +class RebootCountersPacket : public SerialLinkedListAdapter { + RebootCountersPacket(RebootCountersFile& rf) { + img00Count = rf.img00Cnt; + img01Count = rf.img01Cnt; + img10Count = rf.img10Cnt; + img11Count = rf.img11Cnt; + setLinks(); + } + + private: + void setLinks() { + setStart(&img00Count); + img00Count.setNext(&img01Count); + img01Count.setNext(&img10Count); + img10Count.setNext(&img11Count); + setLast(&img11Count); + } + + SerializeElement img00Count = 0; + SerializeElement img01Count = 0; + SerializeElement img10Count = 0; + SerializeElement img11Count = 0; +}; + +class CoreController : public ExtendedControllerBase, public ReceivesParameterMessagesIF { + public: + enum ParamId : uint8_t { PREF_SD = 0, NUM_IDS }; + + static xsc::Chip CURRENT_CHIP; + static xsc::Copy CURRENT_COPY; + + static constexpr char CHIP_PROT_SCRIPT[] = "get-chip-prot-status.sh"; + static constexpr char CHIP_STATE_FILE[] = "/tmp/chip_prot_status.txt"; + static constexpr char CURR_COPY_FILE[] = "/tmp/curr_copy.txt"; + + const std::string VERSION_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + std::string(core::VERSION_FILE_NAME); + const std::string LEGACY_REBOOT_WATCHDOG_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + + std::string(core::LEGACY_REBOOT_WATCHDOG_FILE_NAME); + const std::string REBOOT_WATCHDOG_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + std::string(core::REBOOT_WATCHDOG_FILE_NAME); + const std::string LEAP_SECONDS_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + std::string(core::LEAP_SECONDS_FILE_NAME); + const std::string BACKUP_TIME_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + std::string(core::TIME_FILE_NAME); + const std::string REBOOT_COUNTERS_FILE = + "/" + std::string(core::CONF_FOLDER) + "/" + std::string(core::REBOOT_COUNTER_FILE_NAME); + + static constexpr char CHIP_0_COPY_0_MOUNT_DIR[] = "/tmp/mntupdate-xdi-qspi0-nom-rootfs"; + static constexpr char CHIP_0_COPY_1_MOUNT_DIR[] = "/tmp/mntupdate-xdi-qspi0-gold-rootfs"; + static constexpr char CHIP_1_COPY_0_MOUNT_DIR[] = "/tmp/mntupdate-xdi-qspi1-nom-rootfs"; + static constexpr char CHIP_1_COPY_1_MOUNT_DIR[] = "/tmp/mntupdate-xdi-qspi1-gold-rootfs"; + static constexpr char LIST_DIR_DUMP_WORK_FILE[] = "/tmp/dir_listing.tmp"; + + static constexpr dur_millis_t INIT_SD_CARD_CHECK_TIMEOUT = 5000; + static constexpr dur_millis_t DEFAULT_SD_CARD_CHECK_TIMEOUT = 60000; + + CoreController(object_id_t objectId, bool enableHkSet); + virtual ~CoreController(); + + ReturnValue_t initialize() override; + + ReturnValue_t initializeAfterTaskCreation() override; + + ReturnValue_t executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) override; + + ReturnValue_t handleCommandMessage(CommandMessage* message) override; + void performControlOperation() override; + + /** + * Generate a file containing the chip lock/unlock states inside /tmp/chip_prot_status.txt + * @return + */ + static ReturnValue_t generateChipStateFile(); + static ReturnValue_t incrementAllocationFailureCount(); + static void getCurrentBootCopy(xsc::Chip& chip, xsc::Copy& copy); + static const char* getXscMountDir(xsc::Chip chip, xsc::Copy copy); + + ReturnValue_t updateProtInfo(bool regenerateChipStateFile = true); + + /** + * Checks whether the target chip and copy are write protected and protect set them to a target + * state where applicable. + * @param targetChip + * @param targetCopy + * @param protect Target state + * @param protOperationPerformed [out] Can be used to determine whether any operation + * was performed + * @param updateProtFile Specify whether the protection info file is updated + * @return + */ + ReturnValue_t setBootCopyProtectionAndUpdateFile(xsc::Chip targetChip, xsc::Copy targetCopy, + bool protect); + + bool sdInitFinished() const; + + private: + static constexpr uint32_t BOOT_OFFSET_SECONDS = 15; + static constexpr MutexIF::TimeoutType TIMEOUT_TYPE = MutexIF::TimeoutType::WAITING; + static constexpr uint32_t MUTEX_TIMEOUT = 20; + bool enableHkSet = false; + GpsHyperion::FixMode gpsFix = GpsHyperion::FixMode::NOT_SEEN; + + // States for SD state machine, which is used in non-blocking mode + enum class SdStates { + NONE, + START, + UPDATE_SD_INFO_START, + SKIP_TWO_CYCLES_IF_SD_LOCKED, + MOUNT_SELF, + // Determine operations for other SD card, depending on redundancy configuration + DETERMINE_OTHER, + SET_STATE_OTHER, + // Mount or unmount other + MOUNT_UNMOUNT_OTHER, + // Skip period because the shell command used to generate the info file sometimes is + // missing the last performed operation if executed too early + SKIP_CYCLE_BEFORE_INFO_UPDATE, + UPDATE_SD_INFO_END, + // SD initialization done + IDLE + }; + + enum class SwUpdateSources { SD_0, SD_1, TMP_DIR }; + + static constexpr bool BLOCKING_SD_INIT = false; + + uint32_t* mappedSysRomAddr = nullptr; + SdCardManager* sdcMan = nullptr; + MessageQueueIF* eventQueue = nullptr; + + uint8_t prefSdRaw = sd::SdCard::SLOT_0; + SdStates sdFsmState = SdStates::START; + SdStates fsmStateAfterDelay = SdStates::IDLE; + enum SdCfgMode { PASSIVE, COLD_REDUNDANT, HOT_REDUNDANT }; + + struct SdFsmParams { + SdCfgMode cfgMode = SdCfgMode::COLD_REDUNDANT; + sd::SdCard active = sd::SdCard::NONE; + sd::SdCard other = sd::SdCard::NONE; + std::string activeChar = "0"; + std::string otherChar = "1"; + sd::SdState activeState = sd::SdState::OFF; + sd::SdState otherState = sd::SdState::OFF; + std::pair mountSwitch = {true, true}; + // This flag denotes that the SD card usage is locked. This is relevant if SD cards go off + // to leave appliation using the SD cards some time to detect the SD card is not usable anymore. + // This is relevant if the active SD card is being switched. The SD card will also be locked + // when going from hot-redundant mode to cold-redundant mode. + bool lockSdCardUsage = false; + bool commandPending = true; + bool initFinished = false; + SdCardManager::SdStatePair currentState; + uint16_t cycleCount = 0; + uint16_t skippedCyclesCount = 0; + } sdInfo; + + struct SdCommanding { + bool cmdPending = false; + MessageQueueId_t commander = MessageQueueIF::NO_QUEUE; + DeviceCommandId_t actionId; + } sdCommandingInfo; + + struct DirListingDumpContext { + bool active; + bool firstDump; + size_t dumpedBytes; + size_t totalFileSize; + size_t listingDataOffset; + size_t maxDumpLen; + uint32_t segmentIdx; + MessageQueueId_t commander = MessageQueueIF::NO_QUEUE; + DeviceCommandId_t actionId; + }; + std::array dirListingBuf{}; + DirListingDumpContext dumpContext{}; + + RebootWatchdogFile rebootWatchdogFile = {}; + RebootCountersFile rebootCountersFile = {}; + + CommandExecutor cmdExecutor; + SimpleRingBuffer cmdReplyBuf; + DynamicFIFO cmdRepliesSizes; + bool shellCmdIsExecuting = false; + MessageQueueId_t successRecipient = MessageQueueIF::NO_QUEUE; + + std::string currMntPrefix; + bool timeFileInitDone = false; + bool leapSecondsInitDone = false; + bool performOneShotSdCardOpsSwitch = false; + uint8_t shortSdCardCdCounter = 0; +#if OBSW_THREAD_TRACING == 1 + uint32_t opCounter; +#endif + Countdown sdCardCheckCd = Countdown(INIT_SD_CARD_CHECK_TIMEOUT); + + /** + * First index: Chip. + * Second index: Copy. + */ + bool protArray[2][2]{}; + PeriodicOperationDivider opDivider5; + PeriodicOperationDivider opDivider10; + + PoolEntry tempPoolEntry = PoolEntry(0.0); + PoolEntry psVoltageEntry = PoolEntry(0.0); + PoolEntry plVoltageEntry = PoolEntry(0.0); + + core::HkSet hkSet; + + ParameterHelper paramHelper; + +#if OBSW_SD_CARD_MUST_BE_ON == 1 + bool remountAttemptFlag = true; +#endif + + MessageQueueId_t getCommandQueue() const override; + ReturnValue_t getParameter(uint8_t domainId, uint8_t uniqueIdentifier, + ParameterWrapper* parameterWrapper, const ParameterWrapper* newValues, + uint16_t startAtIndex) override; + ReturnValue_t initializeLocalDataPool(localpool::DataPool& localDataPoolMap, + LocalDataPoolManager& poolManager) override; + + LocalPoolDataSetBase* getDataSetHandle(sid_t sid) override; + ReturnValue_t checkModeCommand(Mode_t mode, Submode_t submode, uint32_t* msToReachTheMode); + void performMountedSdCardOperations(); + ReturnValue_t initVersionFile(); + + void initLeapSeconds(); + ReturnValue_t initLeapSecondsFromFile(); + ReturnValue_t initClockFromTimeFile(); + ReturnValue_t actionUpdateLeapSeconds(const uint8_t* data); + ReturnValue_t writeLeapSecondsToFile(const uint16_t leapSeconds); + ReturnValue_t performSdCardCheck(); + ReturnValue_t backupTimeFileHandler(); + ReturnValue_t initBootCopyFile(); + ReturnValue_t initSdCardBlocking(); + bool startSdStateMachine(sd::SdCard targetActiveSd, SdCfgMode mode, MessageQueueId_t commander, + DeviceCommandId_t actionId); + void initPrint(); + + ReturnValue_t sdStateMachine(); + void updateInternalSdInfo(); + ReturnValue_t sdCardSetup(sd::SdCard sdCard, sd::SdState targetState, std::string sdChar, + bool printOutput = true); + ReturnValue_t executeSwUpdate(SwUpdateSources sourceDir, const uint8_t* data, size_t size); + ReturnValue_t sdColdRedundantBlockingInit(); + + void currentStateSetter(sd::SdCard sdCard, sd::SdState newState); + void executeNextExternalSdCommand(); + void checkExternalSdCommandStatus(); + void performRebootWatchdogHandling(bool recreateFile); + void performRebootCountersHandling(bool recreateFile); + + ReturnValue_t actionListDirectoryIntoFile(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size); + ReturnValue_t actionListDirectoryDumpDirectly(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size); + ReturnValue_t performGracefulShutdown(xsc::Chip targetChip, xsc::Copy targetCopy); + + ReturnValue_t actionListDirectoryCommonCommandCreator(const uint8_t* data, size_t size, + std::ostringstream& oss); + + ReturnValue_t actionXscReboot(const uint8_t* data, size_t size); + ReturnValue_t actionReboot(const uint8_t* data, size_t size); + + ReturnValue_t gracefulShutdownTasks(xsc::Chip chip, xsc::Copy copy, bool& protOpPerformed); + + ReturnValue_t handleProtInfoUpdateLine(std::string nextLine); + ReturnValue_t handleSwitchingSdCardsOffNonBlocking(); + bool handleBootCopyProt(xsc::Chip targetChip, xsc::Copy targetCopy, bool protect); + void rebootWatchdogAlgorithm(RebootWatchdogFile& rf, bool& needsReboot, xsc::Chip& tgtChip, + xsc::Copy& tgtCopy); + void resetRebootWatchdogCounters(xsc::Chip tgtChip, xsc::Copy tgtCopy); + void setRebootMechanismLock(bool lock, xsc::Chip tgtChip, xsc::Copy tgtCopy); + bool parseRebootWatchdogFile(std::string path, RebootWatchdogFile& file); + bool parseRebootCountersFile(std::string path, RebootCountersFile& file); + void rewriteRebootWatchdogFile(RebootWatchdogFile file); + void rewriteRebootCountersFile(RebootCountersFile file); + void announceBootCounts(); + void announceVersionInfo(); + void announceCurrentImageInfo(); + void announceSdInfo(SdCardManager::SdStatePair sdStates); + void readHkData(); + void dirListingDumpHandler(); + bool isNumber(const std::string& s); +}; + +#endif /* BSP_Q7S_CORE_CORECONTROLLER_H_ */ diff --git a/bsp_q7s/core/WatchdogHandler.cpp b/bsp_q7s/core/WatchdogHandler.cpp new file mode 100644 index 0000000..9e3a102 --- /dev/null +++ b/bsp_q7s/core/WatchdogHandler.cpp @@ -0,0 +1,87 @@ +#include "WatchdogHandler.h" + +#include +#include + +#include +#include +#include + +#include "fsfw/serviceinterface.h" +#include "watchdog/definitions.h" + +WatchdogHandler::WatchdogHandler() {} + +void WatchdogHandler::periodicOperation() { + if (watchdogFifoFd != 0) { + if (watchdogFifoFd == RETRY_FIFO_OPEN) { + // Open FIFO write only and non-blocking + watchdogFifoFd = open(watchdog::FIFO_NAME.c_str(), O_WRONLY | O_NONBLOCK); + if (watchdogFifoFd < 0) { + if (errno == ENXIO) { + watchdogFifoFd = RETRY_FIFO_OPEN; + // No printout for now, would be spam + return; + } else { + sif::error << "Opening pipe " << watchdog::FIFO_NAME << " write-only failed with " + << errno << ": " << strerror(errno) << std::endl; + return; + } + } + sif::info << "Opened " << watchdog::FIFO_NAME << " successfully" << std::endl; + performStartHandling(); + } else if (watchdogFifoFd > 0) { + // Write to OBSW watchdog FIFO here + const char writeChar = watchdog::first::IDLE_CHAR; + ssize_t writtenBytes = write(watchdogFifoFd, &writeChar, 1); + if (writtenBytes < 0) { + sif::error << "Errors writing to watchdog FIFO, code " << errno << ": " << strerror(errno) + << std::endl; + } + } + } +} + +ReturnValue_t WatchdogHandler::initialize(bool enableWatchdogFunction) { + using namespace std::filesystem; + this->enableWatchFunction = enableWatchdogFunction; + std::error_code e; + if (not std::filesystem::exists(watchdog::FIFO_NAME, e)) { + // Still return returnvalue::OK for now + sif::info << "Watchdog FIFO " << watchdog::FIFO_NAME << " does not exist, can't initiate" + << " watchdog" << std::endl; + return returnvalue::OK; + } + // Open FIFO write only and non-blocking to prevent SW from killing itself. + watchdogFifoFd = open(watchdog::FIFO_NAME.c_str(), O_WRONLY | O_NONBLOCK); + if (watchdogFifoFd < 0) { + if (errno == ENXIO) { + watchdogFifoFd = RETRY_FIFO_OPEN; + sif::info << "eive-watchdog not running. FIFO can not be opened" << std::endl; + } else { + sif::error << "Opening pipe " << watchdog::FIFO_NAME << " write-only failed with " << errno + << ": " << strerror(errno) << std::endl; + return returnvalue::FAILED; + } + } + return performStartHandling(); +} + +ReturnValue_t WatchdogHandler::performStartHandling() { + char startBuf[2]; + ssize_t writeLen = 1; + startBuf[0] = watchdog::first::START_CHAR; + if (enableWatchFunction) { + writeLen += 1; + startBuf[1] = watchdog::second::WATCH_FLAG; + } + ssize_t writtenBytes = write(watchdogFifoFd, &startBuf, writeLen); + if (writtenBytes < 0) { + sif::error << "WatchdogHandler: Errors writing to watchdog FIFO, code " << errno << ": " + << strerror(errno) << std::endl; + return returnvalue::FAILED; + } else if (writtenBytes != writeLen) { + sif::warning << "WatchdogHandler: Not all bytes were written, possible error" << std::endl; + } + return returnvalue::OK; +} diff --git a/bsp_q7s/core/WatchdogHandler.h b/bsp_q7s/core/WatchdogHandler.h new file mode 100644 index 0000000..5db4228 --- /dev/null +++ b/bsp_q7s/core/WatchdogHandler.h @@ -0,0 +1,23 @@ +#ifndef BSP_Q7S_CORE_WATCHDOGHANDLER_H_ +#define BSP_Q7S_CORE_WATCHDOGHANDLER_H_ + +#include "fsfw/returnvalues/returnvalue.h" + +class WatchdogHandler { + public: + WatchdogHandler(); + + ReturnValue_t initialize(bool enableWatchFunction); + void periodicOperation(); + + private: + // Designated value for rechecking FIFO open + static constexpr int RETRY_FIFO_OPEN = -2; + + int watchdogFifoFd = 0; + bool enableWatchFunction = false; + + ReturnValue_t performStartHandling(); +}; + +#endif /* BSP_Q7S_CORE_WATCHDOGHANDLER_H_ */ diff --git a/bsp_q7s/core/XiphosWdtHandler.cpp b/bsp_q7s/core/XiphosWdtHandler.cpp new file mode 100644 index 0000000..8444b65 --- /dev/null +++ b/bsp_q7s/core/XiphosWdtHandler.cpp @@ -0,0 +1,122 @@ +#include "XiphosWdtHandler.h" + +#include "fsfw/ipc/QueueFactory.h" + +XiphosWdtHandler::XiphosWdtHandler(object_id_t objectId) + : SystemObject(objectId), + requestQueue(QueueFactory::instance()->createMessageQueue()), + actionHelper(this, requestQueue) {} + +ReturnValue_t XiphosWdtHandler::initialize() { + ReturnValue_t result = actionHelper.initialize(); + if (result != returnvalue::OK) { + return result; + } + int retval = xsc_watchdog_init(&wdtHandle); + if (retval != 0) { + sif::error << "XiphosWdtHandler: Initiating watchdog failed with code " << retval << ": " + << strerror(retval) << std::endl; + return ObjectManagerIF::CHILD_INIT_FAILED; + } + if (wdtHandle == nullptr) { + sif::error << "XiphosWdtHandler: WDT handle is nullptr!" << std::endl; + return ObjectManagerIF::CHILD_INIT_FAILED; + } + retval = xsc_watchdog_set_timeout(wdtHandle, timeoutSeconds); + if (retval != 0) { + // This propably means that the default timeout is used. Still continue with task init. + sif::warning << "XiphosWdtHandler: Setting WDT timeout of " << timeoutSeconds + << " seconds failed with code " << result << ": " << strerror(retval) << std::endl; + } + return enableWdt(); +} + +ReturnValue_t XiphosWdtHandler::performOperation(uint8_t opCode) { + CommandMessage command; + ReturnValue_t result; + for (result = requestQueue->receiveMessage(&command); result == returnvalue::OK; + result = requestQueue->receiveMessage(&command)) { + result = actionHelper.handleActionMessage(&command); + if (result == returnvalue::OK) { + continue; + } + sif::warning << "Can not handle message with message type " << command.getMessageType() + << std::endl; + } + if (enabled) { + int retval = xsc_watchdog_keepalive(wdtHandle); + if (retval != 0) { + sif::warning << "XiphosWdtHandler: Feeding WDT failed with code " << retval << ": " + << strerror(retval) << std::endl; + return returnvalue::FAILED; + } + } + return returnvalue::OK; +} + +ReturnValue_t XiphosWdtHandler::executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t *data, size_t size) { + switch (actionId) { + case (ActionId::ENABLE): { + ReturnValue_t result = enableWdt(); + if (result != returnvalue::OK) { + return result; + } + return EXECUTION_FINISHED; + } + case (ActionId::DISABLE): { + ReturnValue_t result = disableWdt(); + if (result != returnvalue::OK) { + return result; + } + return EXECUTION_FINISHED; + } + } + return HasActionsIF::INVALID_ACTION_ID; +} + +ReturnValue_t XiphosWdtHandler::enableWdt() { + int nowayout = 0; + int status = 0; + int retval = xsc_watchdog_get_status(&nowayout, &status); + // If this fails for whatever reason, just try enabling in any case. + if (retval != 0) { + sif::warning << "XiphosWdtHandler: Getting WDT status failed" << std::endl; + } + // Of course the enable API will fail if the device is already on, just perfect, love me some + // good C API... :))) + if (retval != 0 or status == 0) { + retval = xsc_watchdog_enable(wdtHandle); + if (retval != 0) { + sif::error << "XiphosWdtHandler: Enabling WDT failed with code " << retval << ": " + << strerror(retval) << std::endl; + return returnvalue::FAILED; + } + } + enabled = true; + return returnvalue::OK; +} + +ReturnValue_t XiphosWdtHandler::disableWdt() { + int nowayout = 0; + int status = 0; + int retval = xsc_watchdog_get_status(&nowayout, &status); + // If this fails for whatever reason, just try disabling in any case. + if (retval != 0) { + sif::warning << "XiphosWdtHandler: Getting WDT status failed" << std::endl; + } + // Of course the disable API will fail if the device is already off, just perfect, love me some + // good C API... :))) + if (retval != 0 or status == 1) { + retval = xsc_watchdog_disable(wdtHandle); + if (retval != 0) { + sif::error << "XiphosWdtHandler: Disabling WDT failed with code " << retval << ": " + << strerror(retval) << std::endl; + return returnvalue::FAILED; + } + } + enabled = false; + return returnvalue::OK; +} + +MessageQueueId_t XiphosWdtHandler::getCommandQueue() const { return requestQueue->getId(); } diff --git a/bsp_q7s/core/XiphosWdtHandler.h b/bsp_q7s/core/XiphosWdtHandler.h new file mode 100644 index 0000000..a8d73f4 --- /dev/null +++ b/bsp_q7s/core/XiphosWdtHandler.h @@ -0,0 +1,36 @@ +#ifndef BSP_Q7S_CORE_XIPHOSWDTHANDLER_H_ +#define BSP_Q7S_CORE_XIPHOSWDTHANDLER_H_ + +#include +#include +#include +#include + +class XiphosWdtHandler : public SystemObject, public ExecutableObjectIF, public HasActionsIF { + public: + enum ActionId { ENABLE = 0, DISABLE = 1 }; + + XiphosWdtHandler(object_id_t objectId); + ReturnValue_t performOperation(uint8_t opCode) override; + ReturnValue_t initialize() override; + + ReturnValue_t executeAction(ActionId_t actionId, MessageQueueId_t commandedBy, + const uint8_t* data, size_t size) override; + [[nodiscard]] virtual MessageQueueId_t getCommandQueue() const override; + + private: + // Wrappers to ensure idempotency of trash C API. + ReturnValue_t enableWdt(); + ReturnValue_t disableWdt(); + // Timeout duration range specified by Xiphos: 0.001 seconds to 171 seconds. The libxiphos API + // expects an int, so I guess this translates to 1 to 171 seconds. + // WARNING: DO NOT SET THIS HIGHER THAN 80 SECONDS! + // Possible bug in Xiphos/Xilinx kernel driver for watchdog, related to overflow. + int timeoutSeconds = 80; + bool enabled = false; + struct watchdog_s* wdtHandle = nullptr; + MessageQueueIF* requestQueue = nullptr; + ActionHelper actionHelper; +}; + +#endif /* BSP_Q7S_CORE_XIPHOSWDTHANDLER_H_ */ diff --git a/bsp_q7s/core/defs.h b/bsp_q7s/core/defs.h new file mode 100644 index 0000000..a82c1c5 --- /dev/null +++ b/bsp_q7s/core/defs.h @@ -0,0 +1,45 @@ +#ifndef BSP_Q7S_CORE_DEFS_H_ +#define BSP_Q7S_CORE_DEFS_H_ + +#include + +namespace core { + +extern uint8_t FW_VERSION_MAJOR; +extern uint8_t FW_VERSION_MINOR; +extern uint8_t FW_VERSION_REVISION; +extern bool FW_VERSION_HAS_SHA; +extern char FW_VERSION_GIT_SHA[4]; + +static const uint8_t HK_SET_ENTRIES = 3; +static const uint32_t HK_SET_ID = 5; + +enum PoolIds { TEMPERATURE, PS_VOLTAGE, PL_VOLTAGE }; + +/** + * @brief Set storing OBC internal housekeeping data + */ +class HkSet : public StaticLocalDataSet { + public: + HkSet(HasLocalDataPoolIF* owner) : StaticLocalDataSet(owner, HK_SET_ID) {} + + HkSet(object_id_t objectId) : StaticLocalDataSet(sid_t(objectId, HK_SET_ID)) {} + + // On-chip temperature + lp_var_t temperature = lp_var_t(sid.objectId, PoolIds::TEMPERATURE, this); + // Processing system VCC + lp_var_t psVoltage = lp_var_t(sid.objectId, PoolIds::PS_VOLTAGE, this); + // Programmable logic VCC + lp_var_t plVoltage = lp_var_t(sid.objectId, PoolIds::PL_VOLTAGE, this); + + void printSet() { + sif::info << "HkSet::printSet: On-chip temperature: " << this->temperature << " °C" + << std::endl; + sif::info << "HkSet::printSet: PS voltage: " << this->psVoltage << " mV" << std::endl; + sif::info << "HkSet::printSet: PL voltage: " << this->plVoltage << " mV" << std::endl; + } +}; + +} // namespace core + +#endif /* BSP_Q7S_CORE_DEFS_H_ */ diff --git a/bsp_q7s/em/CMakeLists.txt b/bsp_q7s/em/CMakeLists.txt new file mode 100644 index 0000000..5ac6819 --- /dev/null +++ b/bsp_q7s/em/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PRIVATE emObjectFactory.cpp) diff --git a/bsp_q7s/em/emObjectFactory.cpp b/bsp_q7s/em/emObjectFactory.cpp new file mode 100644 index 0000000..0c0c3fa --- /dev/null +++ b/bsp_q7s/em/emObjectFactory.cpp @@ -0,0 +1,184 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/core/CoreController.h" +#include "busConf.h" +#include "common/config/devices/addresses.h" +#include "devConf.h" +#include "dummies/helperFactory.h" +#include "eive/objects.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" +#include "linux/ObjectFactory.h" +#include "linux/callbacks/gpioCallbacks.h" +#include "mission/genericFactory.h" +#include "mission/system/com/comModeTree.h" + +void ObjectFactory::produce(void* args) { + ObjectFactory::setStatics(); + HealthTableIF* healthTable = nullptr; + PusTmFunnel* pusFunnel = nullptr; + CfdpTmFunnel* cfdpFunnel = nullptr; + StorageManagerIF* ipcStore = nullptr; + StorageManagerIF* tmStore = nullptr; + + bool enableHkSets = false; +#if OBSW_ENABLE_PERIODIC_HK == 1 + enableHkSets = true; +#endif + + PersistentTmStores stores; + readFirmwareVersion(); + new XiphosWdtHandler(objects::XIPHOS_WDT); + ObjectFactory::produceGenericObjects(&healthTable, &pusFunnel, &cfdpFunnel, + *SdCardManager::instance(), &ipcStore, &tmStore, stores, 200, + enableHkSets, true); + + LinuxLibgpioIF* gpioComIF = nullptr; + SerialComIF* uartComIF = nullptr; + SpiComIF* spiMainComIF = nullptr; + I2cComIF* i2cComIF = nullptr; + createCommunicationInterfaces(&gpioComIF, &uartComIF, &spiMainComIF, &i2cComIF); + // Adding GPIOs for chip select decoding and initializing them. + q7s::gpioCallbacks::initSpiCsDecoder(gpioComIF); + gpioCallbacks::disableAllDecoder(gpioComIF); + createPlI2cResetGpio(gpioComIF); + + // Hardware is usually not connected to EM, so we need to create dummies which replace lower + // level components. + dummy::DummyCfg dummyCfg; + dummyCfg.addCoreCtrlCfg = false; + dummyCfg.addCamSwitcherDummy = false; +#if OBSW_ADD_SYRLINKS == 1 + dummyCfg.addSyrlinksDummies = false; +#endif +#if OBSW_ADD_PLOC_SUPERVISOR == 1 || OBSW_ADD_PLOC_MPSOC == 1 + dummyCfg.addPlocDummies = false; +#endif +#if OBSW_ADD_TMP_DEVICES == 1 + std::vector> tmpDevsToAdd = {{ + {objects::TMP1075_HANDLER_PLPCDU_0, addresses::TMP1075_PLPCDU_0}, + {objects::TMP1075_HANDLER_PLPCDU_1, addresses::TMP1075_PLPCDU_1}, + {objects::TMP1075_HANDLER_IF_BOARD, addresses::TMP1075_IF_BOARD}, + }}; + createTmpComponents(tmpDevsToAdd); + dummy::Tmp1075Cfg tmpCfg{}; + tmpCfg.addTcsBrd0 = true; + tmpCfg.addTcsBrd1 = true; + tmpCfg.addPlPcdu0 = false; + tmpCfg.addPlPcdu1 = false; + tmpCfg.addIfBrd = false; + dummyCfg.tmp1075Cfg = tmpCfg; +#endif +#if OBSW_ADD_GOMSPACE_PCDU == 1 + dummyCfg.addPowerDummies = false; + // The ACU broke. + dummyCfg.addOnlyAcuDummy = true; +#endif +#if OBSW_ADD_STAR_TRACKER == 1 + dummyCfg.addStrDummy = false; +#endif +#if OBSW_ADD_SCEX_DEVICE == 0 + dummyCfg.addScexDummy = true; +#endif +#if OBSW_ADD_BPX_BATTERY_HANDLER == 1 + dummyCfg.addBpxBattDummy = false; +#endif +#if OBSW_ADD_ACS_BOARD == 1 + dummyCfg.addAcsBoardDummies = false; +#endif +#if OBSW_ADD_PL_PCDU == 0 + dummyCfg.addPlPcduDummy = true; +#endif + + PowerSwitchIF* pwrSwitcher = nullptr; +#if OBSW_ADD_GOMSPACE_PCDU == 0 + pwrSwitcher = new PcduHandlerDummy(objects::PCDU_HANDLER); +#else + createPcduComponents(gpioComIF, &pwrSwitcher, enableHkSets); +#endif + satsystem::EIVE_SYSTEM.setI2cRecoveryParams(pwrSwitcher); + + const char* battAndImtqI2cDev = q7s::I2C_PL_EIVE; + if (core::FW_VERSION_MAJOR >= 4) { + battAndImtqI2cDev = q7s::I2C_PS_EIVE; + } + static_cast(battAndImtqI2cDev); + +#if OBSW_ADD_BPX_BATTERY_HANDLER == 1 + createBpxBatteryComponent(enableHkSets, battAndImtqI2cDev); +#endif + + dummy::createDummies(dummyCfg, *pwrSwitcher, gpioComIF, enableHkSets); + + createPowerController(true, enableHkSets); + + new CoreController(objects::CORE_CONTROLLER, enableHkSets); + + auto* stackHandler = new Stack5VHandler(*pwrSwitcher); + static_cast(stackHandler); + + // Initialize chip select to avoid SPI bus issues. + createRadSensorChipSelect(gpioComIF); + +#if OBSW_ADD_ACS_BOARD == 1 + createAcsBoardComponents(*spiMainComIF, gpioComIF, uartComIF, *pwrSwitcher, true, + adis1650x::Type::ADIS16507); +#else + // Still add all GPIOs for EM. + GpioCookie* acsBoardGpios = new GpioCookie(); + createAcsBoardGpios(*acsBoardGpios); + gpioChecker(gpioComIF->addGpios(acsBoardGpios), "ACS Board"); +#endif + +#if OBSW_ADD_MGT == 1 + createImtqComponents(pwrSwitcher, enableHkSets, battAndImtqI2cDev); +#endif + +#if OBSW_ADD_RW == 1 + createReactionWheelComponents(gpioComIF, pwrSwitcher); +#endif + +#if OBSW_ADD_STAR_TRACKER == 1 + createStrComponents(pwrSwitcher, *SdCardManager::instance()); +#endif /* OBSW_ADD_STAR_TRACKER == 1 */ + +#if OBSW_ADD_SYRLINKS == 1 + createSyrlinksComponents(pwrSwitcher); +#endif + +#if OBSW_ADD_PL_PCDU == 1 + createPlPcduComponents(gpioComIF, spiMainComIF, pwrSwitcher, *stackHandler); +#endif + createPayloadComponents(gpioComIF, *pwrSwitcher); + +#if OBSW_ADD_CCSDS_IP_CORES == 1 + CcsdsIpCoreHandler* ipCoreHandler = nullptr; + CcsdsComponentArgs ccsdsArgs(*gpioComIF, *ipcStore, *tmStore, stores, *pusFunnel, *cfdpFunnel, + &ipCoreHandler, 0, 0); + createCcsdsIpComponentsWrapper(ccsdsArgs); +#endif /* OBSW_ADD_CCSDS_IP_CORES == 1 */ + + /* Test Task */ +#if OBSW_ADD_TEST_CODE == 1 + createTestComponents(gpioComIF); +#endif /* OBSW_ADD_TEST_CODE == 1 */ +#if OBSW_ADD_SCEX_DEVICE == 1 + createScexComponents(q7s::UART_SCEX_DEV, pwrSwitcher, *SdCardManager::instance(), false, + power::Switches::PDU1_CH5_SOLAR_CELL_EXP_5V); +#endif + createAcsController(true, enableHkSets, *SdCardManager::instance()); + HeaterHandler* heaterHandler; + createHeaterComponents(gpioComIF, pwrSwitcher, healthTable, heaterHandler); + createThermalController(*heaterHandler, true); + satsystem::init(true); +} diff --git a/bsp_q7s/fmObjectFactory.cpp b/bsp_q7s/fmObjectFactory.cpp new file mode 100644 index 0000000..71722e5 --- /dev/null +++ b/bsp_q7s/fmObjectFactory.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/core/CoreController.h" +#include "busConf.h" +#include "devConf.h" +#include "devices/addresses.h" +#include "eive/objects.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" +#include "linux/ObjectFactory.h" +#include "linux/callbacks/gpioCallbacks.h" +#include "mission/genericFactory.h" +#include "mission/system/systemTree.h" +#include "mission/tmtc/tmFilters.h" + +void ObjectFactory::produce(void* args) { + ObjectFactory::setStatics(); + HealthTableIF* healthTable = nullptr; + PusTmFunnel* pusFunnel = nullptr; + CfdpTmFunnel* cfdpFunnel = nullptr; + StorageManagerIF* ipcStore = nullptr; + StorageManagerIF* tmStore = nullptr; + + bool enableHkSets = false; +#if OBSW_ENABLE_PERIODIC_HK == 1 + enableHkSets = true; +#endif + + PersistentTmStores stores; + readFirmwareVersion(); + new XiphosWdtHandler(objects::XIPHOS_WDT); + ObjectFactory::produceGenericObjects(&healthTable, &pusFunnel, &cfdpFunnel, + *SdCardManager::instance(), &ipcStore, &tmStore, stores, 200, + true, true); + + LinuxLibgpioIF* gpioComIF = nullptr; + SerialComIF* uartComIF = nullptr; + SpiComIF* spiMainComIF = nullptr; + I2cComIF* i2cComIF = nullptr; + PowerSwitchIF* pwrSwitcher = nullptr; + createCommunicationInterfaces(&gpioComIF, &uartComIF, &spiMainComIF, &i2cComIF); + /* Adding gpios for chip select decoding to the gpioComIf */ + q7s::gpioCallbacks::initSpiCsDecoder(gpioComIF); + gpioCallbacks::disableAllDecoder(gpioComIF); + createPlI2cResetGpio(gpioComIF); + + new CoreController(objects::CORE_CONTROLLER, enableHkSets); + createPcduComponents(gpioComIF, &pwrSwitcher, enableHkSets); + satsystem::EIVE_SYSTEM.setI2cRecoveryParams(pwrSwitcher); + + auto* stackHandler = new Stack5VHandler(*pwrSwitcher); + +#if OBSW_ADD_RAD_SENSORS == 1 + createRadSensorComponent(gpioComIF, *stackHandler); +#endif +#if OBSW_ADD_SUN_SENSORS == 1 + createSunSensorComponents(gpioComIF, spiMainComIF, *pwrSwitcher, q7s::SPI_DEFAULT_DEV, true); +#endif + +#if OBSW_ADD_ACS_BOARD == 1 + createAcsBoardComponents(*spiMainComIF, gpioComIF, uartComIF, *pwrSwitcher, true, + adis1650x::Type::ADIS16505); +#endif + HeaterHandler* heaterHandler; + createHeaterComponents(gpioComIF, pwrSwitcher, healthTable, heaterHandler); +#if OBSW_ADD_TMP_DEVICES == 1 + std::vector> tmpDevsToAdd = {{ + {objects::TMP1075_HANDLER_TCS_0, addresses::TMP1075_TCS_0}, + {objects::TMP1075_HANDLER_TCS_1, addresses::TMP1075_TCS_1}, + {objects::TMP1075_HANDLER_PLPCDU_0, addresses::TMP1075_PLPCDU_0}, + // damaged + // {objects::TMP1075_HANDLER_PLPCDU_1, addresses::TMP1075_PLPCDU_1}, + {objects::TMP1075_HANDLER_IF_BOARD, addresses::TMP1075_IF_BOARD}, + }}; + + createTmpComponents(tmpDevsToAdd); +#endif + createSolarArrayDeploymentComponents(*pwrSwitcher, *gpioComIF); + + const char* battAndImtqI2cDev = q7s::I2C_PL_EIVE; + if (core::FW_VERSION_MAJOR >= 4) { + battAndImtqI2cDev = q7s::I2C_PS_EIVE; + } +#if OBSW_ADD_MGT == 1 + createImtqComponents(pwrSwitcher, enableHkSets, battAndImtqI2cDev); +#endif + createReactionWheelComponents(gpioComIF, pwrSwitcher); + +#if OBSW_ADD_BPX_BATTERY_HANDLER == 1 + createBpxBatteryComponent(enableHkSets, battAndImtqI2cDev); +#endif + createPowerController(true, enableHkSets); + +#if OBSW_ADD_PL_PCDU == 1 + createPlPcduComponents(gpioComIF, spiMainComIF, pwrSwitcher, *stackHandler); +#endif +#if OBSW_ADD_SYRLINKS == 1 + createSyrlinksComponents(pwrSwitcher); +#endif /* OBSW_ADD_SYRLINKS == 1 */ + + createRtdComponents(q7s::SPI_DEFAULT_DEV, gpioComIF, pwrSwitcher, spiMainComIF); + createPayloadComponents(gpioComIF, *pwrSwitcher); + +#if OBSW_ADD_STAR_TRACKER == 1 + createStrComponents(pwrSwitcher, *SdCardManager::instance()); +#endif /* OBSW_ADD_STAR_TRACKER == 1 */ + +#if OBSW_ADD_CCSDS_IP_CORES == 1 + CcsdsIpCoreHandler* ipCoreHandler = nullptr; + CcsdsComponentArgs ccsdsArgs(*gpioComIF, *ipcStore, *tmStore, stores, *pusFunnel, *cfdpFunnel, + &ipCoreHandler, 0, 0); + createCcsdsIpComponentsWrapper(ccsdsArgs); +#endif /* OBSW_ADD_CCSDS_IP_CORES == 1 */ + +#if OBSW_ADD_SCEX_DEVICE == 1 + createScexComponents(q7s::UART_SCEX_DEV, pwrSwitcher, *SdCardManager::instance(), false, + power::Switches::PDU1_CH5_SOLAR_CELL_EXP_5V); +#endif + /* Test Task */ +#if OBSW_ADD_TEST_CODE == 1 + createTestComponents(gpioComIF); +#endif /* OBSW_ADD_TEST_CODE == 1 */ + + createMiscComponents(); + createThermalController(*heaterHandler, false); + createAcsController(true, enableHkSets, *SdCardManager::instance()); + satsystem::init(false); +} diff --git a/bsp_q7s/fs/CMakeLists.txt b/bsp_q7s/fs/CMakeLists.txt new file mode 100644 index 0000000..0e51683 --- /dev/null +++ b/bsp_q7s/fs/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(${OBSW_NAME} PRIVATE helpers.cpp SdCardManager.cpp + FilesystemHelper.cpp) diff --git a/bsp_q7s/fs/FilesystemHelper.cpp b/bsp_q7s/fs/FilesystemHelper.cpp new file mode 100644 index 0000000..8f49d10 --- /dev/null +++ b/bsp_q7s/fs/FilesystemHelper.cpp @@ -0,0 +1,38 @@ +#include "FilesystemHelper.h" + +#include +#include + +#include "SdCardManager.h" +#include "eive/definitions.h" +#include "fsfw/serviceinterface.h" + +FilesystemHelper::FilesystemHelper() {} + +ReturnValue_t FilesystemHelper::checkPath(std::string path) { + SdCardManager* sdcMan = SdCardManager::instance(); + if (sdcMan == nullptr) { + sif::warning << "FilesystemHelper::checkPath: Invalid SD card manager" << std::endl; + return returnvalue::FAILED; + } + if (path.substr(0, sizeof(config::SD_0_MOUNT_POINT)) == std::string(config::SD_0_MOUNT_POINT)) { + if (!sdcMan->isSdCardUsable(sd::SLOT_0)) { + sif::warning << "FilesystemHelper::checkPath: SD card 0 not mounted" << std::endl; + return SD_NOT_MOUNTED; + } + } else if (path.substr(0, sizeof(config::SD_1_MOUNT_POINT)) == + std::string(config::SD_1_MOUNT_POINT)) { + if (!sdcMan->isSdCardUsable(sd::SLOT_1)) { + sif::warning << "FilesystemHelper::checkPath: SD card 1 not mounted" << std::endl; + return SD_NOT_MOUNTED; + } + } + return returnvalue::OK; +} + +ReturnValue_t FilesystemHelper::fileExists(std::string file) { + if (not std::filesystem::exists(file)) { + return FILE_NOT_EXISTS; + } + return returnvalue::OK; +} diff --git a/bsp_q7s/fs/FilesystemHelper.h b/bsp_q7s/fs/FilesystemHelper.h new file mode 100644 index 0000000..ea1b570 --- /dev/null +++ b/bsp_q7s/fs/FilesystemHelper.h @@ -0,0 +1,49 @@ +#ifndef BSP_Q7S_MEMORY_FILESYSTEMHELPER_H_ +#define BSP_Q7S_MEMORY_FILESYSTEMHELPER_H_ + +#include + +#include "eive/resultClassIds.h" +#include "fsfw/returnvalues/returnvalue.h" + +/** + * @brief This class implements often used functions related to the file system management. + * + * @author J. Meier + */ +class FilesystemHelper { + public: + static const uint8_t INTERFACE_ID = CLASS_ID::FILE_SYSTEM_HELPER; + + //! [EXPORT] : [COMMENT] SD card specified with path string not mounted + static const ReturnValue_t SD_NOT_MOUNTED = MAKE_RETURN_CODE(0xA0); + //! [EXPORT] : [COMMENT] Specified file does not exist on filesystem + static const ReturnValue_t FILE_NOT_EXISTS = MAKE_RETURN_CODE(0xA1); + + /** + * @brief In case the path points to a directory on the sd card, the function checks if the + * appropriate SD card is mounted. + * + * @param path Path to check + * + * @return returnvalue::OK if path points to SD card and the appropriate SD card is mounted or if + * path does not point to SD card. + * Return error code if path points to SD card and the corresponding SD card is not + * mounted. + */ + static ReturnValue_t checkPath(std::string path); + + /** + * @brief Checks if the file exists on the filesystem. + * + * @param file File to check + * + * @return returnvalue::OK if file exists, otherwise return error code. + */ + static ReturnValue_t fileExists(std::string file); + + private: + FilesystemHelper(); +}; + +#endif /* BSP_Q7S_MEMORY_FILESYSTEMHELPER_H_ */ diff --git a/bsp_q7s/fs/SdCardManager.cpp b/bsp_q7s/fs/SdCardManager.cpp new file mode 100644 index 0000000..ffbed66 --- /dev/null +++ b/bsp_q7s/fs/SdCardManager.cpp @@ -0,0 +1,584 @@ +#include "SdCardManager.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/memory/scratchApi.h" +#include "eive/definitions.h" +#include "eive/objects.h" +#include "fsfw/ipc/MutexFactory.h" +#include "fsfw/serviceinterface/ServiceInterface.h" +#include "linux/utility/utility.h" + +SdCardManager* SdCardManager::INSTANCE = nullptr; + +SdCardManager::SdCardManager() : SystemObject(objects::SDC_MANAGER), cmdExecutor(256) { + sdLock = MutexFactory::instance()->createMutex(); + prefLock = MutexFactory::instance()->createMutex(); + defaultLock = MutexFactory::instance()->createMutex(); + + MutexGuard mg(prefLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + if (mg.getLockResult() != returnvalue::OK) { + sif::error << "SdCardManager::SdCardManager: Mutex lock failed" << std::endl; + } + uint8_t prefSdRaw = 0; + ReturnValue_t result = scratch::readNumber(scratch::PREFERED_SDC_KEY, prefSdRaw); + + if (result != returnvalue::OK) { + if (result == scratch::KEY_NOT_FOUND) { + sif::warning << "CoreController::sdCardInit: " + "Preferred SD card not set. Setting to 0" + << std::endl; + scratch::writeNumber(scratch::PREFERED_SDC_KEY, static_cast(sd::SdCard::SLOT_0)); + prefSdRaw = sd::SdCard::SLOT_0; + + } else { + // Should not happen. + // TODO: Maybe trigger event? + sif::error << "SdCardManager::SdCardManager: Reading preferred SD card from scratch" + "buffer failed" + << std::endl; + prefSdRaw = sd::SdCard::SLOT_0; + } + } + sdInfo.pref = static_cast(prefSdRaw); +} + +SdCardManager::~SdCardManager() {} + +void SdCardManager::create() { + if (INSTANCE == nullptr) { + INSTANCE = new SdCardManager(); + } +} + +SdCardManager* SdCardManager::instance() { + SdCardManager::create(); + return SdCardManager::INSTANCE; +} + +ReturnValue_t SdCardManager::switchOnSdCard(sd::SdCard sdCard, bool doMountSdCard, + SdStatePair* statusPair) { + ReturnValue_t result = returnvalue::OK; + if (doMountSdCard) { + if (not blocking) { + sif::warning << "SdCardManager::switchOnSdCard: Two-step command but manager is" + " not configured for blocking operation. " + "Forcing blocking mode.." + << std::endl; + blocking = true; + } + } + std::unique_ptr sdStatusPtr; + if (statusPair == nullptr) { + sdStatusPtr = std::make_unique(); + statusPair = sdStatusPtr.get(); + result = getSdCardsStatus(*statusPair); + if (result != returnvalue::OK) { + return result; + } + } + + // Not allowed, this function turns on one SD card + if (sdCard == sd::SdCard::BOTH) { + sif::warning << "SdCardManager::switchOffSdCard: API does not allow sd::SdStatus::BOTH" + << std::endl; + return returnvalue::FAILED; + } + + sd::SdState currentState; + if (sdCard == sd::SdCard::SLOT_0) { + currentState = statusPair->first; + } else if (sdCard == sd::SdCard::SLOT_1) { + currentState = statusPair->second; + } else { + // Should not happen + currentState = sd::SdState::OFF; + } + + if (currentState == sd::SdState::ON) { + if (not doMountSdCard) { + return ALREADY_ON; + } else { + return mountSdCard(sdCard); + } + } else if (currentState == sd::SdState::MOUNTED) { + result = ALREADY_MOUNTED; + } else if (currentState == sd::SdState::OFF) { + result = setSdCardState(sdCard, true); + } else { + result = returnvalue::FAILED; + } + + if (result != returnvalue::OK or not doMountSdCard) { + return result; + } + + return mountSdCard(sdCard); +} + +ReturnValue_t SdCardManager::switchOffSdCard(sd::SdCard sdCard, SdStatePair& sdStates, + bool doUnmountSdCard) { + if (doUnmountSdCard) { + if (not blocking) { + sif::warning << "SdCardManager::switchOffSdCard: Two-step command but manager is" + " not configured for blocking operation. Forcing blocking mode.." + << std::endl; + blocking = true; + } + } + // Not allowed, this function turns off one SD card + if (sdCard == sd::SdCard::BOTH) { + sif::warning << "SdCardManager::switchOffSdCard: API does not allow sd::SdStatus::BOTH" + << std::endl; + return returnvalue::FAILED; + } + if (sdCard == sd::SdCard::SLOT_0) { + if (sdStates.first == sd::SdState::OFF) { + return ALREADY_OFF; + } + } else if (sdCard == sd::SdCard::SLOT_1) { + if (sdStates.second == sd::SdState::OFF) { + return ALREADY_OFF; + } + } + + if (doUnmountSdCard) { + ReturnValue_t result = unmountSdCard(sdCard); + if (result != returnvalue::OK) { + return result; + } + } + + return setSdCardState(sdCard, false); +} + +ReturnValue_t SdCardManager::setSdCardState(sd::SdCard sdCard, bool on) { + using namespace std; + if (cmdExecutor.getCurrentState() == CommandExecutor::States::PENDING) { + return CommandExecutor::COMMAND_PENDING; + } + string sdstring = ""; + string statestring = ""; + if (sdCard == sd::SdCard::SLOT_0) { + sdstring = "0"; + } else if (sdCard == sd::SdCard::SLOT_1) { + sdstring = "1"; + } + if (on) { + currentOp = Operations::SWITCHING_ON; + statestring = "on"; + } else { + currentOp = Operations::SWITCHING_OFF; + statestring = "off"; + } + ostringstream command; + command << "q7hw sd set " << sdstring << " " << statestring; + cmdExecutor.load(command.str(), blocking, printCmdOutput); + ReturnValue_t result = cmdExecutor.execute(); + if (result != returnvalue::OK) { + utility::handleSystemError(cmdExecutor.getLastError(), "SdCardManager::setSdCardState"); + } + return result; +} + +ReturnValue_t SdCardManager::getSdCardsStatus(SdStatePair& sdStates) { + MutexGuard mg(sdLock, LOCK_TYPE, SD_LOCK_TIMEOUT, LOCK_CTX); + sdStates = this->sdStates; + return returnvalue::OK; +} + +ReturnValue_t SdCardManager::mountSdCard(sd::SdCard sdCard) { + using namespace std; + if (cmdExecutor.getCurrentState() == CommandExecutor::States::PENDING) { + sif::warning << "SdCardManager::mountSdCard: Command still pending" << std::endl; + return CommandExecutor::COMMAND_PENDING; + } + if (sdCard == sd::SdCard::BOTH) { + sif::warning << "SdCardManager::mountSdCard: API does not allow sd::SdStatus::BOTH" + << std::endl; + return returnvalue::FAILED; + } + string mountDev; + string mountPoint; + if (sdCard == sd::SdCard::SLOT_0) { + mountDev = SD_0_DEV_NAME; + mountPoint = config::SD_0_MOUNT_POINT; + } else if (sdCard == sd::SdCard::SLOT_1) { + mountDev = SD_1_DEV_NAME; + mountPoint = config::SD_1_MOUNT_POINT; + } + std::error_code e; + if (not filesystem::exists(mountDev, e)) { + sif::warning << "SdCardManager::mountSdCard: Device file does not exists. Make sure to" + " turn on the SD card" + << std::endl; + return MOUNT_ERROR; + } + + if (not blocking) { + currentOp = Operations::MOUNTING; + } + string sdMountCommand = "mount " + mountDev + " " + mountPoint; + cmdExecutor.load(sdMountCommand, blocking, printCmdOutput); + ReturnValue_t result = cmdExecutor.execute(); + if (blocking and result != returnvalue::OK) { + utility::handleSystemError(cmdExecutor.getLastError(), "SdCardManager::mountSdCard"); + } + return result; +} + +ReturnValue_t SdCardManager::unmountSdCard(sd::SdCard sdCard) { + if (cmdExecutor.getCurrentState() == CommandExecutor::States::PENDING) { + return CommandExecutor::COMMAND_PENDING; + } + using namespace std; + if (sdCard == sd::SdCard::BOTH) { + sif::warning << "SdCardManager::unmountSdCard: API does not allow sd::SdStatus::BOTH" + << std::endl; + return returnvalue::FAILED; + } + string mountPoint; + if (sdCard == sd::SdCard::SLOT_0) { + mountPoint = config::SD_0_MOUNT_POINT; + } else if (sdCard == sd::SdCard::SLOT_1) { + mountPoint = config::SD_1_MOUNT_POINT; + } + std::error_code e; + if (not filesystem::exists(mountPoint, e)) { + sif::error << "SdCardManager::unmountSdCard: Default mount point " << mountPoint + << "does not exist" << std::endl; + return UNMOUNT_ERROR; + } + if (filesystem::is_empty(mountPoint)) { + // The mount point will always exist, but if it is empty, that is strong hint that + // the SD card was not mounted properly. Still proceed with operation. + sif::warning << "SdCardManager::unmountSdCard: Mount point is empty!" << std::endl; + } + string sdUnmountCommand = "umount " + mountPoint; + if (not blocking) { + currentOp = Operations::UNMOUNTING; + } + cmdExecutor.load(sdUnmountCommand, blocking, printCmdOutput); + ReturnValue_t result = cmdExecutor.execute(); + if (blocking and result != returnvalue::OK) { + utility::handleSystemError(cmdExecutor.getLastError(), "SdCardManager::unmountSdCard"); + } + return result; +} + +ReturnValue_t SdCardManager::sanitizeState(SdStatePair* statusPair, sd::SdCard prefSdCard) { + std::unique_ptr sdStatusPtr; + ReturnValue_t result = returnvalue::OK; + // Enforce blocking operation for now. Be careful to reset it when returning prematurely! + bool resetNonBlockingState = false; + if (not this->blocking) { + blocking = true; + resetNonBlockingState = true; + } + if (statusPair == nullptr) { + return returnvalue::FAILED; + } + getSdCardsStatus(*statusPair); + + if (statusPair->first == sd::SdState::ON) { + result = mountSdCard(prefSdCard); + } + + result = switchOnSdCard(prefSdCard, true, statusPair); + if (resetNonBlockingState) { + blocking = false; + } + return result; +} + +void SdCardManager::resetState() { + cmdExecutor.reset(); + currentOp = Operations::IDLE; +} + +void SdCardManager::processSdStatusLine(std::string& line, uint8_t& idx, sd::SdCard& currentSd) { + using namespace std; + istringstream iss(line); + string word; + bool slotLine = false; + bool mountLine = false; + while (iss >> word) { + if (word == "Slot") { + slotLine = true; + } + if (word == "Mounted") { + mountLine = true; + } + + if (slotLine) { + if (word == "1:") { + currentSd = sd::SdCard::SLOT_1; + } + + if (word == "on") { + if (currentSd == sd::SdCard::SLOT_0) { + sdStates.first = sd::SdState::ON; + } else { + sdStates.second = sd::SdState::ON; + } + } else if (word == "off") { + MutexGuard mg(sdLock, LOCK_TYPE, SD_LOCK_TIMEOUT, LOCK_CTX); + if (currentSd == sd::SdCard::SLOT_0) { + sdStates.first = sd::SdState::OFF; + } else { + sdStates.second = sd::SdState::OFF; + } + } + } + + if (mountLine) { + MutexGuard mg(sdLock, LOCK_TYPE, SD_LOCK_TIMEOUT, LOCK_CTX); + if (currentSd == sd::SdCard::SLOT_0) { + sdStates.first = sd::SdState::MOUNTED; + } else { + sdStates.second = sd::SdState::MOUNTED; + } + } + + if (idx > 5) { + sif::warning << "SdCardManager::sdCardActive: /tmp/sd_status.txt has more than 6 " + "lines and might be invalid!" + << std::endl; + } + } + idx++; +} + +std::optional SdCardManager::getPreferredSdCard() const { + MutexGuard mg(prefLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + auto res = mg.getLockResult(); + if (res != returnvalue::OK) { + sif::error << "SdCardManager::getPreferredSdCard: Lock error" << std::endl; + } + return sdInfo.pref; +} + +ReturnValue_t SdCardManager::setPreferredSdCard(sd::SdCard sdCard) { + MutexGuard mg(prefLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + if (sdCard == sd::SdCard::BOTH) { + return returnvalue::FAILED; + } + sdInfo.pref = sdCard; + return scratch::writeNumber(scratch::PREFERED_SDC_KEY, static_cast(sdCard)); +} + +ReturnValue_t SdCardManager::updateSdCardStateFile() { + using namespace std; + if (cmdExecutor.getCurrentState() == CommandExecutor::States::PENDING) { + return CommandExecutor::COMMAND_PENDING; + } + // Use q7hw utility and pipe the command output into the state file + std::string updateCmd = "q7hw sd info all > " + std::string(SD_STATE_FILE); + cmdExecutor.load(updateCmd, true, printCmdOutput); + ReturnValue_t result = cmdExecutor.execute(); + if (result != returnvalue::OK) { + utility::handleSystemError(cmdExecutor.getLastError(), "SdCardManager::mountSdCard"); + } + + std::error_code e; + if (not filesystem::exists(SD_STATE_FILE, e)) { + return STATUS_FILE_NEXISTS; + } + + // Now the file should exist in any case. Still check whether it exists. + fstream sdStatus(SD_STATE_FILE); + if (not sdStatus.good()) { + return STATUS_FILE_NEXISTS; + } + string line; + uint8_t idx = 0; + sd::SdCard currentSd = sd::SdCard::SLOT_0; + // Process status file line by line + while (std::getline(sdStatus, line)) { + processSdStatusLine(line, idx, currentSd); + } + if (sdStates.first != sd::SdState::MOUNTED && sdStates.second != sd::SdState::MOUNTED) { + sdCardActive = false; + } + return returnvalue::OK; +} + +const char* SdCardManager::getCurrentMountPrefix() const { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + if (currentPrefix.has_value()) { + return currentPrefix.value().c_str(); + } + return nullptr; +} + +SdCardManager::OpStatus SdCardManager::checkCurrentOp(Operations& currentOp) { + CommandExecutor::States state = cmdExecutor.getCurrentState(); + if (state == CommandExecutor::States::IDLE or state == CommandExecutor::States::COMMAND_LOADED) { + return OpStatus::IDLE; + } + currentOp = this->currentOp; + bool bytesRead = false; + +#if OBSW_ENABLE_TIMERS == 1 + Countdown timer(1000); +#endif + while (true) { + ReturnValue_t result = cmdExecutor.check(bytesRead); + // This timer can prevent deadlocks due to missconfigurations +#if OBSW_ENABLE_TIMERS == 1 + if (timer.hasTimedOut()) { + sif::error << "SdCardManager::checkCurrentOp: Timeout!" << std::endl; + return OpStatus::FAIL; + } +#endif + switch (result) { + case (CommandExecutor::BYTES_READ): { + continue; + } + case (CommandExecutor::EXECUTION_FINISHED): { + return OpStatus::SUCCESS; + } + case (returnvalue::OK): { + return OpStatus::ONGOING; + } + case (returnvalue::FAILED): { + return OpStatus::FAIL; + } + default: { + sif::warning << "SdCardManager::checkCurrentOp: Unhandled case" << std::endl; + } + } + } +} + +void SdCardManager::setBlocking(bool blocking) { this->blocking = blocking; } + +void SdCardManager::setPrintCommandOutput(bool print) { this->printCmdOutput = print; } + +bool SdCardManager::isSdCardUsable(std::optional sdCard) { + { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + if (markedUnusable) { + return false; + } + } + + MutexGuard mg(sdLock, LOCK_TYPE, SD_LOCK_TIMEOUT, LOCK_CTX); + if (not sdCard) { + if (sdStates.first == sd::MOUNTED or sdStates.second == sd::MOUNTED) { + return true; + } + return false; + } + if (sdCard == sd::SLOT_0) { + if (sdStates.first == sd::MOUNTED) { + return true; + } else { + return false; + } + } + if (sdCard == sd::SLOT_1) { + if (sdStates.second == sd::MOUNTED) { + return true; + } else { + return false; + } + } + if (sdCard == sd::BOTH) { + if (sdStates.first == sd::MOUNTED && sdStates.second == sd::MOUNTED) { + return true; + } + } + return false; +} + +ReturnValue_t SdCardManager::isSdCardMountedReadOnly(sd::SdCard sdcard, bool& readOnly) { + std::ostringstream command; + if (sdcard == sd::SdCard::SLOT_0) { + command << "grep -q '" << config::SD_0_MOUNT_POINT << " ext4 rw,' /proc/mounts"; + } else if (sdcard == sd::SdCard::SLOT_1) { + command << "grep -q '" << config::SD_1_MOUNT_POINT << " ext4 rw,' /proc/mounts"; + } else { + return returnvalue::FAILED; + } + ReturnValue_t result = cmdExecutor.load(command.str(), true, false); + if (result != returnvalue::OK) { + return result; + } + result = cmdExecutor.execute(); + if (result == returnvalue::OK) { + readOnly = false; + return result; + } + readOnly = true; + return returnvalue::OK; +} + +ReturnValue_t SdCardManager::remountReadWrite(sd::SdCard sdcard) { + std::ostringstream command; + if (sdcard == sd::SdCard::SLOT_0) { + command << "mount -o remount,rw " << SD_0_DEV_NAME << " " << config::SD_0_MOUNT_POINT; + } else { + command << "mount -o remount,rw " << SD_1_DEV_NAME << " " << config::SD_1_MOUNT_POINT; + } + ReturnValue_t result = cmdExecutor.load(command.str(), true, false); + if (result != returnvalue::OK) { + return result; + } + return cmdExecutor.execute(); +} + +ReturnValue_t SdCardManager::performFsck(sd::SdCard sdcard, bool printOutput, int& linuxError) { + std::ostringstream command; + if (sdcard == sd::SdCard::SLOT_0) { + command << "fsck -y " << SD_0_DEV_NAME; + } else { + command << "fsck -y " << SD_1_DEV_NAME; + } + ReturnValue_t result = cmdExecutor.load(command.str(), true, printOutput); + if (result != returnvalue::OK) { + return result; + } + result = cmdExecutor.execute(); + if (result != returnvalue::OK) { + linuxError = cmdExecutor.getLastError(); + } + return result; +} + +void SdCardManager::setActiveSdCard(sd::SdCard sdCard) { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + sdInfo.active = sdCard; + if (sdInfo.active == sd::SdCard::SLOT_0) { + currentPrefix = config::SD_0_MOUNT_POINT; + } else { + currentPrefix = config::SD_1_MOUNT_POINT; + } +} + +std::optional SdCardManager::getActiveSdCard() const { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + if (markedUnusable) { + return std::nullopt; + } + return sdInfo.active; +} + +void SdCardManager::markUnusable() { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + markedUnusable = true; +} + +void SdCardManager::markUsable() { + MutexGuard mg(defaultLock, LOCK_TYPE, OTHER_TIMEOUT, LOCK_CTX); + markedUnusable = false; +} diff --git a/bsp_q7s/fs/SdCardManager.h b/bsp_q7s/fs/SdCardManager.h new file mode 100644 index 0000000..23a3d19 --- /dev/null +++ b/bsp_q7s/fs/SdCardManager.h @@ -0,0 +1,246 @@ +#ifndef BSP_Q7S_MEMORY_SDCARDACCESSMANAGER_H_ +#define BSP_Q7S_MEMORY_SDCARDACCESSMANAGER_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#include "events/subsystemIdRanges.h" +#include "fsfw/events/Event.h" +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw_hal/linux/CommandExecutor.h" +#include "mission/memory/SdCardMountedIF.h" +#include "mission/memory/definitions.h" +#include "returnvalues/classIds.h" + +class MutexIF; + +/** + * @brief Manages handling of SD cards like switching them on or off or getting the current + * state + */ +class SdCardManager : public SystemObject, public SdCardMountedIF { + friend class CoreController; + + public: + using mountInitCb = ReturnValue_t (*)(void* args); + + enum class Operations { SWITCHING_ON, SWITCHING_OFF, MOUNTING, UNMOUNTING, IDLE }; + + enum class OpStatus { IDLE, TIMEOUT, ONGOING, SUCCESS, FAIL }; + + using SdStatePair = std::pair; + + struct SdInfo { + sd::SdCard pref = sd::SdCard::NONE; + sd::SdCard other = sd::SdCard::NONE; + sd::SdCard active = sd::SdCard::NONE; + } sdInfo; + + static constexpr uint8_t INTERFACE_ID = CLASS_ID::SD_CARD_MANAGER; + + static constexpr ReturnValue_t OP_ONGOING = returnvalue::makeCode(INTERFACE_ID, 0); + static constexpr ReturnValue_t ALREADY_ON = returnvalue::makeCode(INTERFACE_ID, 1); + static constexpr ReturnValue_t ALREADY_MOUNTED = returnvalue::makeCode(INTERFACE_ID, 2); + static constexpr ReturnValue_t ALREADY_OFF = returnvalue::makeCode(INTERFACE_ID, 3); + static constexpr ReturnValue_t STATUS_FILE_NEXISTS = returnvalue::makeCode(INTERFACE_ID, 10); + static constexpr ReturnValue_t STATUS_FILE_FORMAT_INVALID = + returnvalue::makeCode(INTERFACE_ID, 11); + static constexpr ReturnValue_t MOUNT_ERROR = returnvalue::makeCode(INTERFACE_ID, 12); + static constexpr ReturnValue_t UNMOUNT_ERROR = returnvalue::makeCode(INTERFACE_ID, 13); + static constexpr ReturnValue_t SYSTEM_CALL_ERROR = returnvalue::makeCode(INTERFACE_ID, 14); + static constexpr ReturnValue_t POPEN_CALL_ERROR = returnvalue::makeCode(INTERFACE_ID, 15); + + static constexpr uint8_t SUBSYSTEM_ID = SUBSYSTEM_ID::FILE_SYSTEM; + + static constexpr Event SANITIZATION_FAILED = event::makeEvent(SUBSYSTEM_ID, 0, severity::LOW); + static constexpr Event MOUNTED_SD_CARD = event::makeEvent(SUBSYSTEM_ID, 1, severity::INFO); + + // C++17 does not support constexpr std::string yet + static constexpr char SD_0_DEV_NAME[] = "/dev/mmcblk0p1"; + static constexpr char SD_1_DEV_NAME[] = "/dev/mmcblk1p1"; + + static constexpr char SD_STATE_FILE[] = "/tmp/sd_status.txt"; + + virtual ~SdCardManager(); + + static void create(); + + /** + * Returns the single instance of the SD card manager. + */ + static SdCardManager* instance(); + + /** + * Set the preferred SD card which will determine which SD card will be used as the primary + * SD card in hot redundant and cold redundant mode. This function will not switch the + * SD cards which are currently on and mounted, this needs to be implemented by + * an upper layer by using #switchOffSdCard , #switchOnSdCard and #updateSdCardStateFile + * @param sdCard + * @return + */ + ReturnValue_t setPreferredSdCard(sd::SdCard sdCard); + + /** + * Get the currently configured preferred SD card + * @param sdCard + * @return + */ + std::optional getPreferredSdCard() const override; + + /** + * Switch on the specified SD card. + * @param sdCard + * @param doMountSdCard Mount the SD card after switching it on, which is necessary + * to use it + * @param statusPair If the status pair is already available, it can be passed here + * @return - returnvalue::OK on success, ALREADY_ON if it is already on, + * SYSTEM_CALL_ERROR on system error + */ + ReturnValue_t switchOnSdCard(sd::SdCard sdCard, bool doMountSdCard = true, + SdStatePair* statusPair = nullptr); + + /** + * Switch off the specified SD card. + * @param sdCard + * @param doUnmountSdCard Unmount the SD card before switching the card off, which makes + * the operation safer + * @param statusPair If the status pair is already available, it can be passed here + * @return - returnvalue::OK on success, ALREADY_ON if it is already on, + * SYSTEM_CALL_ERROR on system error + */ + ReturnValue_t switchOffSdCard(sd::SdCard sdCard, SdStatePair& sdStates, bool doUnmountSdCard); + + /** + * Get the state of the SD cards. If the state file does not exist, this function will + * take care of updating it. If it does not, the function will use the state file to get + * the status of the SD cards and set the field of the provided boolean pair. + * @param active Pair of booleans, where the first entry is the state of the first SD card + * and the second one the state of the second SD card + * @return - returnvalue::OK if the state was read successfully + * - STATUS_FILE_FORMAT_INVALID if there was an issue with the state file. The user + * should call #updateSdCardStateFile again in that case + * - STATUS_FILE_NEXISTS if the status file does not exist + */ + ReturnValue_t getSdCardsStatus(SdStatePair& active); + + /** + * Mount the specified SD card. This is necessary to use it. + * @param sdCard + * @return + */ + ReturnValue_t mountSdCard(sd::SdCard sdCard); + + /** + * Set the currently active SD card. This does not necessarily mean that the SD card is on or + * mounted + * @param sdCard + */ + void setActiveSdCard(sd::SdCard sdCard) override; + /** + * Get the currently active SD card. This does not necessarily mean that the SD card is on or + * mounted + * @return + */ + std::optional getActiveSdCard() const override; + + /** + * Unmount the specified SD card. This is recommended before switching it off. The SD card + * can't be used after it has been unmounted. + * @param sdCard + * @return + */ + ReturnValue_t unmountSdCard(sd::SdCard sdCard); + + /** + * In case that there is a discrepancy between the preferred SD card and the currently + * mounted one, this function will sanitize the state by attempting to mount the + * currently preferred SD card. If the caller already has state information, it can be + * passed into the function. For now, this operation will be enforced in blocking mode. + * @param statusPair Current SD card status capture with #getSdCardActiveStatus + * @param prefSdCard Preferred SD card captured with #getPreferredSdCard + * @throws std::bad_alloc if one of the two arguments was a nullptr and an allocation failed + * @return + */ + ReturnValue_t sanitizeState(SdStatePair* statusPair = nullptr, + sd::SdCard prefSdCard = sd::SdCard::NONE); + + /** + * If sd::SdCard::NONE is passed as an argument, this function will get the currently + * preferred SD card from the scratch buffer. + * @param prefSdCardPtr + * @return + */ + const char* getCurrentMountPrefix() const override; + + OpStatus checkCurrentOp(Operations& currentOp); + + /** + * If there are issues with the state machine, it can be reset with this function + */ + void resetState(); + + void setBlocking(bool blocking); + void setPrintCommandOutput(bool print); + + /** + * @brief Checks if an SD card is mounted. + * + * @param sdCard The SD card to check + * + * @return true if mounted, otherwise false + */ + bool isSdCardUsable(std::optional sdCard) override; + + ReturnValue_t isSdCardMountedReadOnly(sd::SdCard sdcard, bool& readOnly); + + ReturnValue_t remountReadWrite(sd::SdCard sdcard); + + ReturnValue_t performFsck(sd::SdCard sdcard, bool printOutput, int& linuxError); + + void markUnusable(); + void markUsable(); + + private: + CommandExecutor cmdExecutor; + SdStatePair sdStates; + Operations currentOp = Operations::IDLE; + bool blocking = false; + bool sdCardActive = true; + bool printCmdOutput = true; + bool markedUnusable = false; + MutexIF* sdLock = nullptr; + MutexIF* prefLock = nullptr; + MutexIF* defaultLock = nullptr; + static constexpr MutexIF::TimeoutType LOCK_TYPE = MutexIF::TimeoutType::WAITING; + static constexpr uint32_t SD_LOCK_TIMEOUT = 100; + static constexpr uint32_t OTHER_TIMEOUT = 20; + static constexpr char LOCK_CTX[] = "SdCardManager"; + + SdCardManager(); + + /** + * Update the state file or creates one if it does not exist. You need to call this + * function before calling #sdCardActive + * @return + * - returnvalue::OK if the state file was updated successfully + * - CommandExecutor::COMMAND_PENDING: Non-blocking command is pending + * - returnvalue::FAILED: blocking command failed + */ + ReturnValue_t updateSdCardStateFile(); + + ReturnValue_t setSdCardState(sd::SdCard sdCard, bool on); + + void processSdStatusLine(std::string& line, uint8_t& idx, sd::SdCard& currentSd); + + std::optional currentPrefix; + + static SdCardManager* INSTANCE; +}; + +#endif /* BSP_Q7S_MEMORY_SDCARDACCESSMANAGER_H_ */ diff --git a/bsp_q7s/fs/helpers.cpp b/bsp_q7s/fs/helpers.cpp new file mode 100644 index 0000000..19c2f67 --- /dev/null +++ b/bsp_q7s/fs/helpers.cpp @@ -0,0 +1,11 @@ +#include "helpers.h" + +std::filesystem::path fshelpers::getPrefixedPath(SdCardManager &man, + std::filesystem::path pathWihtoutPrefix) { + auto prefix = man.getCurrentMountPrefix(); + if (prefix == nullptr) { + return pathWihtoutPrefix; + } + auto resPath = prefix / pathWihtoutPrefix; + return resPath; +} diff --git a/bsp_q7s/fs/helpers.h b/bsp_q7s/fs/helpers.h new file mode 100644 index 0000000..b5dc736 --- /dev/null +++ b/bsp_q7s/fs/helpers.h @@ -0,0 +1,14 @@ +#ifndef BSP_Q7S_MEMORY_HELPERS_H_ +#define BSP_Q7S_MEMORY_HELPERS_H_ + +#include + +#include "SdCardManager.h" + +namespace fshelpers { + +std::filesystem::path getPrefixedPath(SdCardManager& man, std::filesystem::path pathWihtoutPrefix); + +} + +#endif /* BSP_Q7S_MEMORY_HELPERS_H_ */ diff --git a/bsp_q7s/main.cpp b/bsp_q7s/main.cpp new file mode 100644 index 0000000..d557cdb --- /dev/null +++ b/bsp_q7s/main.cpp @@ -0,0 +1,22 @@ +#include "q7sConfig.h" + +#if Q7S_SIMPLE_MODE == 0 +#include "obsw.h" +#else +#include "simple/simple.h" +#endif + +#include + +/** + * @brief This is the main program for the target hardware. + * @return + */ +int main(int argc, char* argv[]) { + using namespace std; +#if Q7S_SIMPLE_MODE == 0 + return obsw::obsw(argc, argv); +#else + return simple::simple(); +#endif +} diff --git a/bsp_q7s/memory/CMakeLists.txt b/bsp_q7s/memory/CMakeLists.txt new file mode 100644 index 0000000..4ff840c --- /dev/null +++ b/bsp_q7s/memory/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PRIVATE scratchApi.cpp LocalParameterHandler.cpp) diff --git a/bsp_q7s/memory/LocalParameterHandler.cpp b/bsp_q7s/memory/LocalParameterHandler.cpp new file mode 100644 index 0000000..d1fb635 --- /dev/null +++ b/bsp_q7s/memory/LocalParameterHandler.cpp @@ -0,0 +1,41 @@ +#include "LocalParameterHandler.h" + +#include + +LocalParameterHandler::LocalParameterHandler(std::string sdRelativeName, SdCardMountedIF* sdcMan) + : NVMParameterBase(), sdRelativeName(sdRelativeName), sdcMan(sdcMan) {} + +LocalParameterHandler::~LocalParameterHandler() {} + +ReturnValue_t LocalParameterHandler::initialize() { + ReturnValue_t result = updateFullName(); + if (result != returnvalue::OK) { + return result; + } + result = readJsonFile(); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t LocalParameterHandler::writeJsonFile() { + ReturnValue_t result = updateFullName(); + if (result != returnvalue::OK) { + return result; + } + return NVMParameterBase::writeJsonFile(); +} + +ReturnValue_t LocalParameterHandler::updateFullName() { + std::string mountPrefix; + auto activeSd = sdcMan->getActiveSdCard(); + if (activeSd and sdcMan->isSdCardUsable(activeSd.value())) { + mountPrefix = sdcMan->getCurrentMountPrefix(); + } else { + return SD_NOT_READY; + } + std::string fullname = mountPrefix + "/" + sdRelativeName; + NVMParameterBase::setFullName(fullname); + return returnvalue::OK; +} diff --git a/bsp_q7s/memory/LocalParameterHandler.h b/bsp_q7s/memory/LocalParameterHandler.h new file mode 100644 index 0000000..7462620 --- /dev/null +++ b/bsp_q7s/memory/LocalParameterHandler.h @@ -0,0 +1,106 @@ +#ifndef BSP_Q7S_MEMORY_LOCALPARAMETERHANDLER_H_ +#define BSP_Q7S_MEMORY_LOCALPARAMETERHANDLER_H_ + +#include +#include + +#include + +/** + * @brief Class to handle persistent parameters + * + */ +class LocalParameterHandler : public NVMParameterBase { + public: + static constexpr uint8_t INTERFACE_ID = CLASS_ID::LOCAL_PARAM_HANDLER; + + static constexpr ReturnValue_t SD_NOT_READY = returnvalue::makeCode(INTERFACE_ID, 0); + /** + * @brief Constructor + * + * @param sdRelativeName Absolute name of json file relative to mount + * directory + * of SD card. E.g. conf/example.json + * @param sdcMan Pointer to SD card manager + */ + LocalParameterHandler(std::string sdRelativeName, SdCardMountedIF* sdcMan); + virtual ~LocalParameterHandler(); + + /** + * @brief Will initialize the local parameter handler + * + * @return OK if successful, otherwise error return value + */ + ReturnValue_t initialize(); + + /** + * @brief Function to add parameter to json file. If the json file does + * not yet exist it will be created here. + * + * @param key The string to identify the parameter + * @param value The value to set for this parameter + * + * @return OK if successful, otherwise error return value + * + * @details The function will add the parameter only if it is not already + * present in the json file + */ + template + ReturnValue_t addParameter(std::string key, T value); + + /** + * @brief Function will update a parameter which already exists in the json + * file + * + * @param key The unique string to identify the parameter to update + * @param value The new new value to set + * + * @return OK if successful, otherwise error return value + */ + template + ReturnValue_t updateParameter(std::string key, T value); + + private: + // Name relative to mount point of SD card where parameters will be stored + std::string sdRelativeName; + + SdCardMountedIF* sdcMan; + + virtual ReturnValue_t writeJsonFile(); + + /** + * @brief This function sets the name of the json file dependent on the + * currently active SD card + * + * @return OK if successful, otherwise error return value + */ + ReturnValue_t updateFullName(); +}; + +template +inline ReturnValue_t LocalParameterHandler::addParameter(std::string key, T value) { + ReturnValue_t result = insertValue(key, value); + if (result != returnvalue::OK) { + return result; + } + result = writeJsonFile(); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +template +inline ReturnValue_t LocalParameterHandler::updateParameter(std::string key, T value) { + ReturnValue_t result = setValue(key, value); + if (result != returnvalue::OK) { + return result; + } + result = writeJsonFile(); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +#endif /* BSP_Q7S_MEMORY_LOCALPARAMETERHANDLER_H_ */ diff --git a/bsp_q7s/memory/scratchApi.cpp b/bsp_q7s/memory/scratchApi.cpp new file mode 100644 index 0000000..b5fa08f --- /dev/null +++ b/bsp_q7s/memory/scratchApi.cpp @@ -0,0 +1,50 @@ +#include "scratchApi.h" + +ReturnValue_t scratch::writeString(std::string name, std::string string) { + std::ostringstream oss("xsc_scratch write ", std::ostringstream::ate); + oss << name << " \"" << string << "\""; + int result = std::system(oss.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "scratch::writeString"); + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +ReturnValue_t scratch::readString(std::string key, std::string &string) { + std::ifstream file; + std::string filename; + ReturnValue_t result = readToFile(key, file, filename); + if (result != returnvalue::OK) { + return result; + } + + std::string line; + if (not std::getline(file, line)) { + std::remove(filename.c_str()); + return returnvalue::FAILED; + } + + size_t pos = line.find("="); + if (pos == std::string::npos) { + sif::warning << "scratch::readNumber: Output file format invalid, " + "no \"=\" found" + << std::endl; + // Could not find value + std::remove(filename.c_str()); + return KEY_NOT_FOUND; + } + string = line.substr(pos + 1); + return returnvalue::OK; +} + +ReturnValue_t scratch::clearValue(std::string key) { + std::ostringstream oss("xsc_scratch clear ", std::ostringstream::ate); + oss << key; + int result = std::system(oss.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "scratch::clearValue"); + return returnvalue::FAILED; + } + return returnvalue::OK; +} diff --git a/bsp_q7s/memory/scratchApi.h b/bsp_q7s/memory/scratchApi.h new file mode 100644 index 0000000..b3ea4a6 --- /dev/null +++ b/bsp_q7s/memory/scratchApi.h @@ -0,0 +1,146 @@ +#ifndef BSP_Q7S_MEMORY_SCRATCHAPI_H_ +#define BSP_Q7S_MEMORY_SCRATCHAPI_H_ + +#include +#include +#include +#include +#include + +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw/serviceinterface/ServiceInterface.h" +#include "linux/utility/utility.h" +#include "returnvalues/classIds.h" + +/** + * @brief API for the scratch buffer + */ +namespace scratch { + +static constexpr char PREFERED_SDC_KEY[] = "PREFSD"; +static constexpr char ALLOC_FAILURE_COUNT[] = "ALLOCERR"; + +static constexpr uint8_t INTERFACE_ID = CLASS_ID::SCRATCH_BUFFER; +static constexpr ReturnValue_t KEY_NOT_FOUND = returnvalue::makeCode(INTERFACE_ID, 0); + +ReturnValue_t clearValue(std::string key); + +/** + * Write a string to the scratch buffer + * @param key + * @param string String to write + * @return + */ +ReturnValue_t writeString(std::string key, std::string string); +/** + * Read a string from the scratch buffer + * @param key + * @param string Will be set to read string + * @return + */ +ReturnValue_t readString(std::string key, std::string& string); + +/** + * Write a number to the scratch buffer + * @tparam T + * @tparam + * @param key + * @param num Number. Template allows to set signed, unsigned and floating point numbers + * @return + */ +template ::value>::type> +inline ReturnValue_t writeNumber(std::string key, T num) noexcept; + +/** + * Read a number from the scratch buffer. + * @tparam T + * @tparam + * @param name + * @param num + * @return + */ +template ::value>::type> +inline ReturnValue_t readNumber(std::string key, T& num) noexcept; + +// Anonymous namespace +namespace { + +static uint8_t counter = 0; + +ReturnValue_t readToFile(std::string name, std::ifstream& file, std::string& filename) { + using namespace std; + filename = "/tmp/sro" + std::to_string(counter++); + ostringstream oss; + oss << "xsc_scratch read " << name << " > " << filename; + + int result = std::system(oss.str().c_str()); + if (result != 0) { + if (WEXITSTATUS(result) == 1) { + sif::warning << "scratch::readToFile: Key " << name << " does not exist" << std::endl; + // Could not find value + std::remove(filename.c_str()); + return KEY_NOT_FOUND; + } else { + utility::handleSystemError(result, "scratch::readToFile"); + std::remove(filename.c_str()); + return returnvalue::FAILED; + } + } + file.open(filename); + return returnvalue::OK; +} + +} // End of anonymous namespace + +template ::value>::type> +inline ReturnValue_t writeNumber(std::string key, T num) noexcept { + std::ostringstream oss("xsc_scratch write ", std::ostringstream::ate); + oss << key << " " << std::to_string(num); + int result = std::system(oss.str().c_str()); + if (result != 0) { + utility::handleSystemError(result, "scratch::writeNumber"); + return returnvalue::FAILED; + } + return returnvalue::OK; +} + +template ::value>::type> +inline ReturnValue_t readNumber(std::string key, T& num) noexcept { + using namespace std; + ifstream file; + std::string filename; + ReturnValue_t result = readToFile(key, file, filename); + if (result != returnvalue::OK) { + std::remove(filename.c_str()); + return result; + } + + string line; + if (not std::getline(file, line)) { + std::remove(filename.c_str()); + return returnvalue::FAILED; + } + + size_t pos = line.find("="); + if (pos == string::npos) { + sif::warning << "scratch::readNumber: Output file format invalid, " + "no \"=\" found" + << std::endl; + // Could not find value + std::remove(filename.c_str()); + return KEY_NOT_FOUND; + } + std::string valueAsString = line.substr(pos + 1); + try { + num = std::stoi(valueAsString); + } catch (std::invalid_argument& e) { + sif::warning << "scratch::readNumber: stoi call failed with " << e.what() << std::endl; + } + + std::remove(filename.c_str()); + return returnvalue::OK; +} + +} // namespace scratch + +#endif /* BSP_Q7S_MEMORY_SCRATCHAPI_H_ */ diff --git a/bsp_q7s/objectFactory.cpp b/bsp_q7s/objectFactory.cpp new file mode 100644 index 0000000..bbd6363 --- /dev/null +++ b/bsp_q7s/objectFactory.cpp @@ -0,0 +1,1093 @@ +#include "objectFactory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/acs/StrConfigPathGetter.h" +#include "bsp_q7s/boardtest/Q7STestTask.h" +#include "bsp_q7s/callbacks/gnssCallback.h" +#include "bsp_q7s/callbacks/q7sGpioCallbacks.h" +#include "busConf.h" +#include "ccsdsConfig.h" +#include "devConf.h" +#include "devices/addresses.h" +#include "devices/gpioIds.h" +#include "devices/powerSwitcherList.h" +#include "eive/definitions.h" +#include "eive/objects.h" +#include "fsfw/ipc/QueueFactory.h" +#include "linux/ObjectFactory.h" +#include "linux/boardtest/I2cTestClass.h" +#include "linux/boardtest/SpiTestClass.h" +#include "linux/boardtest/UartTestClass.h" +#include "linux/callbacks/gpioCallbacks.h" +#include "linux/ipcore/AxiPtmeConfig.h" +#include "linux/ipcore/PapbVcInterface.h" +#include "linux/ipcore/PdecHandler.h" +#include "linux/ipcore/Ptme.h" +#include "linux/ipcore/PtmeConfig.h" +#include "linux/payload/FreshSupvHandler.h" +#include "linux/payload/MpsocCommunication.h" +#include "linux/payload/PlocMpsocSpecialComHelper.h" +#include "linux/payload/SerialConfig.h" +#include "mission/config/configfile.h" +#include "mission/power/defs.h" +#include "mission/system/acs/AcsBoardFdir.h" +#include "mission/system/acs/AcsSubsystem.h" +#include "mission/system/acs/RwAssembly.h" +#include "mission/system/acs/SusFdir.h" +#include "mission/system/acs/acsModeTree.h" +#include "mission/system/com/SyrlinksFdir.h" +#include "mission/system/com/comModeTree.h" +#include "mission/system/payload/payloadModeTree.h" +#include "mission/system/power/GomspacePowerFdir.h" +#include "mission/system/tcs/RtdFdir.h" +#include "mission/system/tcs/TcsBoardAssembly.h" +#include "mission/system/tcs/tcsModeTree.h" +#include "mission/tmtc/tmFilters.h" +#include "mission/utility/GlobalConfigHandler.h" +#include "tmtc/pusIds.h" + +#if OBSW_TEST_LIBGPIOD == 1 +#include "linux/boardtest/LibgpiodTest.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "bsp_q7s/core/defs.h" +#include "fsfw/datapoollocal/LocalDataPoolManager.h" +#include "fsfw/tmtcpacket/pus/tm.h" +#include "fsfw/tmtcservices/CommandingServiceBase.h" +#include "fsfw/tmtcservices/PusServiceBase.h" +#include "fsfw_hal/common/gpio/GpioCookie.h" +#include "fsfw_hal/common/gpio/gpioDefinitions.h" +#include "fsfw_hal/devicehandlers/GyroL3GD20Handler.h" +#include "fsfw_hal/devicehandlers/MgmLIS3MDLHandler.h" +#include "fsfw_hal/devicehandlers/MgmRM3100Handler.h" +#include "fsfw_hal/linux/gpio/LinuxLibgpioIF.h" +#include "fsfw_hal/linux/i2c/I2cComIF.h" +#include "fsfw_hal/linux/i2c/I2cCookie.h" +#include "fsfw_hal/linux/serial/SerialComIF.h" +#include "fsfw_hal/linux/serial/SerialCookie.h" +#include "fsfw_hal/linux/spi/SpiComIF.h" +#include "fsfw_hal/linux/spi/SpiCookie.h" +#include "mission/acs/RwHandler.h" +#include "mission/com/CcsdsIpCoreHandler.h" +#include "mission/com/syrlinksDefs.h" +#include "mission/genericFactory.h" +#include "mission/system/acs/AcsBoardAssembly.h" +#include "mission/tmtc/TmFunnelHandler.h" + +using gpio::Direction; +using gpio::Levels; + +ResetArgs RESET_ARGS_GNSS; +std::atomic_bool LINK_STATE = CcsdsIpCoreHandler::LINK_DOWN; +std::atomic_bool PTME_LOCKED = false; +std::atomic_uint16_t signals::I2C_FATAL_ERRORS = 0; +uint8_t core::FW_VERSION_MAJOR = 0; +uint8_t core::FW_VERSION_MINOR = 0; +uint8_t core::FW_VERSION_REVISION = 0; +bool core::FW_VERSION_HAS_SHA = false; +char core::FW_VERSION_GIT_SHA[4] = {}; + +void Factory::setStaticFrameworkObjectIds() { + PusServiceBase::PUS_DISTRIBUTOR = objects::PUS_PACKET_DISTRIBUTOR; + PusServiceBase::PACKET_DESTINATION = objects::PUS_TM_FUNNEL; + + CommandingServiceBase::defaultPacketSource = objects::PUS_PACKET_DISTRIBUTOR; + CommandingServiceBase::defaultPacketDestination = objects::PUS_TM_FUNNEL; + +#if OBSW_Q7S_EM == 1 + DeviceHandlerBase::powerSwitcherId = objects::NO_OBJECT; +#else + DeviceHandlerBase::powerSwitcherId = objects::PCDU_HANDLER; +#endif /* OBSW_Q7S_EM == 1 */ + + LocalDataPoolManager::defaultHkDestination = objects::PUS_SERVICE_3_HOUSEKEEPING; + + VerificationReporter::DEFAULT_RECEIVER = objects::PUS_SERVICE_1_VERIFICATION; +} + +void ObjectFactory::setStatics() { Factory::setStaticFrameworkObjectIds(); } + +void ObjectFactory::createTmpComponents( + std::vector> tmpDevsToAdd) { + const char* tmpI2cDev = q7s::I2C_PS_EIVE; + if (core::FW_VERSION_MAJOR == 4) { + tmpI2cDev = q7s::I2C_PL_EIVE; + } else if (core::FW_VERSION_MAJOR >= 5) { + tmpI2cDev = q7s::I2C_Q7_EIVE; + } + std::vector tmpDevCookies; + + for (size_t idx = 0; idx < tmpDevsToAdd.size(); idx++) { + tmpDevCookies.push_back( + new I2cCookie(tmpDevsToAdd[idx].second, TMP1075::MAX_REPLY_LENGTH, tmpI2cDev)); + auto* tmpDevHandler = + new Tmp1075Handler(tmpDevsToAdd[idx].first, objects::I2C_COM_IF, tmpDevCookies[idx]); + tmpDevHandler->setCustomFdir(new TmpDevFdir(tmpDevsToAdd[idx].first)); + tmpDevHandler->connectModeTreeParent(satsystem::tcs::SUBSYSTEM); + } +} + +void ObjectFactory::createCommunicationInterfaces(LinuxLibgpioIF** gpioComIF, + SerialComIF** uartComIF, SpiComIF** spiMainComIF, + I2cComIF** i2cComIF) { + if (gpioComIF == nullptr or uartComIF == nullptr or spiMainComIF == nullptr) { + sif::error << "ObjectFactory::createCommunicationInterfaces: Invalid passed ComIF pointer" + << std::endl; + } + *gpioComIF = new LinuxLibgpioIF(objects::GPIO_IF); + + /* Communication interfaces */ + new CspComIF(objects::CSP_COM_IF, "CSP_ROUTER", 60); + *i2cComIF = new I2cComIF(objects::I2C_COM_IF); + *uartComIF = new SerialComIF(objects::UART_COM_IF); + *spiMainComIF = new SpiComIF(objects::SPI_MAIN_COM_IF, q7s::SPI_DEFAULT_DEV, **gpioComIF); +} + +void ObjectFactory::createPcduComponents(LinuxLibgpioIF* gpioComIF, PowerSwitchIF** pwrSwitcher, + bool enableHkSets) { + CspCookie* p60DockCspCookie = new CspCookie(P60Dock::MAX_REPLY_SIZE, addresses::P60DOCK, 500); + CspCookie* pdu1CspCookie = new CspCookie(PDU::MAX_REPLY_SIZE, addresses::PDU1, 500); + CspCookie* pdu2CspCookie = new CspCookie(PDU::MAX_REPLY_SIZE, addresses::PDU2, 500); + + auto p60Fdir = new GomspacePowerFdir(objects::P60DOCK_HANDLER); + P60DockHandler* p60dockhandler = new P60DockHandler(objects::P60DOCK_HANDLER, objects::CSP_COM_IF, + p60DockCspCookie, p60Fdir, enableHkSets); + + auto pdu1Fdir = new GomspacePowerFdir(objects::PDU1_HANDLER); + Pdu1Handler* pdu1handler = new Pdu1Handler(objects::PDU1_HANDLER, objects::CSP_COM_IF, + pdu1CspCookie, pdu1Fdir, enableHkSets); + + auto pdu2Fdir = new GomspacePowerFdir(objects::PDU2_HANDLER); + Pdu2Handler* pdu2handler = new Pdu2Handler(objects::PDU2_HANDLER, objects::CSP_COM_IF, + pdu2CspCookie, pdu2Fdir, enableHkSets); + +#if OBSW_ADD_GOMSPACE_ACU == 1 + CspCookie* acuCspCookie = new CspCookie(ACU::MAX_REPLY_SIZE, addresses::ACU, 500); + auto acuFdir = new GomspacePowerFdir(objects::ACU_HANDLER); + ACUHandler* acuhandler = new ACUHandler(objects::ACU_HANDLER, objects::CSP_COM_IF, acuCspCookie, + acuFdir, enableHkSets); +#endif + auto pcduHandler = new PcduHandler(objects::PCDU_HANDLER, 50); + + /** + * Setting PCDU devices to mode normal immediately after start up because PCDU is always + * running. + */ + p60dockhandler->setModeNormal(); + pdu1handler->setModeNormal(); + pdu2handler->setModeNormal(); +#if OBSW_ADD_GOMSPACE_ACU == 1 + acuhandler->setModeNormal(); +#endif + if (pwrSwitcher != nullptr) { + *pwrSwitcher = pcduHandler; + } +#if OBSW_DEBUG_P60DOCK == 1 + p60dockhandler->setDebugMode(true); +#endif +#if OBSW_DEBUG_ACU == 1 + acuhandler->setDebugMode(true); +#endif +} + +ReturnValue_t ObjectFactory::createRadSensorComponent(LinuxLibgpioIF* gpioComIF, + Stack5VHandler& stackHandler) { + createRadSensorChipSelect(gpioComIF); + + SpiCookie* spiCookieRadSensor = + new SpiCookie(addresses::RAD_SENSOR, gpioIds::CS_RAD_SENSOR, radSens::READ_SIZE, + spi::DEFAULT_MAX_1227_MODE, spi::DEFAULT_MAX_1227_SPEED); + spiCookieRadSensor->setMutexParams(MutexIF::TimeoutType::WAITING, spi::RAD_SENSOR_CS_TIMEOUT); + auto radSensor = new RadiationSensorHandler(objects::RAD_SENSOR, objects::SPI_MAIN_COM_IF, + spiCookieRadSensor, gpioComIF, stackHandler); + static_cast(radSensor); +#if OBSW_DEBUG_RAD_SENSOR == 1 + radSensor->enablePeriodicDataPrint(true); +#endif + return returnvalue::OK; +} + +void ObjectFactory::createAcsBoardGpios(GpioCookie& cookie) { + std::stringstream consumer; + GpiodRegularByLineName* gpio = nullptr; + consumer << "0x" << std::hex << objects::GYRO_0_ADIS_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_0_ADIS_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GYRO_0_ADIS_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GYRO_1_L3G_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_1_L3G_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GYRO_1_L3G_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GYRO_2_ADIS_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_2_ADIS_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GYRO_2_ADIS_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GYRO_3_L3G_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_3_L3G_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GYRO_3_L3G_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::MGM_0_LIS3_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::MGM_0_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::MGM_0_LIS3_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::MGM_1_RM3100_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::MGM_1_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::MGM_1_RM3100_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::MGM_2_LIS3_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::MGM_2_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::MGM_2_LIS3_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::MGM_3_RM3100_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::MGM_3_CS, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::MGM_3_RM3100_CS, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GPS_CONTROLLER; + // GNSS reset pins are active low + gpio = new GpiodRegularByLineName(q7s::gpioNames::RESET_GNSS_0, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GNSS_0_NRESET, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GPS_CONTROLLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::RESET_GNSS_1, consumer.str(), Direction::OUT, + Levels::HIGH); + cookie.addGpio(gpioIds::GNSS_1_NRESET, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GYRO_0_ADIS_HANDLER; + // Enable pins must be pulled low for regular operations + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_0_ENABLE, consumer.str(), Direction::OUT, + Levels::LOW); + cookie.addGpio(gpioIds::GYRO_0_ENABLE, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GYRO_2_ADIS_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GYRO_2_ENABLE, consumer.str(), Direction::OUT, + Levels::LOW); + cookie.addGpio(gpioIds::GYRO_2_ENABLE, gpio); + + // Enable pins for GNSS + consumer.str(""); + consumer << "0x" << std::hex << objects::GPS_CONTROLLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GNSS_0_ENABLE, consumer.str(), Direction::OUT, + Levels::LOW); + cookie.addGpio(gpioIds::GNSS_0_ENABLE, gpio); + + consumer.str(""); + consumer << "0x" << std::hex << objects::GPS_CONTROLLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GNSS_1_ENABLE, consumer.str(), Direction::OUT, + Levels::LOW); + cookie.addGpio(gpioIds::GNSS_1_ENABLE, gpio); + + // Select pin. 0 for GPS side A, 1 for GPS side B + consumer.str(""); + consumer << "0x" << std::hex << objects::GPS_CONTROLLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::GNSS_SELECT, consumer.str(), Direction::OUT, + Levels::LOW); + cookie.addGpio(gpioIds::GNSS_SELECT, gpio); +} + +void ObjectFactory::createAcsBoardComponents(SpiComIF& spiComIF, LinuxLibgpioIF* gpioComIF, + SerialComIF* uartComIF, PowerSwitchIF& pwrSwitcher, + bool enableHkSets, adis1650x::Type adisType) { + using namespace gpio; + GpioCookie* gpioCookieAcsBoard = new GpioCookie(); + createAcsBoardGpios(*gpioCookieAcsBoard); + gpioChecker(gpioComIF->addGpios(gpioCookieAcsBoard), "ACS Board"); + + AcsBoardFdir* fdir = nullptr; + static_cast(fdir); + +#if OBSW_ADD_ACS_BOARD == 1 + new AcsBoardPolling(objects::ACS_BOARD_POLLING_TASK, spiComIF, *gpioComIF); + std::string spiDev = q7s::SPI_DEFAULT_DEV; + std::array assemblyChildren; + SpiCookie* spiCookie = + new SpiCookie(addresses::MGM_0_LIS3, gpioIds::MGM_0_LIS3_CS, mgmLis3::MAX_BUFFER_SIZE, + spi::DEFAULT_LIS3_MODE, spi::DEFAULT_LIS3_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto mgmLis3Handler0 = + new MgmLis3CustomHandler(objects::MGM_0_LIS3_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::LIS3_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::MGM_0_LIS3_HANDLER); + mgmLis3Handler0->setCustomFdir(fdir); + assemblyChildren[0] = mgmLis3Handler0; +#if OBSW_TEST_ACS == 1 + mgmLis3Handler->setStartUpImmediately(); + mgmLis3Handler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + mgmLis3Handler->enablePeriodicPrintouts(true, 10); +#endif + spiCookie = + new SpiCookie(addresses::MGM_1_RM3100, gpioIds::MGM_1_RM3100_CS, mgmRm3100::MAX_BUFFER_SIZE, + spi::DEFAULT_RM3100_MODE, spi::DEFAULT_RM3100_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto mgmRm3100Handler1 = + new MgmRm3100CustomHandler(objects::MGM_1_RM3100_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::RM3100_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::MGM_1_RM3100_HANDLER); + mgmRm3100Handler1->setCustomFdir(fdir); + assemblyChildren[1] = mgmRm3100Handler1; +#if OBSW_TEST_ACS == 1 + mgmRm3100Handler->setStartUpImmediately(); + mgmRm3100Handler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + mgmRm3100Handler->enablePeriodicPrintouts(true, 10); +#endif + spiCookie = new SpiCookie(addresses::MGM_2_LIS3, gpioIds::MGM_2_LIS3_CS, mgmLis3::MAX_BUFFER_SIZE, + spi::DEFAULT_LIS3_MODE, spi::DEFAULT_LIS3_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto* mgmLis3Handler2 = + new MgmLis3CustomHandler(objects::MGM_2_LIS3_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::LIS3_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::MGM_2_LIS3_HANDLER); + mgmLis3Handler2->setCustomFdir(fdir); + assemblyChildren[2] = mgmLis3Handler2; +#if OBSW_TEST_ACS == 1 + mgmLis3Handler->setStartUpImmediately(); + mgmLis3Handler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + mgmLis3Handler->enablePeriodicPrintouts(true, 10); +#endif + spiCookie = + new SpiCookie(addresses::MGM_3_RM3100, gpioIds::MGM_3_RM3100_CS, mgmRm3100::MAX_BUFFER_SIZE, + spi::DEFAULT_RM3100_MODE, spi::DEFAULT_RM3100_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto* mgmRm3100Handler3 = + new MgmRm3100CustomHandler(objects::MGM_3_RM3100_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::RM3100_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::MGM_3_RM3100_HANDLER); + mgmRm3100Handler3->setCustomFdir(fdir); + assemblyChildren[3] = mgmRm3100Handler3; +#if OBSW_TEST_ACS == 1 + mgmRm3100Handler->setStartUpImmediately(); + mgmRm3100Handler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + mgmRm3100Handler->enablePeriodicPrintouts(true, 10); +#endif + // Commented until ACS board V2 in in clean room again + // Gyro 0 Side A + spiCookie = + new SpiCookie(addresses::GYRO_0_ADIS, gpioIds::GYRO_0_ADIS_CS, adis1650x::MAXIMUM_REPLY_SIZE, + spi::DEFAULT_ADIS16507_MODE, spi::DEFAULT_ADIS16507_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto adisHandler = new GyrAdis1650XHandler(objects::GYRO_0_ADIS_HANDLER, + objects::ACS_BOARD_POLLING_TASK, spiCookie, adisType); + fdir = new AcsBoardFdir(objects::GYRO_0_ADIS_HANDLER); + adisHandler->setCustomFdir(fdir); + assemblyChildren[4] = adisHandler; +#if OBSW_TEST_ACS == 1 + adisHandler->setStartUpImmediately(); + adisHandler->setToGoToNormalModeImmediately(); +#endif +#if OBSW_DEBUG_ACS == 1 + adisHandler->enablePeriodicPrintouts(true, 10); +#endif + // Gyro 1 Side A + spiCookie = new SpiCookie(addresses::GYRO_1_L3G, gpioIds::GYRO_1_L3G_CS, l3gd20h::MAX_BUFFER_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto gyroL3gHandler1 = + new GyrL3gCustomHandler(objects::GYRO_1_L3G_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::L3G_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::GYRO_1_L3G_HANDLER); + gyroL3gHandler1->setCustomFdir(fdir); + assemblyChildren[5] = gyroL3gHandler1; +#if OBSW_TEST_ACS == 1 + gyroL3gHandler->setStartUpImmediately(); + gyroL3gHandler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + gyroL3gHandler->enablePeriodicPrintouts(true, 10); +#endif + // Gyro 2 Side B + spiCookie = + new SpiCookie(addresses::GYRO_2_ADIS, gpioIds::GYRO_2_ADIS_CS, adis1650x::MAXIMUM_REPLY_SIZE, + spi::DEFAULT_ADIS16507_MODE, spi::DEFAULT_ADIS16507_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + adisHandler = new GyrAdis1650XHandler(objects::GYRO_2_ADIS_HANDLER, + objects::ACS_BOARD_POLLING_TASK, spiCookie, adisType); + fdir = new AcsBoardFdir(objects::GYRO_2_ADIS_HANDLER); + adisHandler->setCustomFdir(fdir); + assemblyChildren[6] = adisHandler; +#if OBSW_TEST_ACS == 1 + adisHandler->setStartUpImmediately(); + adisHandler->setToGoToNormalModeImmediately(); +#endif + // Gyro 3 Side B + spiCookie = new SpiCookie(addresses::GYRO_3_L3G, gpioIds::GYRO_3_L3G_CS, l3gd20h::MAX_BUFFER_SIZE, + spi::DEFAULT_L3G_MODE, spi::DEFAULT_L3G_SPEED); + spiCookie->setMutexParams(MutexIF::TimeoutType::WAITING, spi::ACS_BOARD_CS_TIMEOUT); + auto gyroL3gHandler3 = + new GyrL3gCustomHandler(objects::GYRO_3_L3G_HANDLER, objects::ACS_BOARD_POLLING_TASK, + spiCookie, spi::L3G_TRANSITION_DELAY); + fdir = new AcsBoardFdir(objects::GYRO_3_L3G_HANDLER); + gyroL3gHandler3->setCustomFdir(fdir); + assemblyChildren[7] = gyroL3gHandler3; +#if OBSW_TEST_ACS == 1 + gyroL3gHandler->setStartUpImmediately(); + gyroL3gHandler->setToGoToNormalMode(true); +#endif +#if OBSW_DEBUG_ACS == 1 + gyroL3gHandler->enablePeriodicPrintouts(true, 10); +#endif + bool debugGps = false; +#if OBSW_DEBUG_GPS == 1 + debugGps = true; +#endif + RESET_ARGS_GNSS.gpioComIF = gpioComIF; + RESET_ARGS_GNSS.waitPeriodMs = 10 * 1e3; + auto gpsCtrl = new GpsHyperionLinuxController(objects::GPS_CONTROLLER, objects::NO_OBJECT, + enableHkSets, debugGps); + gpsCtrl->setResetPinTriggerFunction(gps::triggerGpioResetPin, &RESET_ARGS_GNSS); + + ObjectFactory::createAcsBoardAssy(pwrSwitcher, assemblyChildren, gpsCtrl, gpioComIF); +#endif /* OBSW_ADD_ACS_HANDLERS == 1 */ +} + +void ObjectFactory::createHeaterComponents(GpioIF* gpioIF, PowerSwitchIF* pwrSwitcher, + HealthTableIF* healthTable, + HeaterHandler*& heaterHandler) { + using namespace gpio; + GpioCookie* heaterGpiosCookie = new GpioCookie; + GpiodRegularByLineName* gpio = nullptr; + + std::stringstream consumer; + consumer << "0x" << std::hex << objects::HEATER_HANDLER; + /* Pin H2-11 on stack connector */ + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_0, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_0, gpio); + /* Pin H2-12 on stack connector */ + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_1, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_1, gpio); + + /* Pin H2-13 on stack connector */ + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_2, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_2, gpio); + + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_3, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_3, gpio); + + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_4, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_4, gpio); + + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_5, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_5, gpio); + + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_6, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_6, gpio); + + gpio = new GpiodRegularByLineName(q7s::gpioNames::HEATER_7, consumer.str(), Direction::OUT, + Levels::LOW); + heaterGpiosCookie->addGpio(gpioIds::HEATER_7, gpio); + + gpioIF->addGpios(heaterGpiosCookie); + + ObjectFactory::createGenericHeaterComponents(*gpioIF, *pwrSwitcher, heaterHandler); +} + +void ObjectFactory::createSolarArrayDeploymentComponents(PowerSwitchIF& pwrSwitcher, + GpioIF& gpioIF) { + using namespace gpio; + GpioCookie* solarArrayDeplCookie = new GpioCookie; + GpiodRegularByLineName* gpio = nullptr; + + std::stringstream consumer; + consumer << "0x" << std::hex << objects::SOLAR_ARRAY_DEPL_HANDLER; + gpio = new GpiodRegularByLineName(q7s::gpioNames::SA_DPL_PIN_0, consumer.str(), Direction::OUT, + Levels::LOW); + solarArrayDeplCookie->addGpio(gpioIds::DEPLSA1, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::SA_DPL_PIN_1, consumer.str(), Direction::OUT, + Levels::LOW); + solarArrayDeplCookie->addGpio(gpioIds::DEPLSA2, gpio); + ReturnValue_t result = gpioIF.addGpios(solarArrayDeplCookie); + if (result != returnvalue::OK) { + sif::error << "Adding Solar Array Deployment GPIO cookie failed" << std::endl; + } + + new SolarArrayDeploymentHandler(objects::SOLAR_ARRAY_DEPL_HANDLER, gpioIF, pwrSwitcher, + power::Switches::PDU2_CH5_DEPLOYMENT_MECHANISM_8V, + gpioIds::DEPLSA1, gpioIds::DEPLSA2, *SdCardManager::instance()); +} + +void ObjectFactory::createSyrlinksComponents(PowerSwitchIF* pwrSwitcher) { + new SyrlinksComHandler(objects::SYRLINKS_COM_HANDLER); + auto* syrlinksAssy = new SyrlinksAssembly(objects::SYRLINKS_ASSY); + syrlinksAssy->connectModeTreeParent(satsystem::com::SUBSYSTEM); + auto* syrlinksUartCookie = + new SerialCookie(objects::SYRLINKS_HANDLER, q7s::UART_SYRLINKS_DEV, serial::SYRLINKS_BAUD, + syrlinks::MAX_REPLY_SIZE, UartModes::NON_CANONICAL); + syrlinksUartCookie->setParityEven(); + auto syrlinksFdir = new SyrlinksFdir(objects::SYRLINKS_HANDLER); + auto syrlinksHandler = + new SyrlinksHandler(objects::SYRLINKS_HANDLER, objects::SYRLINKS_COM_HANDLER, + syrlinksUartCookie, power::PDU1_CH1_SYRLINKS_12V, syrlinksFdir); + syrlinksHandler->setPowerSwitcher(pwrSwitcher); + syrlinksHandler->connectModeTreeParent(*syrlinksAssy); +#if OBSW_DEBUG_SYRLINKS == 1 + syrlinksHandler->setDebugMode(true); +#endif +} + +void ObjectFactory::createPayloadComponents(LinuxLibgpioIF* gpioComIF, PowerSwitchIF& pwrSwitcher) { + using namespace gpio; + std::stringstream consumer; + auto* camSwitcher = + new CamSwitcher(objects::CAM_SWITCHER, pwrSwitcher, power::PDU2_CH8_PAYLOAD_CAMERA); + camSwitcher->connectModeTreeParent(satsystem::payload::SUBSYSTEM); +#if OBSW_ADD_PLOC_MPSOC == 1 + consumer << "0x" << std::hex << objects::PLOC_MPSOC_HANDLER; + auto gpioConfigMPSoC = new GpiodRegularByLineName(q7s::gpioNames::ENABLE_MPSOC_UART, + consumer.str(), Direction::OUT, Levels::HIGH); + auto mpsocGpioCookie = new GpioCookie; + mpsocGpioCookie->addGpio(gpioIds::ENABLE_MPSOC_UART, gpioConfigMPSoC); + gpioChecker(gpioComIF->addGpios(mpsocGpioCookie), "PLOC MPSoC"); + SerialConfig serialCfg(q7s::UART_PLOC_MPSOC_DEV, serial::PLOC_MPSOC_BAUD, mpsoc::MAX_REPLY_SIZE, + UartModes::NON_CANONICAL); + auto mpsocCommunication = new MpsocCommunication(objects::PLOC_MPSOC_COMMUNICATION, serialCfg); + auto specialComHelper = + new PlocMpsocSpecialComHelper(objects::PLOC_MPSOC_HELPER, *mpsocCommunication); + DhbConfig dhbConf(objects::PLOC_MPSOC_HANDLER); + auto* mpsocHandler = new FreshMpsocHandler( + dhbConf, *mpsocCommunication, *specialComHelper, Gpio(gpioIds::ENABLE_MPSOC_UART, gpioComIF), + objects::PLOC_SUPERVISOR_HANDLER, pwrSwitcher, power::PDU2_CH8_PAYLOAD_CAMERA); + mpsocHandler->connectModeTreeParent(satsystem::payload::SUBSYSTEM); +#endif /* OBSW_ADD_PLOC_MPSOC == 1 */ +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + consumer << "0x" << std::hex << objects::PLOC_SUPERVISOR_HANDLER; + auto gpioConfigSupv = new GpiodRegularByLineName(q7s::gpioNames::ENABLE_SUPV_UART, consumer.str(), + Direction::OUT, Levels::LOW); + auto supvGpioCookie = new GpioCookie; + supvGpioCookie->addGpio(gpioIds::ENABLE_SUPV_UART, gpioConfigSupv); + gpioComIF->addGpios(supvGpioCookie); + const char* plocSupvDev = q7s::UART_PLOC_SUPERVISOR_DEV; + if (not std::filesystem::exists(plocSupvDev)) { + plocSupvDev = q7s::UART_PLOC_SUPERVISOR_DEV_FALLBACK; + } + auto supervisorCookie = + new SerialCookie(objects::PLOC_SUPERVISOR_HANDLER, plocSupvDev, serial::PLOC_SUPV_BAUD, + supv::MAX_PACKET_SIZE * 20, UartModes::NON_CANONICAL); + supervisorCookie->setNoFixedSizeReply(); + new PlocSupvUartManager(objects::PLOC_SUPERVISOR_HELPER); + dhbConf = DhbConfig(objects::PLOC_SUPERVISOR_HANDLER); + auto* supvHandler = + new FreshSupvHandler(dhbConf, supervisorCookie, Gpio(gpioIds::ENABLE_SUPV_UART, gpioComIF), + pwrSwitcher, power::PDU1_CH6_PLOC_12V); + supvHandler->connectModeTreeParent(satsystem::payload::SUBSYSTEM); +#endif /* OBSW_ADD_PLOC_SUPERVISOR == 1 */ + static_cast(consumer); +} + +void ObjectFactory::createReactionWheelComponents(LinuxLibgpioIF* gpioComIF, + PowerSwitchIF* pwrSwitcher) { + using namespace gpio; + GpioCookie* gpioCookieRw = new GpioCookie; + GpioCallback* csRw1 = + new GpioCallback("Chip select reaction wheel 1", Direction::OUT, Levels::HIGH, + &gpioCallbacks::spiCsDecoderCallback, gpioComIF); + gpioCookieRw->addGpio(gpioIds::CS_RW1, csRw1); + GpioCallback* csRw2 = + new GpioCallback("Chip select reaction wheel 2", Direction::OUT, Levels::HIGH, + &gpioCallbacks::spiCsDecoderCallback, gpioComIF); + gpioCookieRw->addGpio(gpioIds::CS_RW2, csRw2); + GpioCallback* csRw3 = + new GpioCallback("Chip select reaction wheel 3", Direction::OUT, Levels::HIGH, + &gpioCallbacks::spiCsDecoderCallback, gpioComIF); + gpioCookieRw->addGpio(gpioIds::CS_RW3, csRw3); + GpioCallback* csRw4 = + new GpioCallback("Chip select reaction wheel 4", Direction::OUT, Levels::HIGH, + &gpioCallbacks::spiCsDecoderCallback, gpioComIF); + gpioCookieRw->addGpio(gpioIds::CS_RW4, csRw4); + + std::stringstream consumer; + GpiodRegularByLineName* gpio = nullptr; + consumer << "0x" << std::hex << objects::RW1; + gpio = new GpiodRegularByLineName(q7s::gpioNames::EN_RW_1, consumer.str(), Direction::OUT, + Levels::LOW); + gpioCookieRw->addGpio(gpioIds::EN_RW1, gpio); + consumer.str(""); + consumer << "0x" << std::hex << objects::RW2; + gpio = new GpiodRegularByLineName(q7s::gpioNames::EN_RW_2, consumer.str(), Direction::OUT, + Levels::LOW); + gpioCookieRw->addGpio(gpioIds::EN_RW2, gpio); + consumer.str(""); + consumer << "0x" << std::hex << objects::RW3; + gpio = new GpiodRegularByLineName(q7s::gpioNames::EN_RW_3, consumer.str(), Direction::OUT, + Levels::LOW); + gpioCookieRw->addGpio(gpioIds::EN_RW3, gpio); + consumer.str(""); + consumer << "0x" << std::hex << objects::RW4; + gpio = new GpiodRegularByLineName(q7s::gpioNames::EN_RW_4, consumer.str(), Direction::OUT, + Levels::LOW); + gpioCookieRw->addGpio(gpioIds::EN_RW4, gpio); + + gpioChecker(gpioComIF->addGpios(gpioCookieRw), "RWs"); + +#if OBSW_ADD_RW == 1 + std::array, 4> rwCookieParams = { + {{addresses::RW1, gpioIds::CS_RW1}, + {addresses::RW2, gpioIds::CS_RW2}, + {addresses::RW3, gpioIds::CS_RW3}, + {addresses::RW4, gpioIds::CS_RW4}}}; + std::array rwCookies = {}; + std::array rwIds = {objects::RW1, objects::RW2, objects::RW3, objects::RW4}; + std::array rwGpioIds = {gpioIds::EN_RW1, gpioIds::EN_RW2, gpioIds::EN_RW3, + gpioIds::EN_RW4}; + std::array rws = {}; + new RwPollingTask(objects::RW_POLLING_TASK, q7s::SPI_RW_DEV, *gpioComIF); + for (uint8_t idx = 0; idx < rwCookies.size(); idx++) { + rwCookies[idx] = new RwCookie(idx, rwCookieParams[idx].first, rwCookieParams[idx].second, + rws::MAX_REPLY_SIZE, spi::RW_MODE, spi::RW_SPEED); + auto* rwHandler = new RwHandler(rwIds[idx], objects::RW_POLLING_TASK, rwCookies[idx], gpioComIF, + rwGpioIds[idx], idx); +#if OBSW_TEST_RW == 1 + rws[idx]->setStartUpImmediately(); +#endif +#if OBSW_DEBUG_RW == 1 + rwHandler->setDebugMode(true); +#endif + rws[idx] = rwHandler; + } + + createRwAssy(*pwrSwitcher, power::Switches::PDU2_CH2_RW_5V, rws, rwIds); +#endif /* OBSW_ADD_RW == 1 */ +} + +ReturnValue_t ObjectFactory::createCcsdsComponents(CcsdsComponentArgs& args) { + using namespace gpio; + // GPIO definitions of signals connected to the virtual channel interfaces of the PTME IP Core + GpioCookie* gpioCookiePtmeIp = new GpioCookie; + GpiodRegularByLineName* gpio = nullptr; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PAPB_EMPTY_SIGNAL_VC0, "PAPB VC0"); + gpioCookiePtmeIp->addGpio(gpioIds::VC0_PAPB_EMPTY, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::PAPB_EMPTY_SIGNAL_VC1, "PAPB VC1"); + gpioCookiePtmeIp->addGpio(gpioIds::VC1_PAPB_EMPTY, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::PAPB_EMPTY_SIGNAL_VC2, "PAPB VC2"); + gpioCookiePtmeIp->addGpio(gpioIds::VC2_PAPB_EMPTY, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::PAPB_EMPTY_SIGNAL_VC3, "PAPB VC3"); + gpioCookiePtmeIp->addGpio(gpioIds::VC3_PAPB_EMPTY, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::PTME_RESETN, "PTME RESETN", + gpio::Direction::OUT, gpio::Levels::HIGH); + gpioCookiePtmeIp->addGpio(gpioIds::PTME_RESETN, gpio); + gpioChecker(args.gpioComIF.addGpios(gpioCookiePtmeIp), "PTME PAPB VCs"); + + // Creating virtual channel interfaces + VirtualChannelIF* vc0 = + new PapbVcInterface(&args.gpioComIF, gpioIds::VC0_PAPB_EMPTY, q7s::UIO_PTME, + q7s::uiomapids::PTME_VC0, config::MAX_SPACEPACKET_TC_SIZE); + VirtualChannelIF* vc1 = + new PapbVcInterface(&args.gpioComIF, gpioIds::VC1_PAPB_EMPTY, q7s::UIO_PTME, + q7s::uiomapids::PTME_VC1, config::MAX_SPACEPACKET_TC_SIZE); + VirtualChannelIF* vc2 = + new PapbVcInterface(&args.gpioComIF, gpioIds::VC2_PAPB_EMPTY, q7s::UIO_PTME, + q7s::uiomapids::PTME_VC2, config::MAX_SPACEPACKET_TC_SIZE); + VirtualChannelIF* vc3 = + new PapbVcInterface(&args.gpioComIF, gpioIds::VC3_PAPB_EMPTY, q7s::UIO_PTME, + q7s::uiomapids::PTME_VC3, config::MAX_SPACEPACKET_TC_SIZE); + // Creating ptme object and adding virtual channel interfaces + Ptme* ptme = new Ptme(objects::PTME); + ptme->addVcInterface(ccsds::VC0, vc0); + ptme->addVcInterface(ccsds::VC1, vc1); + ptme->addVcInterface(ccsds::VC2, vc2); + ptme->addVcInterface(ccsds::VC3, vc3); + AxiPtmeConfig* axiPtmeConfig = + new AxiPtmeConfig(objects::AXI_PTME_CONFIG, q7s::UIO_PTME, q7s::uiomapids::PTME_CONFIG); + PtmeConfig* ptmeConfig = new PtmeConfig(objects::PTME_CONFIG, axiPtmeConfig); + + PtmeGpios gpios; + gpios.enableTxClock = gpioIds::RS485_EN_TX_CLOCK; + gpios.enableTxData = gpioIds::RS485_EN_TX_DATA; + gpios.ptmeResetn = gpioIds::PTME_RESETN; + + *args.ipCoreHandler = + new CcsdsIpCoreHandler(objects::CCSDS_HANDLER, objects::CCSDS_PACKET_DISTRIBUTOR, *ptmeConfig, + LINK_STATE, &args.gpioComIF, gpios, PTME_LOCKED); + // This VC will receive all live TM + auto* vcWithQueue = new VirtualChannel(objects::PTME_VC0_LIVE_TM, ccsds::VC0, "PTME VC0 LIVE TM", + *ptme, LINK_STATE); + auto* liveTask = new LiveTmTask(objects::LIVE_TM_TASK, args.pusFunnel, args.cfdpFunnel, + *vcWithQueue, PTME_LOCKED, config::LIVE_CHANNEL_NORMAL_QUEUE_SIZE, + config::LIVE_CHANNEL_CFDP_QUEUE_SIZE); + args.normalLiveTmDest = liveTask->getNormalLiveQueueId(); + args.cfdpLiveTmDest = liveTask->getCfdpLiveQueueId(); + liveTask->connectModeTreeParent(satsystem::com::SUBSYSTEM); + + // Set up log store. + auto* vc = new VirtualChannel(objects::PTME_VC1_LOG_TM, ccsds::VC1, "PTME VC1 LOG TM", *ptme, + LINK_STATE); + LogStores logStores(args.stores); + // Core task which handles the LOG store and takes care of dumping it as TM using a VC directly + auto* logStore = + new PersistentLogTmStoreTask(objects::LOG_STORE_AND_TM_TASK, args.ipcStore, logStores, *vc, + *SdCardManager::instance(), PTME_LOCKED); + logStore->connectModeTreeParent(satsystem::com::SUBSYSTEM); + + vc = new VirtualChannel(objects::PTME_VC2_HK_TM, ccsds::VC2, "PTME VC2 HK TM", *ptme, LINK_STATE); + // Core task which handles the HK store and takes care of dumping it as TM using a VC directly + auto* hkStore = new PersistentSingleTmStoreTask( + objects::HK_STORE_AND_TM_TASK, args.ipcStore, *args.stores.hkStore, *vc, + persTmStore::DUMP_HK_STORE_DONE, persTmStore::DUMP_HK_CANCELLED, *SdCardManager::instance(), + PTME_LOCKED); + hkStore->connectModeTreeParent(satsystem::com::SUBSYSTEM); + + vc = new VirtualChannel(objects::PTME_VC3_CFDP_TM, ccsds::VC3, "PTME VC3 CFDP TM", *ptme, + LINK_STATE); + // Core task which handles the CFDP store and takes care of dumping it as TM using a VC directly + auto* cfdpTask = new PersistentSingleTmStoreTask( + objects::CFDP_STORE_AND_TM_TASK, args.ipcStore, *args.stores.cfdpStore, *vc, + persTmStore::DUMP_CFDP_STORE_DONE, persTmStore::DUMP_CFDP_CANCELLED, + *SdCardManager::instance(), PTME_LOCKED); + cfdpTask->connectModeTreeParent(satsystem::com::SUBSYSTEM); + + ReturnValue_t result = (*args.ipCoreHandler)->connectModeTreeParent(satsystem::com::SUBSYSTEM); + if (result != returnvalue::OK) { + sif::error + << "ObjectFactory::createCcsdsComponents: Connecting COM subsystem to CCSDS handler failed" + << std::endl; + } + + GpioCookie* gpioCookiePdec = new GpioCookie; + // GPIO also low after linux boot (specified by device-tree) + gpio = new GpiodRegularByLineName(q7s::gpioNames::PDEC_RESET, "PDEC Handler", Direction::OUT, + Levels::LOW); + gpioCookiePdec->addGpio(gpioIds::PDEC_RESET, gpio); + gpioChecker(args.gpioComIF.addGpios(gpioCookiePdec), "PDEC"); + struct UioNames uioNames{}; + uioNames.configMemory = q7s::UIO_PDEC_CONFIG_MEMORY; + uioNames.ramMemory = q7s::UIO_PDEC_RAM; + uioNames.registers = q7s::UIO_PDEC_REGISTERS; + uioNames.irq = q7s::UIO_PDEC_IRQ; + new PdecHandler(objects::PDEC_HANDLER, objects::CCSDS_HANDLER, &args.gpioComIF, + gpioIds::PDEC_RESET, uioNames, args.pdecCfgMemBaseAddr, args.pdecRamBaseAddr); + GpioCookie* gpioRS485Chip = new GpioCookie; + gpio = new GpiodRegularByLineName(q7s::gpioNames::RS485_EN_TX_CLOCK, "RS485 Transceiver", + Direction::OUT, Levels::LOW); + gpioRS485Chip->addGpio(gpioIds::RS485_EN_TX_CLOCK, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::RS485_EN_TX_DATA, "RS485 Transceiver", + Direction::OUT, Levels::LOW); + gpioRS485Chip->addGpio(gpioIds::RS485_EN_TX_DATA, gpio); + // Default configuration enables RX channels (RXEN = LOW) + gpio = new GpiodRegularByLineName(q7s::gpioNames::RS485_EN_RX_CLOCK, "RS485 Transceiver", + Direction::OUT, Levels::LOW); + gpioRS485Chip->addGpio(gpioIds::RS485_EN_RX_CLOCK, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::RS485_EN_RX_DATA, "RS485 Transceiver", + Direction::OUT, Levels::LOW); + gpioRS485Chip->addGpio(gpioIds::RS485_EN_RX_DATA, gpio); + gpioChecker(args.gpioComIF.addGpios(gpioRS485Chip), "RS485 Transceiver"); + return returnvalue::OK; +} + +void ObjectFactory::createPlPcduComponents(LinuxLibgpioIF* gpioComIF, SpiComIF* spiComIF, + PowerSwitchIF* pwrSwitcher, + Stack5VHandler& stackHandler) { + using namespace gpio; + // Create all GPIO components first + GpioCookie* plPcduGpios = new GpioCookie; + GpiodRegularByLineName* gpio = nullptr; + std::string consumer; + // Switch pins are active high + consumer = "PLPCDU_ENB_VBAT_0"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_VBAT0, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_VBAT0, gpio); + consumer = "PLPCDU_ENB_VBAT_1"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_VBAT1, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_VBAT1, gpio); + consumer = "PLPCDU_ENB_DRO"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_DRO, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_DRO, gpio); + consumer = "PLPCDU_ENB_X8"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_X8, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_X8, gpio); + consumer = "PLPCDU_ENB_TX"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_TX, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_TX, gpio); + consumer = "PLPCDU_ENB_MPA"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_MPA, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_MPA, gpio); + consumer = "PLPCDU_ENB_HPA"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ENABLE_HPA, consumer, Direction::OUT, + gpio::Levels::LOW); + plPcduGpios->addGpio(gpioIds::PLPCDU_ENB_HPA, gpio); + + // Chip select pin is active low + consumer = "PLPCDU_ADC_CS"; + gpio = new GpiodRegularByLineName(q7s::gpioNames::PL_PCDU_ADC_CS, consumer, Direction::OUT, + gpio::Levels::HIGH); + plPcduGpios->addGpio(gpioIds::PLPCDU_ADC_CS, gpio); + gpioChecker(gpioComIF->addGpios(plPcduGpios), "PL PCDU"); + SpiCookie* spiCookie = + new SpiCookie(addresses::PLPCDU_ADC, gpioIds::PLPCDU_ADC_CS, plpcdu::MAX_ADC_REPLY_SIZE, + spi::DEFAULT_MAX_1227_MODE, spi::PL_PCDU_MAX_1227_SPEED); + // Create device handler components + auto plPcduHandler = + new PayloadPcduHandler(objects::PLPCDU_HANDLER, objects::SPI_MAIN_COM_IF, spiCookie, + gpioComIF, SdCardManager::instance(), stackHandler, false); + spiCookie->setCallbackMode(PayloadPcduHandler::extConvAsTwoCallback, plPcduHandler); +#if OBSW_TEST_PL_PCDU == 1 + plPcduHandler->setStartUpImmediately(); +#endif +#if OBSW_DEBUG_PL_PCDU == 1 + plPcduHandler->setToGoToNormalModeImmediately(true); + plPcduHandler->enablePeriodicPrintout(true, 10); +#endif + plPcduHandler->connectModeTreeParent(satsystem::payload::SUBSYSTEM); +} + +void ObjectFactory::createTestComponents(LinuxLibgpioIF* gpioComIF) { + new Q7STestTask(objects::TEST_TASK); +#if OBSW_ADD_SPI_TEST_CODE == 1 + new SpiTestClass(objects::SPI_TEST, gpioComIF); +#endif +#if OBSW_ADD_I2C_TEST_CODE == 1 + new I2cTestClass(objects::I2C_TEST, q7s::I2C_PL_EIVE); +#endif +#if OBSW_ADD_UART_TEST_CODE == 1 + // auto* reader= new ScexUartReader(objects::SCEX_UART_READER); + new UartTestClass(objects::UART_TEST); +#endif +} + +void ObjectFactory::createStrComponents(PowerSwitchIF* pwrSwitcher, SdCardManager& sdcMan) { + auto* strAssy = new StrAssembly(objects::STR_ASSY); + strAssy->connectModeTreeParent(satsystem::acs::ACS_SUBSYSTEM); + auto* starTrackerCookie = + new SerialCookie(objects::STAR_TRACKER, q7s::UART_STAR_TRACKER_DEV, serial::STAR_TRACKER_BAUD, + startracker::MAX_FRAME_SIZE * 2 + 2, UartModes::NON_CANONICAL); + starTrackerCookie->setNoFixedSizeReply(); + StrComHandler* strComIF = new StrComHandler(objects::STR_COM_IF); + + const char* paramJsonFile = "/mnt/sd0/startracker/flight-config.json"; + if (paramJsonFile == nullptr) { + sif::error << "No valid Star Tracker parameter JSON file" << std::endl; + } + auto strFdir = new StrFdir(objects::STAR_TRACKER); + auto cfgGetter = new StrConfigPathGetter(sdcMan); + auto starTracker = + new StarTrackerHandler(objects::STAR_TRACKER, objects::STR_COM_IF, starTrackerCookie, + strComIF, power::PDU1_CH2_STAR_TRACKER_5V, *cfgGetter, sdcMan); + starTracker->setPowerSwitcher(pwrSwitcher); + starTracker->connectModeTreeParent(*strAssy); + starTracker->setCustomFdir(strFdir); +} + +void ObjectFactory::createImtqComponents(PowerSwitchIF* pwrSwitcher, bool enableHkSets, + const char* i2cDev) { + auto* imtqAssy = new ImtqAssembly(objects::IMTQ_ASSY); + imtqAssy->connectModeTreeParent(satsystem::acs::ACS_SUBSYSTEM); + + new ImtqPollingTask(objects::IMTQ_POLLING, signals::I2C_FATAL_ERRORS); + I2cCookie* imtqI2cCookie = new I2cCookie(addresses::IMTQ, imtq::MAX_REPLY_SIZE, i2cDev); + auto imtqHandler = new ImtqHandler(objects::IMTQ_HANDLER, objects::IMTQ_POLLING, imtqI2cCookie, + power::Switches::PDU1_CH3_MGT_5V, enableHkSets); + imtqHandler->enableThermalModule(ThermalStateCfg()); + imtqHandler->setPowerSwitcher(pwrSwitcher); + imtqHandler->connectModeTreeParent(*imtqAssy); + static_cast(imtqHandler); +#if OBSW_TEST_IMTQ == 1 + imtqHandler->setStartUpImmediately(); + imtqHandler->setToGoToNormal(true); +#endif +#if OBSW_DEBUG_IMTQ == 1 + imtqHandler->setDebugMode(true); +#endif +} + +void ObjectFactory::createBpxBatteryComponent(bool enableHkSets, const char* i2cDev) { + I2cCookie* bpxI2cCookie = new I2cCookie(addresses::BPX_BATTERY, 100, i2cDev); + BpxBatteryHandler* bpxHandler = new BpxBatteryHandler( + objects::BPX_BATT_HANDLER, objects::I2C_COM_IF, bpxI2cCookie, enableHkSets); + bpxHandler->setStartUpImmediately(); + bpxHandler->setToGoToNormalMode(true); +#if OBSW_DEBUG_BPX_BATT == 1 + bpxHandler->setDebugMode(true); +#endif +} + +void ObjectFactory::createMiscComponents() { new PlocMemoryDumper(objects::PLOC_MEMORY_DUMPER); } + +void ObjectFactory::testAcsBrdAss(AcsBoardAssembly* acsAss) { + CommandMessage msg; + ModeMessage::setModeMessage(&msg, ModeMessage::CMD_MODE_COMMAND, DeviceHandlerIF::MODE_NORMAL, + duallane::A_SIDE); + ReturnValue_t result = MessageQueueSenderIF::sendMessage(acsAss->getCommandQueue(), &msg); + if (result != returnvalue::OK) { + sif::warning << "Sending mode command failed" << std::endl; + } +} + +void ObjectFactory::createRadSensorChipSelect(LinuxLibgpioIF* gpioIF) { + using namespace gpio; + if (gpioIF == nullptr) { + return; + } + GpioCookie* gpioCookieRadSensor = new GpioCookie; + std::stringstream consumer; + consumer << "0x" << std::hex << objects::RAD_SENSOR; + GpiodRegularByLineName* gpio = new GpiodRegularByLineName( + q7s::gpioNames::RAD_SENSOR_CHIP_SELECT, consumer.str(), Direction::OUT, Levels::HIGH); + gpioCookieRadSensor->addGpio(gpioIds::CS_RAD_SENSOR, gpio); + gpio = new GpiodRegularByLineName(q7s::gpioNames::ENABLE_RADFET, consumer.str(), Direction::OUT, + Levels::LOW); + gpioCookieRadSensor->addGpio(gpioIds::ENABLE_RADFET, gpio); + gpioChecker(gpioIF->addGpios(gpioCookieRadSensor), "RAD sensor"); +} + +void ObjectFactory::createPlI2cResetGpio(LinuxLibgpioIF* gpioIF) { + using namespace gpio; + if (common::OBSW_VERSION_MAJOR >= 6 or common::OBSW_VERSION_MAJOR == 4) { + if (gpioIF == nullptr) { + return; + } + GpioCookie* gpioI2cResetnCookie = new GpioCookie; + GpiodRegularByLineName* gpioI2cResetn = new GpiodRegularByLineName( + q7s::gpioNames::PL_I2C_ARESETN, "PL_I2C_ARESETN", Direction::OUT, Levels::HIGH); + gpioI2cResetnCookie->addGpio(gpioIds::PL_I2C_ARESETN, gpioI2cResetn); + gpioChecker(gpioIF->addGpios(gpioI2cResetnCookie), "PL I2C ARESETN"); + // Reset I2C explicitely again. + gpioIF->pullLow(gpioIds::PL_I2C_ARESETN); + TaskFactory::delayTask(1); + gpioIF->pullHigh(gpioIds::PL_I2C_ARESETN); + } +} + +ReturnValue_t ObjectFactory::readFirmwareVersion() { + uint32_t* mappedSysRomAddr = nullptr; + // The SYS ROM FPGA block is only available in those versions. + if (not(common::OBSW_VERSION_MAJOR >= 6) or (common::OBSW_VERSION_MAJOR == 4)) { + return returnvalue::OK; + } + // This has to come before the version announce because it might be required for retrieving + // the firmware version. + UioMapper sysRomMapper(q7s::UIO_SYS_ROM); + ReturnValue_t result = + sysRomMapper.getMappedAdress(&mappedSysRomAddr, UioMapper::Permissions::READ_ONLY); + if (result != returnvalue::OK) { + sif::error << "Getting mapped SYS ROM UIO address failed" << std::endl; + return returnvalue::FAILED; + } + if (mappedSysRomAddr != nullptr) { + uint32_t firstEntry = *(reinterpret_cast(mappedSysRomAddr)); + uint32_t secondEntry = *(reinterpret_cast(mappedSysRomAddr) + 1); + core::FW_VERSION_MAJOR = (firstEntry >> 24) & 0xff; + core::FW_VERSION_MINOR = (firstEntry >> 16) & 0xff; + core::FW_VERSION_REVISION = (firstEntry >> 8) & 0xff; + bool hasGitSha = (firstEntry & 0x0b1); + if (hasGitSha) { + std::memcpy(core::FW_VERSION_GIT_SHA, &secondEntry, 4); + } + } + return returnvalue::OK; +} + +ReturnValue_t ObjectFactory::createCcsdsIpComponentsWrapper(CcsdsComponentArgs& ccsdsArgs) { + ccsdsArgs.pdecCfgMemBaseAddr = config::pdec::PDEC_CONFIG_BASE_ADDR; + ccsdsArgs.pdecRamBaseAddr = config::pdec::PDEC_RAM_ADDR; + if (core::FW_VERSION_MAJOR < 6) { + ccsdsArgs.pdecCfgMemBaseAddr = config::pdec::PDEC_CONFIG_BASE_ADDR_LEGACY; + ccsdsArgs.pdecRamBaseAddr = config::pdec::PDEC_RAM_ADDR_LEGACY; + } + ReturnValue_t result = createCcsdsComponents(ccsdsArgs); +#if OBSW_TM_TO_PTME == 1 + if (ccsdsArgs.normalLiveTmDest != MessageQueueIF::NO_QUEUE) { + ccsdsArgs.pusFunnel.addLiveDestinationByRawId("VC0 NORMAL LIVE TM", ccsdsArgs.normalLiveTmDest, + 0); + } + if (ccsdsArgs.cfdpLiveTmDest != MessageQueueIF::NO_QUEUE) { + ccsdsArgs.cfdpFunnel.addLiveDestinationByRawId("VC0 CFDP LIVE TM", ccsdsArgs.cfdpLiveTmDest, 0); + } +#endif + return result; +} diff --git a/bsp_q7s/objectFactory.h b/bsp_q7s/objectFactory.h new file mode 100644 index 0000000..2e10dd7 --- /dev/null +++ b/bsp_q7s/objectFactory.h @@ -0,0 +1,97 @@ +#ifndef BSP_Q7S_OBJECTFACTORY_H_ +#define BSP_Q7S_OBJECTFACTORY_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "bsp_q7s/fs/SdCardManager.h" + +class LinuxLibgpioIF; +class SerialComIF; +class SpiComIF; +class I2cComIF; +class PowerSwitchIF; +class HealthTableIF; +class AcsBoardAssembly; +class GpioIF; + +extern std::atomic_bool PTME_LOCKED; + +namespace ObjectFactory { + +struct CcsdsComponentArgs { + CcsdsComponentArgs(LinuxLibgpioIF& gpioIF, StorageManagerIF& ipcStore, StorageManagerIF& tmStore, + PersistentTmStores& stores, PusTmFunnel& pusFunnel, CfdpTmFunnel& cfdpFunnel, + CcsdsIpCoreHandler** ipCoreHandler, uint32_t pdecCfgMemBaseAddr, + uint32_t pdecRamBaseAddr) + : gpioComIF(gpioIF), + ipcStore(ipcStore), + tmStore(tmStore), + stores(stores), + pusFunnel(pusFunnel), + cfdpFunnel(cfdpFunnel), + ipCoreHandler(ipCoreHandler), + pdecCfgMemBaseAddr(pdecCfgMemBaseAddr), + pdecRamBaseAddr(pdecRamBaseAddr) {} + LinuxLibgpioIF& gpioComIF; + StorageManagerIF& ipcStore; + StorageManagerIF& tmStore; + PersistentTmStores& stores; + PusTmFunnel& pusFunnel; + CfdpTmFunnel& cfdpFunnel; + CcsdsIpCoreHandler** ipCoreHandler; + uint32_t pdecCfgMemBaseAddr; + uint32_t pdecRamBaseAddr; + MessageQueueId_t normalLiveTmDest = MessageQueueIF::NO_QUEUE; + MessageQueueId_t cfdpLiveTmDest = MessageQueueIF::NO_QUEUE; +}; + +void setStatics(); +void produce(void* args); + +void createCommunicationInterfaces(LinuxLibgpioIF** gpioComIF, SerialComIF** uartComIF, + SpiComIF** spiMainComIF, I2cComIF** i2cComIF); +void createPcduComponents(LinuxLibgpioIF* gpioComIF, PowerSwitchIF** pwrSwitcher, + bool enableHkSets); +void createPlPcduComponents(LinuxLibgpioIF* gpioComIF, SpiComIF* spiComIF, + PowerSwitchIF* pwrSwitcher, Stack5VHandler& stackHandler); +void createTmpComponents(std::vector> tmpDevsToAdd); +void createRadSensorChipSelect(LinuxLibgpioIF* gpioIF); +ReturnValue_t createRadSensorComponent(LinuxLibgpioIF* gpioComIF, Stack5VHandler& handler); +void createAcsBoardGpios(GpioCookie& cookie); +void createAcsBoardComponents(SpiComIF& spiComIF, LinuxLibgpioIF* gpioComIF, SerialComIF* uartComIF, + PowerSwitchIF& pwrSwitcher, bool enableHkSets, + adis1650x::Type adisType); +void createHeaterComponents(GpioIF* gpioIF, PowerSwitchIF* pwrSwitcher, HealthTableIF* healthTable, + HeaterHandler*& heaterHandler); +void createImtqComponents(PowerSwitchIF* pwrSwitcher, bool enableHkSets, const char* i2cDev); +void createBpxBatteryComponent(bool enableHkSets, const char* i2cDev); +void createStrComponents(PowerSwitchIF* pwrSwitcher, SdCardManager& sdcMan); +void createSolarArrayDeploymentComponents(PowerSwitchIF& pwrSwitcher, GpioIF& gpioIF); +void createSyrlinksComponents(PowerSwitchIF* pwrSwitcher); +void createPayloadComponents(LinuxLibgpioIF* gpioComIF, PowerSwitchIF& pwrSwitcher); +void createReactionWheelComponents(LinuxLibgpioIF* gpioComIF, PowerSwitchIF* pwrSwitcher); +ReturnValue_t createCcsdsIpComponentsWrapper(CcsdsComponentArgs& args); +ReturnValue_t createCcsdsComponents(CcsdsComponentArgs& args); +ReturnValue_t readFirmwareVersion(); +void createMiscComponents(); + +void createTestComponents(LinuxLibgpioIF* gpioComIF); +void createPlI2cResetGpio(LinuxLibgpioIF* gpioComIF); + +void testAcsBrdAss(AcsBoardAssembly* assAss); + +}; // namespace ObjectFactory + +#endif /* BSP_Q7S_OBJECTFACTORY_H_ */ diff --git a/bsp_q7s/obsw.cpp b/bsp_q7s/obsw.cpp new file mode 100644 index 0000000..fc49c36 --- /dev/null +++ b/bsp_q7s/obsw.cpp @@ -0,0 +1,151 @@ +#include "obsw.h" + +#include +#include +#include + +#include +#include +#include + +#include "OBSWConfig.h" +#include "bsp_q7s/core/WatchdogHandler.h" +#include "commonConfig.h" +#include "fsfw/tasks/TaskFactory.h" +#include "fsfw/version.h" +#include "mission/acs/defs.h" +#include "mission/com/defs.h" +#include "mission/system/systemTree.h" +#include "q7sConfig.h" +#include "scheduling.h" +#include "watchdog/definitions.h" + +static constexpr int OBSW_ALREADY_RUNNING = -2; +#if OBSW_Q7S_EM == 0 +static const char* DEV_STRING = "Xiphos Q7S FM"; +#else +static const char* DEV_STRING = "Xiphos Q7S EM"; +#endif + +WatchdogHandler WATCHDOG_HANDLER; + +int obsw::obsw(int argc, char* argv[]) { + using namespace fsfw; + std::cout << "-- EIVE OBSW --" << std::endl; + std::cout << "-- Compiled for Linux (" << DEV_STRING << ") --" << std::endl; + std::cout << "-- OBSW v" << common::OBSW_VERSION << " | FSFW v" << fsfw::FSFW_VERSION << " --" + << std::endl; + std::cout << "-- " << __DATE__ << " " << __TIME__ << " --" << std::endl; + +#if Q7S_CHECK_FOR_ALREADY_RUNNING_IMG == 1 + std::error_code e; + // Check special file here. This file is created or deleted by the eive-watchdog application + // or systemd service! + if (std::filesystem::exists(watchdog::RUNNING_FILE_NAME, e)) { + sif::warning << "File " << watchdog::RUNNING_FILE_NAME + << " exists so the software might " + "already be running. Check if obsw systemd service has been stopped." + << std::endl; + return OBSW_ALREADY_RUNNING; + } +#endif + + // Delay the boot if applicable. + bootDelayHandling(); + + bool initWatchFunction = false; + std::string fullExecPath = argv[0]; + if (fullExecPath.find("/usr/bin") != std::string::npos) { + initWatchFunction = true; + } + ReturnValue_t result = WATCHDOG_HANDLER.initialize(initWatchFunction); + if (result != returnvalue::OK) { + std::cerr << "Initiating EIVE watchdog handler failed" << std::endl; + } + + scheduling::initMission(); + + // Command the EIVE system to safe mode +#if OBSW_COMMAND_SAFE_MODE_AT_STARTUP == 1 + // This ensures that the PCDU switches were updated. + TaskFactory::delayTask(1000); + commandComSubsystemRxOnly(); + commandEiveSystemToSafe(); +#else + announceAllModes(); +#endif + + for (;;) { + WATCHDOG_HANDLER.periodicOperation(); + TaskFactory::delayTask(2000); + } + return 0; +} + +void obsw::bootDelayHandling() { + const char* homedir = nullptr; + homedir = getenv("HOME"); + if (homedir == nullptr) { + homedir = getpwuid(getuid())->pw_dir; + } + std::filesystem::path bootDelayFile = std::filesystem::path(homedir) / "boot_delay_secs.txt"; + std::error_code e; + // Init delay handling. + if (std::filesystem::exists(bootDelayFile, e)) { + std::ifstream ifile(bootDelayFile); + std::string lineStr; + unsigned int bootDelaySecs = 0; + unsigned int line = 0; + // Try to reas delay seconds from file. + while (std::getline(ifile, lineStr)) { + std::istringstream iss(lineStr); + if (!(iss >> bootDelaySecs)) { + break; + } + line++; + } + if (line == 0) { + // If the file is empty, assume default of 6 seconds + bootDelaySecs = 6; + } + std::cout << "Delaying OBSW start for " << bootDelaySecs << " seconds" << std::endl; + TaskFactory::delayTask(bootDelaySecs * 1000); + } +} + +void obsw::commandEiveSystemToSafe() { + auto sysQueueId = satsystem::EIVE_SYSTEM.getCommandQueue(); + CommandMessage msg; + ModeMessage::setCmdModeMessage(msg, acs::AcsMode::SAFE, 0); + ReturnValue_t result = + MessageQueueSenderIF::sendMessage(sysQueueId, &msg, MessageQueueIF::NO_QUEUE, false); + if (result != returnvalue::OK) { + sif::error << "obsw: Sending safe mode command to EIVE system failed" << std::endl; + } +} + +void obsw::commandComSubsystemRxOnly() { + auto* comSs = ObjectManager::instance()->get(objects::COM_SUBSYSTEM); + if (comSs == nullptr) { + sif::error << "obsw: Could not retrieve COM subsystem object" << std::endl; + return; + } + CommandMessage msg; + ModeMessage::setCmdModeMessage(msg, com::RX_ONLY, 0); + ReturnValue_t result = MessageQueueSenderIF::sendMessage(comSs->getCommandQueue(), &msg, + MessageQueueIF::NO_QUEUE, false); + if (result != returnvalue::OK) { + sif::error << "obsw: Sending RX_ONLY mode command to COM subsystem failed" << std::endl; + } +} + +void obsw::announceAllModes() { + auto sysQueueId = satsystem::EIVE_SYSTEM.getCommandQueue(); + CommandMessage msg; + ModeMessage::setModeAnnounceMessage(msg, true); + ReturnValue_t result = + MessageQueueSenderIF::sendMessage(sysQueueId, &msg, MessageQueueIF::NO_QUEUE, false); + if (result != returnvalue::OK) { + sif::error << "obsw: Sending safe mode command to EIVE system failed" << std::endl; + } +} diff --git a/bsp_q7s/obsw.h b/bsp_q7s/obsw.h new file mode 100644 index 0000000..8260a60 --- /dev/null +++ b/bsp_q7s/obsw.h @@ -0,0 +1,15 @@ +#ifndef BSP_Q7S_CORE_OBSW_H_ +#define BSP_Q7S_CORE_OBSW_H_ + +namespace obsw { + +int obsw(int argc, char* argv[]); + +void bootDelayHandling(); +void commandEiveSystemToSafe(); +void commandComSubsystemRxOnly(); +void announceAllModes(); + +}; // namespace obsw + +#endif /* BSP_Q7S_CORE_OBSW_H_ */ diff --git a/bsp_q7s/scheduling.cpp b/bsp_q7s/scheduling.cpp new file mode 100644 index 0000000..fd99ab3 --- /dev/null +++ b/bsp_q7s/scheduling.cpp @@ -0,0 +1,702 @@ +#include "scheduling.h" + +#include +#include +#include +#include + +#include +#include + +#include "OBSWConfig.h" +#include "fsfw/objectmanager/ObjectManager.h" +#include "fsfw/objectmanager/ObjectManagerIF.h" +#include "fsfw/platform.h" +#include "fsfw/returnvalues/returnvalue.h" +#include "fsfw/serviceinterface/ServiceInterfaceStream.h" +#include "fsfw/tasks/FixedTimeslotTaskIF.h" +#include "fsfw/tasks/PeriodicTaskIF.h" +#include "fsfw/tasks/TaskFactory.h" +#include "mission/pollingSeqTables.h" +#include "mission/scheduling.h" +#include "mission/utility/InitMission.h" +#include "objectFactory.h" +#include "q7sConfig.h" + +/* This is configured for linux without CR */ +#ifdef PLATFORM_UNIX +ServiceInterfaceStream sif::debug("DEBUG"); +ServiceInterfaceStream sif::info("INFO"); +ServiceInterfaceStream sif::warning("WARNING"); +ServiceInterfaceStream sif::error("ERROR"); +#else +ServiceInterfaceStream sif::debug("DEBUG", true); +ServiceInterfaceStream sif::info("INFO", true); +ServiceInterfaceStream sif::warning("WARNING", true); +ServiceInterfaceStream sif::error("ERROR", true, false, true); +#endif + +ObjectManagerIF* objectManager = nullptr; + +void scheduling::initMission() { + sif::info << "Building global objects.." << std::endl; + try { + /* Instantiate global object manager and also create all objects */ + ObjectManager::instance()->setObjectFactoryFunction(ObjectFactory::produce, nullptr); + } catch (const std::invalid_argument& e) { + sif::error << "scheduling::initMission: Object Construction failed with an " + "invalid argument: " + << e.what(); + std::exit(1); + } + + sif::info << "Initializing all objects.." << std::endl; + ObjectManager::instance()->initialize(); + + /* This function creates and starts all tasks */ + initTasks(); +} + +void scheduling::initTasks() { + TaskFactory* factory = TaskFactory::instance(); + ReturnValue_t result = returnvalue::OK; + if (factory == nullptr) { + /* Should never happen ! */ + return; + } +#if OBSW_PRINT_MISSED_DEADLINES == 1 + void (*missedDeadlineFunc)(void) = TaskFactory::printMissedDeadline; +#else + void (*missedDeadlineFunc)(void) = nullptr; +#endif + +#if OBSW_ADD_SA_DEPL == 1 + // Could add this to the core controller but the core controller does so many thing that I would + // prefer to have the solar array deployment in a seprate task. + PeriodicTaskIF* solarArrayDeplTask = + factory->createPeriodicTask("SOLAR_ARRAY_DEPL", 65, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.4, + missedDeadlineFunc, &RR_SCHEDULING); + result = solarArrayDeplTask->addComponent(objects::SOLAR_ARRAY_DEPL_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SOLAR_ARRAY_DEPL", objects::SOLAR_ARRAY_DEPL_HANDLER); + } +#endif + + // Medium priority, higher than something like payload, but not the highest priority to also + // detect tasks which choke other tasks. + PeriodicTaskIF* xiphosWdtTask = + factory->createPeriodicTask("XIPHOS_WDT", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.4, + missedDeadlineFunc, &RR_SCHEDULING); + result = xiphosWdtTask->addComponent(objects::XIPHOS_WDT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("XIPHOS_WDT", objects::XIPHOS_WDT); + } + + PeriodicTaskIF* coreCtrlTask = factory->createPeriodicTask( + "CORE_CTRL", 55, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.4, missedDeadlineFunc, &RR_SCHEDULING); + result = coreCtrlTask->addComponent(objects::CORE_CONTROLLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CORE_CTRL", objects::CORE_CONTROLLER); + } + + /* TMTC Distribution */ + PeriodicTaskIF* tmTcDistributor = factory->createPeriodicTask( + "TC_DIST", 45, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc, &RR_SCHEDULING); +#if OBSW_ADD_TCPIP_SERVERS == 1 +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + result = tmTcDistributor->addComponent(objects::UDP_TMTC_SERVER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("UDP_TMTC_SERVER", objects::UDP_TMTC_SERVER); + } +#endif +#if OBSW_ADD_TMTC_TCP_SERVER == 1 + result = tmTcDistributor->addComponent(objects::TCP_TMTC_SERVER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("TCP_TMTC_SERVER", objects::TCP_TMTC_SERVER); + } +#endif +#endif + result = tmTcDistributor->addComponent(objects::CCSDS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CCSDS_DISTRIB", objects::CCSDS_PACKET_DISTRIBUTOR); + } + result = tmTcDistributor->addComponent(objects::PUS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_PACKET_DISTRIB", objects::PUS_PACKET_DISTRIBUTOR); + } + result = tmTcDistributor->addComponent(objects::CFDP_DISTRIBUTOR); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CFDP_DISTRIBUTOR", objects::CFDP_DISTRIBUTOR); + } + +#if OBSW_ADD_TCPIP_SERVERS == 1 +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + PeriodicTaskIF* udpPollingTask = factory->createPeriodicTask( + "UDP_TMTC_POLLING", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = udpPollingTask->addComponent(objects::UDP_TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("UDP_POLLING", objects::UDP_TMTC_POLLING_TASK); + } +#endif +#if OBSW_ADD_TMTC_TCP_SERVER == 1 + PeriodicTaskIF* tcpPollingTask = factory->createPeriodicTask( + "TCP_TMTC_POLLING", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = tcpPollingTask->addComponent(objects::TCP_TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("UDP_POLLING", objects::TCP_TMTC_POLLING_TASK); + } +#endif +#endif + + PeriodicTaskIF* genericSysTask = + factory->createPeriodicTask("SYSTEM_TASK", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.5, + missedDeadlineFunc, &RR_SCHEDULING); + result = genericSysTask->addComponent(objects::EIVE_SYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("EIVE_SYSTEM", objects::EIVE_SYSTEM); + } + result = genericSysTask->addComponent(objects::COM_SUBSYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("COM_SUBSYSTEM", objects::COM_SUBSYSTEM); + } + result = genericSysTask->addComponent(objects::SYRLINKS_ASSY); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SYRLINKS_ASSY", objects::SYRLINKS_ASSY); + } + result = genericSysTask->addComponent(objects::PL_SUBSYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PL_SUBSYSTEM", objects::PL_SUBSYSTEM); + } + result = genericSysTask->addComponent(objects::EPS_SUBSYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("EPS_SUBSYSTEM", objects::EPS_SUBSYSTEM); + } + result = genericSysTask->addComponent(objects::INTERNAL_ERROR_REPORTER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("ERROR_REPORTER", objects::INTERNAL_ERROR_REPORTER); + } + result = genericSysTask->addComponent(objects::PUS_SERVICE_17_TEST); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_17", objects::PUS_SERVICE_17_TEST); + } + +#if OBSW_ADD_CCSDS_IP_CORES == 1 + result = genericSysTask->addComponent(objects::CCSDS_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CCSDS Handler", objects::CCSDS_HANDLER); + } + + // Runs in IRQ mode, frequency does not really matter + PeriodicTaskIF* pdecHandlerTask = factory->createPeriodicTask( + "PDEC_HANDLER", 75, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr, &RR_SCHEDULING); + result = pdecHandlerTask->addComponent(objects::PDEC_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PDEC Handler", objects::PDEC_HANDLER); + } + +#endif /* OBSW_ADD_CCSDS_IP_CORE == 1 */ + // All the TM store tasks run in permanent loops, frequency does not matter + PeriodicTaskIF* liveTmTask = factory->createPeriodicTask( + "LIVE_TM", 55, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr, &RR_SCHEDULING); + result = liveTmTask->addComponent(objects::LIVE_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("LIVE_TM", objects::LIVE_TM_TASK); + } + PeriodicTaskIF* logTmTask = factory->createPeriodicTask( + "LOG_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = logTmTask->addComponent(objects::LOG_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("LOG_STORE_AND_TM", objects::LOG_STORE_AND_TM_TASK); + } + PeriodicTaskIF* hkTmTask = + factory->createPeriodicTask("HK_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = hkTmTask->addComponent(objects::HK_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("HK_STORE_AND_TM", objects::HK_STORE_AND_TM_TASK); + } + PeriodicTaskIF* cfdpTmTask = factory->createPeriodicTask( + "CFDP_PSTORE", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr); + result = cfdpTmTask->addComponent(objects::CFDP_STORE_AND_TM_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CFDP_STORE_AND_TM", objects::CFDP_STORE_AND_TM_TASK); + } + + // TODO: Use user priorities for this task. +#if OBSW_ADD_CFDP_COMPONENTS == 1 + PeriodicTaskIF* cfdpTask = + factory->createPeriodicTask("CFDP_HANDLER", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.4, + missedDeadlineFunc, &RR_SCHEDULING); + result = cfdpTask->addComponent(objects::CFDP_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("CFDP", objects::CFDP_HANDLER); + } +#endif + + PeriodicTaskIF* gpsTask = + factory->createPeriodicTask("GPS_TASK", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, 0.4, + missedDeadlineFunc, &RR_SCHEDULING); + result = gpsTask->addComponent(objects::GPS_CONTROLLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("GPS_CTRL", objects::GPS_CONTROLLER); + } + +#if OBSW_ADD_ACS_BOARD == 1 + PeriodicTaskIF* acsBrdPolling = + factory->createPeriodicTask("ACS_BOARD_POLLING", 85, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, + 0.4, missedDeadlineFunc, &RR_SCHEDULING); + result = acsBrdPolling->addComponent(objects::ACS_BOARD_POLLING_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("ACS_BOARD_POLLING", objects::ACS_BOARD_POLLING_TASK); + } +#endif + +#if OBSW_ADD_RW == 1 + PeriodicTaskIF* rwPolling = + factory->createPeriodicTask("RW_POLLING_TASK", 75, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, + 0.4, missedDeadlineFunc, &RR_SCHEDULING); + result = rwPolling->addComponent(objects::RW_POLLING_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("RW_POLLING_TASK", objects::RW_POLLING_TASK); + } +#endif +#if OBSW_ADD_MGT == 1 + PeriodicTaskIF* imtqPolling = + factory->createPeriodicTask("IMTQ_POLLING_TASK", 85, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, + 0.4, missedDeadlineFunc, &RR_SCHEDULING); + result = imtqPolling->addComponent(objects::IMTQ_POLLING); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("IMTQ_POLLING_TASK", objects::IMTQ_POLLING); + } +#endif + +#if OBSW_ADD_SUN_SENSORS == 1 + PeriodicTaskIF* susPolling = + factory->createPeriodicTask("SUS_POLLING_TASK", 85, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, + 0.4, missedDeadlineFunc, &RR_SCHEDULING); + result = susPolling->addComponent(objects::SUS_POLLING_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SUS_POLLING_TASK", objects::SUS_POLLING_TASK); + } +#endif + + PeriodicTaskIF* acsSysTask = + factory->createPeriodicTask("ACS_SYS_TASK", 55, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, 0.4, + missedDeadlineFunc, &RR_SCHEDULING); + result = acsSysTask->addComponent(objects::ACS_SUBSYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("ACS_SUBSYSTEM", objects::ACS_SUBSYSTEM); + } + result = acsSysTask->addComponent(objects::IMTQ_ASSY); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("IMTQ_ASSY", objects::IMTQ_ASSY); + } + result = acsSysTask->addComponent(objects::ACS_BOARD_ASS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("ACS_BOARD_ASS", objects::ACS_BOARD_ASS); + } + result = acsSysTask->addComponent(objects::RW_ASSY); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("RW_ASS", objects::RW_ASSY); + } + result = acsSysTask->addComponent(objects::SUS_BOARD_ASS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SUS_BOARD_ASS", objects::SUS_BOARD_ASS); + } + result = acsSysTask->addComponent(objects::STR_ASSY); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("STR_ASSY", objects::STR_ASSY); + } + result = acsSysTask->addComponent(objects::GPS_0_HEALTH_DEV); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("GPS_0_HEALTH_DEV", objects::GPS_0_HEALTH_DEV); + } + result = acsSysTask->addComponent(objects::GPS_1_HEALTH_DEV); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("GPS_1_HEALTH_DEV", objects::GPS_1_HEALTH_DEV); + } + + PeriodicTaskIF* tcsSystemTask = factory->createPeriodicTask( + "TCS_TASK", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.5, missedDeadlineFunc, &RR_SCHEDULING); +#if OBSW_ADD_THERMAL_TEMP_INSERTER == 1 + tcsSystemTask->addComponent(objects::THERMAL_TEMP_INSERTER); +#endif + scheduling::scheduleRtdSensors(tcsSystemTask); + result = tcsSystemTask->addComponent(objects::TCS_SUBSYSTEM); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("TCS_SUBSYSTEM", objects::TCS_SUBSYSTEM); + } + result = tcsSystemTask->addComponent(objects::TCS_BOARD_ASS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("TCS_BOARD_ASS", objects::TCS_BOARD_ASS); + } +#if OBSW_ADD_TCS_CTRL == 1 + result = tcsSystemTask->addComponent(objects::THERMAL_CONTROLLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("THERMAL_CONTROLLER", objects::THERMAL_CONTROLLER); + } +#endif + result = tcsSystemTask->addComponent(objects::HEATER_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("HEATER_HANDLER", objects::HEATER_HANDLER); + } + result = tcsSystemTask->addComponent(objects::HEATER_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("HEATER_HANDLER", objects::HEATER_HANDLER); + } + +#if OBSW_ADD_SYRLINKS == 1 + PeriodicTaskIF* syrlinksCom = factory->createPeriodicTask( + "SYRLINKS_COM", 65, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + result = syrlinksCom->addComponent(objects::SYRLINKS_COM_HANDLER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SYRLINKS_COM", objects::SYRLINKS_COM_HANDLER); + } +#endif + +#if OBSW_ADD_STAR_TRACKER == 1 + // Relatively high priority to make sure STR COM works well. + PeriodicTaskIF* strHelperTask = + factory->createPeriodicTask("STR_HELPER", 30, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, + missedDeadlineFunc, &RR_SCHEDULING); + result = strHelperTask->addComponent(objects::STR_COM_IF); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("STR_HELPER", objects::STR_COM_IF); + } +#endif /* OBSW_ADD_STAR_TRACKER == 1 */ + +#if OBSW_ADD_PLOC_MPSOC == 1 + PeriodicTaskIF* mpsocHelperTask = factory->createPeriodicTask( + "PLOC_MPSOC_HELPER", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = mpsocHelperTask->addComponent(objects::PLOC_MPSOC_HELPER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PLOC_MPSOC_HELPER", objects::PLOC_MPSOC_HELPER); + } +#endif /* OBSW_ADD_PLOC_MPSOC */ + + // TODO: Use regular scheduler for this task +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + PeriodicTaskIF* supvHelperTask = factory->createPeriodicTask( + "PLOC_SUPV_HELPER", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + result = supvHelperTask->addComponent(objects::PLOC_SUPERVISOR_HELPER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PLOC_SUPV_HELPER", objects::PLOC_SUPERVISOR_HELPER); + } +#endif /* OBSW_ADD_PLOC_SUPERVISOR */ + + FixedTimeslotTaskIF* plTask = factory->createFixedTimeslotTask( + "PL_TASK", 25, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.5, missedDeadlineFunc); + pst::pstPayload(plTask); + +#if OBSW_ADD_SCEX_DEVICE == 1 + PeriodicTaskIF* scexReaderTask; + scheduling::scheduleScexReader(*factory, scexReaderTask); +#endif + + std::vector pusTasks; + createPusTasks(*factory, missedDeadlineFunc, pusTasks); + std::vector pstTasks; + AcsPstCfg cfg; + createPstTasks(*factory, missedDeadlineFunc, pstTasks, cfg); + +#if OBSW_ADD_TEST_CODE == 1 +#if OBSW_TEST_CCSDS_BRIDGE == 1 + PeriodicTaskIF* ptmeTestTask = factory->createPeriodicTask( + "PTME_TEST", 80, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = ptmeTestTask->addComponent(objects::CCSDS_IP_CORE_BRIDGE); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PTME_TEST", objects::CCSDS_IP_CORE_BRIDGE); + } +#endif + std::vector testTasks; + createTestTasks(*factory, missedDeadlineFunc, testTasks); +#endif + + auto taskStarter = [](std::vector& taskVector, std::string name) { + for (const auto& task : taskVector) { + if (task != nullptr) { + task->startTask(); + } else { + sif::error << "Task in vector " << name << " is invalid!" << std::endl; + } + } + }; + + sif::info << "Starting tasks.." << std::endl; + xiphosWdtTask->startTask(); + tmTcDistributor->startTask(); + +#if OBSW_ADD_TCPIP_SERVERS == 1 +#if OBSW_ADD_TMTC_UDP_SERVER == 1 + udpPollingTask->startTask(); +#endif +#if OBSW_ADD_TMTC_TCP_SERVER == 1 + tcpPollingTask->startTask(); +#endif +#endif + + genericSysTask->startTask(); +#if OBSW_ADD_CCSDS_IP_CORES == 1 + pdecHandlerTask->startTask(); +#endif /* OBSW_ADD_CCSDS_IP_CORES == 1 */ + liveTmTask->startTask(); + logTmTask->startTask(); + hkTmTask->startTask(); + cfdpTmTask->startTask(); + + coreCtrlTask->startTask(); +#if OBSW_ADD_SA_DEPL == 1 + solarArrayDeplTask->startTask(); +#endif +#if OBSW_ADD_ACS_BOARD == 1 + acsBrdPolling->startTask(); +#endif +#if OBSW_ADD_SYRLINKS == 1 + syrlinksCom->startTask(); +#endif +#if OBSW_ADD_MGT == 1 + imtqPolling->startTask(); +#endif +#if OBSW_ADD_SUN_SENSORS == 1 + susPolling->startTask(); +#endif + + taskStarter(pstTasks, "PST task vector"); + taskStarter(pusTasks, "PUS task vector"); +#if OBSW_ADD_SCEX_DEVICE == 1 + scexReaderTask->startTask(); +#endif + +#if OBSW_TEST_CCSDS_BRIDGE == 1 + ptmeTestTask->startTask(); +#endif + +#if OBSW_ADD_CFDP_COMPONENTS == 1 + cfdpTask->startTask(); +#endif + +#if OBSW_ADD_STAR_TRACKER == 1 + strHelperTask->startTask(); +#endif /* OBSW_ADD_STAR_TRACKER == 1 */ + +#if OBSW_ADD_RW == 1 + rwPolling->startTask(); +#endif + gpsTask->startTask(); + acsSysTask->startTask(); + if (not tcsSystemTask->isEmpty()) { + tcsSystemTask->startTask(); + } +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + supvHelperTask->startTask(); +#endif /* OBSW_ADD_PLOC_SUPERVISOR == 1 */ +#if OBSW_ADD_PLOC_MPSOC == 1 + mpsocHelperTask->startTask(); +#endif + plTask->startTask(); + +#if OBSW_ADD_TEST_CODE == 1 + taskStarter(testTasks, "Test task vector"); +#endif + + sif::info << "Tasks started.." << std::endl; +} + +void scheduling::createPstTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec, AcsPstCfg cfg) { + ReturnValue_t result = returnvalue::OK; + +#ifdef RELEASE_BUILD + static constexpr float acsPstPeriod = 0.4; +#else + static constexpr float acsPstPeriod = 0.4; +#endif + FixedTimeslotTaskIF* acsTcsPst = + factory.createFixedTimeslotTask("ACS_TCS_PST", 85, PeriodicTaskIF::MINIMUM_STACK_SIZE * 2, + acsPstPeriod, missedDeadlineFunc, &RR_SCHEDULING); + result = pst::pstTcsAndAcs(acsTcsPst, cfg); + if (result != returnvalue::OK) { + if (result == FixedTimeslotTaskIF::SLOT_LIST_EMPTY) { + sif::warning << "scheduling::initTasks: ACS PST is empty" << std::endl; + } else { + sif::error << "scheduling::initTasks: Creating ACS PST failed!" << std::endl; + } + } else { + taskVec.push_back(acsTcsPst); + } + + /* Polling Sequence Table Default */ +#if OBSW_ADD_SPI_TEST_CODE == 0 + FixedTimeslotTaskIF* syrlinksPst = + factory.createFixedTimeslotTask("SYRLINKS", 65, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 0.5, + missedDeadlineFunc, &RR_SCHEDULING); + result = pst::pstSyrlinks(syrlinksPst); + if (result != returnvalue::OK) { + if (result == FixedTimeslotTaskIF::SLOT_LIST_EMPTY) { + sif::warning << "scheduling::initTasks: SPI PST is empty" << std::endl; + } else { + sif::error << "scheduling::initTasks: Creating SPI PST failed!" << std::endl; + } + } else { + taskVec.push_back(syrlinksPst); + } +#endif + +#if OBSW_ADD_I2C_TEST_CODE == 0 + FixedTimeslotTaskIF* i2cPst = + factory.createFixedTimeslotTask("I2C_PS_PST", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 0.6, + missedDeadlineFunc, &RR_SCHEDULING); + pst::TmpSchedConfig tmpSchedConf; +#if OBSW_Q7S_EM == 1 + tmpSchedConf.scheduleTmpDev0 = true; + tmpSchedConf.scheduleTmpDev1 = true; + tmpSchedConf.schedulePlPcduDev0 = true; + tmpSchedConf.schedulePlPcduDev1 = true; + tmpSchedConf.scheduleIfBoardDev = true; +#endif + result = pst::pstI2c(tmpSchedConf, i2cPst); + if (result != returnvalue::OK) { + if (result == FixedTimeslotTaskIF::SLOT_LIST_EMPTY) { + sif::warning << "scheduling::initTasks: I2C PST is empty" << std::endl; + } else { + sif::error << "scheduling::initTasks: Creating I2C PST failed!" << std::endl; + } + } else { + taskVec.push_back(i2cPst); + } +#endif + + FixedTimeslotTaskIF* gomSpacePstTask = + factory.createFixedTimeslotTask("GS_PST_TASK", 65, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, + 0.25, missedDeadlineFunc, &RR_SCHEDULING); + result = pst::pstGompaceCan(gomSpacePstTask); + if (result != returnvalue::OK) { + if (result != FixedTimeslotTaskIF::SLOT_LIST_EMPTY) { + sif::error << "scheduling::initTasks: GomSpace PST initialization failed!" << std::endl; + } + } + taskVec.push_back(gomSpacePstTask); +} + +void scheduling::createPusTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; + /* PUS Services */ + PeriodicTaskIF* pusHighPrio = + factory.createPeriodicTask("PUS_HIGH_PRIO", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, + missedDeadlineFunc, &RR_SCHEDULING); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_1_VERIFICATION); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_VERIF", objects::PUS_SERVICE_1_VERIFICATION); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_5_EVENT_REPORTING); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_EVENTS", objects::PUS_SERVICE_5_EVENT_REPORTING); + } + result = pusHighPrio->addComponent(objects::EVENT_MANAGER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("EVENT_MGMT", objects::EVENT_MANAGER); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_9_TIME_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_TIME", objects::PUS_SERVICE_9_TIME_MGMT); + } + taskVec.push_back(pusHighPrio); + + PeriodicTaskIF* pusMedPrio = + factory.createPeriodicTask("PUS_MED_PRIO", 45, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, + missedDeadlineFunc, &RR_SCHEDULING); + result = pusMedPrio->addComponent(objects::PUS_SERVICE_3_HOUSEKEEPING); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_3", objects::PUS_SERVICE_3_HOUSEKEEPING); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_8_FUNCTION_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_8", objects::PUS_SERVICE_8_FUNCTION_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_15_TM_STORAGE); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_15", objects::PUS_SERVICE_15_TM_STORAGE); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_11_TC_SCHEDULER); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_11", objects::PUS_SERVICE_11_TC_SCHEDULER); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_20_PARAMETERS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_20", objects::PUS_SERVICE_20_PARAMETERS); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_200_MODE_MGMT); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_200", objects::PUS_SERVICE_200_MODE_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_201_HEALTH); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_201", objects::PUS_SERVICE_201_HEALTH); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_2_DEVICE_ACCESS); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("PUS_2", objects::PUS_SERVICE_2_DEVICE_ACCESS); + } + taskVec.push_back(pusMedPrio); +} + +void scheduling::createTestTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { +#if OBSW_ADD_TEST_TASK == 1 && OBSW_ADD_TEST_CODE == 1 + ReturnValue_t result = returnvalue::OK; + static_cast(result); // supress warning in case it is not used + + PeriodicTaskIF* testTask = factory.createPeriodicTask( + "TEST_TASK", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1, missedDeadlineFunc); + + result = testTask->addComponent(objects::TEST_TASK); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("TEST_TASK", objects::TEST_TASK); + } + +#if OBSW_ADD_SPI_TEST_CODE == 1 + result = testTask->addComponent(objects::SPI_TEST); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("SPI_TEST", objects::SPI_TEST); + } +#endif +#if OBSW_ADD_I2C_TEST_CODE == 1 + result = testTask->addComponent(objects::I2C_TEST); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("I2C_TEST", objects::I2C_TEST); + } +#endif +#if OBSW_ADD_UART_TEST_CODE == 1 + result = testTask->addComponent(objects::UART_TEST); + if (result != returnvalue::OK) { + scheduling::printAddObjectError("UART_TEST", objects::UART_TEST); + } +#endif + + taskVec.push_back(testTask); + +#endif // OBSW_ADD_TEST_TASK == 1 && OBSW_ADD_TEST_CODE == 1 +} + +/** + ▄ ▄ + ▌▒█ ▄▀▒▌ + ▌▒▒█ ▄▀▒▒▒▐ + ▐▄▀▒▒▀▀▀▀▄▄▄▀▒▒▒▒▒▐ + ▄▄▀▒░▒▒▒▒▒▒▒▒▒█▒▒▄█▒▐ + ▄▀▒▒▒░░░▒▒▒░░░▒▒▒▀██▀▒▌ + ▐▒▒▒▄▄▒▒▒▒░░░▒▒▒▒▒▒▒▀▄▒▒▌ + ▌░░▌█▀▒▒▒▒▒▄▀█▄▒▒▒▒▒▒▒█▒▐ + ▐░░░▒▒▒▒▒▒▒▒▌██▀▒▒░░░▒▒▒▀▄▌ + ▌░▒▄██▄▒▒▒▒▒▒▒▒▒░░░░░░▒▒▒▒▌ + ▌▒▀▐▄█▄█▌▄░▀▒▒░░░░░░░░░░▒▒▒▐ + ▐▒▒▐▀▐▀▒░▄▄▒▄▒▒▒▒▒▒░▒░▒░▒▒▒▒▌ + ▐▒▒▒▀▀▄▄▒▒▒▄▒▒▒▒▒▒▒▒░▒░▒░▒▒▐ + ▌▒▒▒▒▒▒▀▀▀▒▒▒▒▒▒░▒░▒░▒░▒▒▒▌ + ▐▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒░▒░▒▒▄▒▒▐ + ▀▄▒▒▒▒▒▒▒▒▒▒▒░▒░▒░▒▄▒▒▒▒▌ + ▀▄▒▒▒▒▒▒▒▒▒▒▄▄▄▀▒▒▒▒▄▀ + ▀▄▄▄▄▄▄▀▀▀▒▒▒▒▒▄▄▀ + ▒▒▒▒▒▒▒▒▒▒▀▀ + **/ diff --git a/bsp_q7s/scheduling.h b/bsp_q7s/scheduling.h new file mode 100644 index 0000000..4a49374 --- /dev/null +++ b/bsp_q7s/scheduling.h @@ -0,0 +1,26 @@ +#ifndef BSP_Q7S_INITMISSION_H_ +#define BSP_Q7S_INITMISSION_H_ + +#include + +#include "fsfw/tasks/definitions.h" +#include "mission/pollingSeqTables.h" + +using pst::AcsPstCfg; + +class PeriodicTaskIF; +class TaskFactory; + +namespace scheduling { +void initMission(); +void initTasks(); + +void createPstTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec, AcsPstCfg cfg); +void createPusTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +void createTestTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +}; // namespace scheduling + +#endif /* BSP_Q7S_INITMISSION_H_ */ diff --git a/bsp_q7s/simple/CMakeLists.txt b/bsp_q7s/simple/CMakeLists.txt new file mode 100644 index 0000000..b5c4443 --- /dev/null +++ b/bsp_q7s/simple/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${SIMPLE_OBSW_NAME} PRIVATE simple.cpp) diff --git a/bsp_q7s/simple/simple.cpp b/bsp_q7s/simple/simple.cpp new file mode 100644 index 0000000..ac39c7a --- /dev/null +++ b/bsp_q7s/simple/simple.cpp @@ -0,0 +1,22 @@ +#include "simple.h" + +#include "iostream" +#include "q7sConfig.h" + +#if Q7S_SIMPLE_ADD_FILE_SYSTEM_TEST == 1 +#include "../boardtest/FileSystemTest.h" +#endif + +int simple::simple() { + std::cout << "-- Q7S Simple Application --" << std::endl; +#if Q7S_SIMPLE_ADD_FILE_SYSTEM_TEST == 1 + { + FileSystemTest fileSystemTest; + } +#endif + +#if TE0720_GPIO_TEST + +#endif + return 0; +} diff --git a/bsp_q7s/simple/simple.h b/bsp_q7s/simple/simple.h new file mode 100644 index 0000000..096a7b7 --- /dev/null +++ b/bsp_q7s/simple/simple.h @@ -0,0 +1,10 @@ +#ifndef BSP_Q7S_SIMPLE_SIMPLE_H_ +#define BSP_Q7S_SIMPLE_SIMPLE_H_ + +namespace simple { + +int simple(); + +} + +#endif /* BSP_Q7S_SIMPLE_SIMPLE_H_ */ diff --git a/bsp_q7s/spi/Q7sSpiComIF.cpp b/bsp_q7s/spi/Q7sSpiComIF.cpp new file mode 100644 index 0000000..8455250 --- /dev/null +++ b/bsp_q7s/spi/Q7sSpiComIF.cpp @@ -0,0 +1,5 @@ +#include + +Q7sSpiComIF::Q7sSpiComIF(object_id_t objectId, GpioIF* gpioComIF) : SpiComIF(objectId, gpioComIF) {} + +Q7sSpiComIF::~Q7sSpiComIF() {} diff --git a/bsp_q7s/spi/Q7sSpiComIF.h b/bsp_q7s/spi/Q7sSpiComIF.h new file mode 100644 index 0000000..def754a --- /dev/null +++ b/bsp_q7s/spi/Q7sSpiComIF.h @@ -0,0 +1,32 @@ +#ifndef BSP_Q7S_SPI_Q7SSPICOMIF_H_ +#define BSP_Q7S_SPI_Q7SSPICOMIF_H_ + +#include + +/** + * @brief This additional communication interface is required because the SPI busses behind the + * devices "/dev/spi2.0" and "dev/spidev3.0" are multiplexed to one SPI interface. + * This was necessary because the processing system spi (/dev/spi2.0) does not support + * frequencies lower than 650 kHz. To reach lower frequencies also the CPU frequency must + * be reduced which leads to other effects compromising kernel drivers. + * The nano avionics reaction wheels require a spi frequency between 150 kHz and 300 kHz + * why an additional AXI SPI core has been implemented in the programmable logic. However, + * the spi frequency of the AXI SPI core is not configurable during runtime. Therefore, + * this communication interface multiplexes either the hard-wired SPI or the AXI SPI to + * the SPI interface. The multiplexing is performed via a GPIO connected to a VHDL + * module responsible for switching between the to SPI peripherals. + */ +class Q7sSpiComIF : public SpiComIF { + public: + /** + * @brief Constructor + * + * @param objectId + * @param gpioComIF + * @param gpioSwitchId The gpio ID of the GPIO connected to the SPI mux module in the PL. + */ + Q7sSpiComIF(object_id_t objectId, GpioIF* gpioComIF, gpioId_t gpioSwitchId); + virtual ~Q7sSpiComIF(); +}; + +#endif /* BSP_Q7S_SPI_Q7SSPICOMIF_H_ */ diff --git a/bsp_q7s/xadc/CMakeLists.txt b/bsp_q7s/xadc/CMakeLists.txt new file mode 100644 index 0000000..a8d6181 --- /dev/null +++ b/bsp_q7s/xadc/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(${OBSW_NAME} PRIVATE Xadc.cpp) diff --git a/bsp_q7s/xadc/Xadc.cpp b/bsp_q7s/xadc/Xadc.cpp new file mode 100644 index 0000000..da3fbf7 --- /dev/null +++ b/bsp_q7s/xadc/Xadc.cpp @@ -0,0 +1,149 @@ +#include "Xadc.h" + +#include +#include + +#include + +#include "fsfw/serviceinterface/ServiceInterfaceStream.h" + +Xadc::Xadc() {} + +Xadc::~Xadc() {} + +ReturnValue_t Xadc::getTemperature(float& temperature) { + ReturnValue_t result = returnvalue::OK; + int raw = 0; + int offset = 0; + float scale = 0; + result = readValFromFile(xadc::file::tempRaw.c_str(), raw); + if (result != returnvalue::OK) { + return result; + } + result = readValFromFile(xadc::file::tempOffset.c_str(), offset); + if (result != returnvalue::OK) { + return result; + } + result = readValFromFile(xadc::file::tempScale.c_str(), scale); + if (result != returnvalue::OK) { + return result; + } + temperature = (raw + offset) * scale / 1000; + return result; +} + +ReturnValue_t Xadc::getVccPint(float& vccPint) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccpintRaw, xadc::file::vccpintScale, vccPint); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVccPaux(float& vccPaux) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccpauxRaw, xadc::file::vccpauxScale, vccPaux); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVccInt(float& vccInt) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccintRaw, xadc::file::vccintScale, vccInt); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVccAux(float& vccAux) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccauxRaw, xadc::file::vccauxScale, vccAux); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVccBram(float& vccBram) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccbramRaw, xadc::file::vccbramScale, vccBram); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVccOddr(float& vccOddr) { + ReturnValue_t result = + readVoltageFromSysfs(xadc::file::vccoddrRaw, xadc::file::vccoddrScale, vccOddr); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVrefp(float& vrefp) { + ReturnValue_t result = readVoltageFromSysfs(xadc::file::vrefpRaw, xadc::file::vrefpScale, vrefp); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::getVrefn(float& vrefn) { + ReturnValue_t result = readVoltageFromSysfs(xadc::file::vrefnRaw, xadc::file::vrefnScale, vrefn); + if (result != returnvalue::OK) { + return result; + } + return returnvalue::OK; +} + +ReturnValue_t Xadc::readVoltageFromSysfs(std::string rawFile, std::string scaleFile, + float& voltage) { + ReturnValue_t result = returnvalue::OK; + float raw = 0; + float scale = 0; + result = readValFromFile(rawFile.c_str(), raw); + if (result != returnvalue::OK) { + return result; + } + result = readValFromFile(scaleFile.c_str(), scale); + if (result != returnvalue::OK) { + return result; + } + voltage = calculateVoltage(raw, scale); + return result; +} + +float Xadc::calculateVoltage(int raw, float scale) { return static_cast(raw * scale); } + +template +ReturnValue_t Xadc::readValFromFile(const char* filename, T& val) { + FILE* fp; + fp = fopen(filename, "r"); + if (fp == nullptr) { + sif::warning << "Xadc::readValFromFile: Failed to open file " << filename << std::endl; + return returnvalue::FAILED; + } + char valstring[MAX_STR_LENGTH]{}; + char* returnVal = fgets(valstring, MAX_STR_LENGTH, fp); + if (returnVal == nullptr) { + sif::warning << "Xadc::readValFromFile: Failed to read string from file " << filename + << std::endl; + fclose(fp); + return returnvalue::FAILED; + } + std::istringstream valSstream(valstring); + valSstream >> val; + if (valSstream.bad()) { + sif::warning << "Xadc: Conversion of value to target type failed" << std::endl; + fclose(fp); + return returnvalue::FAILED; + } + fclose(fp); + return returnvalue::OK; +} diff --git a/bsp_q7s/xadc/Xadc.h b/bsp_q7s/xadc/Xadc.h new file mode 100644 index 0000000..be6ab70 --- /dev/null +++ b/bsp_q7s/xadc/Xadc.h @@ -0,0 +1,108 @@ +#ifndef BSP_Q7S_XADC_XADC_H_ +#define BSP_Q7S_XADC_XADC_H_ + +#include + +#include "fsfw/returnvalues/returnvalue.h" + +namespace xadc { +using namespace std; +static const string iioPath = "/sys/bus/iio/devices/iio:device1"; +namespace file { +static const string tempOffset = iioPath + "/in_temp0_offset"; +static const string tempRaw = iioPath + "/in_temp0_raw"; +static const string tempScale = iioPath + "/in_temp0_scale"; +static const string vccintRaw = iioPath + "/in_voltage0_vccint_raw"; +static const string vccintScale = iioPath + "/in_voltage0_vccint_scale"; +static const string vccauxRaw = iioPath + "/in_voltage1_vccaux_raw"; +static const string vccauxScale = iioPath + "/in_voltage1_vccaux_scale"; +static const string vccbramRaw = iioPath + "/in_voltage2_vccbram_raw"; +static const string vccbramScale = iioPath + "/in_voltage2_vccbram_scale"; +static const string vccpintRaw = iioPath + "/in_voltage3_vccpint_raw"; +static const string vccpintScale = iioPath + "/in_voltage3_vccpint_scale"; +static const string vccpauxRaw = iioPath + "/in_voltage4_vccpaux_raw"; +static const string vccpauxScale = iioPath + "/in_voltage4_vccpaux_scale"; +static const string vccoddrRaw = iioPath + "/in_voltage5_vccoddr_raw"; +static const string vccoddrScale = iioPath + "/in_voltage5_vccoddr_scale"; +static const string vrefpRaw = iioPath + "/in_voltage6_vrefp_raw"; +static const string vrefpScale = iioPath + "/in_voltage6_vrefp_scale"; +static const string vrefnRaw = iioPath + "/in_voltage7_vrefn_raw"; +static const string vrefnScale = iioPath + "/in_voltage7_vrefn_scale"; +} // namespace file +} // namespace xadc + +/** + * @brief Class providing access to the data generated by the analog mixed signal module (XADC). + * + * @details Details about the XADC peripheral of the Zynq-7020 can be found in the UG480 "7-Series + * FPGAs and Zynq-7000 SoC XADC Dual 12-Bit 1 MSPS Analog-to-Digital Converter" user guide + * from Xilinx. + * + * @author J. Meier + */ +class Xadc { + public: + /** + * @brief Constructor + */ + Xadc(); + virtual ~Xadc(); + + /** + * @brief Returns on-chip temperature degree celcius + */ + ReturnValue_t getTemperature(float& temperature); + + /** + * @brief Returns PS internal logic supply voltage in millivolts + */ + ReturnValue_t getVccPint(float& vccPint); + + /** + * @brief Returns PS auxiliary supply voltage in millivolts + */ + ReturnValue_t getVccPaux(float& vccPaux); + + /** + * @brief Returns PL internal supply voltage in millivolts + */ + ReturnValue_t getVccInt(float& vccInt); + + /** + * @brief Returns PL auxiliary supply voltage in millivolts + */ + ReturnValue_t getVccAux(float& vccAux); + + /** + * @brief Returns PL block RAM supply voltage in millivolts + */ + ReturnValue_t getVccBram(float& vccBram); + + /** + * @brief Returns the PS DDR I/O supply voltage + */ + ReturnValue_t getVccOddr(float& vcOddr); + + /** + * @brief Returns XADC reference input voltage relative to GND in millivolts + */ + ReturnValue_t getVrefp(float& vrefp); + + /** + * @brief Returns negative reference input voltage. Should normally be 0 V. + */ + ReturnValue_t getVrefn(float& vrefn); + + private: + // Maximum length of the string representation of a value in a xadc sysfs file + static const uint8_t MAX_STR_LENGTH = 15; + + ReturnValue_t readVoltageFromSysfs(std::string rawFile, std::string scaleFile, float& voltage); + + float calculateVoltage(int raw, float scale); + + template + ReturnValue_t readValFromFile(const char* filename, T& val); +}; + +#endif /* BSP_Q7S_XADC_XADC_H_ */ diff --git a/bsp_te0720_1cfa/CMakeLists.txt b/bsp_te0720_1cfa/CMakeLists.txt new file mode 100644 index 0000000..cb02f93 --- /dev/null +++ b/bsp_te0720_1cfa/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(${OBSW_NAME} PUBLIC + InitMission.cpp + main.cpp + ObjectFactory.cpp +) + +add_subdirectory(boardconfig) diff --git a/bsp_te0720_1cfa/InitMission.cpp b/bsp_te0720_1cfa/InitMission.cpp new file mode 100644 index 0000000..a3e3a00 --- /dev/null +++ b/bsp_te0720_1cfa/InitMission.cpp @@ -0,0 +1,227 @@ +#include "InitMission.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OBSWConfig.h" +#include "ObjectFactory.h" +#include "objects/systemObjectList.h" +#include "pollingsequence/pollingSequenceFactory.h" + +ServiceInterfaceStream sif::debug("DEBUG"); +ServiceInterfaceStream sif::info("INFO"); +ServiceInterfaceStream sif::warning("WARNING"); +ServiceInterfaceStream sif::error("ERROR"); + +ObjectManagerIF* objectManager = nullptr; + +void initmission::initMission() { + sif::info << "Building global objects.." << std::endl; + /* Instantiate global object manager and also create all objects */ + ObjectManager::instance()->setObjectFactoryFunction(ObjectFactory::produce, nullptr); + sif::info << "Initializing all objects.." << std::endl; + ObjectManager::instance()->initialize(); + + /* This function creates and starts all tasks */ + initTasks(); +} + +void initmission::initTasks() { + TaskFactory* factory = TaskFactory::instance(); + ReturnValue_t result = returnvalue::OK; + if (factory == nullptr) { + /* Should never happen ! */ + return; + } +#if OBSW_PRINT_MISSED_DEADLINES == 1 + void (*missedDeadlineFunc)(void) = TaskFactory::printMissedDeadline; +#else + void (*missedDeadlineFunc)(void) = nullptr; +#endif + + /* TMTC Distribution */ + PeriodicTaskIF* tmtcDistributor = factory->createPeriodicTask( + "DIST", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmtcDistributor->addComponent(objects::CCSDS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::PUS_PACKET_DISTRIBUTOR); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + result = tmtcDistributor->addComponent(objects::TM_FUNNEL); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + + PeriodicTaskIF* tmtcBridgeTask = factory->createPeriodicTask( + "TMTC_BRIDGE", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = tmtcBridgeTask->addComponent(objects::TMTC_BRIDGE); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Bridge failed" << std::endl; + } + PeriodicTaskIF* tmtcPollingTask = factory->createPeriodicTask( + "TMTC_POLLING", 80, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = tmtcPollingTask->addComponent(objects::TMTC_POLLING_TASK); + if (result != returnvalue::OK) { + sif::error << "Add component TMTC Polling failed" << std::endl; + } + + /* PUS Services */ + std::vector pusTasks; + createPusTasks(*factory, missedDeadlineFunc, pusTasks); + + std::vector pstTasks; + FixedTimeslotTaskIF* pst = factory->createFixedTimeslotTask( + "UART_PST", 70, PeriodicTaskIF::MINIMUM_STACK_SIZE * 4, 1.0, missedDeadlineFunc); + result = pst::pstUart(pst); + if (result != returnvalue::OK) { + sif::error << "InitMission::initTasks: Creating PST failed!" << std::endl; + } + pstTasks.push_back(pst); + +#if OBSW_ADD_PLOC_MPSOC == 1 + PeriodicTaskIF* mpsocHelperTask = factory->createPeriodicTask( + "PLOC_MPSOC_HELPER", 20, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = mpsocHelperTask->addComponent(objects::PLOC_MPSOC_HELPER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PLOC_MPSOC_HELPER", objects::PLOC_MPSOC_HELPER); + } +#endif /* OBSW_ADD_PLOC_MPSOC == 1*/ + +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + PeriodicTaskIF* supvHelperTask = factory->createPeriodicTask( + "PLOC_SUPV_HELPER", 20, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.2, missedDeadlineFunc); + result = supvHelperTask->addComponent(objects::PLOC_SUPERVISOR_HELPER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PLOC_SUPV_HELPER", objects::PLOC_SUPERVISOR_HELPER); + } +#endif /* OBSW_ADD_PLOC_SUPERVISOR == 1 */ + +#if OBSW_ADD_CCSDS_IP_CORES == 1 + PeriodicTaskIF* ccsdsHandlerTask = factory->createPeriodicTask( + "CCSDS_HANDLER", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 2.0, missedDeadlineFunc); + result = ccsdsHandlerTask->addComponent(objects::CCSDS_HANDLER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("CCSDS Handler", objects::CCSDS_HANDLER); + } + + // Minimal distance between two received TCs amounts to 0.6 seconds + // If a command has not been read before the next one arrives, the old command will be + // overwritten by the PDEC. + PeriodicTaskIF* pdecHandlerTask = factory->createPeriodicTask( + "PDEC_HANDLER", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, missedDeadlineFunc); + result = pdecHandlerTask->addComponent(objects::PDEC_HANDLER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PDEC Handler", objects::PDEC_HANDLER); + } +#endif /* OBSW_ADD_CCSDS_IP_CORES == 1 */ + + auto taskStarter = [](std::vector& taskVector, std::string name) { + for (const auto& task : taskVector) { + if (task != nullptr) { + task->startTask(); + } else { + sif::error << "Task in vector " << name << " is invalid!" << std::endl; + } + } + }; + + sif::info << "Starting tasks.." << std::endl; + tmtcDistributor->startTask(); + tmtcBridgeTask->startTask(); + tmtcPollingTask->startTask(); +#if OBSW_ADD_CCSDS_IP_CORE == 1 + pdecHandlerTask->startTask(); + ccsdsHandlerTask->startTask(); +#endif /* #if OBSW_ADD_CCSDS_IP_CORE == 1 */ +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + supvHelperTask->startTask(); +#endif /* OBSW_ADD_PLOC_SUPERVISOR == 1 */ +#if OBSW_ADD_PLOC_MPSOC == 1 + mpsocHelperTask->startTask(); +#endif /* OBSW_ADD_PLOC_MPSOC == 1 */ + + taskStarter(pstTasks, "PST Tasks"); + taskStarter(pusTasks, "PUS Tasks"); + + sif::info << "Tasks started.." << std::endl; +} + +void initmission::createPusTasks(TaskFactory& factory, + TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec) { + ReturnValue_t result = returnvalue::OK; + PeriodicTaskIF* pusVerification = factory.createPeriodicTask( + "PUS_VERIF", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusVerification->addComponent(objects::PUS_SERVICE_1_VERIFICATION); + if (result != returnvalue::OK) { + sif::error << "Object add component failed" << std::endl; + } + taskVec.push_back(pusVerification); + + PeriodicTaskIF* pusEvents = factory.createPeriodicTask( + "PUS_EVENTS", 60, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusEvents->addComponent(objects::PUS_SERVICE_5_EVENT_REPORTING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_EVENTS", objects::PUS_SERVICE_5_EVENT_REPORTING); + } + result = pusEvents->addComponent(objects::EVENT_MANAGER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS_MGMT", objects::EVENT_MANAGER); + } + taskVec.push_back(pusEvents); + + PeriodicTaskIF* pusHighPrio = factory.createPeriodicTask( + "PUS_HIGH_PRIO", 50, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.200, missedDeadlineFunc); + result = pusHighPrio->addComponent(objects::PUS_SERVICE_2_DEVICE_ACCESS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS2", objects::PUS_SERVICE_2_DEVICE_ACCESS); + } + result = pusHighPrio->addComponent(objects::PUS_SERVICE_9_TIME_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS9", objects::PUS_SERVICE_9_TIME_MGMT); + } + taskVec.push_back(pusHighPrio); + + PeriodicTaskIF* pusMedPrio = factory.createPeriodicTask( + "PUS_MED_PRIO", 40, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.8, missedDeadlineFunc); + result = pusMedPrio->addComponent(objects::PUS_SERVICE_8_FUNCTION_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS8", objects::PUS_SERVICE_8_FUNCTION_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_200_MODE_MGMT); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS200", objects::PUS_SERVICE_200_MODE_MGMT); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_20_PARAMETERS); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS20", objects::PUS_SERVICE_20_PARAMETERS); + } + result = pusMedPrio->addComponent(objects::PUS_SERVICE_3_HOUSEKEEPING); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS3", objects::PUS_SERVICE_3_HOUSEKEEPING); + } + taskVec.push_back(pusMedPrio); + + PeriodicTaskIF* pusLowPrio = factory.createPeriodicTask( + "PUS_LOW_PRIO", 30, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.6, missedDeadlineFunc); + result = pusLowPrio->addComponent(objects::PUS_SERVICE_17_TEST); + if (result != returnvalue::OK) { + initmission::printAddObjectError("PUS17", objects::PUS_SERVICE_17_TEST); + } + result = pusLowPrio->addComponent(objects::INTERNAL_ERROR_REPORTER); + if (result != returnvalue::OK) { + initmission::printAddObjectError("INT_ERR_RPRT", objects::INTERNAL_ERROR_REPORTER); + } + taskVec.push_back(pusLowPrio); +} diff --git a/bsp_te0720_1cfa/InitMission.h b/bsp_te0720_1cfa/InitMission.h new file mode 100644 index 0000000..a939987 --- /dev/null +++ b/bsp_te0720_1cfa/InitMission.h @@ -0,0 +1,21 @@ +#ifndef BSP_LINUX_INITMISSION_H_ +#define BSP_LINUX_INITMISSION_H_ + +#include + +#include "fsfw/tasks/definitions.h" + +class PeriodicTaskIF; +class TaskFactory; + +namespace initmission { +void initMission(); +void initTasks(); + +void createPstTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +void createPusTasks(TaskFactory& factory, TaskDeadlineMissedFunction missedDeadlineFunc, + std::vector& taskVec); +}; // namespace initmission + +#endif /* BSP_LINUX_INITMISSION_H_ */ diff --git a/bsp_te0720_1cfa/OBSWConfig.h.in b/bsp_te0720_1cfa/OBSWConfig.h.in new file mode 100644 index 0000000..d2efda3 --- /dev/null +++ b/bsp_te0720_1cfa/OBSWConfig.h.in @@ -0,0 +1,126 @@ +/** + * @brief This file can be used to add preprocessor define for conditional + * code inclusion exclusion or various other project constants and + * properties in one place. + */ +#ifndef FSFWCONFIG_OBSWCONFIG_H_ +#define FSFWCONFIG_OBSWCONFIG_H_ + +#include "commonConfig.h" +#include "OBSWVersion.h" + +/*******************************************************************/ +/** All of the following flags should be enabled for mission code */ +/*******************************************************************/ + +#define OBSW_ADD_CCSDS_IP_CORE 0 +// Set to 1 if all telemetry should be sent to the PTME IP Core +#define OBSW_TM_TO_PTME 0 +// Set to 1 if telecommands are received via the PDEC IP Core +#define OBSW_TC_FROM_PDEC 0 + +#define OBSW_ENABLE_TIMERS 1 +#define OBSW_ADD_MGT 0 +#define OBSW_ADD_BPX_BATTERY_HANDLER 0 +#define OBSW_ADD_STAR_TRACKER 0 +#define OBSW_ADD_PLOC_SUPERVISOR 0 +#define OBSW_ADD_PLOC_MPSOC 1 +#define OBSW_ADD_SUN_SENSORS 0 +#define OBSW_ADD_ACS_BOARD 1 +#define OBSW_ADD_ACS_HANDLERS 0 +#define OBSW_ADD_RW 0 +#define OBSW_ADD_RTD_DEVICES 0 +#define OBSW_ADD_TMP_DEVICES 0 +#define OBSW_ADD_RAD_SENSORS 0 +#define OBSW_ADD_PL_PCDU 0 +#define OBSW_ADD_SYRLINKS 0 +#define OBSW_ENABLE_SYRLINKS_TRANSMIT_TIMEOUT 0 +#define OBSW_SYRLINKS_SIMULATED 1 +#define OBSW_STAR_TRACKER_GROUND_CONFIG 1 +#define OBSW_PRINT_CORE_HK 0 +#define OBSW_INITIALIZE_SWITCHES 0 + +// This is a really tricky switch.. It initializes the PCDU switches to their default states +// at powerup. I think it would be better +// to leave it off for now. It makes testing a lot more difficult and it might mess with +// something the operators might want to do by giving the software too much intelligence +// at the wrong place. The system component might command all the Switches accordingly anyway +#define OBSW_INITIALIZE_SWITCHES 0 +#define OBSW_ENABLE_PERIODIC_HK 0 + +/*******************************************************************/ +/** All of the following flags should be disabled for mission code */ +/*******************************************************************/ + +// Can be used to switch device to NORMAL mode immediately +#define OBSW_SWITCH_TO_NORMAL_MODE_AFTER_STARTUP 1 +#define OBSW_PRINT_MISSED_DEADLINES 1 + +#define OBSW_SYRLINKS_SIMULATED 1 +#define OBSW_ADD_TEST_CODE 0 +#define OBSW_ADD_TEST_TASK 0 +#define OBSW_ADD_TEST_PST 0 +// If this is enabled, all other SPI code should be disabled +#define OBSW_ADD_SPI_TEST_CODE 0 +// If this is enabled, all other I2C code should be disabled +#define OBSW_ADD_I2C_TEST_CODE 0 +#define OBSW_ADD_UART_TEST_CODE 0 + +#define OBSW_TEST_ACS 0 +#define OBSW_DEBUG_ACS 0 +#define OBSW_TEST_SUS 0 +#define OBSW_DEBUG_SUS 0 +#define OBSW_TEST_RTD 0 +#define OBSW_DEBUG_RTD 0 +#define OBSW_TEST_RAD_SENSOR 0 +#define OBSW_DEBUG_RAD_SENSOR 0 +#define OBSW_TEST_PL_PCDU 0 +#define OBSW_DEBUG_PL_PCDU 0 +#define OBSW_TEST_BPX_BATT 0 +#define OBSW_DEBUG_BPX_BATT 0 +#define OBSW_TEST_IMTQ 0 +#define OBSW_DEBUG_IMTQ 0 +#define OBSW_TEST_RW 0 +#define OBSW_DEBUG_RW 0 + +#define OBSW_TEST_LIBGPIOD 0 +#define OBSW_TEST_PLOC_HANDLER 0 +#define OBSW_TEST_CCSDS_BRIDGE 0 +#define OBSW_TEST_CCSDS_PTME 0 +#define OBSW_TEST_TE7020_HEATER 0 +#define OBSW_TEST_GPIO_OPEN_BY_LABEL 0 +#define OBSW_TEST_GPIO_OPEN_BY_LINE_NAME 0 +#define OBSW_DEBUG_P60DOCK 0 + +#define OBSW_PRINT_CORE_HK 0 +#define OBSW_DEBUG_PDU1 0 +#define OBSW_DEBUG_PDU2 0 +#define OBSW_DEBUG_GPS 0 +#define OBSW_DEBUG_ACU 0 +#define OBSW_DEBUG_SYRLINKS 0 + +#define OBSW_DEBUG_PDEC_HANDLER 0 + +#define OBSW_DEBUG_PLOC_SUPERVISOR 1 +#define OBSW_DEBUG_PLOC_MPSOC 1 + +#define OBSW_DEBUG_STARTRACKER 0 +#define OBSW_TCP_SERVER_WIRETAPPING 0 + +/*******************************************************************/ +/** CMake Defines */ +/*******************************************************************/ +#cmakedefine EIVE_BUILD_GPSD_GPS_HANDLER + +#cmakedefine LIBGPS_VERSION_MAJOR @LIBGPS_VERSION_MAJOR@ +#cmakedefine LIBGPS_VERSION_MINOR @LIBGPS_VERSION_MINOR@ + +#ifdef __cplusplus + +#include "objects/systemObjectList.h" +#include "events/subsystemIdRanges.h" +#include "returnvalues/classIds.h" + +#endif + +#endif /* FSFWCONFIG_OBSWCONFIG_H_ */ diff --git a/bsp_te0720_1cfa/ObjectFactory.cpp b/bsp_te0720_1cfa/ObjectFactory.cpp new file mode 100644 index 0000000..2877c4e --- /dev/null +++ b/bsp_te0720_1cfa/ObjectFactory.cpp @@ -0,0 +1,159 @@ +#include "ObjectFactory.h" + +#include "OBSWConfig.h" +#include "busConf.h" +#include "devConf.h" +#include "ccsdsConfig.h" +#include "devices/addresses.h" +#include "devices/gpioIds.h" +#include "fsfw/datapoollocal/LocalDataPoolManager.h" +#include "fsfw/tmtcpacket/pus/tm.h" +#include "fsfw/tmtcservices/CommandingServiceBase.h" +#include "fsfw/tmtcservices/PusServiceBase.h" +#include "fsfw_hal/linux/i2c/I2cComIF.h" +#include "fsfw_hal/linux/i2c/I2cCookie.h" +#include "fsfw_hal/linux/serial/SerialComIF.h" +#include "fsfw_hal/linux/serial/SerialCookie.h" +#include "fsfw_hal/common/gpio/GpioCookie.h" +#include "linux/ObjectFactory.h" +#include "linux/devices/ploc/PlocMPSoCHandler.h" +#include "linux/devices/ploc/PlocMPSoCHelper.h" +#include "linux/devices/ploc/PlocMemoryDumper.h" +#include "linux/devices/ploc/PlocSupervisorHandler.h" +#include "linux/devices/ploc/PlocSupvHelper.h" +#include "linux/obc/AxiPtmeConfig.h" +#include "linux/obc/PapbVcInterface.h" +#include "linux/obc/PdecHandler.h" +#include "linux/obc/Ptme.h" +#include "linux/obc/PtmeConfig.h" +#include "mission/core/GenericFactory.h" +#include "mission/devices/Tmp1075Handler.h" +#include "mission/tmtc/TmFunnel.h" +#include "mission/tmtc/CCSDSHandler.h" +#include "mission/tmtc/VirtualChannel.h" +#include "objects/systemObjectList.h" +#include "test/gpio/DummyGpioIF.h" +#include "tmtc/apid.h" +#include "tmtc/pusIds.h" + +void Factory::setStaticFrameworkObjectIds() { + PusServiceBase::packetSource = objects::PUS_PACKET_DISTRIBUTOR; + PusServiceBase::packetDestination = objects::TM_FUNNEL; + + CommandingServiceBase::defaultPacketSource = objects::PUS_PACKET_DISTRIBUTOR; + CommandingServiceBase::defaultPacketDestination = objects::TM_FUNNEL; + +#if OBSW_TM_TO_PTME == 1 + TmFunnel::downlinkDestination = objects::CCSDS_HANDLER; +#else + TmFunnel::downlinkDestination = objects::TMTC_BRIDGE; +#endif + TmFunnel::storageDestination = objects::NO_OBJECT; + + VerificationReporter::messageReceiver = objects::PUS_SERVICE_1_VERIFICATION; + TmPacketBase::timeStamperId = objects::TIME_STAMPER; +} + +void ObjectFactory::produce(void* args) { + Factory::setStaticFrameworkObjectIds(); + ObjectFactory::produceGenericObjects(); + + LinuxLibgpioIF* gpioComIF = new LinuxLibgpioIF(objects::GPIO_IF);; + newSerialComIF(objects::UART_COM_IF); + +#if OBSW_ADD_PLOC_MPSOC == 1 + UartCookie* mpsocUartCookie = new UartCookie(objects::PLOC_MPSOC_HANDLER, te0720_1cfa::MPSOC_UART, + uart::PLOC_MPSOC_BAUD, mpsoc::MAX_REPLY_SIZE); + mpsocUartCookie->setNoFixedSizeReply(); + PlocMPSoCHelper* plocMpsocHelper = new PlocMPSoCHelper(objects::PLOC_MPSOC_HELPER); + auto dummyGpioIF = new DummyGpioIF(); + PlocMPSoCHandler* plocMPSoCHandler = new PlocMPSoCHandler( + objects::PLOC_MPSOC_HANDLER, objects::UART_COM_IF, mpsocUartCookie, plocMpsocHelper, + Gpio(gpioIds::ENABLE_MPSOC_UART, dummyGpioIF), objects::PLOC_SUPERVISOR_HANDLER); + plocMPSoCHandler->setStartUpImmediately(); +#endif /* OBSW_ADD_PLOC_MPSOC == 1 */ + +#if OBSW_ADD_PLOC_SUPERVISOR == 1 + UartCookie* supervisorCookie = + new UartCookie(objects::PLOC_SUPERVISOR_HANDLER, std::string("/dev/ttyPS1"), + uart::PLOC_SUPV_BAUD, supv::MAX_PACKET_SIZE * 20); + supervisorCookie->setNoFixedSizeReply(); + auto supvGpioIF = new DummyGpioIF(); + auto supvHelper = new PlocSupvHelper(objects::PLOC_SUPERVISOR_HELPER); + new PlocSupervisorHandler(objects::PLOC_SUPERVISOR_HANDLER, objects::UART_COM_IF, + supervisorCookie, Gpio(gpioIds::ENABLE_SUPV_UART, supvGpioIF), + pcdu::PDU1_CH6_PLOC_12V, supvHelper); + +#endif + + new PlocMemoryDumper(objects::PLOC_MEMORY_DUMPER); + +#if OBSW_TEST_LIBGPIOD == 1 +#if OBSW_TEST_GPIO_OPEN_BYLABEL == 1 + /* Configure MIO0 as input */ + GpiodRegular* testGpio = new GpiodRegular("MIO0", Direction::OUT, 0, "/amba_pl/gpio@41200000", 0); +#elif OBSW_TEST_GPIO_OPEN_BY_LINE_NAME + GpiodRegularByLineName* testGpio = + new GpiodRegularByLineName("test-name", "gpio-test", Direction::OUT, 0); +#else + /* Configure MIO0 as input */ + GpiodRegular* testGpio = new GpiodRegular("gpiochip0", 0, "MIO0", gpio::IN, 0); +#endif /* OBSW_TEST_GPIO_LABEL == 1 */ + GpioCookie* gpioCookie = new GpioCookie; + gpioCookie->addGpio(gpioIds::TEST_ID_0, testGpio); + new LibgpiodTest(objects::LIBGPIOD_TEST, objects::GPIO_IF, gpioCookie); +#endif + +#if OBSW_TEST_SUS == 1 + GpioCookie* gpioCookieSus = new GpioCookie; + GpiodRegular* chipSelectSus = new GpiodRegular( + std::string("gpiochip1"), 9, std::string("Chip Select Sus Sensor"), Direction::OUT, 1); + gpioCookieSus->addGpio(gpioIds::CS_SUS_0, chipSelectSus); + gpioComIF->addGpios(gpioCookieSus); + + SpiCookie* spiCookieSus = + new SpiCookie(addresses::SUS_0, std::string("/dev/spidev1.0"), SUS::MAX_CMD_SIZE, + spi::DEFAULT_MAX_1227_MODE, spi::DEFAULT_MAX_1227_SPEED); + + new SusHandler(objects::SUS_0, objects::SPI_COM_IF, spiCookieSus, gpioComIF, gpioIds::CS_SUS_0); +#endif + +#if OBSW_TEST_RAD_SENSOR == 1 + GpioCookie* gpioCookieRadSensor = new GpioCookie; + GpiodRegular* chipSelectRadSensor = new GpiodRegular( + std::string("gpiochip1"), 0, std::string("Chip select radiation sensor"), Direction::OUT, 1); + gpioCookieRadSensor->addGpio(gpioIds::CS_RAD_SENSOR, chipSelectRadSensor); + gpioComIF->addGpios(gpioCookieRadSensor); + + SpiCookie* spiCookieRadSensor = + new SpiCookie(addresses::RAD_SENSOR, gpioIds::CS_RAD_SENSOR, std::string("/dev/spidev1.0"), + SUS::MAX_CMD_SIZE, spi::DEFAULT_MAX_1227_MODE, spi::DEFAULT_MAX_1227_SPEED); + + RadiationSensorHandler* radSensor = + new RadiationSensorHandler(objects::RAD_SENSOR, objects::SPI_COM_IF, spiCookieRadSensor); + radSensor->setStartUpImmediately(); +#endif + +#if OBSW_TEST_TE7020_HEATER == 1 + /* Configuration for MIO0 on TE0720-03-1CFA */ + GpiodRegular* heaterGpio = + new GpiodRegular(std::string("gpiochip0"), 0, std::string("MIO0"), gpio::IN, 0); + GpioCookie* gpioCookie = new GpioCookie; + gpioCookie->addGpio(gpioIds::HEATER_0, heaterGpio); + new HeaterHandler(objects::HEATER_HANDLER, objects::GPIO_IF, gpioCookie, objects::PCDU_HANDLER, + pcdu::TCS_BOARD_8V_HEATER_IN); +#endif + + new I2cComIF(objects::I2C_COM_IF); + + I2cCookie* i2cCookieTmp1075tcs1 = + new I2cCookie(addresses::TMP1075_TCS_1, TMP1075::MAX_REPLY_LENGTH, std::string("/dev/i2c-0")); + I2cCookie* i2cCookieTmp1075tcs2 = + new I2cCookie(addresses::TMP1075_TCS_2, TMP1075::MAX_REPLY_LENGTH, std::string("/dev/i2c-0")); + + /* Temperature sensors */ + new Tmp1075Handler(objects::TMP1075_HANDLER_1, objects::I2C_COM_IF, i2cCookieTmp1075tcs1); + new Tmp1075Handler(objects::TMP1075_HANDLER_2, objects::I2C_COM_IF, i2cCookieTmp1075tcs2); + + static_cast(gpioComIF); +} diff --git a/bsp_te0720_1cfa/ObjectFactory.h b/bsp_te0720_1cfa/ObjectFactory.h new file mode 100644 index 0000000..828f5d3 --- /dev/null +++ b/bsp_te0720_1cfa/ObjectFactory.h @@ -0,0 +1,12 @@ +#ifndef BSP_LINUX_OBJECTFACTORY_H_ +#define BSP_LINUX_OBJECTFACTORY_H_ + +#include +#include + +namespace ObjectFactory { +static const uint32_t TRANSMITTER_TIMEOUT = 86400000; // 1 day +void produce(void* args); +}; // namespace ObjectFactory + +#endif /* BSP_LINUX_OBJECTFACTORY_H_ */ diff --git a/bsp_te0720_1cfa/boardconfig/CMakeLists.txt b/bsp_te0720_1cfa/boardconfig/CMakeLists.txt new file mode 100644 index 0000000..f9136e3 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(${OBSW_NAME} PRIVATE + print.c +) + +target_include_directories(${OBSW_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/bsp_te0720_1cfa/boardconfig/busConf.h b/bsp_te0720_1cfa/boardconfig/busConf.h new file mode 100644 index 0000000..7b51ab3 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/busConf.h @@ -0,0 +1,39 @@ +#ifndef BSP_EGSE_BOARDCONFIG_BUSCONF_H_ +#define BSP_EGSE_BOARDCONFIG_BUSCONF_H_ + +namespace te0720_1cfa { +static constexpr char MPSOC_UART[] = "/dev/ttyPS1"; + +static constexpr char UIO_PDEC_REGISTERS[] = "/dev/uio0"; +static constexpr char UIO_PTME[] = "/dev/uio1"; +static constexpr char UIO_PDEC_CONFIG_MEMORY[] = "/dev/uio2"; +static constexpr char UIO_PDEC_RAM[] = "/dev/uio3"; +static constexpr int MAP_ID_PTME_CONFIG = 3; + +namespace uiomapids { +static const int PTME_VC0 = 0; +static const int PTME_VC1 = 1; +static const int PTME_VC2 = 2; +static const int PTME_VC3 = 3; +static const int PTME_CONFIG = 4; +} // namespace uiomapids + +namespace gpioNames { + static constexpr char PAPB_BUSY_SIGNAL_VC0[] = "papb_busy_signal_vc0"; + static constexpr char PAPB_EMPTY_SIGNAL_VC0[] = "papb_empty_signal_vc0"; + static constexpr char PAPB_BUSY_SIGNAL_VC1[] = "papb_busy_signal_vc1"; + static constexpr char PAPB_EMPTY_SIGNAL_VC1[] = "papb_empty_signal_vc1"; + static constexpr char PAPB_BUSY_SIGNAL_VC2[] = "papb_busy_signal_vc2"; + static constexpr char PAPB_EMPTY_SIGNAL_VC2[] = "papb_empty_signal_vc2"; + static constexpr char PAPB_BUSY_SIGNAL_VC3[] = "papb_busy_signal_vc3"; + static constexpr char PAPB_EMPTY_SIGNAL_VC3[] = "papb_empty_signal_vc3"; + static constexpr char RS485_EN_TX_CLOCK[] = "tx_clock_enable_ltc2872"; + static constexpr char RS485_EN_TX_DATA[] = "tx_data_enable_ltc2872"; + static constexpr char RS485_EN_RX_CLOCK[] = "rx_clock_enable_ltc2872"; + static constexpr char RS485_EN_RX_DATA[] = "rx_data_enable_ltc2872"; + static constexpr char PDEC_RESET[] = "pdec_reset"; +} + +} + +#endif /* BSP_EGSE_BOARDCONFIG_BUSCONF_H_ */ diff --git a/bsp_te0720_1cfa/boardconfig/etl_profile.h b/bsp_te0720_1cfa/boardconfig/etl_profile.h new file mode 100644 index 0000000..54aca34 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/etl_profile.h @@ -0,0 +1,38 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2019 jwellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ +#ifndef __ETL_PROFILE_H__ +#define __ETL_PROFILE_H__ + +#define ETL_CHECK_PUSH_POP + +#define ETL_CPP11_SUPPORTED 1 +#define ETL_NO_NULLPTR_SUPPORT 0 + +#endif diff --git a/bsp_te0720_1cfa/boardconfig/gcov.h b/bsp_te0720_1cfa/boardconfig/gcov.h new file mode 100644 index 0000000..80acdd8 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/gcov.h @@ -0,0 +1,15 @@ +#ifndef LINUX_GCOV_H_ +#define LINUX_GCOV_H_ +#include + +#ifdef GCOV +extern "C" void __gcov_flush(); +#else +void __gcov_flush() { + sif::info << "GCC GCOV: Please supply GCOV=1 in Makefile if " + "coverage information is desired.\n" + << std::flush; +} +#endif + +#endif /* LINUX_GCOV_H_ */ diff --git a/bsp_te0720_1cfa/boardconfig/print.c b/bsp_te0720_1cfa/boardconfig/print.c new file mode 100644 index 0000000..1739e90 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/print.c @@ -0,0 +1,10 @@ +#include +#include + +void printChar(const char* character, bool errStream) { + if (errStream) { + putc(*character, stderr); + return; + } + putc(*character, stdout); +} diff --git a/bsp_te0720_1cfa/boardconfig/print.h b/bsp_te0720_1cfa/boardconfig/print.h new file mode 100644 index 0000000..8e7e2e5 --- /dev/null +++ b/bsp_te0720_1cfa/boardconfig/print.h @@ -0,0 +1,8 @@ +#ifndef HOSTED_BOARDCONFIG_PRINT_H_ +#define HOSTED_BOARDCONFIG_PRINT_H_ + +#include + +void printChar(const char* character, bool errStream); + +#endif /* HOSTED_BOARDCONFIG_PRINT_H_ */ diff --git a/bsp_te0720_1cfa/main.cpp b/bsp_te0720_1cfa/main.cpp new file mode 100644 index 0000000..cb7d987 --- /dev/null +++ b/bsp_te0720_1cfa/main.cpp @@ -0,0 +1,29 @@ +#include + +#include "InitMission.h" +#include "OBSWConfig.h" +#include "OBSWVersion.h" +#include "fsfw/version.h" +#include "fsfw/tasks/TaskFactory.h" + +/** + * @brief This is the main program entry point for the obsw running on the trenz electronic + * te0720-1cfa. + * @return + */ +int main(void) { + using namespace fsfw; + std::cout << "-- EIVE OBSW --" << std::endl; + std::cout << "-- Compiled for Trenz TE0720-1CFA" + << " --" << std::endl; + std::cout << "-- OBSW v" << SW_VERSION << "." << SW_SUBVERSION << "." << SW_REVISION << ", FSFW v" + << FSFW_VERSION << "--" << std::endl; + std::cout << "-- " << __DATE__ << " " << __TIME__ << " --" << std::endl; + + initmission::initMission(); + + for (;;) { + /* Suspend main thread by sleeping it. */ + TaskFactory::delayTask(5000); + } +} diff --git a/clone-submodules-no-privlibs.sh b/clone-submodules-no-privlibs.sh new file mode 100755 index 0000000..48d34bc --- /dev/null +++ b/clone-submodules-no-privlibs.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +root="$(pwd)" +ln -s "$root/hooks" "$root/.git/hooks" + +git submodule update --init fsfw thirdparty/rapidcsv thirdparty/lwgps thirdparty/json diff --git a/cmake/BBBCrossCompileConfig.cmake b/cmake/BBBCrossCompileConfig.cmake new file mode 100644 index 0000000..fcbab88 --- /dev/null +++ b/cmake/BBBCrossCompileConfig.cmake @@ -0,0 +1,103 @@ +# LINUX_ROOTFS should point to the local directory which contains all the +# libraries and includes from the target raspi. +# The following command can be used to do this, replace and the +# local accordingly: +# rsync -vR --progress -rl --delete-after --safe-links pi@:/{lib,usr,opt/vc/lib} +# LINUX_ROOTFS needs to be passed to the CMake command or defined in the +# application CMakeLists.txt before loading the toolchain file. + +# CROSS_COMPILE also needs to be set accordingly or passed to the CMake command +if(NOT DEFINED ENV{LINUX_ROOTFS}) + # Sysroot has not been cached yet and was not set in environment either + if(NOT SYSROOT_PATH) + message(FATAL_ERROR + "Define the LINUX_ROOTFS variable to point to the Raspberry Pi rootfs." + ) + endif() +else() + set(SYSROOT_PATH "$ENV{LINUX_ROOTFS}" CACHE PATH "Local linux root filesystem path") + message(STATUS "Raspberry Pi sysroot: ${SYSROOT_PATH}") +endif() + +if(NOT DEFINED ENV{CROSS_COMPILE}) + set(CROSS_COMPILE "arm-linux-gnueabihf") + message(STATUS + "No CROSS_COMPILE environmental variable set, using default ARM linux " + "cross compiler name ${CROSS_COMPILE}" + ) +else() + set(CROSS_COMPILE "$ENV{CROSS_COMPILE}") + message(STATUS + "Using environmental variable CROSS_COMPILE as cross-compiler: " + "$ENV{CROSS_COMPILE}" + ) +endif() + +# Generally, the debian roots will be a multiarch rootfs where some libraries are put +# into a folder named "arm-linux-gnueabihf". The user can override the folder name if this is +# not the case +if(NOT ENV{MULTIARCH_FOLDER_NAME}) + set(MULTIARCH_FOLDER_NAME "arm-linux-gnueabihf") +else() + set(MUTLIARCH_FOLDER_NAME $ENV{MULTIARCH_FOLDER_NAME}) +endif() + +message(STATUS "Using sysroot path: ${SYSROOT_PATH}") + +set(CROSS_COMPILE_CC "${CROSS_COMPILE}-gcc") +set(CROSS_COMPILE_CXX "${CROSS_COMPILE}-g++") +set(CROSS_COMPILE_LD "${CROSS_COMPILE}-ld") +set(CROSS_COMPILE_AR "${CROSS_COMPILE}-ar") +set(CROSS_COMPILE_RANLIB "${CROSS_COMPILE}-ranlib") +set(CROSS_COMPILE_STRIP "${CROSS_COMPILE}-strip") +set(CROSS_COMPILE_NM "${CROSS_COMPILE}-nm") +set(CROSS_COMPILE_OBJCOPY "${CROSS_COMPILE}-objcopy") +set(CROSS_COMPILE_SIZE "${CROSS_COMPILE}-size") + +# At the very least, cross compile gcc and g++ have to be set! +find_program (CMAKE_C_COMPILER ${CROSS_COMPILE_CC} REQUIRED) +find_program (CMAKE_CXX_COMPILER ${CROSS_COMPILE_CXX} REQUIRED) +# Useful utilities, not strictly necessary +find_program(CMAKE_SIZE ${CROSS_COMPILE_SIZE}) +find_program(CMAKE_OBJCOPY ${CROSS_COMPILE_OBJCOPY}) + +set(CMAKE_CROSSCOMPILING TRUE) +set(CMAKE_SYSROOT "${SYSROOT_PATH}") + +# Define name of the target system +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_PROCESSOR "arm") + +# List of library dirs where LD has to look. Pass them directly through gcc. +# LD_LIBRARY_PATH is not evaluated by arm-*-ld +set(LIB_DIRS + "${SYSROOT_PATH}/lib/${MUTLIARCH_FOLDER_NAME}" + "${SYSROOT_PATH}/usr/local/lib" + "${SYSROOT_PATH}/usr/lib/${MUTLIARCH_FOLDER_NAME}" + "${SYSROOT_PATH}/usr/lib" +) +# You can additionally check the linker paths if you add the +# flags ' -Xlinker --verbose' +set(COMMON_FLAGS "-I${SYSROOT_PATH}/usr/include") +foreach(LIB ${LIB_DIRS}) + set(COMMON_FLAGS "${COMMON_FLAGS} -L${LIB} -Wl,-rpath-link,${LIB}") +endforeach() + +set(CMAKE_C_FLAGS + "-march=armv7-a -mtune=cortex-a8 -mfpu=neon -mfloat-abi=hard ${COMMON_FLAGS}" + CACHE STRING "Flags for Beagle Bone Black" +) +set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" + CACHE STRING "Flags for Beagle Bone Black" +) + +set(CMAKE_FIND_ROOT_PATH + "${CMAKE_INSTALL_PREFIX};${CMAKE_PREFIX_PATH};${CMAKE_SYSROOT}" +) + +# search for programs in the build host directories +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# for libraries and headers in the target directories +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/BuildType.cmake b/cmake/BuildType.cmake new file mode 100644 index 0000000..ee027d1 --- /dev/null +++ b/cmake/BuildType.cmake @@ -0,0 +1,48 @@ +function(set_build_type) + +message(STATUS "Used build generator: ${CMAKE_GENERATOR}") + +# Set a default build type if none was specified +set(DEFAULT_BUILD_TYPE "RelWithDebInfo") +if(EXISTS "${CMAKE_SOURCE_DIR}/.git") + set(DEFAULT_BUILD_TYPE "Debug") +endif() + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS + "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified." + ) + set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE + STRING "Choose the type of build." FORCE + ) + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo" + ) +endif() + +set(RELEASE_BUILD 1 PARENT_SCOPE) + +if(${CMAKE_BUILD_TYPE} MATCHES "Debug") + message(STATUS + "Building Debug application with flags: ${CMAKE_C_FLAGS_DEBUG}" + ) + set(RELEASE_BUILD 0 PARENT_SCOPE) +elseif(${CMAKE_BUILD_TYPE} MATCHES "RelWithDebInfo") + message(STATUS + "Building Release (Debug) application with " + "flags: ${CMAKE_C_FLAGS_RELWITHDEBINFO}" + ) +elseif(${CMAKE_BUILD_TYPE} MATCHES "MinSizeRel") + message(STATUS + "Building Release (Size) application with " + "flags: ${CMAKE_C_FLAGS_MINSIZEREL}" + ) +else() + message(STATUS + "Building Release (Speed) application with " + "flags: ${CMAKE_C_FLAGS_RELEASE}" + ) +endif() + +endfunction() diff --git a/cmake/EiveHelpers.cmake b/cmake/EiveHelpers.cmake new file mode 100644 index 0000000..b210739 --- /dev/null +++ b/cmake/EiveHelpers.cmake @@ -0,0 +1,30 @@ +# Determines the git version with git describe and returns it by setting +# the GIT_INFO list in the parent scope. The list has the following entries +# 1. Full version string +# 2. Major version +# 3. Minor version +# 4. Revision +# 5. (Optional) git SHA hash and commits since tag when applicable +function(determine_version_with_git) + include(GetGitRevisionDescription) + git_describe(VERSION ${ARGN}) + string(FIND ${VERSION} "." VALID_VERSION) + if(VALID_VERSION EQUAL -1) + message(WARNING "Version string ${VERSION} retrieved with git describe is invalid") + return() + endif() + # Parse the version information into pieces. + string(REGEX REPLACE "^v([0-9]+)\\..*" "\\1" _VERSION_MAJOR "${VERSION}") + string(REGEX REPLACE "^v[0-9]+\\.([0-9]+).*" "\\1" _VERSION_MINOR "${VERSION}") + string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" _VERSION_PATCH "${VERSION}") + string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.[0-9]+-(.*)" "\\1" VERSION_SHA1 "${VERSION}") + set(GIT_INFO ${VERSION}) + list(APPEND GIT_INFO ${_VERSION_MAJOR}) + list(APPEND GIT_INFO ${_VERSION_MINOR}) + list(APPEND GIT_INFO ${_VERSION_PATCH}) + if(NOT VERSION_SHA1 STREQUAL VERSION) + list(APPEND GIT_INFO ${VERSION_SHA1}) + endif() + set(GIT_INFO ${GIT_INFO} PARENT_SCOPE) + message(STATUS "eive | Set git version info into GIT_INFO from the git tag ${VERSION}") +endfunction() diff --git a/cmake/GetGitRevisionDescription.cmake b/cmake/GetGitRevisionDescription.cmake new file mode 100644 index 0000000..69ef78b --- /dev/null +++ b/cmake/GetGitRevisionDescription.cmake @@ -0,0 +1,284 @@ +# - Returns a version string from Git +# +# These functions force a re-configure on each git commit so that you can +# trust the values of the variables in your build system. +# +# get_git_head_revision( [ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR]) +# +# Returns the refspec and sha hash of the current head revision +# +# git_describe( [ ...]) +# +# Returns the results of git describe on the source tree, and adjusting +# the output so that it tests false if an error occurs. +# +# git_describe_working_tree( [ ...]) +# +# Returns the results of git describe on the working tree (--dirty option), +# and adjusting the output so that it tests false if an error occurs. +# +# git_get_exact_tag( [ ...]) +# +# Returns the results of git describe --exact-match on the source tree, +# and adjusting the output so that it tests false if there was no exact +# matching tag. +# +# git_local_changes() +# +# Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. +# Uses the return code of "git diff-index --quiet HEAD --". +# Does not regard untracked files. +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2020 Ryan Pavlik +# http://academic.cleardefinition.com +# +# Copyright 2009-2013, Iowa State University. +# Copyright 2013-2020, Ryan Pavlik +# Copyright 2013-2020, Contributors +# SPDX-License-Identifier: BSL-1.0 +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +if(__get_git_revision_description) + return() +endif() +set(__get_git_revision_description YES) + +# We must run the following at "include" time, not at function call time, +# to find the path to this module rather than the path to a calling list file +get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) + +# Function _git_find_closest_git_dir finds the next closest .git directory +# that is part of any directory in the path defined by _start_dir. +# The result is returned in the parent scope variable whose name is passed +# as variable _git_dir_var. If no .git directory can be found, the +# function returns an empty string via _git_dir_var. +# +# Example: Given a path C:/bla/foo/bar and assuming C:/bla/.git exists and +# neither foo nor bar contain a file/directory .git. This wil return +# C:/bla/.git +# +function(_git_find_closest_git_dir _start_dir _git_dir_var) + set(cur_dir "${_start_dir}") + set(git_dir "${_start_dir}/.git") + while(NOT EXISTS "${git_dir}") + # .git dir not found, search parent directories + set(git_previous_parent "${cur_dir}") + get_filename_component(cur_dir "${cur_dir}" DIRECTORY) + if(cur_dir STREQUAL git_previous_parent) + # We have reached the root directory, we are not in git + set(${_git_dir_var} + "" + PARENT_SCOPE) + return() + endif() + set(git_dir "${cur_dir}/.git") + endwhile() + set(${_git_dir_var} + "${git_dir}" + PARENT_SCOPE) +endfunction() + +function(get_git_head_revision _refspecvar _hashvar) + _git_find_closest_git_dir("${CMAKE_CURRENT_SOURCE_DIR}" GIT_DIR) + + if("${ARGN}" STREQUAL "ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR") + set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR TRUE) + else() + set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR FALSE) + endif() + if(NOT "${GIT_DIR}" STREQUAL "") + file(RELATIVE_PATH _relative_to_source_dir "${CMAKE_SOURCE_DIR}" + "${GIT_DIR}") + if("${_relative_to_source_dir}" MATCHES "[.][.]" AND NOT ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) + # We've gone above the CMake root dir. + set(GIT_DIR "") + endif() + endif() + if("${GIT_DIR}" STREQUAL "") + set(${_refspecvar} + "GITDIR-NOTFOUND" + PARENT_SCOPE) + set(${_hashvar} + "GITDIR-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + # Check if the current source dir is a git submodule or a worktree. + # In both cases .git is a file instead of a directory. + # + if(NOT IS_DIRECTORY ${GIT_DIR}) + # The following git command will return a non empty string that + # points to the super project working tree if the current + # source dir is inside a git submodule. + # Otherwise the command will return an empty string. + # + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse + --show-superproject-working-tree + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT "${out}" STREQUAL "") + # If out is empty, GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a submodule + file(READ ${GIT_DIR} submodule) + string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE + ${submodule}) + string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE) + get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) + get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} + ABSOLUTE) + set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") + else() + # GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a worktree + file(READ ${GIT_DIR} worktree_ref) + # The .git directory contains a path to the worktree information directory + # inside the parent git repo of the worktree. + # + string(REGEX REPLACE "gitdir: (.*)$" "\\1" git_worktree_dir + ${worktree_ref}) + string(STRIP ${git_worktree_dir} git_worktree_dir) + _git_find_closest_git_dir("${git_worktree_dir}" GIT_DIR) + set(HEAD_SOURCE_FILE "${git_worktree_dir}/HEAD") + endif() + else() + set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") + endif() + set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") + if(NOT EXISTS "${GIT_DATA}") + file(MAKE_DIRECTORY "${GIT_DATA}") + endif() + + if(NOT EXISTS "${HEAD_SOURCE_FILE}") + return() + endif() + set(HEAD_FILE "${GIT_DATA}/HEAD") + configure_file("${HEAD_SOURCE_FILE}" "${HEAD_FILE}" COPYONLY) + + configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" + "${GIT_DATA}/grabRef.cmake" @ONLY) + include("${GIT_DATA}/grabRef.cmake") + + set(${_refspecvar} + "${HEAD_REF}" + PARENT_SCOPE) + set(${_hashvar} + "${HEAD_HASH}" + PARENT_SCOPE) +endfunction() + +function(git_describe _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash) + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} + "HEAD-HASH-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + # TODO sanitize + #if((${ARGN}" MATCHES "&&") OR + # (ARGN MATCHES "||") OR + # (ARGN MATCHES "\\;")) + # message("Please report the following error to the project!") + # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") + #endif() + + #message(STATUS "Arguments to execute_process: ${ARGN}") + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --always ${hash} ${ARGN} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_describe_working_tree _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --dirty ${ARGN} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_get_exact_tag _var) + git_describe(out --exact-match ${ARGN}) + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_local_changes _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash) + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} + "HEAD-HASH-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD -- + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(res EQUAL 0) + set(${_var} + "CLEAN" + PARENT_SCOPE) + else() + set(${_var} + "DIRTY" + PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/GetGitRevisionDescription.cmake.in b/cmake/GetGitRevisionDescription.cmake.in new file mode 100644 index 0000000..66eee63 --- /dev/null +++ b/cmake/GetGitRevisionDescription.cmake.in @@ -0,0 +1,43 @@ +# +# Internal file for GetGitRevisionDescription.cmake +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright 2009-2012, Iowa State University +# Copyright 2011-2015, Contributors +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# SPDX-License-Identifier: BSL-1.0 + +set(HEAD_HASH) + +file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) + +string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) +if(HEAD_CONTENTS MATCHES "ref") + # named branch + string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") + if(EXISTS "@GIT_DIR@/${HEAD_REF}") + configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) + else() + configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) + file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) + if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") + set(HEAD_HASH "${CMAKE_MATCH_1}") + endif() + endif() +else() + # detached HEAD + configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) +endif() + +if(NOT HEAD_HASH) + file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) + string(STRIP "${HEAD_HASH}" HEAD_HASH) +endif() diff --git a/cmake/HardwareOsPostConfig.cmake b/cmake/HardwareOsPostConfig.cmake new file mode 100644 index 0000000..111e859 --- /dev/null +++ b/cmake/HardwareOsPostConfig.cmake @@ -0,0 +1,66 @@ +function(post_source_hw_os_config) + +if(LINKER_SCRIPT) + add_link_options( + -T${LINKER_SCRIPT} + ) +endif() + +set(C_FLAGS "" CACHE INTERNAL "C flags") + +set(C_DEFS "" + CACHE INTERNAL + "C Defines" +) + +set(CXX_FLAGS ${C_FLAGS}) +set(CXX_DEFS ${C_DEFS}) + +if(CMAKE_VERBOSE) + message(STATUS "C Flags: ${C_FLAGS}") + message(STATUS "CXX Flags: ${CXX_FLAGS}") + message(STATUS "C Defs: ${C_DEFS}") + message(STATUS "CXX Defs: ${CXX_DEFS}") +endif() + +# Generator expression. Can be used to set different C, CXX and ASM flags. +add_compile_options( + $<$:${C_DEFS} ${C_FLAGS}> + $<$:${CXX_DEFS} ${CXX_FLAGS}> + $<$:${ASM_FLAGS}> +) + +set(STRIPPED_OBSW_NAME ${OBSW_BIN_NAME}-stripped) +set(STRIPPED_WATCHDOG_NAME eive-watchdog-stripped) + +if(EIVE_CREATE_UNIQUE_OBSW_BIN) + set(UNIQUE_OBSW_BIN_NAME ${OBSW_BIN_NAME}-$ENV{USERNAME}) +endif() + +add_custom_command( + TARGET ${OBSW_NAME} + POST_BUILD + COMMAND ${CMAKE_STRIP} --strip-all ${OBSW_BIN_NAME} -o ${STRIPPED_OBSW_NAME} + BYPRODUCTS ${STRIPPED_OBSW_NAME} + COMMENT "Generating stripped executable ${STRIPPED_OBSW_NAME}.." +) + +if(UNIQUE_OBSW_BIN_NAME) + add_custom_command( + TARGET ${OBSW_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/${OBSW_BIN_NAME} + ${CMAKE_CURRENT_BINARY_DIR}/${UNIQUE_OBSW_BIN_NAME} + COMMENT "Generating unique EIVE OBSW binary ${UNIQUE_OBSW_BIN_NAME}") +endif() + +add_custom_command( + TARGET ${WATCHDOG_NAME} + POST_BUILD + COMMAND ${CMAKE_STRIP} --strip-all eive-watchdog -o ${STRIPPED_WATCHDOG_NAME} + BYPRODUCTS ${STRIPPED_WATCHDOG_NAME} + COMMENT "Generating stripped executable ${STRIPPED_WATCHDOG_NAME}.." +) + +endfunction() \ No newline at end of file diff --git a/cmake/PreProjectConfig.cmake b/cmake/PreProjectConfig.cmake new file mode 100644 index 0000000..1c998e8 --- /dev/null +++ b/cmake/PreProjectConfig.cmake @@ -0,0 +1,144 @@ +function(obsw_module_config) +endfunction() + +function(pre_source_hw_os_config) + +# FreeRTOS +if(FSFW_OSAL MATCHES freertos) + message(FATAL_ERROR "No FreeRTOS support implemented yet.") +# RTEMS +elseif(FSFW_OSAL STREQUAL rtems) + add_definitions(-DRTEMS) + message(FATAL_ERROR "No RTEMS support implemented yet.") +elseif(FSFW_OSAL STREQUAL linux) + add_definitions(-DUNIX -DLINUX) + find_package(Threads REQUIRED) +# Hosted +else() + set(BSP_PATH "bsp_hosted") + if(WIN32) + add_definitions(-DWIN32) + elseif(UNIX) + find_package(Threads REQUIRED) + add_definitions(-DUNIX -DLINUX) + endif() +endif() + +# Cross-compile information +if(CMAKE_CROSSCOMPILING) + # set(CMAKE_VERBOSE TRUE) + + message(STATUS "Cross-compiling for ${TGT_BSP} target") + message(STATUS "Cross-compile gcc: ${CMAKE_C_COMPILER}") + message(STATUS "Cross-compile g++: ${CMAKE_CXX_COMPILER}") + + if(CMAKE_VERBOSE) + message(STATUS "Cross-compile linker: ${CMAKE_LINKER}") + message(STATUS "Cross-compile size utility: ${CMAKE_SIZE}") + message(STATUS "Cross-compile objcopy utility: ${CMAKE_OBJCOPY}") + message(STATUS "Cross-compile ranlib utility: ${CMAKE_RANLIB}") + message(STATUS "Cross-compile ar utility: ${CMAKE_AR}") + message(STATUS "Cross-compile nm utility: ${CMAKE_NM}") + message(STATUS "Cross-compile strip utility: ${CMAKE_STRIP}") + message(STATUS + "Cross-compile assembler: ${CMAKE_ASM_COMPILER} " + "-x assembler-with-cpp" + ) + message(STATUS "ABI flags: ${ABI_FLAGS}") + message(STATUS "Custom linker script: ${LINKER_SCRIPT}") + endif() + + set_property(CACHE TGT_BSP + PROPERTY STRINGS + "arm/q7s" "arm/raspberrypi" "arm/egse" + ) +endif() + + +if(TGT_BSP) + if (TGT_BSP MATCHES "arm/raspberrypi" OR TGT_BSP MATCHES "arm/beagleboneblack") + set(BSP_PATH "bsp_linux_board") + elseif(TGT_BSP MATCHES "arm/q7s") + set(BSP_PATH "bsp_q7s") + elseif(TGT_BSP MATCHES "arm/egse") + set(BSP_PATH "bsp_egse") + elseif(TGT_BSP MATCHES "arm/te0720-1cfa") + set(BSP_PATH "bsp_te0720_1cfa") + else() + message(WARNING "CMake not configured for this target!") + message(FATAL_ERROR "Target: ${TGT_BSP}!") + endif() +else() + set(BSP_PATH "bsp_hosted") +endif() + +set(BSP_PATH ${BSP_PATH} PARENT_SCOPE) + +endfunction() + +function(pre_project_config) + +# Basic input sanitization +if(DEFINED TGT_BSP) + if(${TGT_BSP} MATCHES "arm/raspberrypi" AND NOT FSFW_OSAL MATCHES linux) + message(STATUS "FSFW OSAL invalid for specified target BSP ${TGT_BSP}!") + message(STATUS "Setting valid FSFW_OSAL: linux") + set(FSFW_OSAL "linux") + endif() +endif() + + +# Disable compiler checks for cross-compiling. +if(FSFW_OSAL MATCHES linux AND TGT_BSP AND EIVE_HARDCODED_TOOLCHAIN_FILE) + if(TGT_BSP MATCHES "arm/q7s" OR TGT_BSP MATCHES "arm/te0720-1cfa") + set(CMAKE_TOOLCHAIN_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Zynq7020CrossCompileConfig.cmake" + PARENT_SCOPE + ) + elseif(TGT_BSP MATCHES "arm/raspberrypi" OR TGT_BSP MATCHES "arm/egse") + if(NOT DEFINED ENV{LINUX_ROOTFS}) + if(NOT DEFINED LINUX_ROOTFS) + message(WARNING "No LINUX_ROOTFS environmental or CMake variable set!") + set(ENV{LINUX_ROOTFS} "$ENV{HOME}/raspberrypi/rootfs") + else() + set(ENV{LINUX_ROOTFS} "${LINUX_ROOTFS}") + endif() + else() + message(STATUS + "LINUX_ROOTFS from environmental variables used: $ENV{LINUX_ROOTFS}" + ) + endif() + + if(NOT DEFINED ENV{RASPBERRY_VERSION}) + if(NOT RASPBERRY_VERSION) + message(STATUS "No RASPBERRY_VERSION specified, setting to 4") + set(RASPBERRY_VERSION "4" CACHE STRING "Raspberry Pi version") + else() + message(STATUS "Setting RASPBERRY_VERSION to ${RASPBERRY_VERSION}") + set(RASPBERRY_VERSION ${RASPBERRY_VERSION} CACHE STRING "Raspberry Pi version") + set(ENV{RASPBERRY_VERSION} ${RASPBERRY_VERSION}) + endif() + else() + message(STATUS + "RASPBERRY_VERSION from environmental variables used: " + "$ENV{RASPBERRY_VERSION}" + ) + endif() + + set(CMAKE_TOOLCHAIN_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/RPiCrossCompileConfig.cmake" + PARENT_SCOPE + ) + elseif(${TGT_BSP} MATCHES "arm/beagleboneblack") + if(LINUX_CROSS_COMPILE) + set(CMAKE_TOOLCHAIN_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/BBBCrossCompileConfig.cmake" + PARENT_SCOPE + ) + endif() + else() + message(WARNING "Target BSP (TGT_BSP) ${TGT_BSP} unknown!") + endif() +endif() + +endfunction() \ No newline at end of file diff --git a/cmake/RPiCrossCompileConfig.cmake b/cmake/RPiCrossCompileConfig.cmake new file mode 100644 index 0000000..5806af5 --- /dev/null +++ b/cmake/RPiCrossCompileConfig.cmake @@ -0,0 +1,148 @@ +# Based on https://github.com/Pro/raspi-toolchain but rewritten completely. + +# Adapted for the FSFW Example +if(NOT DEFINED ENV{RASPBERRY_VERSION}) + message(STATUS "Raspberry Pi version not specified, setting version 4!") + set(RASPBERRY_VERSION 4) +else() + set(RASPBERRY_VERSION $ENV{RASPBERRY_VERSION}) +endif() + + +# LINUX_ROOTFS should point to the local directory which contains all the +# libraries and includes from the target raspi. +# The following command can be used to do this, replace and the +# local accordingly: +# rsync -vR --progress -rl --delete-after --safe-links pi@:/{lib,usr,opt/vc/lib} +# LINUX_ROOTFS needs to be passed to the CMake command or defined in the +# application CMakeLists.txt before loading the toolchain file. + +# CROSS_COMPILE also needs to be set accordingly or passed to the CMake command +if(NOT DEFINED ENV{LINUX_ROOTFS}) + # Sysroot has not been cached yet and was not set in environment either + if(NOT SYSROOT_PATH) + message(FATAL_ERROR + "Define the LINUX_ROOTFS variable to point to the Raspberry Pi rootfs." + ) + endif() +else() + set(SYSROOT_PATH "$ENV{LINUX_ROOTFS}" CACHE PATH "Local linux root filesystem path") + message(STATUS "Raspberry Pi sysroot: ${SYSROOT_PATH}") +endif() + +if(NOT DEFINED ENV{CROSS_COMPILE}) + set(CROSS_COMPILE "arm-linux-gnueabihf") + message(STATUS + "No CROSS_COMPILE environmental variable set, using default ARM linux " + "cross compiler name ${CROSS_COMPILE}" + ) +else() + set(CROSS_COMPILE "$ENV{CROSS_COMPILE}") + message(STATUS + "Using environmental variable CROSS_COMPILE as cross-compiler: " + "$ENV{CROSS_COMPILE}" + ) +endif() + +# Generally, the debian roots will be a multiarch rootfs where some libraries are put +# into a folder named "arm-linux-gnueabihf". The user can override the folder name if this is +# not the case +if(NOT ENV{MULTIARCH_FOLDER_NAME}) + set(MULTIARCH_FOLDER_NAME "arm-linux-gnueabihf") +else() + set(MUTLIARCH_FOLDER_NAME $ENV{MULTIARCH_FOLDER_NAME}) +endif() + +message(STATUS "Using sysroot path: ${SYSROOT_PATH}") + +set(CROSS_COMPILE_CC "${CROSS_COMPILE}-gcc") +set(CROSS_COMPILE_CXX "${CROSS_COMPILE}-g++") +set(CROSS_COMPILE_LD "${CROSS_COMPILE}-ld") +set(CROSS_COMPILE_AR "${CROSS_COMPILE}-ar") +set(CROSS_COMPILE_RANLIB "${CROSS_COMPILE}-ranlib") +set(CROSS_COMPILE_STRIP "${CROSS_COMPILE}-strip") +set(CROSS_COMPILE_NM "${CROSS_COMPILE}-nm") +set(CROSS_COMPILE_OBJCOPY "${CROSS_COMPILE}-objcopy") +set(CROSS_COMPILE_SIZE "${CROSS_COMPILE}-size") + +# At the very least, cross compile gcc and g++ have to be set! +find_program (CMAKE_C_COMPILER ${CROSS_COMPILE_CC} REQUIRED) +find_program (CMAKE_CXX_COMPILER ${CROSS_COMPILE_CXX} REQUIRED) +# Useful utilities, not strictly necessary +find_program(CMAKE_SIZE ${CROSS_COMPILE_SIZE}) +find_program(CMAKE_OBJCOPY ${CROSS_COMPILE_OBJCOPY}) + +set(CMAKE_CROSSCOMPILING TRUE) +set(CMAKE_SYSROOT "${SYSROOT_PATH}") + +# Define name of the target system +set(CMAKE_SYSTEM_NAME "Linux") +if(RASPBERRY_VERSION VERSION_GREATER 1) + set(CMAKE_SYSTEM_PROCESSOR "armv7") +else() + set(CMAKE_SYSTEM_PROCESSOR "arm") +endif() + +# List of library dirs where LD has to look. Pass them directly through gcc. +# LD_LIBRARY_PATH is not evaluated by arm-*-ld +set(LIB_DIRS + "${SYSROOT_PATH}/opt/vc/lib" + "${SYSROOT_PATH}/lib/${MULTIARCH_FOLDER_NAME}" + "${SYSROOT_PATH}/usr/local/lib" + "${SYSROOT_PATH}/usr/lib/${MULTIARCH_FOLDER_NAME}" + "${SYSROOT_PATH}/usr/lib" + "${SYSROOT_PATH}/usr/lib/${MULTIARCH_FOLDER_NAME}/blas" + "${SYSROOT_PATH}/usr/lib/${MULTIARCH_FOLDER_NAME}/lapack" +) +# You can additionally check the linker paths if you add the +# flags ' -Xlinker --verbose' +set(COMMON_FLAGS "-I${SYSROOT_PATH}/usr/include") +foreach(LIB ${LIB_DIRS}) + set(COMMON_FLAGS "${COMMON_FLAGS} -L${LIB} -Wl,-rpath-link,${LIB}") +endforeach() + +if(RASPBERRY_VERSION VERSION_GREATER 3) + set(CMAKE_C_FLAGS + "-mcpu=cortex-a72 -mfpu=neon-vfpv4 -mfloat-abi=hard ${COMMON_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 4" + ) + set(CMAKE_CXX_FLAGS + "${CMAKE_C_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 4" + ) +elseif(RASPBERRY_VERSION VERSION_GREATER 2) + set(CMAKE_C_FLAGS + "-mcpu=cortex-a53 -mfpu=neon-vfpv4 -mfloat-abi=hard ${COMMON_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 3" + ) + set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 3" + ) +elseif(RASPBERRY_VERSION VERSION_GREATER 1) + set(CMAKE_C_FLAGS + "-mcpu=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard ${COMMON_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 2" + ) + set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 2" + ) +else() + set(CMAKE_C_FLAGS + "-mcpu=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard ${COMMON_FLAGS}" + CACHE STRING "Flags for Raspberry Pi 1 B+ Zero" + ) + set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" + CACHE STRING "Flags for Raspberry PI 1 B+ Zero" + ) +endif() + +set(CMAKE_FIND_ROOT_PATH + "${CMAKE_INSTALL_PREFIX};${CMAKE_PREFIX_PATH};${CMAKE_SYSROOT}" +) + +# search for programs in the build host directories +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# for libraries and headers in the target directories +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/Zynq7020CrossCompileConfig.cmake b/cmake/Zynq7020CrossCompileConfig.cmake new file mode 100644 index 0000000..5e269f1 --- /dev/null +++ b/cmake/Zynq7020CrossCompileConfig.cmake @@ -0,0 +1,112 @@ +if(DEFINED ENV{ZYNQ_7020_SYSROOT}) + set(ENV{ZYNQ_7020_ROOTFS} $ENV{ZYNQ_7020_SYSROOT}) +endif() +# CROSS_COMPILE also needs to be set accordingly or passed to the CMake command +if(NOT DEFINED ENV{ZYNQ_7020_ROOTFS}) + # Sysroot has not been cached yet and was not set in environment either + if(NOT DEFINED SYSROOT_PATH) + message(FATAL_ERROR + "Define the ZYNQ_7020_ROOTFS variable to point to the Zynq-7020 rootfs." + ) + endif() +else() + set(SYSROOT_PATH "$ENV{ZYNQ_7020_ROOTFS}" CACHE PATH "Zynq-7020 root filesystem path") +endif() + +if(NOT DEFINED ENV{CROSS_COMPILE}) + set(CROSS_COMPILE "arm-linux-gnueabihf") + message(STATUS + "No CROSS_COMPILE environmental variable set, using default ARM linux " + "cross compiler name ${CROSS_COMPILE}" + ) +else() + set(CROSS_COMPILE "$ENV{CROSS_COMPILE}") + message(STATUS + "Using environmental variable CROSS_COMPILE as cross-compiler: " + "$ENV{CROSS_COMPILE}" + ) +endif() + +message(STATUS "Using sysroot path: ${SYSROOT_PATH}") + +set(CROSS_COMPILE_CC "${CROSS_COMPILE}-gcc") +set(CROSS_COMPILE_CXX "${CROSS_COMPILE}-g++") +set(CROSS_COMPILE_LD "${CROSS_COMPILE}-ld") +set(CROSS_COMPILE_AR "${CROSS_COMPILE}-ar") +set(CROSS_COMPILE_RANLIB "${CROSS_COMPILE}-ranlib") +set(CROSS_COMPILE_STRIP "${CROSS_COMPILE}-strip") +set(CROSS_COMPILE_NM "${CROSS_COMPILE}-nm") +set(CROSS_COMPILE_OBJCOPY "${CROSS_COMPILE}-objcopy") +set(CROSS_COMPILE_SIZE "${CROSS_COMPILE}-size") + +# At the very least, cross compile gcc and g++ have to be set! +find_program (CMAKE_C_COMPILER ${CROSS_COMPILE_CC} HINTS $ENV{CROSS_COMPILE_BIN_PATH} REQUIRED) +find_program (CMAKE_CXX_COMPILER ${CROSS_COMPILE_CXX} HINTS $ENV{CROSS_COMPILE_BIN_PATH} REQUIRED) +# Useful utilities, not strictly necessary +find_program(CMAKE_SIZE ${CROSS_COMPILE_SIZE}) +find_program(CMAKE_OBJCOPY ${CROSS_COMPILE_OBJCOPY}) +find_program(CMAKE_STRIP ${CROSS_COMPILE_STRIP}) + +set(CMAKE_CROSSCOMPILING TRUE) +set(CMAKE_SYSROOT "${SYSROOT_PATH}") + +# Define name of the target system +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_PROCESSOR "armv7") + +# Define the compiler +set(CMAKE_C_COMPILER ${CROSS_COMPILE_CC}) +set(CMAKE_CXX_COMPILER ${CROSS_COMPILE_CXX}) + +if(EIVE_SYSROOT_MAGIC) + # List of library dirs where LD has to look. Pass them directly through gcc. + set(LIB_DIRS + "${SYSROOT_PATH}/usr/include" + "${SYSROOT_PATH}/usr/include/linux" + "${SYSROOT_PATH}/usr/lib" + "${SYSROOT_PATH}/lib" + "${SYSROOT_PATH}" + "${SYSROOT_PATH}/usr/lib/arm-xiphos-linux-gnueabi" + ) + # You can additionally check the linker paths if you add the + # flags ' -Xlinker --verbose' + set(COMMON_FLAGS "-I${SYSROOT_PATH}/usr/lib") + foreach(LIB ${LIB_DIRS}) + set(COMMON_FLAGS "${COMMON_FLAGS} -L${LIB} -Wl,-rpath-link,${LIB}") + endforeach() +endif() + +set(CMAKE_PREFIX_PATH + "${CMAKE_PREFIX_PATH}" + # "${SYSROOT_PATH}/usr/lib/${CROSS_COMPILE}" +) + +set(C_FLAGS + -mcpu=cortex-a9 + -mfpu=neon-vfpv3 + -mfloat-abi=hard + ${COMMON_FLAGS} + -lgpiod +) + +if (TGT_BSP MATCHES "arm/q7s") + set(C_FLAGS ${C_FLAGS} -lxiphos) +endif() + +string (REPLACE ";" " " C_FLAGS "${C_FLAGS}") + +set(CMAKE_C_FLAGS + ${C_FLAGS} + CACHE STRING "C flags for Zynq-7020" +) +set(CMAKE_CXX_FLAGS + "${CMAKE_C_FLAGS}" + CACHE STRING "CPP flags for Zynq-7020" +) + +# search for programs in the build host directories +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# for libraries and headers in the target directories +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/scripts/beagleboneb/crosscompile/bbb_path_helper.sh b/cmake/scripts/beagleboneb/crosscompile/bbb_path_helper.sh new file mode 100644 index 0000000..340713e --- /dev/null +++ b/cmake/scripts/beagleboneb/crosscompile/bbb_path_helper.sh @@ -0,0 +1,3 @@ +export PATH=$PATH:"$HOME/beaglebone//bin" +export CROSS_COMPILE="arm-linux-gnueabihf" +export BBB_ROOTFS="${HOME}/raspberrypi/rootfs" diff --git a/cmake/scripts/beagleboneb/crosscompile/make-debug-cfg.sh b/cmake/scripts/beagleboneb/crosscompile/make-debug-cfg.sh new file mode 100755 index 0000000..ba8d94c --- /dev/null +++ b/cmake/scripts/beagleboneb/crosscompile/make-debug-cfg.sh @@ -0,0 +1,35 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/beagleboneblack" +build_generator="" +builddir="build-Debug-BBB" +defines="LINUX_CROSS_COMPILE=ON" +if [ "${OS}" = "Windows_NT" ]; then + build_generator="MinGW Makefiles" +# Could be other OS but this works for now. +else + build_generator="Unix Makefiles" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l "${builddir}" -d "${defines}" +# Use this if commands are added which should not be printed +# set +x diff --git a/cmake/scripts/beagleboneb/crosscompile/make-release-cfg.sh b/cmake/scripts/beagleboneb/crosscompile/make-release-cfg.sh new file mode 100755 index 0000000..59d548c --- /dev/null +++ b/cmake/scripts/beagleboneb/crosscompile/make-release-cfg.sh @@ -0,0 +1,35 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/beagleboneblack" +build_generator="" +builddir="build-Release-BBB" +defines="LINUX_CROSS_COMPILE=ON" +if [ "${OS}" = "Windows_NT" ]; then + build_generator="MinGW Makefiles" +# Could be other OS but this works for now. +else + build_generator="Unix Makefiles" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l "${builddir}" -d "${defines}" +# Use this if commands are added which should not be printed +# set +x diff --git a/cmake/scripts/beagleboneb/make-debug-cfg.sh b/cmake/scripts/beagleboneb/make-debug-cfg.sh new file mode 100755 index 0000000..ede76ac --- /dev/null +++ b/cmake/scripts/beagleboneb/make-debug-cfg.sh @@ -0,0 +1,35 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/beagleboneblack" +build_generator="" +builddir="build-Debug-BBB" +defines="LINUX_CROSS_COMPILE=OFF" +if [ "${OS}" = "Windows_NT" ]; then + build_generator="MinGW Makefiles" +# Could be other OS but this works for now. +else + build_generator="Unix Makefiles" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l "${builddir}" -d "${defines}" +# Use this if commands are added which should not be printed +# set +x diff --git a/cmake/scripts/cmake-build-cfg.py b/cmake/scripts/cmake-build-cfg.py new file mode 100755 index 0000000..ea21fbd --- /dev/null +++ b/cmake/scripts/cmake-build-cfg.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +@brief CMake configuration helper +@details +This script was written to have a portable way to perform the CMake configuration with various parameters on +different OSes. It was first written for the FSFW Example, but could be adapted to be more generic +in the future. + +Run cmake_build_config.py --help to get more information. +""" +import os +import sys +import argparse +import shutil +import stat + + +def main(): + print("-- Python CMake build configurator utility --") + + print("Parsing command line arguments..") + parser = argparse.ArgumentParser( + description="Processing arguments for CMake build configuration." + ) + parser.add_argument( + "-o", + "--osal", + type=str, + choices=["freertos", "linux", "rtems", "host"], + help="FSFW OSAL. Valid arguments: host, linux, rtems, freertos", + ) + parser.add_argument( + "-b", + "--buildtype", + type=str, + choices=["debug", "release", "size", "reldeb"], + help="CMake build type. Valid arguments: debug, release, size, reldeb (Release with Debug " + "Information)", + default="debug", + ) + parser.add_argument("-l", "--builddir", type=str, help="Specify build directory.") + parser.add_argument( + "-g", "--generator", type=str, help="CMake Generator", choices=["make", "ninja"] + ) + parser.add_argument( + "-d", + "--defines", + help="Additional custom defines passed to CMake (supply without -D prefix!)", + nargs="*", + type=str, + ) + parser.add_argument( + "-t", + "--target-bsp", + type=str, + help="Target BSP, combination of architecture and machine", + ) + + args = parser.parse_args() + + print("Determining source location..") + source_location = determine_source_location() + print(f"Determined source location: {source_location}") + + print("Building cmake configuration command..") + + if args.osal is None: + print("No FSFW OSAL specified, setting to default (host)..") + osal = "host" + else: + osal = args.osal + + if args.generator is None: + generator_cmake_arg = "" + else: + if args.generator == "make": + if os.name == "nt": + generator_cmake_arg = '-G "MinGW Makefiles"' + else: + generator_cmake_arg = '-G "Unix Makefiles"' + elif args.generator == "ninja": + generator_cmake_arg = "-G Ninja" + else: + generator_cmake_arg = args.generator + + if args.buildtype == "debug": + cmake_build_type = "Debug" + elif args.buildtype == "release": + cmake_build_type = "Release" + elif args.buildtype == "size": + cmake_build_type = "MinSizeRel" + else: + cmake_build_type = "RelWithDebInfo" + + if args.target_bsp is not None: + cmake_target_cfg_cmd = f'-DTGT_BSP="{args.target_bsp}"' + else: + cmake_target_cfg_cmd = "" + + define_string = "" + if args.defines is not None: + define_list = args.defines[0].split() + for define in define_list: + define_string += f"-D{define} " + + build_folder = cmake_build_type + if args.builddir is not None: + build_folder = args.builddir + + build_path = source_location + os.path.sep + build_folder + if os.path.isdir(build_path): + remove_old_dir = input( + f"{build_folder} folder already exists. Remove old directory? [y/n]: " + ) + if str(remove_old_dir).lower() in ["yes", "y", 1]: + remove_old_dir = True + else: + build_folder = determine_new_folder() + build_path = source_location + os.path.sep + build_folder + remove_old_dir = False + if remove_old_dir: + rm_build_dir(build_path) + os.chdir(source_location) + os.mkdir(build_folder) + print(f"Navigating into build directory: {build_path}") + os.chdir(build_folder) + + cmake_command = ( + f'cmake {generator_cmake_arg} -DFSFW_OSAL="{osal}" ' + f'-DCMAKE_BUILD_TYPE="{cmake_build_type}" {cmake_target_cfg_cmd} ' + f"{define_string} {source_location}" + ) + # Remove redundant spaces + cmake_command = " ".join(cmake_command.split()) + print("Running CMake command: ") + print(f'" {cmake_command} "') + os.system(cmake_command) + print("-- CMake configuration done. --") + + +def rm_build_dir(path: str): + # On windows the permissions of the build directory may have been set to read-only. If this + # is the case the permissions are changed before trying to delete the directory. + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + shutil.rmtree(path) + + +def determine_source_location() -> str: + index = 0 + while not os.path.isdir("fsfw"): + index += 1 + os.chdir("..") + if index >= 5: + print( + "Error: Could not find source directory (determined by looking for fsfw folder!)" + ) + sys.exit(1) + return os.getcwd() + + +def determine_new_folder() -> str: + new_folder = input(f"Use different folder name? [y/n]: ") + if str(new_folder).lower() in ["yes", "y", 1]: + new_folder_name = input("New folder name: ") + return new_folder_name + else: + print("Aborting configuration.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/cmake/scripts/egse/egse_path_helper_win.sh b/cmake/scripts/egse/egse_path_helper_win.sh new file mode 100644 index 0000000..4bda17b --- /dev/null +++ b/cmake/scripts/egse/egse_path_helper_win.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Script to set path to raspberry pi toolchain +# Run script with: source egse_path_helper_win.sh +TOOLCHAIN_PATH="/c/SysGCC/raspberry/bin" +if [ $# -eq 1 ];then + export PATH=$PATH:"$1" +else + export PATH=$PATH:$TOOLCHAIN_PATH +fi + +echo "Path of toolchain set to $TOOLCHAIN_PATH" +export CROSS_COMPILE="arm-linux-gnueabihf" +export RASPBERRY_VERSION="4" \ No newline at end of file diff --git a/cmake/scripts/egse/make-debug-cfg.sh b/cmake/scripts/egse/make-debug-cfg.sh new file mode 100644 index 0000000..9a61137 --- /dev/null +++ b/cmake/scripts/egse/make-debug-cfg.sh @@ -0,0 +1,34 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + + +os_fsfw="linux" +tgt_bsp="arm/egse" +build_generator="make" +build_dir="build-Debug-egse" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x diff --git a/cmake/scripts/host/host-make-debug.sh b/cmake/scripts/host/host-make-debug.sh new file mode 100755 index 0000000..cb7a3fb --- /dev/null +++ b/cmake/scripts/host/host-make-debug.sh @@ -0,0 +1,41 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +root_dir="" +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + root_dir=$(realpath "../..") + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" + root_dir=${EIVE_OBSW_ROOT} +fi + +build_generator="make" +os_fsfw="host" +builddir="cmake-build-debug" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -l "${builddir}" +# Use this if commands are added which should not be printed +set +x +cd ${root_dir}/${builddir} diff --git a/cmake/scripts/host/host-make-release.sh b/cmake/scripts/host/host-make-release.sh new file mode 100755 index 0000000..5aee761 --- /dev/null +++ b/cmake/scripts/host/host-make-release.sh @@ -0,0 +1,39 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + root_dir=$(realpath "../..") + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +build_generator="make" +os_fsfw="host" +builddir="cmake-build-release" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "release" -l "${builddir}" +# Use this if commands are added which should not be printed +set +x +cd ${root_dir}/${builddir} diff --git a/cmake/scripts/host/host-ninja-debug.sh b/cmake/scripts/host/host-ninja-debug.sh new file mode 100755 index 0000000..5b5c68f --- /dev/null +++ b/cmake/scripts/host/host-ninja-debug.sh @@ -0,0 +1,38 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +build_generator="ninja" +os_fsfw="host" +builddir="cmake-build-debug" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -l "${builddir}" +# Use this if commands are added which should not be printed +# set +x + diff --git a/cmake/scripts/linux/host-make-debug.sh b/cmake/scripts/linux/host-make-debug.sh new file mode 100755 index 0000000..0ea1d76 --- /dev/null +++ b/cmake/scripts/linux/host-make-debug.sh @@ -0,0 +1,37 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +build_generator="make" +os_fsfw="linux" +builddir="cmake-build-debug" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -l "${builddir}" +# Use this if commands are added which should not be printed +# set +x diff --git a/cmake/scripts/linux/host-make-release.sh b/cmake/scripts/linux/host-make-release.sh new file mode 100755 index 0000000..89cb0f4 --- /dev/null +++ b/cmake/scripts/linux/host-make-release.sh @@ -0,0 +1,37 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +build_generator="Unix Makefiles" +os_fsfw="linux" +builddir="cmake-build-release" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "release" -l "${builddir}" +# Use this if commands are added which should not be printed +# set +x diff --git a/cmake/scripts/linux/host-ninja-debug.sh b/cmake/scripts/linux/host-ninja-debug.sh new file mode 100755 index 0000000..2514635 --- /dev/null +++ b/cmake/scripts/linux/host-ninja-debug.sh @@ -0,0 +1,38 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +build_generator="ninja" +os_fsfw="linux" +builddir="cmake-build-debug" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -l "${builddir}" +# Use this if commands are added which should not be printed +# set +x + diff --git a/cmake/scripts/q7s/q7s-make-debug.sh b/cmake/scripts/q7s/q7s-make-debug.sh new file mode 100755 index 0000000..a9536c9 --- /dev/null +++ b/cmake/scripts/q7s/q7s-make-debug.sh @@ -0,0 +1,50 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [ -z "${EIVE_OBSW_ROOT}" ]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_defs="EIVE_Q7S_EM=ON" +fi + +build_defs="${build_defs} CMAKE_EXPORT_COMPILE_COMMANDS=ON" + +os_fsfw="linux" +tgt_bsp="arm/q7s" +build_dir="cmake-build-debug-q7s" +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_dir="${build_dir}-em" +fi + +build_generator="make" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l"${build_dir}" -d "${build_defs}" +set +x + +cd ${init_dir} diff --git a/cmake/scripts/q7s/q7s-make-release.sh b/cmake/scripts/q7s/q7s-make-release.sh new file mode 100755 index 0000000..f5e970d --- /dev/null +++ b/cmake/scripts/q7s/q7s-make-release.sh @@ -0,0 +1,50 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [[ -z ${EIVE_OBSW_ROOT} ]]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_defs="EIVE_Q7S_EM=ON" +fi + +build_defs="${build_defs} CMAKE_EXPORT_COMPILE_COMMANDS=ON" + +os_fsfw="linux" +tgt_bsp="arm/q7s" +build_dir="cmake-build-release-q7s" +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_dir="${build_dir}-em" +fi +build_generator="make" + +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "release" -t "${tgt_bsp}" \ + -l"${build_dir}" -d "${build_defs}" +set +x + +cd ${init_dir} diff --git a/cmake/scripts/q7s/q7s-make-size.sh b/cmake/scripts/q7s/q7s-make-size.sh new file mode 100755 index 0000000..f75edb7 --- /dev/null +++ b/cmake/scripts/q7s/q7s-make-size.sh @@ -0,0 +1,35 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/q7s" +build_dir="build-Release-Q7S" +build_generator="" +if [ "${OS}" = "Windows_NT" ]; then + build_generator="MinGW Makefiles" + python="py" +# Could be other OS but this works for now. +else + build_generator="Unix Makefiles" + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "size" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x diff --git a/cmake/scripts/q7s/q7s-ninja-debug.sh b/cmake/scripts/q7s/q7s-ninja-debug.sh new file mode 100755 index 0000000..ad50b6a --- /dev/null +++ b/cmake/scripts/q7s/q7s-ninja-debug.sh @@ -0,0 +1,48 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [[ -z ${EIVE_OBSW_ROOT} ]]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_defs="EIVE_Q7S_EM=ON" +fi + +os_fsfw="linux" +tgt_bsp="arm/q7s" +build_dir="cmake-build-debug-q7s" +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_dir="${build_dir}-em" +fi + +build_generator="ninja" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l "${build_dir}" -d "${build_defs}" +set +x + +cd ${init_dir} diff --git a/cmake/scripts/q7s/q7s-ninja-release.sh b/cmake/scripts/q7s/q7s-ninja-release.sh new file mode 100755 index 0000000..f0587f5 --- /dev/null +++ b/cmake/scripts/q7s/q7s-ninja-release.sh @@ -0,0 +1,48 @@ +#!/bin/bash +cfg_script_name="cmake-build-cfg.py" +init_dir=$(pwd) +if [[ -z ${EIVE_OBSW_ROOT} ]]; then + counter=0 + while [ ${counter} -lt 5 ] + do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) + done + + if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 + fi +else + cfg_script_name="${EIVE_OBSW_ROOT}/cmake/scripts/${cfg_script_name}" +fi + +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_defs="EIVE_Q7S_EM=ON" +fi + +os_fsfw="linux" +tgt_bsp="arm/q7s" +build_dir="cmake-build-release-q7s" +if [ ! -z "${EIVE_Q7S_EM}" ]; then + build_dir="${build_dir}-em" +fi + +build_generator="ninja" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "release" -t "${tgt_bsp}" \ + -l"${build_dir}" -d "${build_defs}" +set +x + +cd ${init_dir} diff --git a/cmake/scripts/rpi/make-debug-cfg.sh b/cmake/scripts/rpi/make-debug-cfg.sh new file mode 100755 index 0000000..f4d006c --- /dev/null +++ b/cmake/scripts/rpi/make-debug-cfg.sh @@ -0,0 +1,34 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + + +os_fsfw="linux" +tgt_bsp="arm/raspberrypi" +build_generator="make" +build_dir="build-Debug-RPi" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x diff --git a/cmake/scripts/rpi/make-release-cfg.sh b/cmake/scripts/rpi/make-release-cfg.sh new file mode 100755 index 0000000..56b4873 --- /dev/null +++ b/cmake/scripts/rpi/make-release-cfg.sh @@ -0,0 +1,33 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/raspberrypi" +build_generator="" +build_dir="build-Release-RPi" +if [ "${OS}" = "Windows_NT" ]; then + build_generator="MinGW Makefiles" +# Could be other OS but this works for now. +else + build_generator="Unix Makefiles" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "release" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x diff --git a/cmake/scripts/rpi/ninja-debug-cfg.sh b/cmake/scripts/rpi/ninja-debug-cfg.sh new file mode 100755 index 0000000..13096fd --- /dev/null +++ b/cmake/scripts/rpi/ninja-debug-cfg.sh @@ -0,0 +1,34 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + +os_fsfw="linux" +tgt_bsp="arm/raspberrypi" +build_generator="ninja" +build_dir="build-Debug-RPi" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x + diff --git a/cmake/scripts/rpi/rpi_path_helper.sh b/cmake/scripts/rpi/rpi_path_helper.sh new file mode 100644 index 0000000..9504428 --- /dev/null +++ b/cmake/scripts/rpi/rpi_path_helper.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# This script can be used to set the path to the cross-compile toolchain +# A default path is set if the path is not supplied via command line +if [ $# -eq 1 ];then + export PATH=$PATH:"$1" +else + # TODO: make version configurable via shell argument + export PATH=$PATH:"/opt/cross-pi-gcc/bin" + export CROSS_COMPILE="arm-linux-gnueabihf" + export RASPBERRY_VERSION="4" + export RASPBIAN_ROOTFS="${HOME}/raspberrypi/rootfs" +fi + +# It is also recommended to set up a custom shell script to perform the +# sysroot synchronization so that any software is built with the library and +# headers of the Raspberry Pi. This can for example be dome with the rsync +# command. +# The following command can be used, and the local +# need to be set accordingly. + +# rsync -vR --progress -rl --delete-after --safe-links pi@:/{lib,usr,opt/vc/lib} + +# It is recommended to use $HOME/raspberrypi/rootfs as the rootfs path, +# so the default RASPBIAN_ROOTFS variable set in the CMakeLists.txt is correct. diff --git a/cmake/scripts/rpi/rpi_path_helper_win.sh b/cmake/scripts/rpi/rpi_path_helper_win.sh new file mode 100644 index 0000000..2b590e9 --- /dev/null +++ b/cmake/scripts/rpi/rpi_path_helper_win.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# This script can be used to set the path to the cross-compile toolchain +# A default path is set if the path is not supplied via command line +if [ $# -eq 1 ];then + export PATH=$PATH:"$1" +else + # TODO: make version configurable via shell argument + export PATH=$PATH:"/c/SysGCC/raspberry/bin" + export CROSS_COMPILE="arm-linux-gnueabihf" + export RASPBERRY_VERSION="4" + export RASPBIAN_ROOTFS="/c/Users//raspberrypi/rootfs" +fi + +# It is also recommended to set up a custom shell script to perform the +# sysroot synchronization so that any software is built with the library and +# headers of the Raspberry Pi. This can for example be dome with the rsync +# command. +# The following command can be used, and the local +# need to be set accordingly. + +# rsync -vR --progress -rl --delete-after --safe-links pi@:/{lib,usr,opt/vc/lib} + diff --git a/cmake/scripts/te0720-1cfa/make-debug-cfg.sh b/cmake/scripts/te0720-1cfa/make-debug-cfg.sh new file mode 100644 index 0000000..46008c4 --- /dev/null +++ b/cmake/scripts/te0720-1cfa/make-debug-cfg.sh @@ -0,0 +1,34 @@ +#!/bin/sh +counter=0 +cfg_script_name="cmake-build-cfg.py" +while [ ${counter} -lt 5 ] +do + cd .. + if [ -f ${cfg_script_name} ];then + break + fi + counter=$((counter=counter + 1)) +done + +if [ "${counter}" -ge 5 ];then + echo "${cfg_script_name} not found in upper directories!" + exit 1 +fi + + +os_fsfw="linux" +tgt_bsp="arm/te0720-1cfa" +build_generator="make" +build_dir="build-Debug-te0720-1cfa" +if [ "${OS}" = "Windows_NT" ]; then + python="py" +# Could be other OS but this works for now. +else + python="python3" +fi + +echo "Running command (without the leading +):" +set -x # Print command +${python} ${cfg_script_name} -o "${os_fsfw}" -g "${build_generator}" -b "debug" -t "${tgt_bsp}" \ + -l"${build_dir}" +# set +x diff --git a/cmake/scripts/te0720-1cfa/win-env-te0720-1cfa.sh b/cmake/scripts/te0720-1cfa/win-env-te0720-1cfa.sh new file mode 100644 index 0000000..e77de4c --- /dev/null +++ b/cmake/scripts/te0720-1cfa/win-env-te0720-1cfa.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Run with: source q7s-env-win-sh [OPTIONS] +function help () { + echo "source q7s-env-win-sh [options] -t|--toolchain= -s|--sysroot=" +} + +TOOLCHAIN_PATH="/c/Xilinx/Vitis/2019.2/gnu/aarch32/nt/gcc-arm-linux-gnueabi/bin" +SYSROOT="/c/Users/${USER}/eive-software/sysroots-petalinux-2019-2/cortexa9t2hf-neon-xilinx-linux-gnueabi" + +for i in "$@"; do + case $i in + -t=*|--toolchain=*) + TOOLCHAIN_PATH="${i#*=}" + shift + ;; + -s=*|--sysroot=*) + SYSROOT="${i#*=}" + shift + ;; + -h|--help) + help + shift + ;; + -*|--*) + echo "Unknown option $i" + help + return + ;; + *) + ;; + esac +done + +if [ -d "$TOOLCHAIN_PATH" ]; then + export PATH=$PATH:"/c/Xilinx/Vitis/2019.2/gnu/aarch32/nt/gcc-arm-linux-gnueabi/bin" + export CROSS_COMPILE="arm-linux-gnueabihf" + echo "Set toolchain path to /c/Xilinx/Vitis/2019.2/gnu/aarch32/nt/gcc-arm-linux-gnueabi/bin" +else + echo "Toolchain path $TOOLCHAIN_PATH does not exist" + return +fi + +if [ -d "$SYSROOT" ]; then + export ZYNQ_7020_SYSROOT=$SYSROOT + echo "Set sysroot path to $SYSROOT" +else + echo "Sysroot path $SYSROOT does not exist" + return +fi \ No newline at end of file diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt new file mode 100644 index 0000000..9040988 --- /dev/null +++ b/common/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(config) diff --git a/common/config/CMakeLists.txt b/common/config/CMakeLists.txt new file mode 100644 index 0000000..ca29622 --- /dev/null +++ b/common/config/CMakeLists.txt @@ -0,0 +1,3 @@ +target_include_directories(${OBSW_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_sources(${OBSW_NAME} PRIVATE commonConfig.cpp) diff --git a/common/config/ccsdsConfig.h b/common/config/ccsdsConfig.h new file mode 100644 index 0000000..9736ae5 --- /dev/null +++ b/common/config/ccsdsConfig.h @@ -0,0 +1,8 @@ +#ifndef COMMON_CONFIG_CCSDSCONFIG_H_ +#define COMMON_CONFIG_CCSDSCONFIG_H_ + +namespace ccsds { +enum { VC0, VC1, VC2, VC3 }; +} + +#endif /* COMMON_CONFIG_CCSDSCONFIG_H_ */ diff --git a/common/config/commonConfig.cpp b/common/config/commonConfig.cpp new file mode 100644 index 0000000..cdc4e23 --- /dev/null +++ b/common/config/commonConfig.cpp @@ -0,0 +1,11 @@ +#include "commonConfig.h" + +#include "eive/definitions.h" +#include "fsfw/tmtcpacket/ccsds/defs.h" + +const fsfw::Version common::OBSW_VERSION{OBSW_VERSION_MAJOR, OBSW_VERSION_MINOR, + OBSW_VERSION_REVISION, OBSW_VERSION_CST_GIT_SHA1}; +const uint16_t common::PUS_PACKET_ID = + ccsds::getTcSpacePacketIdFromApid(config::EIVE_PUS_APID, true); +const uint16_t common::CFDP_PACKET_ID = + ccsds::getTcSpacePacketIdFromApid(config::EIVE_CFDP_APID, false); diff --git a/common/config/commonConfig.h.in b/common/config/commonConfig.h.in new file mode 100644 index 0000000..96dc948 --- /dev/null +++ b/common/config/commonConfig.h.in @@ -0,0 +1,43 @@ +#ifndef COMMON_CONFIG_COMMONCONFIG_H_ +#define COMMON_CONFIG_COMMONCONFIG_H_ + +#include +#include "fsfw/version.h" + +#cmakedefine RELEASE_BUILD + +#cmakedefine RASPBERRY_PI +#cmakedefine XIPHOS_Q7S +#cmakedefine BEAGLEBONEBLACK +#cmakedefine EGSE +#cmakedefine TE0720_1CFA + +/* These defines should be disabled for mission code but are useful for +debugging. */ +#define OBSW_VERBOSE_LEVEL 1 + +#define OBSW_ADD_LWGPS_TEST 0 + +// Disable this for mission code. It allows exchanging TMTC packets via the Ethernet port +#define OBSW_ADD_TCPIP_SERVERS 1 + +#define OBSW_ADD_CFDP_COMPONENTS 1 + +namespace common { + +static constexpr uint8_t OBSW_VERSION_MAJOR = @OBSW_VERSION_MAJOR@; +static constexpr uint8_t OBSW_VERSION_MINOR = @OBSW_VERSION_MINOR@; +static constexpr uint8_t OBSW_VERSION_REVISION = @OBSW_VERSION_REVISION@; +// CST: Commits since tag +static const char OBSW_VERSION_CST_GIT_SHA1[] = "@OBSW_VERSION_CST_GIT_SHA1@"; + + +static constexpr uint32_t OBSW_MAX_SCHEDULED_TCS = @OBSW_MAX_SCHEDULED_TCS@; + +extern const fsfw::Version OBSW_VERSION; + +extern const uint16_t PUS_PACKET_ID; +extern const uint16_t CFDP_PACKET_ID; +} + +#endif /* COMMON_CONFIG_COMMONCONFIG_H_ */ diff --git a/common/config/devConf.h b/common/config/devConf.h new file mode 100644 index 0000000..de33cf4 --- /dev/null +++ b/common/config/devConf.h @@ -0,0 +1,78 @@ +#ifndef COMMON_CONFIG_DEVCONF_H_ +#define COMMON_CONFIG_DEVCONF_H_ + +#include + +#include + +#include "commonConfig.h" +#include "fsfw/timemanager/clockDefinitions.h" +#include "fsfw_hal/linux/serial/SerialCookie.h" +#include "fsfw_hal/linux/spi/spiDefinitions.h" + +/** + * SPI configuration will be contained here to let the device handlers remain independent + * of SPI specific properties. + */ +namespace spi { + +// Default values, changing them is not supported for now +static constexpr uint32_t DEFAULT_LIS3_SPEED = 976'000; +static constexpr uint32_t LIS3_TRANSITION_DELAY = 5000; +static constexpr spi::SpiModes DEFAULT_LIS3_MODE = spi::SpiModes::MODE_3; + +static constexpr uint32_t DEFAULT_RM3100_SPEED = 976'000; +static constexpr uint32_t RM3100_TRANSITION_DELAY = 5000; +static constexpr spi::SpiModes DEFAULT_RM3100_MODE = spi::SpiModes::MODE_3; + +static constexpr uint32_t DEFAULT_L3G_SPEED = 976'000; +static constexpr uint32_t L3G_TRANSITION_DELAY = 5000; +static constexpr spi::SpiModes DEFAULT_L3G_MODE = spi::SpiModes::MODE_3; + +/** + * Some MAX1227 could not be reached with frequencies around 4 MHz. Maybe this is caused by + * the decoder and buffer circuits. Thus frequency is here defined to 1 MHz. + */ +static const uint32_t SUS_MAX1227_SPI_FREQ = 976'000; +static constexpr spi::SpiModes SUS_MAX_1227_MODE = spi::SpiModes::MODE_3; + +static constexpr dur_millis_t RAD_SENSOR_CS_TIMEOUT = 120; +static constexpr uint32_t DEFAULT_MAX_1227_SPEED = 976'000; +static constexpr spi::SpiModes DEFAULT_MAX_1227_MODE = spi::SpiModes::MODE_3; + +static constexpr uint32_t PL_PCDU_MAX_1227_SPEED = 976'000; + +static constexpr uint32_t DEFAULT_ADIS16507_SPEED = 976'000; +static constexpr spi::SpiModes DEFAULT_ADIS16507_MODE = spi::SpiModes::MODE_3; + +static constexpr uint32_t RW_SPEED = 300'000; +static constexpr spi::SpiModes RW_MODE = spi::SpiModes::MODE_0; + +#ifdef RELEASE_BUILD +static constexpr uint8_t CS_FACTOR = 1; +#else +static constexpr uint8_t CS_FACTOR = 3; +#endif + +static constexpr dur_millis_t RTD_CS_TIMEOUT = 50 * CS_FACTOR; +static constexpr uint32_t RTD_SPEED = 2'000'000; +static constexpr spi::SpiModes RTD_MODE = spi::SpiModes::MODE_3; + +static constexpr dur_millis_t SUS_CS_TIMEOUT = 50 * CS_FACTOR; +static constexpr dur_millis_t ACS_BOARD_CS_TIMEOUT = 50 * CS_FACTOR; + +} // namespace spi + +namespace serial { + +static constexpr size_t HYPERION_GPS_REPLY_MAX_BUFFER = 1024; +static constexpr UartBaudRate SYRLINKS_BAUD = UartBaudRate::RATE_38400; +static constexpr UartBaudRate SCEX_BAUD = UartBaudRate::RATE_115200; +static constexpr UartBaudRate GNSS_BAUD = UartBaudRate::RATE_9600; +static constexpr UartBaudRate PLOC_MPSOC_BAUD = UartBaudRate::RATE_115200; +static constexpr UartBaudRate PLOC_SUPV_BAUD = UartBaudRate::RATE_921600; +static constexpr UartBaudRate STAR_TRACKER_BAUD = UartBaudRate::RATE_921600; + +} // namespace serial + +#endif /* COMMON_CONFIG_DEVCONF_H_ */ diff --git a/common/config/devices/addresses.h b/common/config/devices/addresses.h new file mode 100644 index 0000000..e159857 --- /dev/null +++ b/common/config/devices/addresses.h @@ -0,0 +1,90 @@ +#ifndef FSFWCONFIG_DEVICES_ADDRESSES_H_ +#define FSFWCONFIG_DEVICES_ADDRESSES_H_ + +#include + +#include + +#include "objects/systemObjectList.h" + +namespace addresses { +/* Logical addresses have uint32_t datatype */ +enum LogicAddress : address_t { + PCDU, + + MGM_0_LIS3 = objects::MGM_0_LIS3_HANDLER, + MGM_1_RM3100 = objects::MGM_1_RM3100_HANDLER, + MGM_2_LIS3 = objects::MGM_2_LIS3_HANDLER, + MGM_3_RM3100 = objects::MGM_3_RM3100_HANDLER, + + GYRO_0_ADIS = objects::GYRO_0_ADIS_HANDLER, + GYRO_1_L3G = objects::GYRO_1_L3G_HANDLER, + GYRO_2_ADIS = objects::GYRO_2_ADIS_HANDLER, + GYRO_3_L3G = objects::GYRO_3_L3G_HANDLER, + + RAD_SENSOR = objects::RAD_SENSOR, + + SUS_0 = objects::SUS_0_N_LOC_XFYFZM_PT_XF, + SUS_1 = objects::SUS_1_N_LOC_XBYFZM_PT_XB, + SUS_2 = objects::SUS_2_N_LOC_XFYBZB_PT_YB, + SUS_3 = objects::SUS_3_N_LOC_XFYBZF_PT_YF, + SUS_4 = objects::SUS_4_N_LOC_XMYFZF_PT_ZF, + SUS_5 = objects::SUS_5_N_LOC_XFYMZB_PT_ZB, + SUS_6 = objects::SUS_6_R_LOC_XFYBZM_PT_XF, + SUS_7 = objects::SUS_7_R_LOC_XBYBZM_PT_XB, + SUS_8 = objects::SUS_8_R_LOC_XBYBZB_PT_YB, + SUS_9 = objects::SUS_9_R_LOC_XBYBZB_PT_YF, + SUS_10 = objects::SUS_10_N_LOC_XMYBZF_PT_ZF, + SUS_11 = objects::SUS_11_R_LOC_XBYMZB_PT_ZB, + + /* Dummy and Test Addresses */ + DUMMY_ECHO = 129, + DUMMY_GPS0 = 130, + DUMMY_GPS1 = 131, +}; + +enum I2cAddress : address_t { + BPX_BATTERY = 0x07, + IMTQ = 0x10, + TMP1075_TCS_0 = 0x48, + TMP1075_TCS_1 = 0x49, + TMP1075_PLPCDU_0 = 0x4A, + TMP1075_PLPCDU_1 = 0x4B, + TMP1075_IF_BOARD = 0x4C, +}; + +enum spiAddresses : address_t { + RTD_IC_0, + RTD_IC_1, + RTD_IC_2, + RTD_IC_3, + RTD_IC_4, + RTD_IC_5, + RTD_IC_6, + RTD_IC_7, + RTD_IC_8, + RTD_IC_9, + RTD_IC_10, + RTD_IC_11, + RTD_IC_12, + RTD_IC_13, + RTD_IC_14, + RTD_IC_15, + RW1, + RW2, + RW3, + RW4, + PLPCDU_ADC +}; + +/* Addresses of devices supporting the CSP protocol */ +enum cspAddresses : uint8_t { + P60DOCK = 4, + ACU = 2, + PDU1 = 3, + /* PDU2 occupies X4 slot of P60Dock */ + PDU2 = 6 +}; +} // namespace addresses + +#endif /* FSFWCONFIG_DEVICES_ADDRESSES_H_ */ diff --git a/common/config/devices/gpioIds.h b/common/config/devices/gpioIds.h new file mode 100644 index 0000000..2cab2c3 --- /dev/null +++ b/common/config/devices/gpioIds.h @@ -0,0 +1,128 @@ +#ifndef FSFWCONFIG_DEVICES_GPIOIDS_H_ +#define FSFWCONFIG_DEVICES_GPIOIDS_H_ + +#include + +namespace gpioIds { +enum gpioId_t { + HEATER_0, + HEATER_1, + HEATER_2, + HEATER_3, + HEATER_4, + HEATER_5, + HEATER_6, + HEATER_7, + DEPLSA1, + DEPLSA2, + + MGM_0_LIS3_CS, + MGM_1_RM3100_CS, + GYRO_0_ADIS_CS, + GYRO_1_L3G_CS, + GYRO_2_ADIS_CS, + GYRO_3_L3G_CS, + MGM_2_LIS3_CS, + MGM_3_RM3100_CS, + + GNSS_0_NRESET, + GNSS_1_NRESET, + GNSS_0_ENABLE, + GNSS_1_ENABLE, + GNSS_SELECT, + + GYRO_0_ENABLE, + GYRO_2_ENABLE, + + TEST_ID_0, + TEST_ID_1, + + RTD_IC_0, + RTD_IC_1, + RTD_IC_2, + RTD_IC_3, + RTD_IC_4, + RTD_IC_5, + RTD_IC_6, + RTD_IC_7, + RTD_IC_8, + RTD_IC_9, + RTD_IC_10, + RTD_IC_11, + RTD_IC_12, + RTD_IC_13, + RTD_IC_14, + RTD_IC_15, + + CS_SUS_0, + CS_SUS_1, + CS_SUS_2, + CS_SUS_3, + CS_SUS_4, + CS_SUS_5, + CS_SUS_6, + CS_SUS_7, + CS_SUS_8, + CS_SUS_9, + CS_SUS_10, + CS_SUS_11, + + SPI_MUX_BIT_0, + SPI_MUX_BIT_1, + SPI_MUX_BIT_2, + SPI_MUX_BIT_3, + SPI_MUX_BIT_4, + SPI_MUX_BIT_5, + + CS_RAD_SENSOR, + ENABLE_RADFET, + + PL_I2C_ARESETN, + + PAPB_BUSY_N, + PAPB_EMPTY, + + EN_RW1, + EN_RW2, + EN_RW3, + EN_RW4, + + CS_RW1, + CS_RW2, + CS_RW3, + CS_RW4, + + EN_RW_CS, + + SPI_MUX, + VC0_PAPB_EMPTY, + VC1_PAPB_EMPTY, + VC2_PAPB_EMPTY, + VC3_PAPB_EMPTY, + PTME_RESETN, + + PDEC_RESET, + + RS485_EN_TX_DATA, + RS485_EN_TX_CLOCK, + RS485_EN_RX_DATA, + RS485_EN_RX_CLOCK, + + BIT_RATE_SEL, + + PLPCDU_ENB_VBAT0, + PLPCDU_ENB_VBAT1, + PLPCDU_ENB_DRO, + PLPCDU_ENB_X8, + PLPCDU_ENB_TX, + PLPCDU_ENB_HPA, + PLPCDU_ENB_MPA, + PLPCDU_ADC_CS, + + ENABLE_MPSOC_UART, + ENABLE_SUPV_UART + +}; +} + +#endif /* FSFWCONFIG_DEVICES_GPIOIDS_H_ */ diff --git a/common/config/devices/powerSwitcherList.h b/common/config/devices/powerSwitcherList.h new file mode 100644 index 0000000..940d558 --- /dev/null +++ b/common/config/devices/powerSwitcherList.h @@ -0,0 +1,6 @@ +#ifndef FSFWCONFIG_DEVICES_POWERSWITCHERLIST_H_ +#define FSFWCONFIG_DEVICES_POWERSWITCHERLIST_H_ + +#include + +#endif /* FSFWCONFIG_DEVICES_POWERSWITCHERLIST_H_ */ diff --git a/common/config/eive/definitions.h b/common/config/eive/definitions.h new file mode 100644 index 0000000..4c96c3a --- /dev/null +++ b/common/config/eive/definitions.h @@ -0,0 +1,135 @@ +#ifndef COMMON_CONFIG_DEFINITIONS_H_ +#define COMMON_CONFIG_DEFINITIONS_H_ + +#include + +namespace config { + +static constexpr char SD_0_MOUNT_POINT[] = "/mnt/sd0"; +static constexpr char SD_1_MOUNT_POINT[] = "/mnt/sd1"; + +static constexpr char OBSW_UPDATE_ARCHIVE_FILE_NAME[] = "eive-sw-update.tar.xz"; +static constexpr char STRIPPED_OBSW_BINARY_FILE_NAME[] = "eive-obsw-stripped"; +static constexpr char OBSW_VERSION_FILE_NAME[] = "obsw_version.txt"; +static constexpr char PUS_SEQUENCE_COUNT_FILE[] = "pus-sequence-count.txt"; +static constexpr char CFDP_SEQUENCE_COUNT_FILE[] = "cfdp-sequence-count.txt"; + +static constexpr char OBSW_PATH[] = "/usr/bin/eive-obsw"; +static constexpr char OBSW_VERSION_FILE_PATH[] = "/usr/share/eive-obsw/obsw_version.txt"; + +// ISO8601 timestamp. +static constexpr char FILE_DATE_FORMAT[] = "%FT%H%M%SZ"; + +// Leap Seconds as of 2024-03-04 +static constexpr uint16_t LEAP_SECONDS = 37; + +static constexpr uint16_t EIVE_PUS_APID = 0x65; +static constexpr uint16_t EIVE_CFDP_APID = 0x66; +static constexpr uint16_t EIVE_LOCAL_CFDP_ENTITY_ID = EIVE_CFDP_APID; +static constexpr uint16_t EIVE_GROUND_CFDP_ENTITY_ID = 1; + +static constexpr uint32_t PL_PCDU_TRANSITION_TIMEOUT_MS = 20 * 60 * 1000; +static constexpr uint32_t LONGEST_MODE_TIMEOUT_SECONDS = PL_PCDU_TRANSITION_TIMEOUT_MS / 1000; + +/* Add mission configuration flags here */ +static constexpr uint32_t OBSW_FILESYSTEM_HANDLER_QUEUE_SIZE = 50; +static constexpr uint32_t PLOC_UPDATER_QUEUE_SIZE = 50; +static constexpr uint32_t STR_IMG_HELPER_QUEUE_SIZE = 50; + +static constexpr uint8_t LIVE_TM = 0; + +static constexpr size_t MAX_SPACEPACKET_TC_SIZE = 2048; + +/* Limits for filename and path checks */ +static constexpr uint32_t MAX_PATH_SIZE = 200; +static constexpr uint32_t MAX_FILENAME_SIZE = 100; + +static constexpr uint32_t SA_DEPL_INIT_BUFFER_SECS = 120; +// Burn time for autonomous deployment +static constexpr uint32_t SA_DEPL_BURN_TIME_SECS = 180; +static constexpr uint32_t SA_DEPL_WAIT_TIME_SECS = 45 * 60; +// HW constraints (current limit) mean that the GPIO channels need to be switched on in alternation +static constexpr uint32_t LEGACY_SA_DEPL_CHANNEL_ALTERNATION_INTERVAL_SECS = 5; +// Maximum allowed burn time allowed by the software. +static constexpr uint32_t SA_DEPL_MAX_BURN_TIME = 180; + +static constexpr size_t CFDP_MAX_FILE_SEGMENT_LEN = 900; + +static constexpr uint32_t CCSDS_HANDLER_QUEUE_SIZE = 50; +static constexpr uint8_t NUMBER_OF_VIRTUAL_CHANNELS = 4; +static constexpr uint32_t VC0_LIVE_TM_QUEUE_SIZE = 300; +// There are three individual log stores! +static constexpr uint32_t MISC_STORE_QUEUE_SIZE = 200; +static constexpr uint32_t OK_STORE_QUEUE_SIZE = 350; +static constexpr uint32_t NOK_STORE_QUEUE_SIZE = 350; +static constexpr uint32_t HK_STORE_QUEUE_SIZE = 300; +static constexpr uint32_t CFDP_STORE_QUEUE_SIZE = 300; + +static constexpr uint32_t LIVE_CHANNEL_NORMAL_QUEUE_SIZE = 250; +static constexpr uint32_t LIVE_CHANNEL_CFDP_QUEUE_SIZE = 350; + +static constexpr uint32_t CFDP_MAX_FSM_CALL_COUNT_SRC_HANDLER = 10; +static constexpr uint32_t CFDP_MAX_FSM_CALL_COUNT_DEST_HANDLER = 300; +static constexpr uint32_t CFDP_SHORT_DELAY_MS = 40; +static constexpr uint32_t CFDP_REGULAR_DELAY_MS = 200; + +static constexpr uint32_t MAX_PUS_FUNNEL_QUEUE_DEPTH = 100; +static constexpr uint32_t MAX_CFDP_FUNNEL_QUEUE_DEPTH = LIVE_CHANNEL_CFDP_QUEUE_SIZE; +static constexpr uint32_t VERIFICATION_SERVICE_QUEUE_DEPTH = 120; +static constexpr uint32_t HK_SERVICE_QUEUE_DEPTH = 60; +static constexpr uint32_t ACTION_SERVICE_QUEUE_DEPTH = 60; + +static constexpr uint32_t UDP_MAX_STORED_CMDS = 200; +static constexpr uint32_t UDP_MSG_QUEUE_DEPTH = UDP_MAX_STORED_CMDS; +static constexpr uint32_t TCP_MAX_STORED_CMDS = 350; +static constexpr uint32_t TCP_MSG_QUEUE_DEPTH = TCP_MAX_STORED_CMDS; +static constexpr uint32_t TCP_MAX_NUMBER_TMS_SENT_PER_CYCLE = TCP_MSG_QUEUE_DEPTH; + +namespace spiSched { + +static constexpr uint32_t SCHED_BLOCK_1_SUS_READ_MS = 15; +static constexpr uint32_t SCHED_BLOCK_2_SENSOR_READ_MS = 30; +static constexpr uint32_t SCHED_BLOCK_3_READ_IMTQ_MGM_MS = 43; +static constexpr uint32_t SCHED_BLOCK_4_ACS_CTRL_MS = 45; +static constexpr uint32_t SCHED_BLOCK_5_ACTUATOR_MS = 55; +static constexpr uint32_t SCHED_BLOCK_6_IMTQ_BLOCK_2_MS = 105; +static constexpr uint32_t SCHED_BLOCK_RTD = 150; +static constexpr uint32_t SCHED_BLOCK_7_RW_READ_MS = 300; +static constexpr uint32_t SCHED_BLOCK_8_PLPCDU_MS = 320; +static constexpr uint32_t SCHED_BLOCK_9_RAD_SENS_MS = 340; +static constexpr uint32_t SCHED_BLOCK_10_PWR_CTRL_MS = 350; + +// 15 ms for FM +static constexpr float SCHED_BLOCK_1_PERIOD = static_cast(SCHED_BLOCK_1_SUS_READ_MS) / 400.0; +static constexpr float SCHED_BLOCK_2_PERIOD = + static_cast(SCHED_BLOCK_2_SENSOR_READ_MS) / 400.0; +static constexpr float SCHED_BLOCK_3_PERIOD = + static_cast(SCHED_BLOCK_3_READ_IMTQ_MGM_MS) / 400.0; +static constexpr float SCHED_BLOCK_4_PERIOD = static_cast(SCHED_BLOCK_4_ACS_CTRL_MS) / 400.0; +static constexpr float SCHED_BLOCK_5_PERIOD = static_cast(SCHED_BLOCK_5_ACTUATOR_MS) / 400.0; +static constexpr float SCHED_BLOCK_6_PERIOD = + static_cast(SCHED_BLOCK_6_IMTQ_BLOCK_2_MS) / 400.0; +static constexpr float SCHED_BLOCK_RTD_PERIOD = static_cast(SCHED_BLOCK_RTD) / 400.0; +static constexpr float SCHED_BLOCK_7_PERIOD = static_cast(SCHED_BLOCK_7_RW_READ_MS) / 400.0; +static constexpr float SCHED_BLOCK_8_PERIOD = static_cast(SCHED_BLOCK_8_PLPCDU_MS) / 400.0; +static constexpr float SCHED_BLOCK_9_PERIOD = static_cast(SCHED_BLOCK_9_RAD_SENS_MS) / 400.0; +static constexpr float SCHED_BLOCK_10_PERIOD = + static_cast(SCHED_BLOCK_10_PWR_CTRL_MS) / 400.0; + +} // namespace spiSched + +namespace pdec { + +// Pre FW v6.0.0 +static constexpr uint32_t PDEC_CONFIG_BASE_ADDR_LEGACY = 0x24000000; +static constexpr uint32_t PDEC_RAM_ADDR_LEGACY = 0x26000000; + +// Post FW v6.0.0 +static constexpr uint32_t PDEC_CONFIG_BASE_ADDR = 0x4000000; +static constexpr uint32_t PDEC_RAM_ADDR = 0x7000000; + +} // namespace pdec + +} // namespace config + +#endif /* COMMON_CONFIG_DEFINITIONS_H_ */ diff --git a/common/config/eive/eventSubsystemIds.h b/common/config/eive/eventSubsystemIds.h new file mode 100644 index 0000000..370a00b --- /dev/null +++ b/common/config/eive/eventSubsystemIds.h @@ -0,0 +1,50 @@ +#ifndef COMMON_CONFIG_COMMONSUBSYSTEMIDS_H_ +#define COMMON_CONFIG_COMMONSUBSYSTEMIDS_H_ + +#include + +namespace SUBSYSTEM_ID { + +enum : uint8_t { + COMMON_SUBSYSTEM_ID_START = FW_SUBSYSTEM_ID_RANGE, + ACS_SUBSYSTEM = 112, + PCDU_HANDLER = 113, + HEATER_HANDLER = 114, + SA_DEPL_HANDLER = 115, + PLOC_MPSOC_HANDLER = 116, + IMTQ_HANDLER = 117, + RW_HANDLER = 118, + STR_HANDLER = 119, + PLOC_SUPERVISOR_HANDLER = 120, + FILE_SYSTEM = 121, + PLOC_UPDATER = 122, + PLOC_MEMORY_DUMPER = 123, + PDEC_HANDLER = 124, + STR_HELPER = 125, + PLOC_MPSOC_HELPER = 126, + PL_PCDU_HANDLER = 127, + ACS_BOARD_ASS = 128, + SUS_BOARD_ASS = 129, + TCS_BOARD_ASS = 130, + GPS_HANDLER = 131, + P60_DOCK_HANDLER = 132, + PDU1_HANDLER = 133, + PDU2_HANDLER = 134, + ACU_HANDLER = 135, + PLOC_SUPV_HELPER = 136, + SYRLINKS = 137, + SCEX_HANDLER = 138, + CONFIGHANDLER = 139, + CORE = 140, + TCS_CONTROLLER = 141, + COM_SUBSYSTEM = 142, + PERSISTENT_TM_STORE = 143, + SYRLINKS_COM = 144, + SUS_HANDLER = 145, + CFDP_APP = 146, + COMMON_SUBSYSTEM_ID_END + +}; +} + +#endif /* COMMON_CONFIG_COMMONSUBSYSTEMIDS_H_ */ diff --git a/common/config/eive/objects.h b/common/config/eive/objects.h new file mode 100644 index 0000000..797bea1 --- /dev/null +++ b/common/config/eive/objects.h @@ -0,0 +1,188 @@ +#ifndef COMMON_CONFIG_COMMONOBJECTS_H_ +#define COMMON_CONFIG_COMMONOBJECTS_H_ + +#include + +namespace objects { +enum commonObjects : uint32_t { + /* First Byte 0x50-0x52 reserved for PUS Services **/ + CCSDS_PACKET_DISTRIBUTOR = 0x50000100, + PUS_PACKET_DISTRIBUTOR = 0x50000200, + TCP_TMTC_SERVER = 0x50000300, + UDP_TMTC_SERVER = 0x50000301, + TCP_TMTC_POLLING_TASK = 0x50000400, + UDP_TMTC_POLLING_TASK = 0x50000401, + FILE_SYSTEM_HANDLER = 0x50000500, + SDC_MANAGER = 0x50000550, + PTME = 0x50000600, + PDEC_HANDLER = 0x50000700, + CCSDS_HANDLER = 0x50000800, + + /* 0x49 ('I') for Communication Interfaces **/ + UART_COM_IF = 0x49030003, + SCEX_UART_READER = 0x49010006, + + /* 0x43 ('C') for Controllers */ + THERMAL_CONTROLLER = 0x43400001, + ACS_CONTROLLER = 0x43000002, + CORE_CONTROLLER = 0x43000003, + POWER_CONTROLLER = 0x43000004, + GLOBAL_JSON_CFG = 0x43000006, + XIPHOS_WDT = 0x43000007, + + /* 0x44 ('D') for device handlers */ + MGM_0_LIS3_HANDLER = 0x44120006, + MGM_1_RM3100_HANDLER = 0x44120107, + MGM_2_LIS3_HANDLER = 0x44120208, + MGM_3_RM3100_HANDLER = 0x44120309, + GYRO_0_ADIS_HANDLER = 0x44120010, + GYRO_1_L3G_HANDLER = 0x44120111, + GYRO_2_ADIS_HANDLER = 0x44120212, + GYRO_3_L3G_HANDLER = 0x44120313, + RW1 = 0x44120047, + RW2 = 0x44120148, + RW3 = 0x44120249, + RW4 = 0x44120350, + STAR_TRACKER = 0x44130001, + GPS_CONTROLLER = 0x44130045, + GPS_0_HEALTH_DEV = 0x44130046, + GPS_1_HEALTH_DEV = 0x44130047, + + IMTQ_POLLING = 0x44140013, + IMTQ_HANDLER = 0x44140014, + TMP1075_HANDLER_TCS_0 = 0x44420004, + TMP1075_HANDLER_TCS_1 = 0x44420005, + TMP1075_HANDLER_PLPCDU_0 = 0x44420006, + TMP1075_HANDLER_PLPCDU_1 = 0x44420007, + TMP1075_HANDLER_IF_BOARD = 0x44420008, + + PCDU_HANDLER = 0x442000A1, + P60DOCK_HANDLER = 0x44250000, + PDU1_HANDLER = 0x44250001, + PDU2_HANDLER = 0x44250002, + ACU_HANDLER = 0x44250003, + BPX_BATT_HANDLER = 0x44260000, + PLPCDU_HANDLER = 0x44300000, + RAD_SENSOR = 0x443200A5, + PLOC_UPDATER = 0x44330000, + PLOC_MEMORY_DUMPER = 0x44330001, + STR_COM_IF = 0x44330002, + PLOC_MPSOC_HELPER = 0x44330003, + AXI_PTME_CONFIG = 0x44330004, + PTME_CONFIG = 0x44330005, + PTME_VC0_LIVE_TM = 0x44330006, + PTME_VC1_LOG_TM = 0x44330007, + PTME_VC2_HK_TM = 0x44330008, + PTME_VC3_CFDP_TM = 0x44330009, + PLOC_MPSOC_HANDLER = 0x44330015, + PLOC_SUPERVISOR_HANDLER = 0x44330016, + PLOC_SUPERVISOR_HELPER = 0x44330017, + PLOC_MPSOC_COMMUNICATION = 0x44330018, + SCEX = 0x44330032, + SOLAR_ARRAY_DEPL_HANDLER = 0x444100A2, + HEATER_HANDLER = 0x444100A4, + + /** + * Not yet specified which pt1000 will measure which device/location in the satellite. + * Therefore object ids are named according to the IC naming of the RTDs in the schematic. + */ + RTD_0_IC3_PLOC_HEATSPREADER = 0x44420016, + RTD_1_IC4_PLOC_MISSIONBOARD = 0x44420017, + RTD_2_IC5_4K_CAMERA = 0x44420018, + RTD_3_IC6_DAC_HEATSPREADER = 0x44420019, + RTD_4_IC7_STARTRACKER = 0x44420020, + RTD_5_IC8_RW1_MX_MY = 0x44420021, + RTD_6_IC9_DRO = 0x44420022, + RTD_7_IC10_SCEX = 0x44420023, + RTD_8_IC11_X8 = 0x44420024, + RTD_9_IC12_HPA = 0x44420025, + RTD_10_IC13_PL_TX = 0x44420026, + RTD_11_IC14_MPA = 0x44420027, + RTD_12_IC15_ACU = 0x44420028, + RTD_13_IC16_PLPCDU_HEATSPREADER = 0x44420029, + RTD_14_IC17_TCS_BOARD = 0x44420030, + RTD_15_IC18_IMTQ = 0x44420031, + + // Name convention for SUS devices + // SUS___LOC_XYZ_PT_ + // LOC: Location + // PT: Pointing + // N/R: Nominal/Redundant + // F/M/B: Forward/Middle/Backwards + SUS_0_N_LOC_XFYFZM_PT_XF = 0x44120032, + SUS_6_R_LOC_XFYBZM_PT_XF = 0x44120038, + + SUS_1_N_LOC_XBYFZM_PT_XB = 0x44120033, + SUS_7_R_LOC_XBYBZM_PT_XB = 0x44120039, + + SUS_2_N_LOC_XFYBZB_PT_YB = 0x44120034, + SUS_8_R_LOC_XBYBZB_PT_YB = 0x44120040, + + SUS_3_N_LOC_XFYBZF_PT_YF = 0x44120035, + SUS_9_R_LOC_XBYBZB_PT_YF = 0x44120041, + + SUS_4_N_LOC_XMYFZF_PT_ZF = 0x44120036, + SUS_10_N_LOC_XMYBZF_PT_ZF = 0x44120042, + + SUS_5_N_LOC_XFYMZB_PT_ZB = 0x44120037, + SUS_11_R_LOC_XBYMZB_PT_ZB = 0x44120043, + + SYRLINKS_HANDLER = 0x445300A3, + SYRLINKS_COM_HANDLER = 0x445300A4, + + /* 0x49 ('I') for Communication Interfaces */ + ACS_BOARD_POLLING_TASK = 0x49060004, + RW_POLLING_TASK = 0x49060005, + SPI_RTD_COM_IF = 0x49060006, + SUS_POLLING_TASK = 0x49060007, + + // 0x60 for other stuff + HEATER_0_PLOC_PROC_BRD = 0x60000000, + HEATER_1_PCDU_BRD = 0x60000001, + HEATER_2_ACS_BRD = 0x60000002, + HEATER_3_OBC_BRD = 0x60000003, + HEATER_4_CAMERA = 0x60000004, + HEATER_5_STR = 0x60000005, + HEATER_6_DRO = 0x60000006, + HEATER_7_SYRLINKS = 0x60000007, + + // 0x73 ('s') for assemblies and system/subsystem components + ACS_BOARD_ASS = 0x73000001, + SUS_BOARD_ASS = 0x73000002, + TCS_BOARD_ASS = 0x73000003, + RW_ASSY = 0x73000004, + CAM_SWITCHER = 0x73000006, + SYRLINKS_ASSY = 0x73000007, + IMTQ_ASSY = 0x73000008, + STR_ASSY = 0x73000009, + EIVE_SYSTEM = 0x73010000, + ACS_SUBSYSTEM = 0x73010001, + PL_SUBSYSTEM = 0x73010002, + TCS_SUBSYSTEM = 0x73010003, + COM_SUBSYSTEM = 0x73010004, + EPS_SUBSYSTEM = 0x73010005, + + TM_FUNNEL = 0x73000100, + PUS_TM_FUNNEL = 0x73000101, + CFDP_TM_FUNNEL = 0x73000102, + CFDP_HANDLER = 0x73000205, + CFDP_DISTRIBUTOR = 0x73000206, + CFDP_FAULT_HANDLER = 0x73000207, + MISC_TM_STORE = 0x73020001, + OK_TM_STORE = 0x73020002, + NOT_OK_TM_STORE = 0x73020003, + HK_TM_STORE = 0x73020004, + CFDP_TM_STORE = 0x73030000, + + LIVE_TM_TASK = 0x73040000, + LOG_STORE_AND_TM_TASK = 0x73040001, + HK_STORE_AND_TM_TASK = 0x73040002, + CFDP_STORE_AND_TM_TASK = 0x73040003, + DOWNLINK_RAM_STORE = 0x73040004, + + // Other stuff + THERMAL_TEMP_INSERTER = 0x90000003, +}; +} + +#endif /* COMMON_CONFIG_COMMONOBJECTS_H_ */ diff --git a/common/config/eive/resultClassIds.h b/common/config/eive/resultClassIds.h new file mode 100644 index 0000000..8c30dd1 --- /dev/null +++ b/common/config/eive/resultClassIds.h @@ -0,0 +1,50 @@ +#ifndef COMMON_CONFIG_COMMONCLASSIDS_H_ +#define COMMON_CONFIG_COMMONCLASSIDS_H_ + +#include + +#include + +namespace CLASS_ID { +enum commonClassIds : uint8_t { + COMMON_CLASS_ID_START = FW_CLASS_ID_COUNT, + PCDU_HANDLER, // PCDU + HEATER_HANDLER, // HEATER + SYRLINKS_HANDLER, // SYRLINKS + IMTQ_HANDLER, // IMTQ + RW_HANDLER, // RWHA + STR_HANDLER, // STRH + DWLPWRON_CMD, // DWLPWRON + MPSOC_TM, // MPTM + PLOC_SUPERVISOR_HANDLER, // PLSV + PLOC_SUPV_HELPER, // PLSPVhLP + SUS_HANDLER, // SUSS + CCSDS_IP_CORE_BRIDGE, // IPCI + PTME, // PTME + PLOC_UPDATER, // PLUD + STR_HELPER, // STRHLP + GOM_SPACE_HANDLER, // GOMS + PLOC_MEMORY_DUMPER, // PLMEMDUMP + PDEC_HANDLER, // PDEC + CCSDS_HANDLER, // CCSDS + RATE_SETTER, // RS + ARCSEC_JSON_BASE, // JSONBASE + NVM_PARAM_BASE, // NVMB + FILE_SYSTEM_HELPER, // FSHLP + PLOC_MPSOC_HELPER, // PLMPHLP + SA_DEPL_HANDLER, // SADPL + MPSOC_RETURN_VALUES_IF, // MPSOCRTVIF + SUPV_RETURN_VALUES_IF, // SPVRTVIF + ACS_CTRL, // ACSCTRL + ACS_MEKF, // ACSMEKF + SD_CARD_MANAGER, // SDMA + LOCAL_PARAM_HANDLER, // LPH + PERSISTENT_TM_STORE, // PTM + TM_SINK, // TMS + VIRTUAL_CHANNEL, // VCS + PLOC_MPSOC_COM, // PLMPCOM + COMMON_CLASS_ID_END // [EXPORT] : [END] +}; +} + +#endif /* COMMON_CONFIG_COMMONCLASSIDS_H_ */ diff --git a/common/config/lwgps_opts.h b/common/config/lwgps_opts.h new file mode 100644 index 0000000..2be39f1 --- /dev/null +++ b/common/config/lwgps_opts.h @@ -0,0 +1,48 @@ +/** + * \file lwgps_opts_template.h + * \brief LwGPS configuration file + */ + +/* + * Copyright (c) 2020 Tilen MAJERLE + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * This file is part of LwGPS - Lightweight GPS NMEA parser library. + * + * Author: Tilen MAJERLE + * Version: v2.1.0 + */ +#ifndef LWGPS_HDR_OPTS_H +#define LWGPS_HDR_OPTS_H + +/* Rename this file to "lwgps_opts.h" for your application */ + +/* + * Open "include/lwgps/lwgps_opt.h" and + * copy & replace here settings you want to change values + */ + +#ifndef __DOXYGEN__ +#define __DOXYGEN__ 0 +#endif + +#endif /* LWGPS_HDR_OPTS_H */ diff --git a/common/config/tmtc/pusIds.h b/common/config/tmtc/pusIds.h new file mode 100644 index 0000000..b44d7e2 --- /dev/null +++ b/common/config/tmtc/pusIds.h @@ -0,0 +1,25 @@ +#ifndef COMMON_CONFIG_TMTC_PUSIDS_H_ +#define COMMON_CONFIG_TMTC_PUSIDS_H_ + +namespace pus { +enum Ids { + PUS_SERVICE_1 = 1, + PUS_SERVICE_2 = 2, + PUS_SERVICE_3 = 3, + PUS_SERVICE_3_PSB = 3, + PUS_SERVICE_5 = 5, + PUS_SERVICE_6 = 6, + PUS_SERVICE_8 = 8, + PUS_SERVICE_9 = 9, + PUS_SERVICE_11 = 11, + PUS_SERVICE_15 = 15, + PUS_SERVICE_17 = 17, + PUS_SERVICE_19 = 19, + PUS_SERVICE_20 = 20, + PUS_SERVICE_23 = 23, + PUS_SERVICE_200 = 200, + PUS_SERVICE_201 = 201, +}; +}; + +#endif /* COMMON_CONFIG_TMTC_PUSIDS_H_ */ diff --git a/doc/XSC-1542-6025-i_Q7RevB_User_Manual.pdf b/doc/XSC-1542-6025-i_Q7RevB_User_Manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0ab8798535e7082adae4d17382b76d4a49dd8b70 GIT binary patch literal 1573503 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%0A&Y0zMlCr*cSnR!Kgfl`mZtg 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

edfo*nZyceW%mZvH=QayT>%&RQ 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

!b8jr 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 z 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

W#wo;*IEjHYRd%pRYY08tUFX{upa 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