diff --git a/CMakeLists.txt b/CMakeLists.txt index dc6e333..79c3419 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,9 @@ project(sc2-driver-io) # Set C++ standard to C++20 set(CMAKE_CXX_STANDARD 20) +# Find libcurl (required for InfluxDB HTTP writes) +find_package(CURL REQUIRED) + # Add shared sources to build set(SOURCES ${SOURCES} @@ -17,6 +20,8 @@ set(SOURCES gps/gps.cpp 3rdparty/serial/serialib.cpp Config.cpp + influx/src/InfluxWriter.cpp + influx/src/CsvParser.cpp ) # Add the header files @@ -30,6 +35,9 @@ set(HEADERS gps/gps.h 3rdparty/serial/serialib.h Config.h + influx/include/TelemetryRecord.h + influx/include/InfluxWriter.h + influx/include/CsvParser.h ) # Add the executable target @@ -41,4 +49,6 @@ if(UNIX) target_link_libraries(${PROJECT_NAME} PRIVATE pthread) endif() -target_include_directories(${PROJECT_NAME} PRIVATE ./) +target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl) + +target_include_directories(${PROJECT_NAME} PRIVATE ./ influx/include) diff --git a/data_processor/dataUnpacker.cpp b/data_processor/dataUnpacker.cpp index 6eab51f..73d4edf 100644 --- a/data_processor/dataUnpacker.cpp +++ b/data_processor/dataUnpacker.cpp @@ -4,6 +4,132 @@ #include "dataUnpacker.h" +// ───────────────────────────────────────────────────────────────────────────── +// buildTelemetryRecord +// Maps all live DataUnpacker fields into a TelemetryRecord for InfluxDB. +// ───────────────────────────────────────────────────────────────────────────── + +TelemetryRecord DataUnpacker::buildTelemetryRecord() const { + TelemetryRecord r; + + // ── MCC / Motor Control ────────────────────────────────────────────────── + r.accelerator_pedal = getAcceleratorPedal(); + r.speed = getSpeed(); + r.mc_status = static_cast(getMcStatus()); + r.crz_pwr_mode = getCrzPwrMode(); + r.crz_spd_mode = getCrzSpdMode(); + r.crz_pwr_setpt = getCrzPwrSetpt(); + r.crz_spd_setpt = getCrzSpdSetpt(); + r.eco = getEco(); + r.main_telem = getMainTelem(); + r.motor_power = getMotorPower(); + + // ── High Voltage / Shutdown ────────────────────────────────────────────── + r.driver_eStop = getDriverEStop(); + r.external_eStop = getExternalEStop(); + r.crash = getCrash(); + r.discharge_enable = getDischargeEnable(); + r.charge_enable = getChargeEnable(); + r.isolation = getIsolation(); + r.mcu_hv_en = getMcuHvEn(); + r.mcu_stat_fdbk = getMcuStatFdbk(); + + // ── High Voltage / MPS ─────────────────────────────────────────────────── + r.low_contactor = getLowContactor(); + r.use_dcdc = getUseDcdc(); + + // ── Battery / Supplemental ─────────────────────────────────────────────── + r.supplemental_voltage = getSupplementalVoltage(); + r.est_supplemental_soc = getEstSupplementalSoc(); + + // ── Main IO / Sensors ──────────────────────────────────────────────────── + r.park_brake = getParkingBrake(); + r.mainIO_temp = getMainIOTemp(); + r.motor_controller_temp = getMotorControllerTemp(); + r.motor_temp = getMotorTemp(); + + // ── Main IO / Lights ───────────────────────────────────────────────────── + r.l_turn_led_en = getLTurnLedEn(); + r.r_turn_led_en = getRTurnLedEn(); + r.headlights_led_en = getHeadlights(); + r.hazards = getHazards(); + + // ── Main IO / Firmware Heartbeats ──────────────────────────────────────── + r.bms_can_heartbeat = getBmsCanHeartbeat(); + r.mainIO_heartbeat = getMainIOHeartbeat(); + + // ── Solar / MPPT ───────────────────────────────────────────────────────── + r.mppt_current_out = getMpptCurrentOut(); + r.string1_temp = getString1Temp(); + r.string2_temp = getString2Temp(); + r.string3_temp = getString3Temp(); + + // ── Battery / BMS CAN ──────────────────────────────────────────────────── + r.pack_temp = getPackTemp(); + r.pack_current = getPackCurrent(); + r.pack_voltage = getPackVoltage(); + r.soc = getSoc(); + r.fan_speed = static_cast(getFanSpeed()); + r.bms_input_voltage = getBmsInputVoltage(); + + // ── Battery / BMS Faults ───────────────────────────────────────────────── + r.bps_fault = getBpsFault(); + r.voltage_failsafe = getVoltageFailsafe(); + r.current_failsafe = getCurrentFailsafe(); + r.relay_failsafe = getRelayFailsafe(); + r.cell_balancing_active = getCellBalancingActive(); + r.charge_interlock_failsafe = getChargeInterlockFailsafe(); + r.thermistor_b_value_table_invalid = getThermistorBValueTableInvalid(); + r.input_power_supply_failsafe = getInputPowerSupplyFailsafe(); + + // ── Battery / Cell Group Voltages ──────────────────────────────────────── + const auto& cgv = getCellGroupVoltages(); + if (cgv.size() > 0) r.cell_group1_voltage = cgv[0]; + if (cgv.size() > 1) r.cell_group2_voltage = cgv[1]; + if (cgv.size() > 2) r.cell_group3_voltage = cgv[2]; + if (cgv.size() > 3) r.cell_group4_voltage = cgv[3]; + if (cgv.size() > 4) r.cell_group5_voltage = cgv[4]; + if (cgv.size() > 5) r.cell_group6_voltage = cgv[5]; + if (cgv.size() > 6) r.cell_group7_voltage = cgv[6]; + if (cgv.size() > 7) r.cell_group8_voltage = cgv[7]; + if (cgv.size() > 8) r.cell_group9_voltage = cgv[8]; + if (cgv.size() > 9) r.cell_group10_voltage = cgv[9]; + if (cgv.size() > 10) r.cell_group11_voltage = cgv[10]; + if (cgv.size() > 11) r.cell_group12_voltage = cgv[11]; + if (cgv.size() > 12) r.cell_group13_voltage = cgv[12]; + if (cgv.size() > 13) r.cell_group14_voltage = cgv[13]; + if (cgv.size() > 14) r.cell_group15_voltage = cgv[14]; + if (cgv.size() > 15) r.cell_group16_voltage = cgv[15]; + if (cgv.size() > 16) r.cell_group17_voltage = cgv[16]; + if (cgv.size() > 17) r.cell_group18_voltage = cgv[17]; + if (cgv.size() > 18) r.cell_group19_voltage = cgv[18]; + if (cgv.size() > 19) r.cell_group20_voltage = cgv[19]; + if (cgv.size() > 20) r.cell_group21_voltage = cgv[20]; + if (cgv.size() > 21) r.cell_group22_voltage = cgv[21]; + if (cgv.size() > 22) r.cell_group23_voltage = cgv[22]; + if (cgv.size() > 23) r.cell_group24_voltage = cgv[23]; + if (cgv.size() > 24) r.cell_group25_voltage = cgv[24]; + if (cgv.size() > 25) r.cell_group26_voltage = cgv[25]; + if (cgv.size() > 26) r.cell_group27_voltage = cgv[26]; + if (cgv.size() > 27) r.cell_group28_voltage = cgv[27]; + if (cgv.size() > 28) r.cell_group29_voltage = cgv[28]; + if (cgv.size() > 29) r.cell_group30_voltage = cgv[29]; + if (cgv.size() > 30) r.cell_group31_voltage = cgv[30]; + + // ── Software / Timestamps ──────────────────────────────────────────────── + r.tstamp_hr = static_cast(getTstampHr()); + r.tstamp_mn = static_cast(getTstampMn()); + r.tstamp_sc = static_cast(getTstampSc()); + r.tstamp_ms = static_cast(getTstampMs()); + + // ── Software / GPS ─────────────────────────────────────────────────────── + r.lat = getLat(); + r.lon = getLon(); + r.elev = getElev(); + + return r; +} + double bytesToDouble(const std::vector& data, int start_pos) { double number; diff --git a/data_processor/dataUnpacker.h b/data_processor/dataUnpacker.h index c3d0aa3..2088645 100644 --- a/data_processor/dataUnpacker.h +++ b/data_processor/dataUnpacker.h @@ -16,6 +16,7 @@ #include "backend/dataFetcher.h" #include "3rdparty/rapidjson/document.h" #include "3rdparty/rapidjson/filereadstream.h" +#include "TelemetryRecord.h" using namespace rapidjson; @@ -118,6 +119,10 @@ class DataUnpacker float getElev() const { return elev; } const std::vector& getCellGroupVoltages() const { return cell_group_voltages; } + + // Build a TelemetryRecord from current live data for InfluxDB publishing + TelemetryRecord buildTelemetryRecord() const; + private: bool checkRestartEnable(); diff --git a/influx/include/CsvParser.h b/influx/include/CsvParser.h new file mode 100644 index 0000000..6171651 --- /dev/null +++ b/influx/include/CsvParser.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include "TelemetryRecord.h" + +/** + * CsvParser + * + * Reads a CSV file whose columns match the signal names in data_format.json + * and returns a vector of TelemetryRecord objects. + * + * The first row of the CSV must be a header row containing field names. + * Missing or empty values are silently skipped (field retains its default). + */ +class CsvParser { +public: + /** + * Parse the CSV file at @p filePath. + * + * @throws std::runtime_error if the file cannot be opened. + * @returns A vector of fully-populated TelemetryRecord structs. + */ + static std::vector parse(const std::string& filePath); + +private: + // Split a single CSV line respecting quoted fields. + static std::vector splitLine(const std::string& line); + + // Apply one (header, value) pair to a TelemetryRecord. + static void applyField(TelemetryRecord& rec, + const std::string& name, + const std::string& value); + + // Helpers + static double toDouble(const std::string& s, double fallback = 0.0); + static bool toBool (const std::string& s, bool fallback = false); + static int64_t toInt64 (const std::string& s, int64_t fallback = 0); +}; diff --git a/influx/include/InfluxWriter.h b/influx/include/InfluxWriter.h new file mode 100644 index 0000000..e840188 --- /dev/null +++ b/influx/include/InfluxWriter.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include "TelemetryRecord.h" + +/** + * InfluxWriter + * + * Converts TelemetryRecord structs to InfluxDB Line Protocol strings and + * POSTs them to InfluxDB Cloud via libcurl. + * + * All numeric values are written as floats (no integer 'i' suffix) to prevent + * schema conflicts when a field contains mixed whole/fractional values across + * different records. + * + * Connection parameters are read from environment variables: + * INFLUX_URL – e.g. https://us-east-1-1.aws.cloud2.influxdata.com + * INFLUX_TOKEN – All-access or write token + * INFLUX_ORG – Organisation ID (hex string) + * INFLUX_BUCKET – Bucket name, e.g. sc2-telemetry + */ +class InfluxWriter { +public: + /** + * Load connection parameters from environment variables. + * @throws std::runtime_error if any required variable is missing. + */ + explicit InfluxWriter(); + + /** + * Convert @p rec to a single InfluxDB Line Protocol line. + * + * @param rec The telemetry record to encode. + * @param timestampNs Nanosecond-precision Unix timestamp. + * Pass 0 to omit (InfluxDB uses server time). + * @returns A line-protocol string, e.g.: + * sc2_telemetry speed=42.3,soc=87.1 1700000000000000000 + */ + std::string toLineProtocol(const TelemetryRecord& rec, + int64_t timestampNs = 0) const; + + /** + * HTTP POST a single Line Protocol string to InfluxDB. + * + * @throws std::runtime_error on curl/HTTP error or non-2xx response. + */ + void write(const std::string& lineProtocol) const; + + /** + * HTTP POST multiple Line Protocol lines (one per element) in a single + * request. Lines are joined with '\n'. + */ + void writeBatch(const std::vector& lines) const; + +private: + std::string url_; + std::string token_; + std::string org_; + std::string bucket_; + + // Perform the actual HTTP POST; throws on failure. + void httpPost(const std::string& body) const; +}; diff --git a/influx/include/TelemetryRecord.h b/influx/include/TelemetryRecord.h new file mode 100644 index 0000000..d7e4eb6 --- /dev/null +++ b/influx/include/TelemetryRecord.h @@ -0,0 +1,219 @@ +#pragma once +#include +#include + +/** + * TelemetryRecord + * + * Mirrors the full signal set defined in data/data_format.json. + * Field types match the data_format spec: + * float → double (4-byte float fields) + * bool → bool + * uint8 → uint8_t + * uint16 → uint16_t + * uint32 → uint32_t + * uint64 → uint64_t + * int32 → int32_t + * + * Default values are set to the "expected" safe state from data_format.json + * (the min value for most fields, max for contactors/heartbeats). + */ +struct TelemetryRecord { + + // ── MCC / Motor Control ─────────────────────────────────────────────────── + double accelerator_pedal = 0.0; + double speed = 0.0; + uint8_t mcc_state = 0; + bool fr_telem = false; + bool crz_pwr_mode = false; + bool crz_spd_mode = false; + double crz_pwr_setpt = 0.0; + double crz_spd_setpt = 0.0; + bool eco = false; + bool main_telem = false; + bool foot_brake = false; + double regen_brake = 0.0; + double motor_current = 0.0; + double motor_power = 0.0; + uint8_t mc_status = 0; + + // ── High Voltage / Shutdown ─────────────────────────────────────────────── + bool driver_eStop = false; + bool external_eStop = false; + bool crash = false; + bool discharge_enable = false; + bool discharge_enabled = false; + bool charge_enable = false; + bool charge_enabled = false; + bool isolation = false; + bool mcu_hv_en = false; + bool mcu_stat_fdbk = false; + + // ── High Voltage / MPS ─────────────────────────────────────────────────── + bool mppt_contactor = true; + bool motor_controller_contactor = true; + bool low_contactor = true; + double dcdc_current = 0.0; + bool dcdc_deg = true; + bool use_dcdc = false; + bool use_supp = false; + bool bms_mpio1 = false; + + // ── Battery / Supplemental ──────────────────────────────────────────────── + double supplemental_current = 0.0; + double supplemental_voltage = 0.0; + bool supplemental_deg = true; + double est_supplemental_soc = 0.0; + + // ── Main IO / Sensors ───────────────────────────────────────────────────── + bool park_brake = false; + double air_temp = 0.0; + double brake_temp = 0.0; + double dcdc_temp = 0.0; + double mainIO_temp = 0.0; + double motor_controller_temp = 0.0; + double motor_temp = 0.0; + double road_temp = 0.0; + + // ── Main IO / Lights ────────────────────────────────────────────────────── + bool l_turn_led_en = false; + bool r_turn_led_en = false; + bool brake_led_en = false; + bool headlights_led_en = false; + bool hazards = false; + + // ── Main IO / Power Bus ─────────────────────────────────────────────────── + double main_5V_bus = 0.0; + double main_12V_bus = 0.0; + double main_24V_bus = 0.0; + double main_5V_current = 0.0; + double main_12V_current = 0.0; + double main_24V_current = 0.0; + + // ── Main IO / Firmware Heartbeats ───────────────────────────────────────── + bool bms_can_heartbeat = true; + bool hv_can_heartbeat = true; + bool mainIO_heartbeat = true; + bool mcc_can_heartbeat = true; + bool mppt_can_heartbeat = true; + + // ── Solar Array / MPPT ──────────────────────────────────────────────────── + bool mppt_mode = false; + double mppt_current_out = 0.0; + double mppt_power_out = 0.0; + double string1_temp = 20.0; + double string2_temp = 20.0; + double string3_temp = 20.0; + double string1_V_in = 0.0; + double string2_V_in = 0.0; + double string3_V_in = 0.0; + double string1_I_in = 0.0; + double string2_I_in = 0.0; + double string3_I_in = 0.0; + + // ── Battery / BMS CAN ───────────────────────────────────────────────────── + double pack_temp = 0.0; + double pack_internal_temp = 0.0; + double pack_current = 0.0; + double pack_voltage = 77.5; + double pack_power = 0.0; + uint16_t populated_cells = 0; + double soc = 0.0; + double soh = 100.0; + double pack_amphours = 57.0; + double adaptive_total_capacity = 57.0; + uint8_t fan_speed = 0; + double pack_resistance = 0.0; + double bms_input_voltage = 12.0; + + // ── Battery / BMS Faults ────────────────────────────────────────────────── + bool bps_fault = false; + bool voltage_failsafe = false; + bool current_failsafe = false; + bool relay_failsafe = false; + bool cell_balancing_active = true; + bool charge_interlock_failsafe = false; + bool thermistor_b_value_table_invalid = false; + bool input_power_supply_failsafe = false; + bool discharge_limit_enforcement_fault = false; + bool charger_safety_relay_fault = false; + bool internal_hardware_fault = false; + bool internal_heatsink_fault = false; + bool internal_software_fault = false; + bool highest_cell_voltage_too_high_fault = false; + bool lowest_cell_voltage_too_low_fault = false; + bool pack_too_hot_fault = false; + bool high_voltage_interlock_signal_fault = false; + bool precharge_circuit_malfunction = false; + bool abnormal_state_of_charge_behavior = false; + bool internal_communication_fault = false; + bool cell_balancing_stuck_off_fault = false; + bool weak_cell_fault = false; + bool low_cell_voltage_fault = false; + bool open_wiring_fault = false; + bool current_sensor_fault = false; + bool highest_cell_voltage_over_5V_fault = false; + bool cell_asic_fault = false; + bool weak_pack_fault = false; + bool fan_monitor_fault = false; + bool thermistor_fault = false; + bool external_communication_fault = false; + bool redundant_power_supply_fault = false; + bool high_voltage_isolation_fault = false; + bool input_power_supply_fault = false; + bool charge_limit_enforcement_fault = false; + + // ── Battery / Cell Group Voltages ───────────────────────────────────────── + double cell_group1_voltage = 3.0; + double cell_group2_voltage = 3.0; + double cell_group3_voltage = 3.0; + double cell_group4_voltage = 3.0; + double cell_group5_voltage = 3.0; + double cell_group6_voltage = 3.0; + double cell_group7_voltage = 3.0; + double cell_group8_voltage = 3.0; + double cell_group9_voltage = 3.0; + double cell_group10_voltage = 3.0; + double cell_group11_voltage = 3.0; + double cell_group12_voltage = 3.0; + double cell_group13_voltage = 3.0; + double cell_group14_voltage = 3.0; + double cell_group15_voltage = 3.0; + double cell_group16_voltage = 3.0; + double cell_group17_voltage = 3.0; + double cell_group18_voltage = 3.0; + double cell_group19_voltage = 3.0; + double cell_group20_voltage = 3.0; + double cell_group21_voltage = 3.0; + double cell_group22_voltage = 3.0; + double cell_group23_voltage = 3.0; + double cell_group24_voltage = 3.0; + double cell_group25_voltage = 3.0; + double cell_group26_voltage = 3.0; + double cell_group27_voltage = 3.0; + double cell_group28_voltage = 3.0; + double cell_group29_voltage = 3.0; + double cell_group30_voltage = 3.0; + double cell_group31_voltage = 3.0; + + // ── Software / Timestamps ───────────────────────────────────────────────── + uint16_t tstamp_ms = 0; + uint8_t tstamp_sc = 0; + uint8_t tstamp_mn = 0; + uint8_t tstamp_hr = 0; + uint64_t tstamp_unix = 0; + + // ── Software / GPS ──────────────────────────────────────────────────────── + double lat = 43.0731; + double lon = -89.4012; + double elev = 0.0; + + // ── Software / Lap Counter ──────────────────────────────────────────────── + int32_t lap_count = 0; + int32_t current_section = 0; + uint32_t lap_duration = 0; + + // ── Race Strategy / Model Outputs ───────────────────────────────────────── + double optimized_target_power = 0.0; + double maximum_distance_traveled = 0.0; +}; diff --git a/influx/src/CsvParser.cpp b/influx/src/CsvParser.cpp new file mode 100644 index 0000000..a7b5a52 --- /dev/null +++ b/influx/src/CsvParser.cpp @@ -0,0 +1,298 @@ +#include "CsvParser.h" + +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Public entry point +// ───────────────────────────────────────────────────────────────────────────── + +std::vector CsvParser::parse(const std::string& filePath) { + std::ifstream file(filePath); + if (!file.is_open()) { + throw std::runtime_error("CsvParser: cannot open file: " + filePath); + } + + std::vector records; + std::string line; + + // ── Header row ─────────────────────────────────────────────────────────── + if (!std::getline(file, line)) { + return records; // empty file + } + std::vector headers = splitLine(line); + + // ── Data rows ──────────────────────────────────────────────────────────── + while (std::getline(file, line)) { + if (line.empty()) continue; + + std::vector values = splitLine(line); + TelemetryRecord rec; + + size_t count = std::min(headers.size(), values.size()); + for (size_t i = 0; i < count; ++i) { + applyField(rec, headers[i], values[i]); + } + records.push_back(rec); + } + + return records; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Private helpers +// ───────────────────────────────────────────────────────────────────────────── + +std::vector CsvParser::splitLine(const std::string& line) { + std::vector fields; + std::string field; + bool inQuotes = false; + + for (size_t i = 0; i < line.size(); ++i) { + char c = line[i]; + if (c == '"') { + if (inQuotes && i + 1 < line.size() && line[i + 1] == '"') { + field += '"'; + ++i; // skip escaped quote + } else { + inQuotes = !inQuotes; + } + } else if (c == ',' && !inQuotes) { + fields.push_back(field); + field.clear(); + } else { + field += c; + } + } + fields.push_back(field); // last field + return fields; +} + +double CsvParser::toDouble(const std::string& s, double fallback) { + if (s.empty()) return fallback; + try { return std::stod(s); } catch (...) { return fallback; } +} + +bool CsvParser::toBool(const std::string& s, bool fallback) { + if (s.empty()) return fallback; + std::string lower = s; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + if (lower == "true" || lower == "1" || lower == "yes") return true; + if (lower == "false" || lower == "0" || lower == "no") return false; + return fallback; +} + +int64_t CsvParser::toInt64(const std::string& s, int64_t fallback) { + if (s.empty()) return fallback; + try { return static_cast(std::stoll(s)); } catch (...) { return fallback; } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Field mapping (header name → struct member) +// ───────────────────────────────────────────────────────────────────────────── + +void CsvParser::applyField(TelemetryRecord& r, + const std::string& name, + const std::string& value) { + // ── MCC / Motor Control ────────────────────────────────────────────────── + if (name == "accelerator_pedal") { r.accelerator_pedal = toDouble(value); return; } + if (name == "speed") { r.speed = toDouble(value); return; } + if (name == "mcc_state") { r.mcc_state = static_cast(toInt64(value)); return; } + if (name == "fr_telem") { r.fr_telem = toBool(value); return; } + if (name == "crz_pwr_mode") { r.crz_pwr_mode = toBool(value); return; } + if (name == "crz_spd_mode") { r.crz_spd_mode = toBool(value); return; } + if (name == "crz_pwr_setpt") { r.crz_pwr_setpt = toDouble(value); return; } + if (name == "crz_spd_setpt") { r.crz_spd_setpt = toDouble(value); return; } + if (name == "eco") { r.eco = toBool(value); return; } + if (name == "main_telem") { r.main_telem = toBool(value); return; } + if (name == "foot_brake") { r.foot_brake = toBool(value); return; } + if (name == "regen_brake") { r.regen_brake = toDouble(value); return; } + if (name == "motor_current") { r.motor_current = toDouble(value); return; } + if (name == "motor_power") { r.motor_power = toDouble(value); return; } + if (name == "mc_status") { r.mc_status = static_cast(toInt64(value)); return; } + + // ── High Voltage / Shutdown ────────────────────────────────────────────── + if (name == "driver_eStop") { r.driver_eStop = toBool(value); return; } + if (name == "external_eStop") { r.external_eStop = toBool(value); return; } + if (name == "crash") { r.crash = toBool(value); return; } + if (name == "discharge_enable") { r.discharge_enable = toBool(value); return; } + if (name == "discharge_enabled") { r.discharge_enabled = toBool(value); return; } + if (name == "charge_enable") { r.charge_enable = toBool(value); return; } + if (name == "charge_enabled") { r.charge_enabled = toBool(value); return; } + if (name == "isolation") { r.isolation = toBool(value); return; } + if (name == "mcu_hv_en") { r.mcu_hv_en = toBool(value); return; } + if (name == "mcu_stat_fdbk") { r.mcu_stat_fdbk = toBool(value); return; } + + // ── High Voltage / MPS ────────────────────────────────────────────────── + if (name == "mppt_contactor") { r.mppt_contactor = toBool(value); return; } + if (name == "motor_controller_contactor") { r.motor_controller_contactor = toBool(value); return; } + if (name == "low_contactor") { r.low_contactor = toBool(value); return; } + if (name == "dcdc_current") { r.dcdc_current = toDouble(value); return; } + if (name == "dcdc_deg") { r.dcdc_deg = toBool(value); return; } + if (name == "use_dcdc") { r.use_dcdc = toBool(value); return; } + if (name == "use_supp") { r.use_supp = toBool(value); return; } + if (name == "bms_mpio1") { r.bms_mpio1 = toBool(value); return; } + + // ── Battery / Supplemental ─────────────────────────────────────────────── + if (name == "supplemental_current") { r.supplemental_current = toDouble(value); return; } + if (name == "supplemental_voltage") { r.supplemental_voltage = toDouble(value); return; } + if (name == "supplemental_deg") { r.supplemental_deg = toBool(value); return; } + if (name == "est_supplemental_soc") { r.est_supplemental_soc = toDouble(value); return; } + + // ── Main IO / Sensors ──────────────────────────────────────────────────── + if (name == "park_brake") { r.park_brake = toBool(value); return; } + if (name == "air_temp") { r.air_temp = toDouble(value); return; } + if (name == "brake_temp") { r.brake_temp = toDouble(value); return; } + if (name == "dcdc_temp") { r.dcdc_temp = toDouble(value); return; } + if (name == "mainIO_temp") { r.mainIO_temp = toDouble(value); return; } + if (name == "motor_controller_temp") { r.motor_controller_temp = toDouble(value); return; } + if (name == "motor_temp") { r.motor_temp = toDouble(value); return; } + if (name == "road_temp") { r.road_temp = toDouble(value); return; } + + // ── Main IO / Lights ───────────────────────────────────────────────────── + if (name == "l_turn_led_en") { r.l_turn_led_en = toBool(value); return; } + if (name == "r_turn_led_en") { r.r_turn_led_en = toBool(value); return; } + if (name == "brake_led_en") { r.brake_led_en = toBool(value); return; } + if (name == "headlights_led_en") { r.headlights_led_en = toBool(value); return; } + if (name == "hazards") { r.hazards = toBool(value); return; } + + // ── Main IO / Power Bus ────────────────────────────────────────────────── + if (name == "main_5V_bus") { r.main_5V_bus = toDouble(value); return; } + if (name == "main_12V_bus") { r.main_12V_bus = toDouble(value); return; } + if (name == "main_24V_bus") { r.main_24V_bus = toDouble(value); return; } + if (name == "main_5V_current") { r.main_5V_current = toDouble(value); return; } + if (name == "main_12V_current") { r.main_12V_current = toDouble(value); return; } + if (name == "main_24V_current") { r.main_24V_current = toDouble(value); return; } + + // ── Main IO / Firmware Heartbeats ──────────────────────────────────────── + if (name == "bms_can_heartbeat") { r.bms_can_heartbeat = toBool(value); return; } + if (name == "hv_can_heartbeat") { r.hv_can_heartbeat = toBool(value); return; } + if (name == "mainIO_heartbeat") { r.mainIO_heartbeat = toBool(value); return; } + if (name == "mcc_can_heartbeat") { r.mcc_can_heartbeat = toBool(value); return; } + if (name == "mppt_can_heartbeat") { r.mppt_can_heartbeat = toBool(value); return; } + + // ── Solar / MPPT ───────────────────────────────────────────────────────── + if (name == "mppt_mode") { r.mppt_mode = toBool(value); return; } + if (name == "mppt_current_out") { r.mppt_current_out = toDouble(value); return; } + if (name == "mppt_power_out") { r.mppt_power_out = toDouble(value); return; } + if (name == "string1_temp") { r.string1_temp = toDouble(value); return; } + if (name == "string2_temp") { r.string2_temp = toDouble(value); return; } + if (name == "string3_temp") { r.string3_temp = toDouble(value); return; } + if (name == "string1_V_in") { r.string1_V_in = toDouble(value); return; } + if (name == "string2_V_in") { r.string2_V_in = toDouble(value); return; } + if (name == "string3_V_in") { r.string3_V_in = toDouble(value); return; } + if (name == "string1_I_in") { r.string1_I_in = toDouble(value); return; } + if (name == "string2_I_in") { r.string2_I_in = toDouble(value); return; } + if (name == "string3_I_in") { r.string3_I_in = toDouble(value); return; } + + // ── Battery / BMS CAN ──────────────────────────────────────────────────── + if (name == "pack_temp") { r.pack_temp = toDouble(value); return; } + if (name == "pack_internal_temp") { r.pack_internal_temp = toDouble(value); return; } + if (name == "pack_current") { r.pack_current = toDouble(value); return; } + if (name == "pack_voltage") { r.pack_voltage = toDouble(value); return; } + if (name == "pack_power") { r.pack_power = toDouble(value); return; } + if (name == "populated_cells") { r.populated_cells = static_cast(toInt64(value)); return; } + if (name == "soc") { r.soc = toDouble(value); return; } + if (name == "soh") { r.soh = toDouble(value); return; } + if (name == "pack_amphours") { r.pack_amphours = toDouble(value); return; } + if (name == "adaptive_total_capacity") { r.adaptive_total_capacity = toDouble(value); return; } + if (name == "fan_speed") { r.fan_speed = static_cast(toInt64(value)); return; } + if (name == "pack_resistance") { r.pack_resistance = toDouble(value); return; } + if (name == "bms_input_voltage") { r.bms_input_voltage = toDouble(value); return; } + + // ── Battery / BMS Faults ───────────────────────────────────────────────── + if (name == "bps_fault") { r.bps_fault = toBool(value); return; } + if (name == "voltage_failsafe") { r.voltage_failsafe = toBool(value); return; } + if (name == "current_failsafe") { r.current_failsafe = toBool(value); return; } + if (name == "relay_failsafe") { r.relay_failsafe = toBool(value); return; } + if (name == "cell_balancing_active") { r.cell_balancing_active = toBool(value); return; } + if (name == "charge_interlock_failsafe") { r.charge_interlock_failsafe = toBool(value); return; } + if (name == "thermistor_b_value_table_invalid") { r.thermistor_b_value_table_invalid = toBool(value); return; } + if (name == "input_power_supply_failsafe") { r.input_power_supply_failsafe = toBool(value); return; } + if (name == "discharge_limit_enforcement_fault") { r.discharge_limit_enforcement_fault = toBool(value); return; } + if (name == "charger_safety_relay_fault") { r.charger_safety_relay_fault = toBool(value); return; } + if (name == "internal_hardware_fault") { r.internal_hardware_fault = toBool(value); return; } + if (name == "internal_heatsink_fault") { r.internal_heatsink_fault = toBool(value); return; } + if (name == "internal_software_fault") { r.internal_software_fault = toBool(value); return; } + if (name == "highest_cell_voltage_too_high_fault") { r.highest_cell_voltage_too_high_fault = toBool(value); return; } + if (name == "lowest_cell_voltage_too_low_fault") { r.lowest_cell_voltage_too_low_fault = toBool(value); return; } + if (name == "pack_too_hot_fault") { r.pack_too_hot_fault = toBool(value); return; } + if (name == "high_voltage_interlock_signal_fault") { r.high_voltage_interlock_signal_fault = toBool(value); return; } + if (name == "precharge_circuit_malfunction") { r.precharge_circuit_malfunction = toBool(value); return; } + if (name == "abnormal_state_of_charge_behavior") { r.abnormal_state_of_charge_behavior = toBool(value); return; } + if (name == "internal_communication_fault") { r.internal_communication_fault = toBool(value); return; } + if (name == "cell_balancing_stuck_off_fault") { r.cell_balancing_stuck_off_fault = toBool(value); return; } + if (name == "weak_cell_fault") { r.weak_cell_fault = toBool(value); return; } + if (name == "low_cell_voltage_fault") { r.low_cell_voltage_fault = toBool(value); return; } + if (name == "open_wiring_fault") { r.open_wiring_fault = toBool(value); return; } + if (name == "current_sensor_fault") { r.current_sensor_fault = toBool(value); return; } + if (name == "highest_cell_voltage_over_5V_fault") { r.highest_cell_voltage_over_5V_fault = toBool(value); return; } + if (name == "cell_asic_fault") { r.cell_asic_fault = toBool(value); return; } + if (name == "weak_pack_fault") { r.weak_pack_fault = toBool(value); return; } + if (name == "fan_monitor_fault") { r.fan_monitor_fault = toBool(value); return; } + if (name == "thermistor_fault") { r.thermistor_fault = toBool(value); return; } + if (name == "external_communication_fault") { r.external_communication_fault = toBool(value); return; } + if (name == "redundant_power_supply_fault") { r.redundant_power_supply_fault = toBool(value); return; } + if (name == "high_voltage_isolation_fault") { r.high_voltage_isolation_fault = toBool(value); return; } + if (name == "input_power_supply_fault") { r.input_power_supply_fault = toBool(value); return; } + if (name == "charge_limit_enforcement_fault") { r.charge_limit_enforcement_fault = toBool(value); return; } + + // ── Battery / Cell Group Voltages ──────────────────────────────────────── + if (name == "cell_group1_voltage") { r.cell_group1_voltage = toDouble(value); return; } + if (name == "cell_group2_voltage") { r.cell_group2_voltage = toDouble(value); return; } + if (name == "cell_group3_voltage") { r.cell_group3_voltage = toDouble(value); return; } + if (name == "cell_group4_voltage") { r.cell_group4_voltage = toDouble(value); return; } + if (name == "cell_group5_voltage") { r.cell_group5_voltage = toDouble(value); return; } + if (name == "cell_group6_voltage") { r.cell_group6_voltage = toDouble(value); return; } + if (name == "cell_group7_voltage") { r.cell_group7_voltage = toDouble(value); return; } + if (name == "cell_group8_voltage") { r.cell_group8_voltage = toDouble(value); return; } + if (name == "cell_group9_voltage") { r.cell_group9_voltage = toDouble(value); return; } + if (name == "cell_group10_voltage") { r.cell_group10_voltage = toDouble(value); return; } + if (name == "cell_group11_voltage") { r.cell_group11_voltage = toDouble(value); return; } + if (name == "cell_group12_voltage") { r.cell_group12_voltage = toDouble(value); return; } + if (name == "cell_group13_voltage") { r.cell_group13_voltage = toDouble(value); return; } + if (name == "cell_group14_voltage") { r.cell_group14_voltage = toDouble(value); return; } + if (name == "cell_group15_voltage") { r.cell_group15_voltage = toDouble(value); return; } + if (name == "cell_group16_voltage") { r.cell_group16_voltage = toDouble(value); return; } + if (name == "cell_group17_voltage") { r.cell_group17_voltage = toDouble(value); return; } + if (name == "cell_group18_voltage") { r.cell_group18_voltage = toDouble(value); return; } + if (name == "cell_group19_voltage") { r.cell_group19_voltage = toDouble(value); return; } + if (name == "cell_group20_voltage") { r.cell_group20_voltage = toDouble(value); return; } + if (name == "cell_group21_voltage") { r.cell_group21_voltage = toDouble(value); return; } + if (name == "cell_group22_voltage") { r.cell_group22_voltage = toDouble(value); return; } + if (name == "cell_group23_voltage") { r.cell_group23_voltage = toDouble(value); return; } + if (name == "cell_group24_voltage") { r.cell_group24_voltage = toDouble(value); return; } + if (name == "cell_group25_voltage") { r.cell_group25_voltage = toDouble(value); return; } + if (name == "cell_group26_voltage") { r.cell_group26_voltage = toDouble(value); return; } + if (name == "cell_group27_voltage") { r.cell_group27_voltage = toDouble(value); return; } + if (name == "cell_group28_voltage") { r.cell_group28_voltage = toDouble(value); return; } + if (name == "cell_group29_voltage") { r.cell_group29_voltage = toDouble(value); return; } + if (name == "cell_group30_voltage") { r.cell_group30_voltage = toDouble(value); return; } + if (name == "cell_group31_voltage") { r.cell_group31_voltage = toDouble(value); return; } + + // ── Software / Timestamps ──────────────────────────────────────────────── + if (name == "tstamp_ms") { r.tstamp_ms = static_cast(toInt64(value)); return; } + if (name == "tstamp_sc") { r.tstamp_sc = static_cast (toInt64(value)); return; } + if (name == "tstamp_mn") { r.tstamp_mn = static_cast (toInt64(value)); return; } + if (name == "tstamp_hr") { r.tstamp_hr = static_cast (toInt64(value)); return; } + if (name == "tstamp_unix") { r.tstamp_unix = static_cast(toInt64(value)); return; } + + // ── Software / GPS ─────────────────────────────────────────────────────── + if (name == "lat") { r.lat = toDouble(value); return; } + if (name == "lon") { r.lon = toDouble(value); return; } + if (name == "elev") { r.elev = toDouble(value); return; } + + // ── Software / Lap Counter ─────────────────────────────────────────────── + if (name == "lap_count") { r.lap_count = static_cast (toInt64(value)); return; } + if (name == "current_section") { r.current_section = static_cast (toInt64(value)); return; } + if (name == "lap_duration") { r.lap_duration = static_cast(toInt64(value)); return; } + + // ── Race Strategy ──────────────────────────────────────────────────────── + if (name == "optimized_target_power") { r.optimized_target_power = toDouble(value); return; } + if (name == "maximum_distance_traveled"){ r.maximum_distance_traveled = toDouble(value); return; } + + // Unknown column — silently ignored. +} diff --git a/influx/src/InfluxWriter.cpp b/influx/src/InfluxWriter.cpp new file mode 100644 index 0000000..446532a --- /dev/null +++ b/influx/src/InfluxWriter.cpp @@ -0,0 +1,339 @@ +#include "InfluxWriter.h" + +#include +#include +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Constructor — load config from environment +// ───────────────────────────────────────────────────────────────────────────── + +static std::string requireEnv(const char* name) { + const char* v = std::getenv(name); + if (!v || v[0] == '\0') { + throw std::runtime_error( + std::string("InfluxWriter: missing required env var: ") + name); + } + return std::string(v); +} + +InfluxWriter::InfluxWriter() + : url_ (requireEnv("INFLUX_URL")) + , token_ (requireEnv("INFLUX_TOKEN")) + , org_ (requireEnv("INFLUX_ORG")) + , bucket_(requireEnv("INFLUX_BUCKET")) +{} + +// ───────────────────────────────────────────────────────────────────────────── +// toLineProtocol +// ───────────────────────────────────────────────────────────────────────────── + +// Helper: append a float field to the stream. +// All numbers are written without integer 'i' suffix to avoid schema conflicts. +static void appendFloat(std::ostringstream& os, const char* key, double val, bool& first) { + if (!first) os << ','; + os << key << '=' << std::setprecision(6) << val; + first = false; +} + +// Helper: append a boolean field (true/false string form for InfluxDB). +static void appendBool(std::ostringstream& os, const char* key, bool val, bool& first) { + if (!first) os << ','; + os << key << '=' << (val ? "true" : "false"); + first = false; +} + +std::string InfluxWriter::toLineProtocol(const TelemetryRecord& r, + int64_t timestampNs) const { + std::ostringstream os; + os << std::fixed; + + // Measurement name (no spaces, no commas — safe as-is) + os << "sc2_telemetry "; + + bool first = true; + + // ── MCC / Motor Control ────────────────────────────────────────────────── + appendFloat(os, "accelerator_pedal", r.accelerator_pedal, first); + appendFloat(os, "speed", r.speed, first); + appendFloat(os, "mcc_state", r.mcc_state, first); + appendBool (os, "fr_telem", r.fr_telem, first); + appendBool (os, "crz_pwr_mode", r.crz_pwr_mode, first); + appendBool (os, "crz_spd_mode", r.crz_spd_mode, first); + appendFloat(os, "crz_pwr_setpt", r.crz_pwr_setpt, first); + appendFloat(os, "crz_spd_setpt", r.crz_spd_setpt, first); + appendBool (os, "eco", r.eco, first); + appendBool (os, "main_telem", r.main_telem, first); + appendBool (os, "foot_brake", r.foot_brake, first); + appendFloat(os, "regen_brake", r.regen_brake, first); + appendFloat(os, "motor_current", r.motor_current, first); + appendFloat(os, "motor_power", r.motor_power, first); + appendFloat(os, "mc_status", r.mc_status, first); + + // ── High Voltage / Shutdown ────────────────────────────────────────────── + appendBool (os, "driver_eStop", r.driver_eStop, first); + appendBool (os, "external_eStop", r.external_eStop, first); + appendBool (os, "crash", r.crash, first); + appendBool (os, "discharge_enable", r.discharge_enable, first); + appendBool (os, "discharge_enabled", r.discharge_enabled, first); + appendBool (os, "charge_enable", r.charge_enable, first); + appendBool (os, "charge_enabled", r.charge_enabled, first); + appendBool (os, "isolation", r.isolation, first); + appendBool (os, "mcu_hv_en", r.mcu_hv_en, first); + appendBool (os, "mcu_stat_fdbk", r.mcu_stat_fdbk, first); + + // ── High Voltage / MPS ────────────────────────────────────────────────── + appendBool (os, "mppt_contactor", r.mppt_contactor, first); + appendBool (os, "motor_controller_contactor",r.motor_controller_contactor, first); + appendBool (os, "low_contactor", r.low_contactor, first); + appendFloat(os, "dcdc_current", r.dcdc_current, first); + appendBool (os, "dcdc_deg", r.dcdc_deg, first); + appendBool (os, "use_dcdc", r.use_dcdc, first); + appendBool (os, "use_supp", r.use_supp, first); + appendBool (os, "bms_mpio1", r.bms_mpio1, first); + + // ── Battery / Supplemental ─────────────────────────────────────────────── + appendFloat(os, "supplemental_current", r.supplemental_current, first); + appendFloat(os, "supplemental_voltage", r.supplemental_voltage, first); + appendBool (os, "supplemental_deg", r.supplemental_deg, first); + appendFloat(os, "est_supplemental_soc", r.est_supplemental_soc, first); + + // ── Main IO / Sensors ──────────────────────────────────────────────────── + appendBool (os, "park_brake", r.park_brake, first); + appendFloat(os, "air_temp", r.air_temp, first); + appendFloat(os, "brake_temp", r.brake_temp, first); + appendFloat(os, "dcdc_temp", r.dcdc_temp, first); + appendFloat(os, "mainIO_temp", r.mainIO_temp, first); + appendFloat(os, "motor_controller_temp", r.motor_controller_temp, first); + appendFloat(os, "motor_temp", r.motor_temp, first); + appendFloat(os, "road_temp", r.road_temp, first); + + // ── Main IO / Lights ───────────────────────────────────────────────────── + appendBool (os, "l_turn_led_en", r.l_turn_led_en, first); + appendBool (os, "r_turn_led_en", r.r_turn_led_en, first); + appendBool (os, "brake_led_en", r.brake_led_en, first); + appendBool (os, "headlights_led_en", r.headlights_led_en, first); + appendBool (os, "hazards", r.hazards, first); + + // ── Main IO / Power Bus ────────────────────────────────────────────────── + appendFloat(os, "main_5V_bus", r.main_5V_bus, first); + appendFloat(os, "main_12V_bus", r.main_12V_bus, first); + appendFloat(os, "main_24V_bus", r.main_24V_bus, first); + appendFloat(os, "main_5V_current", r.main_5V_current, first); + appendFloat(os, "main_12V_current", r.main_12V_current, first); + appendFloat(os, "main_24V_current", r.main_24V_current, first); + + // ── Main IO / Firmware Heartbeats ──────────────────────────────────────── + appendBool (os, "bms_can_heartbeat", r.bms_can_heartbeat, first); + appendBool (os, "hv_can_heartbeat", r.hv_can_heartbeat, first); + appendBool (os, "mainIO_heartbeat", r.mainIO_heartbeat, first); + appendBool (os, "mcc_can_heartbeat", r.mcc_can_heartbeat, first); + appendBool (os, "mppt_can_heartbeat", r.mppt_can_heartbeat, first); + + // ── Solar / MPPT ───────────────────────────────────────────────────────── + appendBool (os, "mppt_mode", r.mppt_mode, first); + appendFloat(os, "mppt_current_out", r.mppt_current_out, first); + appendFloat(os, "mppt_power_out", r.mppt_power_out, first); + appendFloat(os, "string1_temp", r.string1_temp, first); + appendFloat(os, "string2_temp", r.string2_temp, first); + appendFloat(os, "string3_temp", r.string3_temp, first); + appendFloat(os, "string1_V_in", r.string1_V_in, first); + appendFloat(os, "string2_V_in", r.string2_V_in, first); + appendFloat(os, "string3_V_in", r.string3_V_in, first); + appendFloat(os, "string1_I_in", r.string1_I_in, first); + appendFloat(os, "string2_I_in", r.string2_I_in, first); + appendFloat(os, "string3_I_in", r.string3_I_in, first); + + // ── Battery / BMS CAN ──────────────────────────────────────────────────── + appendFloat(os, "pack_temp", r.pack_temp, first); + appendFloat(os, "pack_internal_temp", r.pack_internal_temp, first); + appendFloat(os, "pack_current", r.pack_current, first); + appendFloat(os, "pack_voltage", r.pack_voltage, first); + appendFloat(os, "pack_power", r.pack_power, first); + appendFloat(os, "populated_cells", r.populated_cells, first); + appendFloat(os, "soc", r.soc, first); + appendFloat(os, "soh", r.soh, first); + appendFloat(os, "pack_amphours", r.pack_amphours, first); + appendFloat(os, "adaptive_total_capacity", r.adaptive_total_capacity, first); + appendFloat(os, "fan_speed", r.fan_speed, first); + appendFloat(os, "pack_resistance", r.pack_resistance, first); + appendFloat(os, "bms_input_voltage", r.bms_input_voltage, first); + + // ── Battery / BMS Faults ───────────────────────────────────────────────── + appendBool (os, "bps_fault", r.bps_fault, first); + appendBool (os, "voltage_failsafe", r.voltage_failsafe, first); + appendBool (os, "current_failsafe", r.current_failsafe, first); + appendBool (os, "relay_failsafe", r.relay_failsafe, first); + appendBool (os, "cell_balancing_active", r.cell_balancing_active, first); + appendBool (os, "charge_interlock_failsafe", r.charge_interlock_failsafe, first); + appendBool (os, "thermistor_b_value_table_invalid", r.thermistor_b_value_table_invalid, first); + appendBool (os, "input_power_supply_failsafe", r.input_power_supply_failsafe, first); + appendBool (os, "discharge_limit_enforcement_fault", r.discharge_limit_enforcement_fault, first); + appendBool (os, "charger_safety_relay_fault", r.charger_safety_relay_fault, first); + appendBool (os, "internal_hardware_fault", r.internal_hardware_fault, first); + appendBool (os, "internal_heatsink_fault", r.internal_heatsink_fault, first); + appendBool (os, "internal_software_fault", r.internal_software_fault, first); + appendBool (os, "highest_cell_voltage_too_high_fault", r.highest_cell_voltage_too_high_fault, first); + appendBool (os, "lowest_cell_voltage_too_low_fault", r.lowest_cell_voltage_too_low_fault, first); + appendBool (os, "pack_too_hot_fault", r.pack_too_hot_fault, first); + appendBool (os, "high_voltage_interlock_signal_fault", r.high_voltage_interlock_signal_fault, first); + appendBool (os, "precharge_circuit_malfunction", r.precharge_circuit_malfunction, first); + appendBool (os, "abnormal_state_of_charge_behavior", r.abnormal_state_of_charge_behavior, first); + appendBool (os, "internal_communication_fault", r.internal_communication_fault, first); + appendBool (os, "cell_balancing_stuck_off_fault", r.cell_balancing_stuck_off_fault, first); + appendBool (os, "weak_cell_fault", r.weak_cell_fault, first); + appendBool (os, "low_cell_voltage_fault", r.low_cell_voltage_fault, first); + appendBool (os, "open_wiring_fault", r.open_wiring_fault, first); + appendBool (os, "current_sensor_fault", r.current_sensor_fault, first); + appendBool (os, "highest_cell_voltage_over_5V_fault", r.highest_cell_voltage_over_5V_fault, first); + appendBool (os, "cell_asic_fault", r.cell_asic_fault, first); + appendBool (os, "weak_pack_fault", r.weak_pack_fault, first); + appendBool (os, "fan_monitor_fault", r.fan_monitor_fault, first); + appendBool (os, "thermistor_fault", r.thermistor_fault, first); + appendBool (os, "external_communication_fault", r.external_communication_fault, first); + appendBool (os, "redundant_power_supply_fault", r.redundant_power_supply_fault, first); + appendBool (os, "high_voltage_isolation_fault", r.high_voltage_isolation_fault, first); + appendBool (os, "input_power_supply_fault", r.input_power_supply_fault, first); + appendBool (os, "charge_limit_enforcement_fault", r.charge_limit_enforcement_fault, first); + + // ── Battery / Cell Group Voltages ──────────────────────────────────────── + appendFloat(os, "cell_group1_voltage", r.cell_group1_voltage, first); + appendFloat(os, "cell_group2_voltage", r.cell_group2_voltage, first); + appendFloat(os, "cell_group3_voltage", r.cell_group3_voltage, first); + appendFloat(os, "cell_group4_voltage", r.cell_group4_voltage, first); + appendFloat(os, "cell_group5_voltage", r.cell_group5_voltage, first); + appendFloat(os, "cell_group6_voltage", r.cell_group6_voltage, first); + appendFloat(os, "cell_group7_voltage", r.cell_group7_voltage, first); + appendFloat(os, "cell_group8_voltage", r.cell_group8_voltage, first); + appendFloat(os, "cell_group9_voltage", r.cell_group9_voltage, first); + appendFloat(os, "cell_group10_voltage", r.cell_group10_voltage, first); + appendFloat(os, "cell_group11_voltage", r.cell_group11_voltage, first); + appendFloat(os, "cell_group12_voltage", r.cell_group12_voltage, first); + appendFloat(os, "cell_group13_voltage", r.cell_group13_voltage, first); + appendFloat(os, "cell_group14_voltage", r.cell_group14_voltage, first); + appendFloat(os, "cell_group15_voltage", r.cell_group15_voltage, first); + appendFloat(os, "cell_group16_voltage", r.cell_group16_voltage, first); + appendFloat(os, "cell_group17_voltage", r.cell_group17_voltage, first); + appendFloat(os, "cell_group18_voltage", r.cell_group18_voltage, first); + appendFloat(os, "cell_group19_voltage", r.cell_group19_voltage, first); + appendFloat(os, "cell_group20_voltage", r.cell_group20_voltage, first); + appendFloat(os, "cell_group21_voltage", r.cell_group21_voltage, first); + appendFloat(os, "cell_group22_voltage", r.cell_group22_voltage, first); + appendFloat(os, "cell_group23_voltage", r.cell_group23_voltage, first); + appendFloat(os, "cell_group24_voltage", r.cell_group24_voltage, first); + appendFloat(os, "cell_group25_voltage", r.cell_group25_voltage, first); + appendFloat(os, "cell_group26_voltage", r.cell_group26_voltage, first); + appendFloat(os, "cell_group27_voltage", r.cell_group27_voltage, first); + appendFloat(os, "cell_group28_voltage", r.cell_group28_voltage, first); + appendFloat(os, "cell_group29_voltage", r.cell_group29_voltage, first); + appendFloat(os, "cell_group30_voltage", r.cell_group30_voltage, first); + appendFloat(os, "cell_group31_voltage", r.cell_group31_voltage, first); + + // ── Software / Timestamps ──────────────────────────────────────────────── + appendFloat(os, "tstamp_ms", r.tstamp_ms, first); + appendFloat(os, "tstamp_sc", r.tstamp_sc, first); + appendFloat(os, "tstamp_mn", r.tstamp_mn, first); + appendFloat(os, "tstamp_hr", r.tstamp_hr, first); + appendFloat(os, "tstamp_unix", static_cast(r.tstamp_unix), first); + + // ── Software / GPS ─────────────────────────────────────────────────────── + appendFloat(os, "lat", r.lat, first); + appendFloat(os, "lon", r.lon, first); + appendFloat(os, "elev", r.elev, first); + + // ── Software / Lap ─────────────────────────────────────────────────────── + appendFloat(os, "lap_count", r.lap_count, first); + appendFloat(os, "current_section", r.current_section, first); + appendFloat(os, "lap_duration", r.lap_duration, first); + + // ── Race Strategy ──────────────────────────────────────────────────────── + appendFloat(os, "optimized_target_power", r.optimized_target_power, first); + appendFloat(os, "maximum_distance_traveled", r.maximum_distance_traveled, first); + + // Optional nanosecond timestamp + if (timestampNs != 0) { + os << ' ' << timestampNs; + } + + return os.str(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Public write helpers +// ───────────────────────────────────────────────────────────────────────────── + +void InfluxWriter::write(const std::string& lineProtocol) const { + httpPost(lineProtocol); +} + +void InfluxWriter::writeBatch(const std::vector& lines) const { + if (lines.empty()) return; + std::string body; + for (size_t i = 0; i < lines.size(); ++i) { + body += lines[i]; + if (i + 1 < lines.size()) body += '\n'; + } + httpPost(body); +} + +// ───────────────────────────────────────────────────────────────────────────── +// libcurl HTTP POST +// ───────────────────────────────────────────────────────────────────────────── + +// Collects the HTTP response body so we can surface error messages. +static size_t curlWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { + std::string* out = static_cast(userdata); + out->append(ptr, size * nmemb); + return size * nmemb; +} + +void InfluxWriter::httpPost(const std::string& body) const { + // Build endpoint URL + std::string endpoint = url_ + + "/api/v2/write?org=" + org_ + + "&bucket=" + bucket_ + + "&precision=ns"; + + CURL* curl = curl_easy_init(); + if (!curl) { + throw std::runtime_error("InfluxWriter: curl_easy_init() failed"); + } + + std::string responseBody; + struct curl_slist* headers = nullptr; + + std::string authHeader = "Authorization: Token " + token_; + headers = curl_slist_append(headers, authHeader.c_str()); + headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8"); + + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + std::string err = curl_easy_strerror(res); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + throw std::runtime_error("InfluxWriter: curl error: " + err); + } + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + // InfluxDB returns 204 No Content on success + if (httpCode < 200 || httpCode >= 300) { + throw std::runtime_error( + "InfluxWriter: HTTP " + std::to_string(httpCode) + ": " + responseBody); + } +} diff --git a/influx/src/tester_main.cpp b/influx/src/tester_main.cpp new file mode 100644 index 0000000..c4d49f2 --- /dev/null +++ b/influx/src/tester_main.cpp @@ -0,0 +1,238 @@ +/** + * main.cpp — SC2 Telemetry Tester (C++ edition) + * + * Reads the CSV test dataset and writes records to InfluxDB Cloud using the + * InfluxDB v2 Line Protocol API. + * + * Usage: + * ./sc2_telemetry_tester [--batch] [--csv ] [--delay ] + * + * Flags: + * --batch Send all records in one HTTP batch (default: stream 1/s) + * --csv Path to CSV file (default: ../data/test_telemetry.csv) + * --delay Stream delay in milliseconds (default: 1000) + * + * Required environment variables (set in .env or export): + * INFLUX_URL, INFLUX_TOKEN, INFLUX_ORG, INFLUX_BUCKET + */ + +#include "CsvParser.h" +#include "InfluxWriter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Graceful shutdown +// ───────────────────────────────────────────────────────────────────────────── + +static volatile bool g_running = true; + +static void onSignal(int /*sig*/) { + g_running = false; +} + +// ───────────────────────────────────────────────────────────────────────────── +// .env loader (key=value, ignores # comments, trims whitespace) +// ───────────────────────────────────────────────────────────────────────────── + +static std::string trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n\"'"); + if (start == std::string::npos) return ""; + size_t end = s.find_last_not_of(" \t\r\n\"'"); + return s.substr(start, end - start + 1); +} + +static void loadDotEnv(const std::string& path = ".env") { + std::ifstream file(path); + if (!file.is_open()) return; // .env is optional if vars are already exported + + std::string line; + while (std::getline(file, line)) { + // Strip comments and blank lines + size_t hash = line.find('#'); + if (hash != std::string::npos) line = line.substr(0, hash); + line = trim(line); + if (line.empty()) continue; + + size_t eq = line.find('='); + if (eq == std::string::npos) continue; + + std::string key = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + if (!key.empty()) { + // Don't overwrite if already in environment +#if defined(_WIN32) + if (!std::getenv(key.c_str())) _putenv_s(key.c_str(), val.c_str()); +#else + setenv(key.c_str(), val.c_str(), 0 /* no overwrite */); +#endif + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Timestamp helper — nanoseconds since Unix epoch +// ───────────────────────────────────────────────────────────────────────────── + +static int64_t nowNs() { + using namespace std::chrono; + return static_cast( + duration_cast( + system_clock::now().time_since_epoch()) + .count()); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Argument parsing +// ───────────────────────────────────────────────────────────────────────────── + +struct Args { + bool batch = false; + std::string csvPath = "../data/test_telemetry.csv"; + int delayMs = 1000; +}; + +static Args parseArgs(int argc, char** argv) { + Args a; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--batch") == 0) { + a.batch = true; + } else if (std::strcmp(argv[i], "--csv") == 0 && i + 1 < argc) { + a.csvPath = argv[++i]; + } else if (std::strcmp(argv[i], "--delay") == 0 && i + 1 < argc) { + a.delayMs = std::stoi(argv[++i]); + } else { + std::cerr << "Unknown argument: " << argv[i] << "\n" + << "Usage: sc2_telemetry_tester [--batch] [--csv ] [--delay ]\n"; + std::exit(1); + } + } + return a; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Stream mode — one record per ms, looping +// ───────────────────────────────────────────────────────────────────────────── + +static void runStream(const std::vector& records, + const InfluxWriter& writer, + int delayMs) { + std::cout << "[stream] Sending " << records.size() + << " records, looping. Ctrl+C to stop.\n"; + + size_t index = 0; + int sent = 0; + + while (g_running) { + const TelemetryRecord& rec = records[index % records.size()]; + std::string lp = writer.toLineProtocol(rec, nowNs()); + + try { + writer.write(lp); + ++sent; + std::cout << "\r[stream] Sent " << sent << " records" << std::flush; + } catch (const std::exception& ex) { + std::cerr << "\n[stream] ERROR: " << ex.what() << "\n"; + } + + ++index; + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + } + + std::cout << "\n[stream] Stopped after " << sent << " records.\n"; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Batch mode — all records in one HTTP request +// ───────────────────────────────────────────────────────────────────────────── + +static void runBatch(const std::vector& records, + const InfluxWriter& writer) { + std::cout << "[batch] Building " << records.size() << " line-protocol lines...\n"; + + std::vector lines; + lines.reserve(records.size()); + + // Space timestamps 1 second apart starting from now + int64_t ts = nowNs(); + const int64_t step = 1'000'000'000LL; // 1 second in nanoseconds + + for (const auto& rec : records) { + lines.push_back(writer.toLineProtocol(rec, ts)); + ts += step; + } + + std::cout << "[batch] Sending " << lines.size() << " records in one request...\n"; + writer.writeBatch(lines); + std::cout << "[batch] Done.\n"; +} + +// ───────────────────────────────────────────────────────────────────────────── +// main +// ───────────────────────────────────────────────────────────────────────────── + +int main(int argc, char** argv) { + // Register signal handlers + std::signal(SIGINT, onSignal); + std::signal(SIGTERM, onSignal); + + // Load .env (from the directory where the binary is run) + loadDotEnv(".env"); + // Also try parent directory (for running from build/) + loadDotEnv("../.env"); + + Args args = parseArgs(argc, argv); + + // ── Parse CSV ──────────────────────────────────────────────────────────── + std::cout << "[init] Parsing CSV: " << args.csvPath << "\n"; + std::vector records; + try { + records = CsvParser::parse(args.csvPath); + } catch (const std::exception& ex) { + std::cerr << "[error] Failed to parse CSV: " << ex.what() << "\n"; + return 1; + } + std::cout << "[init] Parsed " << records.size() << " records.\n"; + + if (records.empty()) { + std::cerr << "[error] CSV contained no records.\n"; + return 1; + } + + // ── Init InfluxDB writer ───────────────────────────────────────────────── + InfluxWriter writer; + try { + // InfluxWriter constructor throws if env vars are missing + } catch (const std::exception& ex) { + std::cerr << "[error] InfluxWriter init failed: " << ex.what() << "\n"; + return 1; + } + + std::cout << "[init] InfluxDB configured. Bucket: " + << (std::getenv("INFLUX_BUCKET") ? std::getenv("INFLUX_BUCKET") : "?") + << "\n"; + + // ── Run mode ───────────────────────────────────────────────────────────── + if (args.batch) { + try { + runBatch(records, writer); + } catch (const std::exception& ex) { + std::cerr << "[error] Batch write failed: " << ex.what() << "\n"; + return 1; + } + } else { + runStream(records, writer, args.delayMs); + } + + return 0; +} diff --git a/main.cpp b/main.cpp index 428aa5f..d8732d1 100644 --- a/main.cpp +++ b/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include "InfluxWriter.h" // Global flag for clean shutdown volatile bool g_running = true; @@ -53,6 +54,16 @@ int main(int argc, char *argv[]) { // Initialize the data unpacker (telemetry processor) DataUnpacker unpacker; + // Initialize InfluxDB writer (reads INFLUX_URL, INFLUX_TOKEN, INFLUX_ORG, INFLUX_BUCKET from env) + std::unique_ptr influxWriter; + try { + influxWriter = std::make_unique(); + std::cout << "InfluxDB writer initialized successfully." << std::endl; + } catch (const std::exception& e) { + std::cerr << "WARNING: InfluxDB not configured: " << e.what() << std::endl; + std::cerr << " Set INFLUX_URL, INFLUX_TOKEN, INFLUX_ORG, INFLUX_BUCKET to enable." << std::endl; + } + // Start file sync process in background startFileSync(); @@ -62,12 +73,24 @@ int main(int argc, char *argv[]) { // Main application loop std::cout << "System running. Press Ctrl+C to shutdown gracefully." << std::endl; + auto lastInfluxWrite = std::chrono::steady_clock::now(); while (g_running) { // Sleep for a short period to avoid busy waiting std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Here we could add periodic status checks or maintenance tasks - // For now, just keep the application alive + + // Publish telemetry to InfluxDB every 2 seconds + if (influxWriter) { + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastInfluxWrite).count() >= 2) { + try { + TelemetryRecord rec = unpacker.buildTelemetryRecord(); + influxWriter->write(influxWriter->toLineProtocol(rec)); + } catch (const std::exception& e) { + std::cerr << "InfluxDB write error: " << e.what() << std::endl; + } + lastInfluxWrite = now; + } + } } // Graceful shutdown