From 0a7021a5532a48d9f5f9798bf1c330ad2fa287b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Mon, 17 Aug 2026 20:15:00 +0200 Subject: [PATCH 1/7] feat/logger: added base data logger --- CMakeLists.txt | 2 + .../DataLogger/Tests/test_base_logger.cpp | 245 ++++++++++++++++++ ThirdParty/DataLogger/Tests/test_helper.hpp | 128 +++++++++ ThirdParty/DataLogger/base.hpp | 68 +++++ ThirdParty/DataLogger/types.hpp | 17 ++ 5 files changed, 460 insertions(+) create mode 100644 ThirdParty/DataLogger/Tests/test_base_logger.cpp create mode 100644 ThirdParty/DataLogger/Tests/test_helper.hpp create mode 100644 ThirdParty/DataLogger/base.hpp create mode 100644 ThirdParty/DataLogger/types.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 180b08b..7e5e617 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,3 +62,5 @@ endfunction() create_test(test_average_module Modules/Sensors/tests/test_average_policy.cpp) create_test(test_outlier_module Modules/Sensors/tests/test_outlier_policy.cpp) + +create_test(test_base_logger ThirdParty/DataLogger/Tests/test_base_logger.cpp) diff --git a/ThirdParty/DataLogger/Tests/test_base_logger.cpp b/ThirdParty/DataLogger/Tests/test_base_logger.cpp new file mode 100644 index 0000000..d760cc0 --- /dev/null +++ b/ThirdParty/DataLogger/Tests/test_base_logger.cpp @@ -0,0 +1,245 @@ + +#include + +#include "ThirdParty/DataLogger/base.hpp" +#include "ThirdParty/DataLogger/Tests/test_helper.hpp" + +enum MyLoggerKinds { + LOG_HEALTH, + LOG_INTEGER, + LOG_DOUBLE, + LOG_2048B +}; + +struct MyLogger : public BaseDataLogger<0xAE, MyLoggerKinds, SampleTestStorage> { +public: + MyLogger () = default; + MyLogger (SampleTestStorage& storage) : BaseDataLogger(storage) {} + + void logInteger (int value) { + writeRecord(LOG_INTEGER, &value, sizeof(int)); + } + void logDouble (double value) { + writeRecord(LOG_DOUBLE, &value, sizeof(double)); + } + + void logSplitInTransaction () { + uint8_t buffer[2048]; + for (size_t offset = 0; offset < 2048; offset ++) { + buffer[offset] = (offset ^ (offset << 4)) * 0b1011 + 24; + } + + writeRecord(LOG_2048B, buffer, 2048); + } +}; + +TEST(TestBaseDataLogger, TestTransactionSimpleWrite) { + MyLogger logger; + + g_now_us_ = 21; + logger.logInteger(42); + g_now_us_ = 0x11256032; + logger.logDouble(3.14L); + + SampleTestStorage& storage = logger.getStorage(); + EXPECT_EQ(storage.str().size(), (size_t) 0); + logger.tick(); + EXPECT_EQ(storage.str().size(), 2 * sizeof(LogHeader) + 4 + 8); + + uint8_t test_array[28] = { + 0xAE, 1, 4, 0, 21, 0, 0, 0, + 42, 0, 0, 0, + 0xAE, 2, 8, 0, 0x32, 0x60, 0x25, 0x11, + 0, 0, 0, 0, 0, 0, 0, 0 // copy 3.14L here + }; + double db = 3.14L; + memcpy(test_array + 20, &db, 8); + + std::string str = storage.str(); + for (size_t off = 0; off < sizeof(test_array); off ++) { + EXPECT_EQ((uint8_t) str[off], test_array[off]) + << "At pos " << off; + } +} + +TEST(TestBaseDataLogger, TestWithFailure) { + MyLogger logger; + SuccessQueue queue; + queue.push(true); + queue.push(false); + logger.getStorage() = SampleTestStorage( + true, + queue + ); + + g_now_us_ = 21; + logger.logInteger(42); + g_now_us_ = 0x11256032; + logger.logDouble(3.14L); + logger.tick(); + + SampleTestStorage& storage = logger.getStorage(); + EXPECT_EQ(storage.str().size(), 12); + + uint8_t test_array[12] = { + 0xAE, 1, 4, 0, 21, 0, 0, 0, + 42, 0, 0, 0, + }; + std::string str = storage.str(); + for (size_t off = 0; off < sizeof(test_array); off ++) { + EXPECT_EQ((uint8_t) str[off], test_array[off]) + << "At pos " << off; + } +} + +TEST(TestBaseDataLogger, TestInTransaction) { + MyLogger logger; + + g_now_us_ = 21; + logger.logSplitInTransaction(); + logger.tick(); + + SampleTestStorage& storage = logger.getStorage(); + EXPECT_EQ(storage.str().size(), 2056); + + uint8_t test_array[8] = { + 0xAE, 3, 0, 8, 21, 0, 0, 0, + }; + std::string str = storage.str(); + for (size_t off = 0; off < sizeof(test_array); off ++) { + EXPECT_EQ((uint8_t) str[off], test_array[off]) + << "At pos " << off; + } + for (size_t offset = 0; offset < 16; offset ++) { + EXPECT_EQ((uint8_t) str[offset + 8], ((offset ^ (offset << 4)) * 0b1011 + 24) & 0xFF) + << "At pos " << (offset + 8); + } +} + +TEST(TestBaseDataLogger, TestTransactionFailure) { + for (bool bHeader : { false, true }) { + for (bool bPayload : { false, true }) { + if (bHeader && bPayload) continue ; + + MyLogger logger; + SuccessQueue queue; + queue.push(bHeader); + queue.push(bPayload); + logger.getStorage() = SampleTestStorage( + true, + queue + ); + + logger.logSplitInTransaction(); + logger.tick(); + + SampleTestStorage& storage = logger.getStorage(); + EXPECT_EQ(storage.str().size(), 0); + } + } +} + +#define RUN_TEST(SZE, ...) { \ + uint8_t test_array[SZE] = __VA_ARGS__; \ + EXPECT_EQ(storage.str().size(), SZE); \ + std::string str = storage.str(); \ + for (size_t off = 0; off < SZE; off ++) { \ + EXPECT_EQ((uint8_t) str[off], test_array[off]) \ + << "At pos " << off; \ + } \ + storage.clear(); \ +} + + +TEST(TestBaseDataLogger, TestHealth) { + MyLogger logger; + SuccessQueue queue; + queue.push(true); // health + queue.push(false); // int + queue.push(true); // health + queue.push(true); // int + queue.push(true); // double + queue.push(true); // health + queue.push(true); // 2048b - header + queue.push(true); // 2048b - payload + queue.push(true); // health + logger.getStorage() = SampleTestStorage( + true, + queue + ); + + SampleTestStorage& storage = logger.getStorage(); + + g_now_us_ = 0; g_next_us_ = 2; + logger.logStorageHealth(); + logger.tick(); + + RUN_TEST(28, { + 0xAE, 0, 20, 0, 0, 0, 0, 0, + + 0, 0, 0, 0, // bytes_written + 0, 0, 0, 0, // write_count_ + 0, 0, 0, 0, // write_fail_count_ + 0, 0, 0, 0, // max_write_time_us_ + 0, 0, 0, 0 // tick_count_ + }) + + g_next_us_ = 4; + logger.logInteger(0); + logger.tick(); + + RUN_TEST(0, {}) + + g_next_us_ = 8; + logger.logStorageHealth(); + logger.tick(); + + RUN_TEST(28, { + 0xAE, 0, 20, 0, 4, 0, 0, 0, + + 28, 0, 0, 0, // bytes_written + 2, 0, 0, 0, // write_count_ + 1, 0, 0, 0, // write_fail_count_ + 2, 0, 0, 0, // max_write_time_us_ + 2, 0, 0, 0 // tick_count_ + }) + + logger.logInteger(0); + logger.tick(); + logger.logDouble(0.L); + logger.tick(); + + storage.clear(); + + logger.logStorageHealth(); + logger.tick(); + + RUN_TEST(28, { + 0xAE, 0, 20, 0, 8, 0, 0, 0, + + 84, 0, 0, 0, // bytes_written + 5, 0, 0, 0, // write_count_ + 1, 0, 0, 0, // write_fail_count_ + 4, 0, 0, 0, // max_write_time_us_ + 5, 0, 0, 0 // tick_count_ + }) + + g_next_us_ = 1000; + logger.logSplitInTransaction(); + logger.tick(); + + storage.clear(); + + logger.logStorageHealth(); + logger.tick(); + + RUN_TEST(28, { + 0xAE, 0, 20, 0, 232, 3, 0, 0, + + 120, 8, 0, 0, // bytes_written + 7, 0, 0, 0, // write_count_ + 1, 0, 0, 0, // write_fail_count_ + 224, 3, 0, 0, // max_write_time_us_ + 7, 0, 0, 0 // tick_count_ + }) +} diff --git a/ThirdParty/DataLogger/Tests/test_helper.hpp b/ThirdParty/DataLogger/Tests/test_helper.hpp new file mode 100644 index 0000000..5d451c0 --- /dev/null +++ b/ThirdParty/DataLogger/Tests/test_helper.hpp @@ -0,0 +1,128 @@ + +#include + +using namespace std; + +uint32_t g_now_us_ = 0; +uint32_t g_next_us_ = 0; +uint32_t application_now_us () { + return g_now_us_; +} +void update_us_on_write () { + if (g_next_us_ > g_now_us_) { + g_now_us_ = g_next_us_; + } +} + +struct SuccessQueue { +private: + std::queue works; + + bool default_ = true; +public: + SuccessQueue () = default; + SuccessQueue (bool def) : default_ (def) {} + + void push (bool value) { + works.push(value); + } + bool poll () { + if (works.empty()) { + return default_; + } + + bool result = works.front(); + works.pop(); + return result; + } +}; + +struct TransactionContent { + bool success; + std::string buffer_copy; +}; + +struct SampleTestStorage { +private: + std::stringstream stream_buffer; + std::stringstream stream_result; + + bool ready_ = true; + + SuccessQueue write_works_; + std::vector transactions_; +public: + SampleTestStorage (const SampleTestStorage &storage) { + stream_buffer.str(storage.stream_buffer.str()); + stream_result.str(storage.stream_result.str()); + + ready_ = storage.ready_; + write_works_ = storage.write_works_; + transactions_ = storage.transactions_; + } + void operator=(const SampleTestStorage &storage) { + stream_buffer.str(storage.stream_buffer.str()); + stream_result.str(storage.stream_result.str()); + + ready_ = storage.ready_; + write_works_ = storage.write_works_; + transactions_ = storage.transactions_; + } + + SampleTestStorage () = default; + SampleTestStorage (bool ready) : ready_(ready) {} + SampleTestStorage (bool ready, SuccessQueue write_works) : ready_(ready), write_works_(write_works) {} + + std::string str () { return stream_result.str(); } + + bool ready () { return ready_; } + void tick () { + stream_result << stream_buffer.str(); + stream_buffer.str(""); + } + void clear () { + stream_buffer.str(""); + stream_result.str(""); + transactions_.clear(); + } + + StorageHealth withInternalHealth (StorageHealth health) { + return health; + } + + uint32_t now_us () { + return application_now_us(); + } + + void beginTransaction () { + transactions_.push_back({ true, stream_buffer.str() }); + } + void endTransaction () { + if (transactions_.size() == 0) return ; + + TransactionContent tr = transactions_.back(); + transactions_.pop_back(); + + if (!tr.success) { + stream_buffer.str(tr.buffer_copy); + } + } + + bool write (const uint8_t* payload, uint16_t payload_len) { + update_us_on_write(); + + if (write_works_.poll()) { + for (uint16_t offset = 0; offset < payload_len; offset ++) { + stream_buffer << payload[offset]; + } + + return true; + } + + if (transactions_.size()) { + transactions_.back().success = false; + } + + return false; + } +}; diff --git a/ThirdParty/DataLogger/base.hpp b/ThirdParty/DataLogger/base.hpp new file mode 100644 index 0000000..8bd452f --- /dev/null +++ b/ThirdParty/DataLogger/base.hpp @@ -0,0 +1,68 @@ + +#include "ThirdParty/DataLogger/types.hpp" + +template +struct BaseDataLogger { +private: + Storage storage_; + + StorageHealth health_; +protected: + BaseDataLogger () = default; + BaseDataLogger (Storage& storage) : storage_(storage) {} + + void writeRecord (EnumKinds recordType, const void* payload, uint16_t payload_len) { + if (!storage_.ready()) return; + + LogHeader hdr; + hdr.magic = Magic; + hdr.record_type = static_cast(recordType); + hdr.length = payload_len; + hdr.timestamp_us = storage_.now_us(); + + const uint32_t t0 = hdr.timestamp_us; + + // Write header + payload as a single contiguous write. + // Plume's ring buffer handles the byte-level copy. + uint8_t buf[sizeof(LogHeader) + 1024]; // stack buffer for small records + + bool result; + if (sizeof(LogHeader) + payload_len <= sizeof(buf)) { + memcpy(buf, &hdr, sizeof(hdr)); + memcpy(buf + sizeof(hdr), payload, payload_len); + result = storage_.write(buf, sizeof(hdr) + payload_len); + } else { + // Large record: write header then payload separately + storage_.beginTransaction(); + storage_.write(reinterpret_cast(&hdr), sizeof(hdr)); + result = storage_.write(reinterpret_cast(payload), payload_len); + storage_.endTransaction(); + } + + const uint32_t elapsed_us = storage_.now_us() - t0; + if (elapsed_us > health_.max_write_time_us_) health_.max_write_time_us_ = elapsed_us; + + health_.write_count_++; + if (!result) { + health_.write_fail_count_++; + } else { + health_.bytes_written_ += sizeof(hdr) + payload_len; + } + } +public: + void tick () { + storage_.tick(); + + health_.tick_count_ ++; + } + + Storage &getStorage () { + return storage_; + } + + void logStorageHealth () { + auto fullHealth = storage_.withInternalHealth(health_); + + writeRecord(EnumKinds::LOG_HEALTH, &fullHealth, sizeof(fullHealth)); + } +}; diff --git a/ThirdParty/DataLogger/types.hpp b/ThirdParty/DataLogger/types.hpp new file mode 100644 index 0000000..e6da159 --- /dev/null +++ b/ThirdParty/DataLogger/types.hpp @@ -0,0 +1,17 @@ + +#include + +struct LogHeader { + uint8_t magic; + uint8_t record_type; + uint16_t length; + uint32_t timestamp_us; +}; + +struct StorageHealth { + uint32_t bytes_written_ = 0; + uint32_t write_count_ = 0; + uint32_t write_fail_count_ = 0; + uint32_t max_write_time_us_ = 0; + uint32_t tick_count_ = 0; +}; From b01e73a577c884e414638768336356f7da2fd63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Mon, 17 Aug 2026 21:10:57 +0200 Subject: [PATCH 2/7] feat/logger: added loggers for engine, eth and lox --- Application/Data/data.cpp | 4 +- Application/Data/data.hpp | 2 +- Application/Data/propulsion/fields.hpp | 42 +++--- Modules/Sensors/impl/lox/pressure_ota.hpp | 4 + ThirdParty/DataLogger/Tests/test_helper.hpp | 2 +- ThirdParty/DataLogger/base.hpp | 4 +- ThirdParty/DataLogger/loggers/engine.hpp | 41 ++++++ ThirdParty/DataLogger/loggers/eth.hpp | 31 +++++ ThirdParty/DataLogger/loggers/lox.hpp | 38 ++++++ ThirdParty/DataLogger/types.hpp | 134 +++++++++++++++++++- 10 files changed, 273 insertions(+), 29 deletions(-) create mode 100644 ThirdParty/DataLogger/loggers/engine.hpp create mode 100644 ThirdParty/DataLogger/loggers/eth.hpp create mode 100644 ThirdParty/DataLogger/loggers/lox.hpp diff --git a/Application/Data/data.cpp b/Application/Data/data.cpp index b008c31..60369ca 100644 --- a/Application/Data/data.cpp +++ b/Application/Data/data.cpp @@ -6,9 +6,9 @@ using namespace prc; StateStore::StateStore() { data_ = State::MANUAL; } -const DataDump &PrcStore::get() const { +const DataDump &PrcStore::get(uint32_t timestamp) const { data_.prc_state = stateStore.get(); - data_.prc_timestamp_ms = HAL_GetTick(); + data_.prc_timestamp_ms = timestamp; data_.boardIdentity = boardIdentityStore.get(); data_.valves = valvesStore.get(); data_.intranetCmd = intranetCmdStore.get(); diff --git a/Application/Data/data.hpp b/Application/Data/data.hpp index 8215039..ab0147c 100644 --- a/Application/Data/data.hpp +++ b/Application/Data/data.hpp @@ -157,7 +157,7 @@ class PrcStore { PropSensorsStoreLox propSensorsStoreLox; void set(const DataDump &value); - const DataDump &get() const; + const DataDump &get(uint32_t timestamp) const; DataDump *get_ref(); static inline PrcStore &get_instance() { diff --git a/Application/Data/propulsion/fields.hpp b/Application/Data/propulsion/fields.hpp index bbfe9cc..5f3ec29 100644 --- a/Application/Data/propulsion/fields.hpp +++ b/Application/Data/propulsion/fields.hpp @@ -1,33 +1,33 @@ #ifndef X_PRC_SENSORS_STORE_ENGINE #define X_PRC_SENSORS_STORE_ENGINE(...) \ - X_FIELD (double, pressure_C, ##__VA_ARGS__) \ - X_FIELD (double, pressure_OIN, ##__VA_ARGS__) \ - X_FIELD (double, pressure_EIN, ##__VA_ARGS__) \ - X_FIELD (double, temperature_C, ##__VA_ARGS__) \ - X_FIELD (double, temperature_OIN, ##__VA_ARGS__) \ - X_FIELD (double, temperature_EIN, ##__VA_ARGS__) + X_FIELD (double, pressure_C __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD (double, pressure_OIN __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD (double, pressure_EIN __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD (double, temperature_C __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD (double, temperature_OIN __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD (double, temperature_EIN __VA_OPT__(, ##__VA_ARGS__)) #endif #ifndef X_PRC_SENSORS_STORE_LOX #define X_PRC_SENSORS_STORE_LOX(...) \ - X_raw_FIELD(double, pressure_OTA1, ##__VA_ARGS__) \ - X_raw_FIELD(double, pressure_OTA2, ##__VA_ARGS__) \ - X_raw_FIELD(double, pressure_OTA3, ##__VA_ARGS__) \ - X_proc_FIELD(double, pressure_OTA, ##__VA_ARGS__) \ - X_FIELD(double, pressure_HPO, ##__VA_ARGS__) \ - X_FIELD(double, temperature_OTA1, ##__VA_ARGS__) \ - X_FIELD(double, temperature_OTA2, ##__VA_ARGS__) \ - X_FIELD(double, temperature_OTA3, ##__VA_ARGS__) \ - X_FIELD(double, temperature_OTA4, ##__VA_ARGS__) \ - X_FIELD(double, FLS, ##__VA_ARGS__) + X_raw_FIELD(double, pressure_OTA1 __VA_OPT__(, ##__VA_ARGS__)) \ + X_raw_FIELD(double, pressure_OTA2 __VA_OPT__(, ##__VA_ARGS__)) \ + X_raw_FIELD(double, pressure_OTA3 __VA_OPT__(, ##__VA_ARGS__)) \ + X_proc_FIELD(double, pressure_OTA __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, pressure_HPO __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, temperature_OTA1 __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, temperature_OTA2 __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, temperature_OTA3 __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, temperature_OTA4 __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, FLS __VA_OPT__(, ##__VA_ARGS__)) #endif #ifndef X_PRC_SENSORS_STORE_ETH #define X_PRC_SENSORS_STORE_ETH(...) \ - X_raw_FIELD(double, pressure_ETA1, ##__VA_ARGS__) \ - X_raw_FIELD(double, pressure_ETA2, ##__VA_ARGS__) \ - X_raw_FIELD(double, pressure_ETA3, ##__VA_ARGS__) \ - X_proc_FIELD(double, pressure_ETA, ##__VA_ARGS__) \ - X_FIELD(double, pressure_HPE, ##__VA_ARGS__) + X_raw_FIELD(double, pressure_ETA1 __VA_OPT__(, ##__VA_ARGS__)) \ + X_raw_FIELD(double, pressure_ETA2 __VA_OPT__(, ##__VA_ARGS__)) \ + X_raw_FIELD(double, pressure_ETA3 __VA_OPT__(, ##__VA_ARGS__)) \ + X_proc_FIELD(double, pressure_ETA __VA_OPT__(, ##__VA_ARGS__)) \ + X_FIELD(double, pressure_HPE __VA_OPT__(, ##__VA_ARGS__)) #endif diff --git a/Modules/Sensors/impl/lox/pressure_ota.hpp b/Modules/Sensors/impl/lox/pressure_ota.hpp index 0a9c56f..d5cee54 100644 --- a/Modules/Sensors/impl/lox/pressure_ota.hpp +++ b/Modules/Sensors/impl/lox/pressure_ota.hpp @@ -37,6 +37,10 @@ using PressureOtaSensorModule = multi::Module< sensata::SensataErrorPipeline >, // OTA2 physically not present on this board, commented out for now. + // Also modify ThirdParty/DataLogger/types.hpp to change the pressure frame with + // number of sensors to 3 instead of 2. + // Also modify ThirdParty/DataLogger/Client/rules.yaml to add the pressure frame + // data to the csv generator. // multi::PressureSensorParam< // sensata::PressureSensata>, // LOX_SETTER_POLICY(prc::PropSensorsStoreLox::set_pressure_OTA2), diff --git a/ThirdParty/DataLogger/Tests/test_helper.hpp b/ThirdParty/DataLogger/Tests/test_helper.hpp index 5d451c0..d151e1e 100644 --- a/ThirdParty/DataLogger/Tests/test_helper.hpp +++ b/ThirdParty/DataLogger/Tests/test_helper.hpp @@ -86,7 +86,7 @@ struct SampleTestStorage { transactions_.clear(); } - StorageHealth withInternalHealth (StorageHealth health) { + BaseStorageHealth withInternalHealth (BaseStorageHealth health) { return health; } diff --git a/ThirdParty/DataLogger/base.hpp b/ThirdParty/DataLogger/base.hpp index 8bd452f..8015f92 100644 --- a/ThirdParty/DataLogger/base.hpp +++ b/ThirdParty/DataLogger/base.hpp @@ -1,4 +1,4 @@ - +#pragma once #include "ThirdParty/DataLogger/types.hpp" template @@ -6,7 +6,7 @@ struct BaseDataLogger { private: Storage storage_; - StorageHealth health_; + BaseStorageHealth health_; protected: BaseDataLogger () = default; BaseDataLogger (Storage& storage) : storage_(storage) {} diff --git a/ThirdParty/DataLogger/loggers/engine.hpp b/ThirdParty/DataLogger/loggers/engine.hpp new file mode 100644 index 0000000..153b7f1 --- /dev/null +++ b/ThirdParty/DataLogger/loggers/engine.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "ThirdParty/DataLogger/base.hpp" + +template +struct EngingDataLogger : public BaseDataLogger { +public: + EngingDataLogger () = default; + EngingDataLogger (Storage& storage) : BaseDataLogger(storage) {} + + void logDataDump (prc::DataDump &dump) { + writeRecord(engine::RecordType::LOG_DATA_DUMP, &dump, sizeof(dump)); + } + + void logChamberFrame (pressure_temperature_frame frame) { + writeRecord(engine::RecordType::LOG_CHAMBER_FRAME, &frame, sizeof(frame)); + } + + void logEinPFrame (pressures_frame frame) { + writeRecord(engine::RecordType::LOG_EIN_P_FRAME, &frame, sizeof(frame)); + } + void logEinTFrame (temperature_frame frame) { + writeRecord(engine::RecordType::LOG_EIN_T_FRAME, &frame, sizeof(frame)); + } + + void logOinPFrame (pressures_frame frame) { + writeRecord(engine::RecordType::LOG_OIN_P_FRAME, &frame, sizeof(frame)); + } + void logOinTFrame (temperature_frame frame) { + writeRecord(engine::RecordType::LOG_OIN_T_FRAME, &frame, sizeof(frame)); + } + + // TODO choose the type of bundled old + new, find types of + // FSM states. + //void logFsmTransition (??? old_fsm, ??? new_fsm) { + // writeRecord(engine::RecordType::LOG_FSM_TRANSITION, ???, ???) + //} + + void logError (engine::ErrorKind kind) { + writeRecord(engine::RecordType::LOG_ERROR, &kind, sizeof(EngineErrorKind)); + } +}; diff --git a/ThirdParty/DataLogger/loggers/eth.hpp b/ThirdParty/DataLogger/loggers/eth.hpp new file mode 100644 index 0000000..a3d7457 --- /dev/null +++ b/ThirdParty/DataLogger/loggers/eth.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "ThirdParty/DataLogger/base.hpp" + +template +struct EthDataLogger : public BaseDataLogger { +public: + EthDataLogger () = default; + EthDataLogger (Storage& storage) : BaseDataLogger(storage) {} + + void logDataDump (prc::DataDump &dump) { + writeRecord(eth::RecordType::LOG_DATA_DUMP, &dump, sizeof(dump)); + } + + void logHPE (pressures_frame frame) { + writeRecord(eth::RecordType::LOG_HPE_FRAME, &frame, sizeof(frame)); + } + + void logETAPressureFrame (const EtaPressureFrame &frame) { + writeRecord(eth::RecordType::LOG_ETA_P_FRAME, &frame, sizeof(frame)); + } + + // TODO choose the type of bundled old + new, find types of + // FSM states. + //void logFsmTransition (??? old_fsm, ??? new_fsm) { + // writeRecord(EngineRecordType::LOG_FSM_TRANSITION, ???, ???) + //} + + void logError (eth::ErrorKind kind) { + writeRecord(eth::RecordType::LOG_ERROR, &kind, sizeof(EngineErrorKind)); + } +}; diff --git a/ThirdParty/DataLogger/loggers/lox.hpp b/ThirdParty/DataLogger/loggers/lox.hpp new file mode 100644 index 0000000..5a97bf3 --- /dev/null +++ b/ThirdParty/DataLogger/loggers/lox.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "ThirdParty/DataLogger/base.hpp" + +template +struct LoxDataLogger : public BaseDataLogger { +public: + LoxDataLogger () = default; + LoxDataLogger (Storage& storage) : BaseDataLogger(storage) {} + + void logDataDump (prc::DataDump &dump) { + writeRecord(lox::RecordType::LOG_DATA_DUMP, &dump, sizeof(dump)); + } + + void logFLS (double frame) { + writeRecord(lox::RecordType::LOG_FLS, &frame, sizeof(frame)); + } + + void logHPO (pressures_frame frame) { + writeRecord(lox::RecordType::LOG_HPO_FRAME, &frame, sizeof(frame)); + } + + void logOTAPressureFrame (const OtaPressureFrame &frame) { + writeRecord(lox::RecordType::LOG_OTA_P_FRAME, &frame, sizeof(frame)); + } + void logOTATemperatureFrame (const OtaTemperatureFrame &frame) { + writeRecord(lox::RecordType::LOG_OTA_T_FRAME, &frame, sizeof(frame)); + } + + // TODO choose the type of bundled old + new, find types of + // FSM states. + //void logFsmTransition (??? old_fsm, ??? new_fsm) { + // writeRecord(EngineRecordType::LOG_FSM_TRANSITION, ???, ???) + //} + + void logError (lox::ErrorKind kind) { + writeRecord(lox::RecordType::LOG_ERROR, &kind, sizeof(EngineErrorKind)); + } +}; diff --git a/ThirdParty/DataLogger/types.hpp b/ThirdParty/DataLogger/types.hpp index e6da159..66eb321 100644 --- a/ThirdParty/DataLogger/types.hpp +++ b/ThirdParty/DataLogger/types.hpp @@ -1,5 +1,7 @@ - +#pragma once #include +#include "Application/Data/data.hpp" +#include "Modules/Sensors/impl/std/multi.hpp" struct LogHeader { uint8_t magic; @@ -8,10 +10,138 @@ struct LogHeader { uint32_t timestamp_us; }; -struct StorageHealth { +struct BaseStorageHealth { uint32_t bytes_written_ = 0; uint32_t write_count_ = 0; uint32_t write_fail_count_ = 0; uint32_t max_write_time_us_ = 0; uint32_t tick_count_ = 0; }; + +struct pressures_frame { + double pressure; + double pressure_mean; +}; +struct temperature_frame { + double temperature; + double temperature_mean; +}; +struct pressure_temperature_frame { + double pressure; + double pressure_mean; + + double temperature; + double temperature_mean; +}; + +namespace engine { + + enum ErrorKind { + CHAMBER_ERROR, + + EIN_P_ERROR, + EIN_T_ERROR, + + OIN_P_ERROR, + OIN_T_ERROR + }; + + enum RecordType { + LOG_HEALTH, + + LOG_DATA_DUMP, + LOG_FSM_TRANSITION, + + LOG_CHAMBER_FRAME, // { P, T, P_mean, C_mean } + + LOG_EIN_P_FRAME, // { P, P_mean } + LOG_EIN_T_FRAME, // { T, T_mean } + LOG_OIN_P_FRAME, // { P, P_mean } + LOG_OIN_T_FRAME, // { T, T_mean } + + LOG_ERROR // Send an EngineErrorKind + }; +}; + +namespace lox { + + // For now, no OTA2 is present + using OtaPressureFrame = multi::internal::PipelineReturnValue< + multi::UseOutlier, + multi::UseUnpack, + 2 + >; + + struct OtaTemperatureFrame { + uint8_t sensor_id; + + double temperature; + double temperature_mean; + }; + + enum ErrorKind { + FLS_ERROR, + + HPO_P_ERROR, + OTA_P_ERROR, + + OTA1_P_ERROR, + OTA2_P_ERROR, + OTA3_P_ERROR, + + OTA1_T_ERROR, + OTA2_T_ERROR, + OTA3_T_ERROR, + OTA4_T_ERROR + }; + + enum RecordType { + LOG_HEALTH, + LOG_DATA_DUMP, + LOG_FSM_TRANSITION, + + LOG_FLS, // fill level + + LOG_HPO_FRAME, // { P, P_mean } + LOG_OTA_P_FRAME, // OtaPressureFrame + LOG_OTA_T_FRAME, // OtaTemperatureFrame + + LOG_ERROR // Send a LoxErrorKind + }; + +}; // namespace lox; + + + +namespace eth { + + // For now, no OTA3 is present + using EtaPressureFrame = multi::internal::PipelineReturnValue< + multi::UseOutlier, + multi::UseUnpack, + 2 + >; + + enum ErrorKind { + FLS_ERROR, + + HPE_P_ERROR, + ETA_P_ERROR, + + ETA1_P_ERROR, + ETA2_P_ERROR, + ETA3_P_ERROR, + }; + + enum RecordType { + LOG_HEALTH, + + LOG_DATA_DUMP, + LOG_FSM_TRANSITION, + + LOG_HPE_FRAME, // { P, P_mean } + LOG_ETA_P_FRAME, // OtaPressureFrame + + LOG_ERROR // Send a LoxErrorKind + }; +}; From 8b21c9110e25c7468afce1e5da95c302272de6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Mon, 17 Aug 2026 21:36:36 +0200 Subject: [PATCH 3/7] feat/logger: added plume storage --- .gitmodules | 3 + Core/Src/main.c | 10 +- Drivers/Plume/2026_C_AV_PLUME | 1 + Drivers/Plume/Impl/plume_driver.cpp | 344 ++++++++++++++++++ .../Tests/Hardware/plume_manual_test.cpp | 68 ++++ .../Plume/Tests/Hardware/plume_manual_test.h | 4 + .../Plume/Tests/Hardware/plume_validate.py | 64 ++++ Drivers/Plume/plume_driver.hpp | 50 +++ Drivers/Plume/plume_storage.hpp | 46 +++ Drivers/Plume/sd_hardware_init.c | 102 ++++++ Drivers/Plume/sd_hardware_init.h | 26 ++ Drivers/Plume/types.hpp | 19 + ThirdParty/DataLogger/types.hpp | 10 + 13 files changed, 744 insertions(+), 3 deletions(-) create mode 160000 Drivers/Plume/2026_C_AV_PLUME create mode 100644 Drivers/Plume/Impl/plume_driver.cpp create mode 100644 Drivers/Plume/Tests/Hardware/plume_manual_test.cpp create mode 100644 Drivers/Plume/Tests/Hardware/plume_manual_test.h create mode 100644 Drivers/Plume/Tests/Hardware/plume_validate.py create mode 100644 Drivers/Plume/plume_driver.hpp create mode 100644 Drivers/Plume/plume_storage.hpp create mode 100644 Drivers/Plume/sd_hardware_init.c create mode 100644 Drivers/Plume/sd_hardware_init.h create mode 100644 Drivers/Plume/types.hpp diff --git a/.gitmodules b/.gitmodules index f889239..a5d714a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "ThirdParty/SignalUtils"] path = ThirdParty/SignalUtils url = https://github.com/EPFLRocketTeam/2026_C_AV_SIGNAL_UTILS.git +[submodule "Drivers/Plume/2026_C_AV_PLUME"] + path = Drivers/Plume/2026_C_AV_PLUME + url = https://github.com/EPFLRocketTeam/2026_C_AV_PLUME.git diff --git a/Core/Src/main.c b/Core/Src/main.c index bb77855..1cd8f7b 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -32,6 +32,7 @@ #include "../../Application/FlightControl/prc_fsm_c_api.h" #include "../../Application/FlightControl/prc_can.hpp" #include "../../Application/main_app.h" +#include "../../Drivers/Plume/sd_hardware_init.h" #include "CAN.h" #include "usbd_cdc_if.h" @@ -214,6 +215,9 @@ int main(void) /* MCU Configuration--------------------------------------------------------*/ + SCB_EnableICache(); + SCB_EnableDCache(); + /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); @@ -235,7 +239,7 @@ int main(void) MX_GPIO_Init(); MX_FDCAN1_Init(); MX_I2C1_Init(); - //MX_SDMMC1_SD_Init(); + MX_SDMMC1_SD_Init(); MX_TIM4_Init(); MX_USB_DEVICE_Init(); MX_ADC3_Init(); @@ -691,7 +695,7 @@ static void MX_SDMMC1_SD_Init(void) { /* USER CODE BEGIN SDMMC1_Init 0 */ - + sd_pre_init(); /* USER CODE END SDMMC1_Init 0 */ /* USER CODE BEGIN SDMMC1_Init 1 */ @@ -708,7 +712,7 @@ static void MX_SDMMC1_SD_Init(void) Error_Handler(); } /* USER CODE BEGIN SDMMC1_Init 2 */ - + sd_post_init(&hsd1); /* USER CODE END SDMMC1_Init 2 */ } diff --git a/Drivers/Plume/2026_C_AV_PLUME b/Drivers/Plume/2026_C_AV_PLUME new file mode 160000 index 0000000..ec5cba7 --- /dev/null +++ b/Drivers/Plume/2026_C_AV_PLUME @@ -0,0 +1 @@ +Subproject commit ec5cba7fc0dd256e6b1323b210d73efdda2e8916 diff --git a/Drivers/Plume/Impl/plume_driver.cpp b/Drivers/Plume/Impl/plume_driver.cpp new file mode 100644 index 0000000..5c72d93 --- /dev/null +++ b/Drivers/Plume/Impl/plume_driver.cpp @@ -0,0 +1,344 @@ + +#include "plume_driver.hpp" +#include +#include +#include "app_timebase.h" + +extern "C" { + #include "plume/writer.h" + #include "plume/status.h" + #include "plume/const.h" +}; + +/* ── DMA bounce buffer ──────────────────────────────────────────────────── */ +/* SDMMC1 IDMA can only access AXI SRAM (RAM_D1, 0x24000000). + * The Plume arena lives in RAM_D2 (0x30000000) which IDMA cannot reach. + * We memcpy into this bounce buffer before every DMA write. + * Size = PLUME_MAX_BATCH_SIZE blocks × 512 B = 32 KB. */ +static uint8_t s_dma_bounce[PLUME_MAX_BATCH_SIZE * 512] + __attribute__((aligned(32))); + +/* ── DMA completion flags (set from IRQ context) ─────────────────────────── */ +volatile uint8_t g_sd_dma_complete = 1; /* 1 = idle/done */ +volatile uint8_t g_sd_dma_error = 0; + +/* ── SD timing instrumentation ──────────────────────────────────────────── */ +static SdTimingStats s_sd_timing = {}; + +/* Public accessor for printing from main.cpp */ +SdTimingStats sd_timing_snapshot() { + SdTimingStats snap = s_sd_timing; + /* Reset for next window */ + s_sd_timing.dma_count = 0; + s_sd_timing.dma_error_count = 0; + s_sd_timing.max_xfer_us = 0; + s_sd_timing.max_prog_us = 0; + s_sd_timing.max_cycle_us = 0; + s_sd_timing.sum_cycle_us = 0; + s_sd_timing.total_blocks = 0; + s_sd_timing.min_batch = 0xFFFFFFFF; + s_sd_timing.max_batch = 0; + s_sd_timing.last_error_code = 0; + return snap; +} + +extern "C" void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd) { + (void)hsd; + s_sd_timing.dma_cb_us = app_timebase_now_us(); + g_sd_dma_complete = 1; +} + +extern "C" void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { + s_sd_timing.dma_cb_us = app_timebase_now_us(); + s_sd_timing.dma_error_count++; + s_sd_timing.last_error_code = hsd->ErrorCode; + g_sd_dma_error = 1; + g_sd_dma_complete = 1; /* unblock the ready check */ +} + +#define DBG(...) printf(" - " #__VA_ARGS__ ": %u \r\n", __VA_ARGS__); +uint8_t plume_stm32_disk_information (SD_HandleTypeDef* hsd, struct plume_disk* disk_info) { + if (hsd->State != HAL_SD_STATE_READY) { + return -50; + } + + disk_info->number_blocks = hsd->SdCard.LogBlockNbr; + disk_info->block_size = hsd->SdCard.LogBlockSize; + printf("Information on disk: \r\n"); + printf(" - number blocks : %u\r\n", (uint32_t) disk_info->number_blocks); + printf(" - block size : %u\r\n", (uint32_t) disk_info->block_size); + DBG(hsd->SdCard.BlockNbr); + DBG(hsd->SdCard.BlockSize); + DBG(hsd->SdCard.CardSpeed); + DBG(hsd->SdCard.CardType); + DBG(hsd->SdCard.CardVersion); + DBG(hsd->SdCard.Class); + DBG(hsd->SdCard.LogBlockNbr); + DBG(hsd->SdCard.LogBlockSize); + DBG(hsd->SdCard.RelCardAdd); + + return PLUME_OK; +} +uint8_t plume_stm32_read_block (SD_HandleTypeDef* hsd, struct plume_context* context, uint8_t* buffer, uint64_t block_id) { + /* HAL_SD_ReadBlocks() on STM32H7 reads SDMMC FIFO via CPU → data goes + * into D-cache naturally. No cache maintenance needed for the read path. + * (IDMA is only used by HAL_SD_ReadBlocks_DMA.) */ + HAL_StatusTypeDef status = HAL_SD_ReadBlocks(hsd, buffer, (uint32_t) block_id, 1, HAL_MAX_DELAY); + if (status == HAL_OK) { + return PLUME_OK; + } + + return -45; +} +uint8_t plume_stm32_write_block (SD_HandleTypeDef* hsd, struct plume_context* context, const uint8_t* buffer, uint64_t block_id) { + /* Wait for card to reach TRANSFER state (previous write programming done). */ + uint32_t t0 = HAL_GetTick(); + while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER) { + if (HAL_GetTick() - t0 > 500) { + HAL_SD_Abort(hsd); + return PLUME_OK_RETRY; + } + } + + /* Copy arena data (RAM_D2) into AXI SRAM bounce buffer for IDMA. */ + memcpy(s_dma_bounce, buffer, 512); + + /* Flush D-cache so IDMA reads committed data from AXI SRAM. */ + SCB_CleanDCache_by_Addr((uint32_t*)s_dma_bounce, 512); + + s_sd_timing.last_batch_size = 1; + s_sd_timing.total_blocks += 1; + if (1 < s_sd_timing.min_batch) s_sd_timing.min_batch = 1; + if (1 > s_sd_timing.max_batch) s_sd_timing.max_batch = 1; + + g_sd_dma_complete = 0; + g_sd_dma_error = 0; + s_sd_timing.dma_start_us = app_timebase_now_us(); + + HAL_StatusTypeDef status = HAL_SD_WriteBlocks_DMA(hsd, s_dma_bounce, (uint32_t)block_id, 1); + if (status != HAL_OK) { + g_sd_dma_complete = 1; + s_sd_timing.dma_start_us = 0; + return PLUME_OK_RETRY; + } + return PLUME_OK_SENT_DMA; +} + +uint8_t plume_stm32_write_blocks (SD_HandleTypeDef* hsd, struct plume_context* context, const uint8_t* buffer, uint64_t block_id, uint32_t num_blocks) { + /* Wait for card to reach TRANSFER state. */ + uint32_t t0 = HAL_GetTick(); + while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER) { + if (HAL_GetTick() - t0 > 500) { + HAL_SD_Abort(hsd); + return PLUME_OK_RETRY; + } + } + + /* NOTE: CMD23 (SET_BLOCK_COUNT) removed intentionally. + * The HAL uses open-ended CMD25 + CMD12 (STOP_TRANSMISSION) to end + * multi-block writes. Sending CMD23 before HAL_SD_WriteBlocks_DMA() + * causes the card to auto-stop, so the HAL's subsequent CMD12 gets + * CCRCFAIL/CTIMEOUT → false ErrorCallback on every single write. */ + + /* Clamp to bounce buffer capacity. */ + if (num_blocks > PLUME_MAX_BATCH_SIZE) { + num_blocks = PLUME_MAX_BATCH_SIZE; + } + + /* Copy arena data (RAM_D2) into AXI SRAM bounce buffer for IDMA. */ + memcpy(s_dma_bounce, buffer, num_blocks * 512); + + /* Flush D-cache for the entire batch so IDMA sees committed data. */ + SCB_CleanDCache_by_Addr((uint32_t*)s_dma_bounce, num_blocks * 512); + + /* ── Record batch size and DMA start timestamp ── */ + s_sd_timing.last_batch_size = num_blocks; + s_sd_timing.total_blocks += num_blocks; + if (num_blocks < s_sd_timing.min_batch) s_sd_timing.min_batch = num_blocks; + if (num_blocks > s_sd_timing.max_batch) s_sd_timing.max_batch = num_blocks; + + g_sd_dma_complete = 0; + g_sd_dma_error = 0; + s_sd_timing.dma_start_us = app_timebase_now_us(); + + HAL_StatusTypeDef status = HAL_SD_WriteBlocks_DMA(hsd, s_dma_bounce, (uint32_t)block_id, num_blocks); + if (status != HAL_OK) { + g_sd_dma_complete = 1; + s_sd_timing.dma_start_us = 0; /* don't record broken DMA */ + return PLUME_OK_RETRY; + } + return PLUME_OK_SENT_DMA; +} + +uint8_t plume_stm32_write_block_ready (SD_HandleTypeDef* hsd, struct plume_context* context) { + if (!g_sd_dma_complete) { + return 0; /* DMA transfer still in progress */ + } + /* DMA finished — also wait for the card to finish programming. */ + if (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER) { + return 0; + } + + /* ── Record timing stats ── */ + uint64_t now = app_timebase_now_us(); + if (s_sd_timing.dma_start_us > 0) { + uint32_t xfer = (uint32_t)(s_sd_timing.dma_cb_us - s_sd_timing.dma_start_us); + uint32_t prog = (uint32_t)(now - s_sd_timing.dma_cb_us); + uint32_t cycle = (uint32_t)(now - s_sd_timing.dma_start_us); + if (xfer > s_sd_timing.max_xfer_us) s_sd_timing.max_xfer_us = xfer; + if (prog > s_sd_timing.max_prog_us) s_sd_timing.max_prog_us = prog; + if (cycle > s_sd_timing.max_cycle_us) s_sd_timing.max_cycle_us = cycle; + s_sd_timing.sum_cycle_us += cycle; + s_sd_timing.dma_count++; + } + + if (g_sd_dma_error) { + g_sd_dma_error = 0; /* consume the error — caller will see data not written */ + } + + return 1; +} + + + + +bool SDCardInterface::init_sd_card ( + SD_HandleTypeDef* hsd, + uint8_t* arena_buffer, + size_t arena_length +) { + if (hsd->State != HAL_SD_STATE_READY) { + return PLUME_EBAD_DISK; + } + + driver.driver_ptr = hsd; + + driver.disk_information = + PLUME_DISK_INFORMATION_FN_TYPE + plume_stm32_disk_information; + driver.read_block = + PLUME_READ_BLOCK_FN_TYPE + plume_stm32_read_block; + driver.write_block = + PLUME_WRITE_BLOCK_FN_TYPE + plume_stm32_write_block; + driver.write_block_ready = + PLUME_WRITE_BLOCK_READY_FN_TYPE + plume_stm32_write_block_ready; + driver.write_blocks = + PLUME_WRITE_BLOCKS_FN_TYPE + plume_stm32_write_blocks; + + context.arena_buffer = arena_buffer; + context.arena_length = arena_length; + + uint8_t err_code = plume_init(&context, &driver); + if (err_code == PLUME_EBAD_DISK) { + /* PLUME_EBAD_DISK means block 0 doesn't have the Plume settings marker. + * Only auto-format if block 0 looks genuinely blank (all 0x00 or 0xFF). + * If block 0 has other data (corrupted Plume card or foreign FS), refuse + * to format so we never accidentally overwrite recoverable flight data. */ + bool block0_blank = true; + for (size_t i = 0; i < 512 && i < arena_length; ++i) { + if (arena_buffer[i] != 0x00 && arena_buffer[i] != 0xFF) { + block0_blank = false; + break; + } + } + if (!block0_blank) { + printf("[SD] Block 0 has non-blank data (not 0x00/0xFF) — refusing auto-format.\r\n"); + printf("[SD] If this card needs reformatting, clear it manually first.\r\n"); + return false; + } + + printf("[SD] Card not formatted (block 0 blank), performing quick format...\r\n"); + /* Quick format: write settings page (block 0) + clear FAT region. + * Use blocking (polling) HAL writes — no DMA complexity for one-time init. */ + constexpr uint64_t fat_size = 64; + + /* Write block 0: settings page */ + for (size_t i = 0; i < arena_length && i < 512; ++i) + arena_buffer[i] = 0x00; + arena_buffer[0] = PLUME_PAGE_SETTINGS; + memcpy(arena_buffer + 1, &fat_size, sizeof(uint64_t)); + + SCB_CleanDCache_by_Addr((uint32_t*)arena_buffer, 512); + HAL_StatusTypeDef hal_rc = HAL_SD_WriteBlocks(hsd, arena_buffer, 0, 1, 1000); + if (hal_rc != HAL_OK) { + printf("[SD] Quick format: failed to write settings block (HAL=%d)\r\n", (int)hal_rc); + return false; + } + /* Wait for card programming */ + while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER) {} + + /* Clear FAT blocks (1..fat_size-1) so binary search finds them empty */ + for (size_t i = 0; i < 512; ++i) + arena_buffer[i] = 0x00; + SCB_CleanDCache_by_Addr((uint32_t*)arena_buffer, 512); + for (uint64_t blk = 1; blk < fat_size; ++blk) { + hal_rc = HAL_SD_WriteBlocks(hsd, arena_buffer, (uint32_t)blk, 1, 1000); + if (hal_rc != HAL_OK) { + printf("[SD] Quick format: failed at FAT block %u (HAL=%d)\r\n", + (unsigned)blk, (int)hal_rc); + return false; + } + while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER) {} + } + printf("[SD] Quick format done (%u FAT blocks written)\r\n", (unsigned)fat_size); + + /* Retry init */ + err_code = plume_init(&context, &driver); + } + + if (err_code != PLUME_OK) { + printf("Failure of init: %u\r\n", err_code); + } + + return err_code == PLUME_OK; +} +bool SDCardInterface::open_file () { + return plume_open_write(&context) == PLUME_OK; +} + +size_t SDCardInterface::number_files_remaining () { + return context.fat_size - context.next_file_block; +} +size_t SDCardInterface::disk_size_remaining () { + return (context.disk_info.number_blocks - context.next_valid_block) * context.disk_info.block_size; +} + +void SDCardInterface::beginTransaction () { + if (inTransaction) { + return ; + } + + inTransaction = true; + transactionFailed = false; + + snapshot = plume_save(&context); +} +void SDCardInterface::endTransaction () { + if (!inTransaction) { + return ; + } + + inTransaction = false; + lastTxFailed_ = transactionFailed; + + if (transactionFailed) { + plume_rollback(&context, &snapshot); + } +} + +uint8_t SDCardInterface::write (const uint8_t* buffer, int length) { + uint8_t worked = plume_write(&context, buffer, length); + + if (inTransaction && worked != PLUME_OK) { + transactionFailed = true; + } + + return worked; +} +uint8_t SDCardInterface::tick () { + return plume_tick(&context); +} diff --git a/Drivers/Plume/Tests/Hardware/plume_manual_test.cpp b/Drivers/Plume/Tests/Hardware/plume_manual_test.cpp new file mode 100644 index 0000000..d8a15bc --- /dev/null +++ b/Drivers/Plume/Tests/Hardware/plume_manual_test.cpp @@ -0,0 +1,68 @@ + +extern "C" { + #include "stm32hal.h" + #include "plume_manual_test.h" + #include "plume/status.h" + #include +} + +#include "plume_driver.hpp" + +const size_t plume_manual_test_arena_length = 64 * 1024; +uint8_t plume_manual_test_arena_buffer[plume_manual_test_arena_length] \ + __attribute__((aligned(32))) \ + __attribute__((section(".AXI_SRAM"))); + +void plume_manual_test (SD_HandleTypeDef *hsd) { + SDCardInterface interface; + + printf("[PLUME] SD Card & Plume -- Manual Test\r\n"); + printf("[PLUME] Initializing SD Card...\r\n"); + + if (!interface.init_sd_card(hsd, plume_manual_test_arena_buffer, plume_manual_test_arena_length)) { + printf("[PLUME] Failure of init. Exiting...\r\n"); + return ; + } + + printf("[PLUME] Remaining number of files: %lu\r\n", interface.number_files_remaining()); + printf("[PLUME] Remaining disk size: %lu\r\n", interface.disk_size_remaining()); + + printf("[PLUME] Opening file...\r\n"); + if (!interface.open_file()) { + printf("[PLUME] Failure of open. Exiting...\r\n"); + return ; + } + + printf("[PLUME] Remaining number of files: %lu\r\n", interface.number_files_remaining()); + printf("[PLUME] Remaining disk size: %lu\r\n", interface.disk_size_remaining()); + + printf("[PLUME] Writing 'Hello, World !\\n'\r\n"); + if (interface.write((const uint8_t*) "Hello, World !\n", 16) != PLUME_OK) { + printf("[PLUME] Failure of write. Exiting\r\n"); + return ; + } + + printf("[PLUME] Starting write of 256 kB.\r\n"); + uint32_t start_tick = HAL_GetTick(); + for (uint32_t i = 0; i < 256 * 256; i ++) { + uint32_t j = ((i ^ 0b1101100110111000) << 16) | i; + + uint8_t status = interface.write((const uint8_t*) (&j), sizeof(uint32_t)); + if (status != PLUME_OK) { + printf("[PLUME] Failure of write for %u. Error code: %u\r\n", i, (uint32_t) status); + return ; + } + + status = interface.tick(); + if (status != PLUME_OK) { + printf("[PLUME] Failure of tick at %u. Error code: %u\r\n", i, (uint32_t) status); + return ; + } + } + + uint32_t end_tick = HAL_GetTick(); + printf("[PLUME] Done in %u ticks.\r\n", end_tick - start_tick); + + printf("[PLUME] Remaining number of files: %lu\r\n", interface.number_files_remaining()); + printf("[PLUME] Remaining disk size: %lu\r\n", interface.disk_size_remaining()); +} diff --git a/Drivers/Plume/Tests/Hardware/plume_manual_test.h b/Drivers/Plume/Tests/Hardware/plume_manual_test.h new file mode 100644 index 0000000..3466cd4 --- /dev/null +++ b/Drivers/Plume/Tests/Hardware/plume_manual_test.h @@ -0,0 +1,4 @@ + +#include "stm32hal.h" + +void plume_manual_test (SD_HandleTypeDef *hsd); diff --git a/Drivers/Plume/Tests/Hardware/plume_validate.py b/Drivers/Plume/Tests/Hardware/plume_validate.py new file mode 100644 index 0000000..04ae37e --- /dev/null +++ b/Drivers/Plume/Tests/Hardware/plume_validate.py @@ -0,0 +1,64 @@ + +import argparse +import struct + +def hello_world (args): + return b"Hello, World !\n" + bytes([0]) +def data_buffer (args): + buffer = [] + + for i in range(256 * 256): + j = ((i ^ 0b1101100110111000) << 16) | i + + buffer.append(struct.pack("tick(); + } + + auto withInternalHealth (BaseStorageHealth health) { + StorageHealth fullHealth; + fullHealth.health = health; + fullHealth.timing = sd_timing_snapshot(); + fullHealth.disk_size_remaining = sd_->disk_size_remaining(); + fullHealth.arena_total_bytes = sd_->arena_total_bytes(); + fullHealth.arena_used_bytes = sd_->arena_used_bytes(); + + return fullHealth; + } + uint32_t now_us () { + return HAL_GetTick() * 1000; + } + + void beginTransaction () { + sd_->beginTransaction(); + } + void endTransaction () { + sd_->endTransaction(); + } + + bool write (const uint8_t* payload, uint16_t payload_len) { + return plume_is_ok(sd_->write(payload, payload_len)); + } +}; diff --git a/Drivers/Plume/sd_hardware_init.c b/Drivers/Plume/sd_hardware_init.c new file mode 100644 index 0000000..5887b0f --- /dev/null +++ b/Drivers/Plume/sd_hardware_init.c @@ -0,0 +1,102 @@ +/** + * @file sd_hardware_init.c + * @brief SDMMC1 hardware initialization for SD card access. + * + * This file is tracked by git (unlike stm32h7xx_hal_msp.c / stm32h7xx_it.c + * which are CubeMX-generated and gitignored). + * + * Call sd_pre_init() from main.c BEFORE MX_SDMMC1_SD_Init(). + * This configures PLL2 (200 MHz), SDMMC1 peripheral clock, GPIO pins, + * and NVIC for DMA interrupts — everything that HAL_SD_MspInit would + * normally do, but in a reproducible, version-controlled file. + * + * After HAL_SD_Init completes, call sd_post_init() to switch to + * High Speed mode (SDR25, 50 MHz). + */ + +#include "main.h" +#include + +/* ------------------------------------------------------------------ */ +/* Pre-init: clock, GPIO, NVIC — call BEFORE MX_SDMMC1_SD_Init() */ +/* ------------------------------------------------------------------ */ +void sd_pre_init(void) +{ + GPIO_InitTypeDef GPIO_InitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + /* ---- PLL2 → 200 MHz kernel clock for SDMMC1 ---- */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_SDMMC; + PeriphClkInitStruct.SdmmcClockSelection = RCC_SDMMCCLKSOURCE_PLL2; + PeriphClkInitStruct.PLL2.PLL2M = 32; /* HSI 64 MHz / 32 = 2 MHz VCO input */ + PeriphClkInitStruct.PLL2.PLL2N = 200; /* 2 MHz × 200 = 400 MHz VCO */ + PeriphClkInitStruct.PLL2.PLL2P = 2; + PeriphClkInitStruct.PLL2.PLL2Q = 2; + PeriphClkInitStruct.PLL2.PLL2R = 2; /* 400 / 2 = 200 MHz */ + PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0; + PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2VCOMEDIUM; + PeriphClkInitStruct.PLL2.PLL2FRACN = 0; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + /* Non-fatal: HAL_SD_Init will fail gracefully */ + } + + /* ---- SDMMC1 peripheral clock ---- */ + __HAL_RCC_SDMMC1_CLK_ENABLE(); + + /* ---- GPIO ---- */ + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOD_CLK_ENABLE(); + + /* PC8 = D0 PC9 = D1 PC10 = D2 PC11 = D3 (AF12, pull-up) + * PC12 = CK (AF12, no pull) + * PD2 = CMD (AF12, pull-up) + */ + GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + GPIO_InitStruct.Alternate = GPIO_AF12_SDMMC1; + HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = GPIO_PIN_12; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = GPIO_PIN_2; + GPIO_InitStruct.Pull = GPIO_PULLUP; + HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); + + /* ---- NVIC for DMA / interrupt-driven writes ---- */ + /* Priority 1 (was 2): reduce ISR preemption that causes 100ms+ DMA + * transfer outliers. Must remain below SPI DMA priority (0). */ + HAL_NVIC_SetPriority(SDMMC1_IRQn, 1, 0); + HAL_NVIC_EnableIRQ(SDMMC1_IRQn); +} + +/* ------------------------------------------------------------------ */ +/* Post-init: HS mode switch — call AFTER successful HAL_SD_Init() */ +/* ------------------------------------------------------------------ */ +void sd_post_init(SD_HandleTypeDef *hsd) +{ + if (hsd->State != HAL_SD_STATE_READY) return; + + /* Switch card to High Speed (SDR25, up to 50 MHz) */ + if (HAL_SD_ConfigSpeedBusOperation(hsd, SDMMC_SPEED_MODE_HIGH) == HAL_OK) + { + /* PLL2R = 200 MHz, ClockDiv = 2 → SDMMC_CK = 200/(2×2) = 50 MHz */ + MODIFY_REG(hsd->Instance->CLKCR, SDMMC_CLKCR_CLKDIV, 2U); + hsd->Init.ClockDiv = 2; + } +} + +/* ------------------------------------------------------------------ */ +/* SDMMC1 IRQ handler — needed for HAL_SD_WriteBlocks_DMA() */ +/* Overrides the weak Default_Handler from startup_stm32h743zitx.s */ +/* ------------------------------------------------------------------ */ +extern SD_HandleTypeDef hsd1; /* defined in main.c */ + +void SDMMC1_IRQHandler(void) +{ + HAL_SD_IRQHandler(&hsd1); +} \ No newline at end of file diff --git a/Drivers/Plume/sd_hardware_init.h b/Drivers/Plume/sd_hardware_init.h new file mode 100644 index 0000000..3ff4849 --- /dev/null +++ b/Drivers/Plume/sd_hardware_init.h @@ -0,0 +1,26 @@ +#ifndef SD_HARDWARE_INIT_H +#define SD_HARDWARE_INIT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "main.h" + +/** + * @brief Configure PLL2, GPIO, SDMMC1 clock, and NVIC. + * Call BEFORE MX_SDMMC1_SD_Init(). + */ +void sd_pre_init(void); + +/** + * @brief Switch to High Speed mode (50 MHz SDMMC clock). + * Call AFTER successful HAL_SD_Init(). + */ +void sd_post_init(SD_HandleTypeDef *hsd); + +#ifdef __cplusplus +} +#endif + +#endif /* SD_HARDWARE_INIT_H */ \ No newline at end of file diff --git a/Drivers/Plume/types.hpp b/Drivers/Plume/types.hpp new file mode 100644 index 0000000..cb4bc1c --- /dev/null +++ b/Drivers/Plume/types.hpp @@ -0,0 +1,19 @@ + +#include + +/* SD card DMA timing statistics (populated by plume_driver, read from main) */ +struct SdTimingStats { + volatile uint64_t dma_start_us; + volatile uint64_t dma_cb_us; + uint32_t last_batch_size; + uint32_t dma_count; + uint32_t dma_error_count; + uint32_t max_xfer_us; /* max DMA transfer time (start→callback) */ + uint32_t max_prog_us; /* max card programming (callback→ready) */ + uint32_t max_cycle_us; /* max full cycle (start→ready) */ + uint64_t sum_cycle_us; + uint32_t total_blocks; + uint32_t min_batch; + uint32_t max_batch; + uint32_t last_error_code; /* hsd->ErrorCode from last error callback */ +}; diff --git a/ThirdParty/DataLogger/types.hpp b/ThirdParty/DataLogger/types.hpp index 66eb321..27df796 100644 --- a/ThirdParty/DataLogger/types.hpp +++ b/ThirdParty/DataLogger/types.hpp @@ -2,6 +2,7 @@ #include #include "Application/Data/data.hpp" #include "Modules/Sensors/impl/std/multi.hpp" +#include "Drivers/Plume/types.hpp" struct LogHeader { uint8_t magic; @@ -18,6 +19,15 @@ struct BaseStorageHealth { uint32_t tick_count_ = 0; }; +struct StorageHealth { + BaseStorageHealth health; + SdTimingStats timing; + + size_t disk_size_remaining; + size_t arena_used_bytes; + size_t arena_total_bytes; +}; + struct pressures_frame { double pressure; double pressure_mean; From e802c8ac2c583759405f635fb46902f1ce883a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Tue, 18 Aug 2026 14:28:44 +0200 Subject: [PATCH 4/7] feat/logger: added client code using c++-26 reflection --- ThirdParty/DataLogger/Client/annotations.hpp | 32 +++++++ ThirdParty/DataLogger/Client/header.hpp | 88 ++++++++++++++++++ ThirdParty/DataLogger/Client/main.cpp | 28 ++++++ ThirdParty/DataLogger/Client/types.hpp | 16 ++++ ThirdParty/DataLogger/Client/value.hpp | 94 ++++++++++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 ThirdParty/DataLogger/Client/annotations.hpp create mode 100644 ThirdParty/DataLogger/Client/header.hpp create mode 100644 ThirdParty/DataLogger/Client/main.cpp create mode 100644 ThirdParty/DataLogger/Client/types.hpp create mode 100644 ThirdParty/DataLogger/Client/value.hpp diff --git a/ThirdParty/DataLogger/Client/annotations.hpp b/ThirdParty/DataLogger/Client/annotations.hpp new file mode 100644 index 0000000..d89b1ea --- /dev/null +++ b/ThirdParty/DataLogger/Client/annotations.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +namespace csv { + +struct ignore_t {}; +inline constexpr ignore_t ignore{}; + +struct rename { + char value[32]{}; + + constexpr rename(const char *s) { + std::size_t i = 0; + while (s[i] != '\0' && i < 31) { value[i] = s[i]; ++i; } + } + + constexpr std::string_view name() const { + std::size_t len = 0; + while (len < 32 && value[len] != '\0') ++len; + return std::string_view(value, len); + } +}; + +} + +#if defined(__glibcxx_reflection) && __glibcxx_reflection >= 202506L + #define CSV_IGNORE [[=csv::ignore]] + #define CSV_RENAME(name) [[=csv::rename(name)]] +#else + #define CSV_IGNORE + #define CSV_RENAME(name) +#endif diff --git a/ThirdParty/DataLogger/Client/header.hpp b/ThirdParty/DataLogger/Client/header.hpp new file mode 100644 index 0000000..54a9248 --- /dev/null +++ b/ThirdParty/DataLogger/Client/header.hpp @@ -0,0 +1,88 @@ + +#include "./types.hpp" +#include "./annotations.hpp" +#include +#include +#include + +namespace csv { + +template +struct header { + using type = T; + + bool first = true; + std::string field; +}; + +}; + +template +std::ostream& operator<<(std::ostream& os, const csv::header& x); + +#define CSV_HEADER_BASE_FN(type) \ + std::ostream& operator<< (std::ostream& os, const csv::header &x) { \ + if (!x.first) os << ","; \ + os << x.field; \ + return os; \ + } + +#define X(type) \ + template<> \ + CSV_HEADER_BASE_FN(type) +X_PRIMITIVE_TYPES +#undef X + + +template + requires std::is_enum_v +CSV_HEADER_BASE_FN(T) + +template +std::ostream& operator<<(std::ostream &os, const csv::header> &x) { + for (size_t i = 0; i < N; i ++) { + os << csv::header{ x.first && (i == 0), x.field + "[" + std::to_string(i) + "]" }; + } + return os; +} + +template + requires std::is_aggregate_v +std::ostream& operator<<(std::ostream &os, const csv::header& x) { + bool first = x.first; + + static constexpr auto members = + std::define_static_array( + std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()) + ); + + template for (constexpr auto member : members) { + constexpr bool skip = ([member] { + for (auto anno : std::meta::annotations_of(member)) + if (std::meta::remove_cv(std::meta::type_of(anno)) == ^^csv::ignore_t) + return true; + return false; + })(); + + if constexpr (skip) continue; + + constexpr std::string_view name_view = ([member]() -> std::string_view { + for (auto anno : std::meta::annotations_of(member)) { + if (std::meta::remove_cv(std::meta::type_of(anno)) == ^^csv::rename) { + auto renamed = std::meta::extract(anno); + return std::define_static_string(renamed.name()); + } + } + return std::meta::identifier_of(member); + })(); + + std::string name = std::string(name_view); + + using FieldT = [: std::meta::type_of(member) :]; + + os << csv::header{ first, (x.field == "") ? name : (x.field + "." + name) }; + first = false; + } + + return os; +} diff --git a/ThirdParty/DataLogger/Client/main.cpp b/ThirdParty/DataLogger/Client/main.cpp new file mode 100644 index 0000000..bed820f --- /dev/null +++ b/ThirdParty/DataLogger/Client/main.cpp @@ -0,0 +1,28 @@ + +#include +#include + +#include +#include "./header.hpp" +#include "./value.hpp" +#include "./annotations.hpp" + +struct CsvChannel {}; + +enum E { A, B }; +struct SampleType { + int a; + + CSV_RENAME("state") + E b; + + CSV_IGNORE + int c; + + std::array e; +}; + +int main (void) { + std::cout << csv::header{} << std::endl; + std::cout << csv::value{ {42, (E) 3, 3, { 0, 1, 0, 0 }} } << std::endl; +} diff --git a/ThirdParty/DataLogger/Client/types.hpp b/ThirdParty/DataLogger/Client/types.hpp new file mode 100644 index 0000000..a626c70 --- /dev/null +++ b/ThirdParty/DataLogger/Client/types.hpp @@ -0,0 +1,16 @@ + +#define X_PRIMITIVE_TYPES \ + X(bool) \ + X(unsigned char) \ + X(char) \ + X(signed char) \ + X(unsigned short) \ + X(short) \ + X(unsigned int) \ + X(int) \ + X(unsigned long) \ + X(long) \ + X(long long) \ + X(unsigned long long) \ + X(float) \ + X(double) diff --git a/ThirdParty/DataLogger/Client/value.hpp b/ThirdParty/DataLogger/Client/value.hpp new file mode 100644 index 0000000..260783d --- /dev/null +++ b/ThirdParty/DataLogger/Client/value.hpp @@ -0,0 +1,94 @@ + +#include "./types.hpp" +#include +#include +#include + +namespace csv { + +template +struct value { + using type = T; + + T value; + bool first = true; +}; + +}; + +template +std::ostream& operator<<(std::ostream& os, const csv::value& x); + +#define CSV_VALUE_BASE_FN(type) \ + std::ostream& operator<< (std::ostream& os, const csv::value &x) { \ + if (!x.first) os << ","; \ + os << x.value; \ + return os; \ + } + +#define X(type) \ + template<> \ + CSV_VALUE_BASE_FN(type) +X_PRIMITIVE_TYPES +#undef X + +template + requires std::is_enum_v +std::ostream& operator<<(std::ostream &os, const csv::value& x) { + if (!x.first) os << ","; + + static constexpr auto enumerators = std::define_static_array( + std::meta::enumerators_of(^^T) + ); + + bool found = false; + template for (constexpr auto e : enumerators) { + if (!found && x.value == [:e:]) { + os << std::meta::identifier_of(e); + found = true; + } + } + + if (!found) { + os << "UNKNOWN"; + } + + return os; +} + +template +std::ostream& operator<<(std::ostream &os, const csv::value> &x) { + for (size_t i = 0; i < N; i ++) { + os << csv::value{ x.value[i], x.first && (i == 0) }; + } + return os; +} + +template + requires std::is_aggregate_v +std::ostream& operator<<(std::ostream &os, const csv::value& x) { + bool first = x.first; + + static constexpr auto members = + std::define_static_array( + std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()) + ); + + template for (constexpr auto member : members) { + constexpr bool skip = ([member] { + for (auto anno : std::meta::annotations_of(member)) + if (std::meta::remove_cv(std::meta::type_of(anno)) == ^^csv::ignore_t) + return true; + return false; + })(); + + if constexpr (skip) continue; + + using FieldT = [: std::meta::type_of(member) :]; + + os << csv::value{ x.value.[: member :], first }; + first = false; + } + + return os; +} From 434ef5b0afec8fac9beed68af6288e634cebed44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Tue, 18 Aug 2026 16:28:19 +0200 Subject: [PATCH 5/7] feat/logger: finished channel writers --- Application/Data/data.cpp | 2 - Modules/Sensors/pipelines/branch.hpp | 3 +- Modules/Sensors/pipelines/error.hpp | 3 +- Modules/Sensors/pipelines/outlier.hpp | 4 +- ThirdParty/DataLogger/Client/channel.hpp | 51 ++++++++++++ ThirdParty/DataLogger/Client/container.hpp | 91 ++++++++++++++++++++++ ThirdParty/DataLogger/Client/header.hpp | 23 ++++-- ThirdParty/DataLogger/Client/main.cpp | 35 ++++----- ThirdParty/DataLogger/Client/outputs.hpp | 0 ThirdParty/DataLogger/Client/types.hpp | 30 +++---- ThirdParty/DataLogger/Client/value.hpp | 20 ++++- ThirdParty/DataLogger/loggers/engine.hpp | 8 +- ThirdParty/DataLogger/loggers/eth.hpp | 10 +-- ThirdParty/DataLogger/loggers/lox.hpp | 10 +-- ThirdParty/DataLogger/types.hpp | 16 ++++ 15 files changed, 240 insertions(+), 66 deletions(-) create mode 100644 ThirdParty/DataLogger/Client/channel.hpp create mode 100644 ThirdParty/DataLogger/Client/container.hpp create mode 100644 ThirdParty/DataLogger/Client/outputs.hpp diff --git a/Application/Data/data.cpp b/Application/Data/data.cpp index 60369ca..eba5775 100644 --- a/Application/Data/data.cpp +++ b/Application/Data/data.cpp @@ -1,7 +1,5 @@ #include "Application/Data/data.hpp" -#include "stm32h7xx_hal.h" - using namespace prc; StateStore::StateStore() { data_ = State::MANUAL; } diff --git a/Modules/Sensors/pipelines/branch.hpp b/Modules/Sensors/pipelines/branch.hpp index d19f5f1..c7c73d5 100644 --- a/Modules/Sensors/pipelines/branch.hpp +++ b/Modules/Sensors/pipelines/branch.hpp @@ -25,8 +25,9 @@ struct BranchUnpackPipeline { static constexpr std::size_t NumberSetters = sizeof...(Setters); public: + template inline auto ingest ( - const std::array &data, + const std::array &data, const std::array &valid) noexcept { std::apply([&data, &valid](auto&... setter) { size_t idx = 0; diff --git a/Modules/Sensors/pipelines/error.hpp b/Modules/Sensors/pipelines/error.hpp index 2e60147..4367316 100644 --- a/Modules/Sensors/pipelines/error.hpp +++ b/Modules/Sensors/pipelines/error.hpp @@ -8,7 +8,8 @@ struct IfPipeline { Success success; Error error; public: - void ingest (const result &data) noexcept { + template + void ingest (const result &data) noexcept { if (data.is_success()) { success.ingest(data.get_value()); } else { diff --git a/Modules/Sensors/pipelines/outlier.hpp b/Modules/Sensors/pipelines/outlier.hpp index 4c86e67..12fe9e1 100644 --- a/Modules/Sensors/pipelines/outlier.hpp +++ b/Modules/Sensors/pipelines/outlier.hpp @@ -10,9 +10,9 @@ namespace outlier_pipeline { template struct Frame { /* Array of values from the NumberInputs sensors */ - double values[NumberInputs]; + std::array values; /* Whether the sensor is an outlier */ - bool is_outlier[NumberInputs]; + std::array is_outlier; /* Number of used data points */ size_t number_used; diff --git a/ThirdParty/DataLogger/Client/channel.hpp b/ThirdParty/DataLogger/Client/channel.hpp new file mode 100644 index 0000000..a939100 --- /dev/null +++ b/ThirdParty/DataLogger/Client/channel.hpp @@ -0,0 +1,51 @@ + +#include "./header.hpp" +#include "./value.hpp" +#include +#include +#include +#include + +template +struct CsvChannel { +private: + std::function get_stream; + std::string stream_name; + + std::ostream* os = nullptr; + bool header_written = false; + bool stream_init = false; + + void init_stream () { + if (stream_init) return ; + stream_init = true; + + os = &get_stream(stream_name); + } +public: + CsvChannel () = default; + CsvChannel ( + std::function get_st, + std::string st_name + ) : stream_name(st_name), get_stream(get_st) {} + + void write_header () { + if (header_written) return ; + header_written = true; + + init_stream(); + + *os << csv::header{ .first = true, .field = "ts_us" } + << csv::header{ .first = false, .field = "" } + << "\n"; + } + + void aggregate (uint64_t timestamp_ms, const T &object) { + init_stream(); + write_header(); + + *os << csv::value{ .value = timestamp_ms, .first = true } + << csv::value{ .value = object, .first = false } + << "\n"; + } +}; diff --git a/ThirdParty/DataLogger/Client/container.hpp b/ThirdParty/DataLogger/Client/container.hpp new file mode 100644 index 0000000..d28ede9 --- /dev/null +++ b/ThirdParty/DataLogger/Client/container.hpp @@ -0,0 +1,91 @@ + +#include +#include "./channel.hpp" +#include "../types.hpp" + +#define X_CHANNELS \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_HEALTH, StorageHealth, "prc/engine/StorageHealth.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_DATA_DUMP, prc::DataDump, "prc/engine/DataDump.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_FSM_TRANSITION, engine_fsm_transition, "prc/engine/FsmTransitions.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_CHAMBER_FRAME, pressure_temperature_frame, "prc/engine/sensors/Chamber.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_EIN_P_FRAME, pressures_frame, "prc/engine/sensors/EIN-P.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_EIN_T_FRAME, temperature_frame, "prc/engine/sensors/EIN-T.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_OIN_P_FRAME, pressures_frame, "prc/engine/sensors/OIN-P.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_OIN_T_FRAME, temperature_frame, "prc/engine/sensors/OIN-T.csv") \ + X_CHANNEL(ENGINE_LOGGER_MAGIC, engine, LOG_ERROR, engine::ErrorKind, "prc/engine/Errors.csv") \ + \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_HEALTH, StorageHealth, "prc/lox/StorageHealth.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_DATA_DUMP, prc::DataDump, "prc/lox/DataDump.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_FSM_TRANSITION, dpr_fsm_transition, "prc/lox/FsmTransitions.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_FLS, double, "prc/lox/sensors/FLS.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_HPO_FRAME, pressures_frame, "prc/lox/sensors/HPO.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_OTA_P_FRAME, lox::OtaPressureFrame, "prc/lox/sensors/OTA-P.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_OTA_T_FRAME, lox::OtaTemperatureFrame, "prc/lox/sensors/OTA-T.csv") \ + X_CHANNEL(LOX_LOGGER_MAGIC, lox, LOG_ERROR, lox::ErrorKind, "prc/lox/Errors.csv") \ + \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_HEALTH, StorageHealth, "prc/eth/StorageHealth.csv") \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_DATA_DUMP, prc::DataDump, "prc/eth/DataDump.csv") \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_FSM_TRANSITION, dpr_fsm_transition, "prc/eth/FsmTransitions.csv") \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_HPE_FRAME, pressures_frame, "prc/eth/sensors/HPE.csv") \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_ETA_P_FRAME, eth::EtaPressureFrame, "prc/eth/sensors/ETA.csv") \ + X_CHANNEL(ETH_LOGGER_MAGIC, eth, LOG_ERROR, eth::ErrorKind, "prc/eth/Errors.csv") + +#define CONCAT_IMPL(a, b) a##b +#define CONCAT(a, b) CONCAT_IMPL(a, b) + +#define CHANNEL_VAR(ns, RecordType) CONCAT(Channel_, CONCAT(ns, _##RecordType)) + +struct CsvChannelContainer { +private: + #define X_CHANNEL(Magic, ns, RecordType, Typename, FileName) \ + CsvChannel CHANNEL_VAR(ns, RecordType); + X_CHANNELS + #undef X_CHANNEL + + bool has_init = false; + uint8_t exp_magic = 0; + bool init_with_magic (uint8_t magic) { + if (has_init) return magic == exp_magic; + exp_magic = magic; + has_init = true; + + #define X_CHANNEL(Magic, ns, RecordType, Typename, FileName) \ + if (Magic == magic) { CHANNEL_VAR(ns, RecordType).write_header(); } + X_CHANNELS + #undef X_CHANNEL + return true; + } +public: + CsvChannelContainer ( + std::function get_channel_stream + ) { + #define X_CHANNEL(Magic, ns, RecordType, Typename, FileName) \ + CHANNEL_VAR(ns, RecordType) = CsvChannel(get_channel_stream, FileName); + X_CHANNELS + #undef X_CHANNEL + } + + void ingest (LogHeader header, const void* payload) { + if (!init_with_magic(header.magic)) { + throw std::runtime_error("Invalid magic."); + } + + bool found = false; + + #define X_CHANNEL(Magic, ns, RecordType, Typename, FileName) \ + if (Magic == header.magic && ns::RecordType == header.record_type) { \ + CHANNEL_VAR(ns, RecordType).aggregate(header.timestamp_us, *((const Typename*) payload)); \ + found = true; \ + } + X_CHANNELS + #undef X_CHANNEL + + if (!found) { + throw std::runtime_error("Invalid record type."); + } + } + +}; + +#undef CONCAT_IMPL +#undef CONCAT \ No newline at end of file diff --git a/ThirdParty/DataLogger/Client/header.hpp b/ThirdParty/DataLogger/Client/header.hpp index 54a9248..fb08de7 100644 --- a/ThirdParty/DataLogger/Client/header.hpp +++ b/ThirdParty/DataLogger/Client/header.hpp @@ -17,18 +17,31 @@ struct header { }; -template -std::ostream& operator<<(std::ostream& os, const csv::header& x); +#define X(type) \ + std::ostream& operator<< (std::ostream& os, const csv::header &x); +X_PRIMITIVE_TYPES +#undef X + +template + requires std::is_enum_v +std::ostream& operator<< (std::ostream& os, const csv::header &x); + +template +std::ostream& operator<<(std::ostream &os, const csv::header> &x); + +template + requires std::is_class_v +std::ostream& operator<<(std::ostream &os, const csv::header& x); #define CSV_HEADER_BASE_FN(type) \ std::ostream& operator<< (std::ostream& os, const csv::header &x) { \ if (!x.first) os << ","; \ - os << x.field; \ + if (x.field == "") os << "value"; \ + else os << x.field; \ return os; \ } #define X(type) \ - template<> \ CSV_HEADER_BASE_FN(type) X_PRIMITIVE_TYPES #undef X @@ -47,7 +60,7 @@ std::ostream& operator<<(std::ostream &os, const csv::header> & } template - requires std::is_aggregate_v + requires std::is_class_v std::ostream& operator<<(std::ostream &os, const csv::header& x) { bool first = x.first; diff --git a/ThirdParty/DataLogger/Client/main.cpp b/ThirdParty/DataLogger/Client/main.cpp index bed820f..11034b1 100644 --- a/ThirdParty/DataLogger/Client/main.cpp +++ b/ThirdParty/DataLogger/Client/main.cpp @@ -3,26 +3,21 @@ #include #include -#include "./header.hpp" -#include "./value.hpp" -#include "./annotations.hpp" - -struct CsvChannel {}; - -enum E { A, B }; -struct SampleType { - int a; - - CSV_RENAME("state") - E b; - - CSV_IGNORE - int c; - - std::array e; -}; +#include "./container.hpp" int main (void) { - std::cout << csv::header{} << std::endl; - std::cout << csv::value{ {42, (E) 3, 3, { 0, 1, 0, 0 }} } << std::endl; + CsvChannelContainer container( + [](const std::string &_) -> std::ostream& { + std::ostream &st = std::cout; + return st; + } + ); + + prc::DataDump dump; + container.ingest({ + .magic = ENGINE_LOGGER_MAGIC, + .record_type = engine::LOG_DATA_DUMP, + .length = sizeof(dump), + .timestamp_us = 124 + }, &dump); } diff --git a/ThirdParty/DataLogger/Client/outputs.hpp b/ThirdParty/DataLogger/Client/outputs.hpp new file mode 100644 index 0000000..e69de29 diff --git a/ThirdParty/DataLogger/Client/types.hpp b/ThirdParty/DataLogger/Client/types.hpp index a626c70..9084b53 100644 --- a/ThirdParty/DataLogger/Client/types.hpp +++ b/ThirdParty/DataLogger/Client/types.hpp @@ -1,16 +1,18 @@ +#define X_prim(T) X(T) X(volatile T) + #define X_PRIMITIVE_TYPES \ - X(bool) \ - X(unsigned char) \ - X(char) \ - X(signed char) \ - X(unsigned short) \ - X(short) \ - X(unsigned int) \ - X(int) \ - X(unsigned long) \ - X(long) \ - X(long long) \ - X(unsigned long long) \ - X(float) \ - X(double) + X_prim(bool) \ + X_prim(unsigned char) \ + X_prim(char) \ + X_prim(signed char) \ + X_prim(unsigned short) \ + X_prim(short) \ + X_prim(unsigned int) \ + X_prim(int) \ + X_prim(unsigned long) \ + X_prim(long) \ + X_prim(long long) \ + X_prim(unsigned long long) \ + X_prim(float) \ + X_prim(double) diff --git a/ThirdParty/DataLogger/Client/value.hpp b/ThirdParty/DataLogger/Client/value.hpp index 260783d..5ef7942 100644 --- a/ThirdParty/DataLogger/Client/value.hpp +++ b/ThirdParty/DataLogger/Client/value.hpp @@ -16,8 +16,21 @@ struct value { }; -template -std::ostream& operator<<(std::ostream& os, const csv::value& x); +#define X(type) \ + std::ostream& operator<< (std::ostream& os, const csv::value &x); +X_PRIMITIVE_TYPES +#undef X + +template + requires std::is_enum_v +std::ostream& operator<< (std::ostream& os, const csv::value &x); + +template +std::ostream& operator<<(std::ostream &os, const csv::value> &x); + +template + requires std::is_class_v +std::ostream& operator<<(std::ostream &os, const csv::value& x); #define CSV_VALUE_BASE_FN(type) \ std::ostream& operator<< (std::ostream& os, const csv::value &x) { \ @@ -27,7 +40,6 @@ std::ostream& operator<<(std::ostream& os, const csv::value& x); } #define X(type) \ - template<> \ CSV_VALUE_BASE_FN(type) X_PRIMITIVE_TYPES #undef X @@ -65,7 +77,7 @@ std::ostream& operator<<(std::ostream &os, const csv::value> &x } template - requires std::is_aggregate_v + requires std::is_class_v std::ostream& operator<<(std::ostream &os, const csv::value& x) { bool first = x.first; diff --git a/ThirdParty/DataLogger/loggers/engine.hpp b/ThirdParty/DataLogger/loggers/engine.hpp index 153b7f1..b704700 100644 --- a/ThirdParty/DataLogger/loggers/engine.hpp +++ b/ThirdParty/DataLogger/loggers/engine.hpp @@ -29,11 +29,9 @@ struct EngingDataLogger : public BaseDataLogger -struct EthDataLogger : public BaseDataLogger { +struct EthDataLogger : public BaseDataLogger { public: EthDataLogger () = default; EthDataLogger (Storage& storage) : BaseDataLogger(storage) {} @@ -19,11 +19,9 @@ struct EthDataLogger : public BaseDataLogger -struct LoxDataLogger : public BaseDataLogger { +struct LoxDataLogger : public BaseDataLogger { public: LoxDataLogger () = default; LoxDataLogger (Storage& storage) : BaseDataLogger(storage) {} @@ -26,11 +26,9 @@ struct LoxDataLogger : public BaseDataLogger #include "Application/Data/data.hpp" +#include "Application/Data/fsm.hpp" +#include "Application/Data/engine_fsm.hpp" #include "Modules/Sensors/impl/std/multi.hpp" #include "Drivers/Plume/types.hpp" +constexpr uint8_t ENGINE_LOGGER_MAGIC = 0xA4; +constexpr uint8_t LOX_LOGGER_MAGIC = 0xC1; +constexpr uint8_t ETH_LOGGER_MAGIC = 0x8B; + struct LogHeader { uint8_t magic; uint8_t record_type; @@ -44,6 +50,16 @@ struct pressure_temperature_frame { double temperature_mean; }; +struct engine_fsm_transition { + prc::EngineState old_state; + prc::EngineState new_state; +}; + +struct dpr_fsm_transition { + prc::State old_state; + prc::State new_state; +}; + namespace engine { enum ErrorKind { From 7fab545f75d0741b77ad26db9c7354f8f8460ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Hollender?= Date: Tue, 18 Aug 2026 21:49:02 +0200 Subject: [PATCH 6/7] feat/logger: setup plume manual --- .cproject | 10 +- 2026_C_AV_PRC.ioc | 942 +++++++++--------- Application/FlightControl/engine_state.cpp | 3 +- Application/FlightControl/prc_state.cpp | 4 +- Application/app_timebase.cpp | 212 ++++ Application/app_timebase.h | 19 + Application/main.cpp | 2 + Core/Src/main.c | 14 +- Drivers/FC_CAN/2026_C_AV_FC_PRC_INTRANET | 2 +- Drivers/Plume/Impl/plume_driver.cpp | 25 +- .../Tests/Hardware/plume_manual_test.cpp | 10 +- .../Plume/Tests/Hardware/plume_manual_test.h | 2 +- Drivers/Plume/plume_driver.hpp | 2 +- Drivers/Plume/plume_storage.hpp | 2 +- Modules/Sensors/pipelines/outlier.hpp | 2 +- ThirdParty/DataLogger/Client/.gitignore | 1 + ThirdParty/DataLogger/Client/main.cpp | 2 +- 17 files changed, 762 insertions(+), 492 deletions(-) create mode 100644 Application/app_timebase.cpp create mode 100644 Application/app_timebase.h create mode 100644 ThirdParty/DataLogger/Client/.gitignore diff --git a/.cproject b/.cproject index 05a6594..b2d688b 100644 --- a/.cproject +++ b/.cproject @@ -37,6 +37,7 @@ + @@ -68,6 +69,7 @@ + @@ -95,6 +97,7 @@ + @@ -230,6 +235,7 @@ +