From 742a845bb273944a375ccee76995e8b676b54be4 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Thu, 11 Jul 2024 21:13:02 +0200 Subject: [PATCH 01/41] fix: fixup vitals panel --- gs/src/lib/components/Localiser.svelte | 35 ++++++----- gs/src/lib/panels/VitalsPanel.svelte | 86 +++++++++++--------------- gs/src/lib/stores/state.ts | 2 +- 3 files changed, 54 insertions(+), 69 deletions(-) diff --git a/gs/src/lib/components/Localiser.svelte b/gs/src/lib/components/Localiser.svelte index ee93aeb97..3b4765659 100644 --- a/gs/src/lib/components/Localiser.svelte +++ b/gs/src/lib/components/Localiser.svelte @@ -4,11 +4,10 @@ import {inputEmerg} from "$lib/stores/state"; import {GrandDataDistributor} from "$lib"; - export let max:number = 60000; + export let max:number = 13000; export let loc:number = 1000; /* should be < 16000 */ export let turning:boolean; - - // todo let current_segment:Segment = 'L1'; + export let showLabels:boolean = true; // SVG elements let progress_container:SVGGElement; @@ -122,20 +121,22 @@ - Forward A - Backward A - Lane-switch Straight - Forward B - Backward B - Lane-switch curve - - Backward C - - Forward C - + {#if showLabels} + Forward A + Backward A + Lane-switch Straight + Forward B + Backward B + Lane-switch curve + + Backward C + + Forward C + + {/if} diff --git a/gs/src/lib/panels/VitalsPanel.svelte b/gs/src/lib/panels/VitalsPanel.svelte index 080dfaf14..8065c21a8 100644 --- a/gs/src/lib/panels/VitalsPanel.svelte +++ b/gs/src/lib/panels/VitalsPanel.svelte @@ -1,13 +1,5 @@ @@ -122,12 +122,8 @@
- - {#if width > 550} - - {:else} - - {/if} + + @@ -159,36 +155,24 @@
- - - - -
+ +
- -
+ +
- - - - - - - - - - - - - - - - - + + {#if width > 550} + + {:else} + + {/if} + + {/if} diff --git a/gs/src/lib/stores/state.ts b/gs/src/lib/stores/state.ts index a046311a5..95b05be08 100644 --- a/gs/src/lib/stores/state.ts +++ b/gs/src/lib/stores/state.ts @@ -2,7 +2,7 @@ import { writable, type Writable } from 'svelte/store'; import {RunMode} from "$lib/types"; import {PlotBuffer} from "$lib"; -export const detailTabSet: Writable = writable(1); +export const detailTabSet: Writable = writable(0); export const inputSpeed: Writable = writable(50); export const inputEmerg: Writable = writable(-1); export const inputTurn: Writable = writable(RunMode.ShortRun); From a389f8473542edcd57ed72384c85d4410d89b236 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Thu, 11 Jul 2024 21:15:02 +0200 Subject: [PATCH 02/41] fix: reshuffle the insulation in the table --- gs/src/lib/panels/VitalsPanel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gs/src/lib/panels/VitalsPanel.svelte b/gs/src/lib/panels/VitalsPanel.svelte index 8065c21a8..c6ed4aa7b 100644 --- a/gs/src/lib/panels/VitalsPanel.svelte +++ b/gs/src/lib/panels/VitalsPanel.svelte @@ -77,7 +77,7 @@ $: tableArr2 = [ ["Insulation", $ins, "Insulation-", $insn], - ["Insulation+", $insp, "IMD Voltage", $imdv], + ["IMD Voltage", $imdv, "Insulation+", $insp], ] const location = storeManager.getStore("Localisation"); From 1791cc18096144d1e0342181c66facb5c6a0c0e7 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Thu, 11 Jul 2024 22:25:41 +0200 Subject: [PATCH 03/41] feat: everything is a limitable state --- gs/src/lib/components/generic/Store.svelte | 24 +++ gs/src/lib/components/generic/Table.svelte | 5 + gs/src/lib/index.ts | 2 + gs/src/lib/namedDatatypeEnum.ts | 198 ++++++++++++++++++++ gs/src/lib/panels/VitalsPanel.svelte | 82 ++------ gs/src/lib/panels/tabs/BatteriesTab.svelte | 135 +++++-------- gs/src/lib/panels/tabs/LeviTab.svelte | 41 ++-- gs/src/lib/panels/tabs/PneumaticsTab.svelte | 8 +- gs/src/lib/panels/tabs/RunInitTab.svelte | 27 +-- gs/src/lib/types.ts | 1 + gs/src/routes/+layout.svelte | 6 +- 11 files changed, 320 insertions(+), 209 deletions(-) create mode 100644 gs/src/lib/components/generic/Store.svelte create mode 100644 gs/src/lib/namedDatatypeEnum.ts diff --git a/gs/src/lib/components/generic/Store.svelte b/gs/src/lib/components/generic/Store.svelte new file mode 100644 index 000000000..9231be4d2 --- /dev/null +++ b/gs/src/lib/components/generic/Store.svelte @@ -0,0 +1,24 @@ + + +{$store} \ No newline at end of file diff --git a/gs/src/lib/components/generic/Table.svelte b/gs/src/lib/components/generic/Table.svelte index 033ef6a2c..c6343f639 100644 --- a/gs/src/lib/components/generic/Table.svelte +++ b/gs/src/lib/components/generic/Table.svelte @@ -1,4 +1,7 @@ diff --git a/gs/src/lib/panels/tabs/PneumaticsTab.svelte b/gs/src/lib/panels/tabs/PneumaticsTab.svelte index 542978891..897a38951 100644 --- a/gs/src/lib/panels/tabs/PneumaticsTab.svelte +++ b/gs/src/lib/panels/tabs/PneumaticsTab.svelte @@ -1,13 +1,17 @@ diff --git a/gs/src/lib/panels/tabs/RunInitTab.svelte b/gs/src/lib/panels/tabs/RunInitTab.svelte index 29c083711..1092f61e2 100644 --- a/gs/src/lib/panels/tabs/RunInitTab.svelte +++ b/gs/src/lib/panels/tabs/RunInitTab.svelte @@ -8,33 +8,20 @@ SpeedsInput, GrandDataDistributor, Chart } from "$lib"; import {getModalStore, type ModalComponent} from "@skeletonlabs/skeleton"; + import {DatatypeEnum} from "$lib/namedDatatypeEnum"; const storeManager = GrandDataDistributor.getInstance().stores; - const accelX = storeManager.getStore("AccelerationX") - const accelY = storeManager.getStore("AccelerationY") - const accelZ = storeManager.getStore("AccelerationZ") - const gyroX = storeManager.getStore("GyroscopeX") - const gyroY = storeManager.getStore("GyroscopeY") - const gyroZ = storeManager.getStore("GyroscopeZ") - const state = storeManager.getStore("FSMState"); - // const mainpcb_connected = storeManager.getStore(""); - // const propulsion_connected = storeManager.getStore("PropulsionVRefInt"); - // const levitation_connected = storeManager.getStore(""); - // const mainpcb_connected = storeManager.getStore(""); - // const mainpcb_connected = storeManager.getStore(""); - - let tableArr2:any[][]; $: tableArr2 = [ - ["Acceleration X", $accelX], - ["Acceleration Y", $accelY], - ["Acceleration Z", $accelZ], - ["Gyroscope X", $gyroX], - ["Gyroscope Y", $gyroY], - ["Gyroscope Z", $gyroZ], + ["Acceleration X", DatatypeEnum.ACCELERATIONX], + ["Acceleration Y", DatatypeEnum.ACCELERATIONY], + ["Acceleration Z", DatatypeEnum.ACCELERATIONZ], + ["Gyroscope X", DatatypeEnum.GYROSCOPEX], + ["Gyroscope Y", DatatypeEnum.GYROSCOPEY], + ["Gyroscope Z", DatatypeEnum.GYROSCOPEZ], ] const modalStore = getModalStore(); diff --git a/gs/src/lib/types.ts b/gs/src/lib/types.ts index 6057419b7..cc6139c03 100644 --- a/gs/src/lib/types.ts +++ b/gs/src/lib/types.ts @@ -4,6 +4,7 @@ export const NamedCommandValues:NamedCommand[] = ["DefaultCommand", "Heartbeat", /*AUTO GENERATED USING npm run generate:datatypes */ export type NamedDatatype = "YourNewDatatypeName" | "DefaultDatatype" | "PropulsionVoltage" | "PropulsionCurrent" | "PropulsionVRefInt" | "LevitationTemperature" | "AccelerationX" | "AccelerationY" | "AccelerationZ" | "GyroscopeX" | "GyroscopeY" | "GyroscopeZ" | "IMDGeneralInfo" | "IMDIsolationDetails" | "IMDVoltageDetails" | "InsulationNegative" | "InsulationPositive" | "InsulationOriginal" | "DefaultBMSLow" | "DiagnosticBMSLow" | "DefaultBMSHigh" | "DiagnosticBMSHigh" | "BatteryVoltageLow" | "BatteryVoltageHigh" | "TotalBatteryVoltageLow" | "TotalBatteryVoltageHigh" | "BatteryTemperatureLow" | "BatteryTemperatureHigh" | "BatteryBalanceLow" | "BatteryBalanceHigh" | "SingleCellVoltageLow" | "SingleCellTemperatureLow" | "ChargeStateLow" | "ChargeStateHigh" | "BatteryCurrentLow" | "BatteryCurrentHigh" | "BatteryEnergyParamsLow" | "BatteryEnergyParamsHigh" | "BatteryMaxVoltageLow" | "BatteryEstimatedChargeLow" | "BatteryMinTemperatureLow" | "BatteryMaxTemperatureLow" | "BatteryMinBalancingLow" | "BatteryMaxBalancingLow" | "BatteryMinVoltageLow" | "BatteryMinVoltageHigh" | "BatteryMaxVoltageHigh" | "BatteryEstimatedChargeHigh" | "BatteryMinTemperatureHigh" | "BatteryMaxTemperatureHigh" | "BatteryMinBalancingHigh" | "BatteryMaxBalancingHigh" | "BatteryEventLow" | "BatteryEventHigh" | "BrakeTemperature" | "PropulsionSpeed" | "FSMState" | "FSMEvent" | "EndOfTrackTriggered" | "Localisation" | "Velocity" | "Acceleration" | "Direction" | "BrakePressure" | "UnknownCanId" | "Info" | "Presure_VB" | "Average_Temp_VB_Bottom" | "Average_Temp_VB_top" | "Temp_HEMS_1" | "Temp_HEMS_2" | "Temp_HEMS_3" | "Temp_HEMS_4" | "Temp_Motor_1" | "Temp_Motor_2" | "Ambient_presure" | "Ambient_temp" | "Temp_EMS_1" | "Temp_EMS_2" | "Temp_EMS_3" | "Temp_EMS_4" | "SingleCellVoltageHigh_1" | "SingleCellTemperatureHigh_1" | "SingleCellVoltageHigh_2" | "SingleCellTemperatureHigh_2" | "SingleCellVoltageHigh_3" | "SingleCellTemperatureHigh_3" | "SingleCellVoltageHigh_4" | "SingleCellTemperatureHigh_4" | "SingleCellVoltageHigh_5" | "SingleCellTemperatureHigh_5" | "SingleCellVoltageHigh_6" | "SingleCellTemperatureHigh_6" | "SingleCellVoltageHigh_7" | "SingleCellTemperatureHigh_7" | "SingleCellVoltageHigh_8" | "SingleCellTemperatureHigh_8" | "SingleCellVoltageHigh_9" | "SingleCellTemperatureHigh_9" | "SingleCellVoltageHigh_10" | "SingleCellTemperatureHigh_10" | "SingleCellVoltageHigh_11" | "SingleCellTemperatureHigh_11" | "SingleCellVoltageHigh_12" | "SingleCellTemperatureHigh_12" | "SingleCellVoltageHigh_13" | "SingleCellTemperatureHigh_13" | "SingleCellVoltageHigh_14" | "SingleCellTemperatureHigh_14" | "Module1MaxVoltage" | "Module2MaxVoltage" | "Module3MaxVoltage" | "Module4MaxVoltage" | "Module5MaxVoltage" | "Module6MaxVoltage" | "Module7MaxVoltage" | "Module8MaxVoltage" | "Module1MinVoltage" | "Module2MinVoltage" | "Module3MinVoltage" | "Module4MinVoltage" | "Module5MinVoltage" | "Module6MinVoltage" | "Module7MinVoltage" | "Module8MinVoltage" | "Module1MaxTemperature" | "Module2MaxTemperature" | "Module3MaxTemperature" | "Module4MaxTemperature" | "Module5MaxTemperature" | "Module6MaxTemperature" | "Module7MaxTemperature" | "Module8MaxTemperature" | "Module1MinTemperature" | "Module2MinTemperature" | "Module3MinTemperature" | "Module4MinTemperature" | "Module5MinTemperature" | "Module6MinTemperature" | "Module7MinTemperature" | "Module8MinTemperature" | "Module1AvgVoltage" | "Module2AvgVoltage" | "Module3AvgVoltage" | "Module4AvgVoltage" | "Module5AvgVoltage" | "Module6AvgVoltage" | "Module7AvgVoltage" | "Module8AvgVoltage" | "Module1AvgTemperature" | "Module2AvgTemperature" | "Module3AvgTemperature" | "Module4AvgTemperature" | "Module5AvgTemperature" | "Module6AvgTemperature" | "Module7AvgTemperature" | "Module8AvgTemperature" | "ResponseHeartbeat" | "LeviInstruction" | "LowPressureSensor" | "HighPressureSensor" | "levi_hems_gap_a" | "levi_hems_gap_b" | "levi_hems_gap_c" | "levi_hems_gap_d" | "levi_ems_gap_a" | "levi_ems_gap_b" | "levi_ems_gap_c" | "levi_ems_gap_d" | "levi_hems_current_a1" | "levi_hems_current_a2" | "levi_hems_current_b1" | "levi_hems_current_b2" | "levi_hems_current_c1" | "levi_hems_current_c2" | "levi_hems_current_d1" | "levi_hems_current_d2" | "levi_ems_current_ab" | "levi_ems_current_cd" | "levi_hems_airgap" | "levi_hems_pitch" | "levi_hems_roll" | "levi_ems_offset_ab" | "levi_ems_offset_cd" | "levi_hems_power" | "levi_ems_power" | "levi_volt_min" | "levi_volt_max" | "levi_volt_avg" | "BrakingCommDebug" | "BrakingSignalDebug" | "BrakingBoolDebug" | "BrakingRearmDebug" | "PropGPIODebug" | "ReceivedCan" | "SendingCANEvent" +export const NamedDatatypeValues = ["YourNewDatatypeName", "DefaultDatatype", "PropulsionVoltage", "PropulsionCurrent", "PropulsionVRefInt", "LevitationTemperature", "AccelerationX", "AccelerationY", "AccelerationZ", "GyroscopeX", "GyroscopeY", "GyroscopeZ", "IMDGeneralInfo", "IMDIsolationDetails", "IMDVoltageDetails", "InsulationNegative", "InsulationPositive", "InsulationOriginal", "DefaultBMSLow", "DiagnosticBMSLow", "DefaultBMSHigh", "DiagnosticBMSHigh", "BatteryVoltageLow", "BatteryVoltageHigh", "TotalBatteryVoltageLow", "TotalBatteryVoltageHigh", "BatteryTemperatureLow", "BatteryTemperatureHigh", "BatteryBalanceLow", "BatteryBalanceHigh", "SingleCellVoltageLow", "SingleCellTemperatureLow", "ChargeStateLow", "ChargeStateHigh", "BatteryCurrentLow", "BatteryCurrentHigh", "BatteryEnergyParamsLow", "BatteryEnergyParamsHigh", "BatteryMaxVoltageLow", "BatteryEstimatedChargeLow", "BatteryMinTemperatureLow", "BatteryMaxTemperatureLow", "BatteryMinBalancingLow", "BatteryMaxBalancingLow", "BatteryMinVoltageLow", "BatteryMinVoltageHigh", "BatteryMaxVoltageHigh", "BatteryEstimatedChargeHigh", "BatteryMinTemperatureHigh", "BatteryMaxTemperatureHigh", "BatteryMinBalancingHigh", "BatteryMaxBalancingHigh", "BatteryEventLow", "BatteryEventHigh", "BrakeTemperature", "PropulsionSpeed", "FSMState", "FSMEvent", "EndOfTrackTriggered", "Localisation", "Velocity", "Acceleration", "Direction", "BrakePressure", "UnknownCanId", "Info", "Presure_VB", "Average_Temp_VB_Bottom", "Average_Temp_VB_top", "Temp_HEMS_1", "Temp_HEMS_2", "Temp_HEMS_3", "Temp_HEMS_4", "Temp_Motor_1", "Temp_Motor_2", "Ambient_presure", "Ambient_temp", "Temp_EMS_1", "Temp_EMS_2", "Temp_EMS_3", "Temp_EMS_4", "SingleCellVoltageHigh_1", "SingleCellTemperatureHigh_1", "SingleCellVoltageHigh_2", "SingleCellTemperatureHigh_2", "SingleCellVoltageHigh_3", "SingleCellTemperatureHigh_3", "SingleCellVoltageHigh_4", "SingleCellTemperatureHigh_4", "SingleCellVoltageHigh_5", "SingleCellTemperatureHigh_5", "SingleCellVoltageHigh_6", "SingleCellTemperatureHigh_6", "SingleCellVoltageHigh_7", "SingleCellTemperatureHigh_7", "SingleCellVoltageHigh_8", "SingleCellTemperatureHigh_8", "SingleCellVoltageHigh_9", "SingleCellTemperatureHigh_9", "SingleCellVoltageHigh_10", "SingleCellTemperatureHigh_10", "SingleCellVoltageHigh_11", "SingleCellTemperatureHigh_11", "SingleCellVoltageHigh_12", "SingleCellTemperatureHigh_12", "SingleCellVoltageHigh_13", "SingleCellTemperatureHigh_13", "SingleCellVoltageHigh_14", "SingleCellTemperatureHigh_14", "Module1MaxVoltage", "Module2MaxVoltage", "Module3MaxVoltage", "Module4MaxVoltage", "Module5MaxVoltage", "Module6MaxVoltage", "Module7MaxVoltage", "Module8MaxVoltage", "Module1MinVoltage", "Module2MinVoltage", "Module3MinVoltage", "Module4MinVoltage", "Module5MinVoltage", "Module6MinVoltage", "Module7MinVoltage", "Module8MinVoltage", "Module1MaxTemperature", "Module2MaxTemperature", "Module3MaxTemperature", "Module4MaxTemperature", "Module5MaxTemperature", "Module6MaxTemperature", "Module7MaxTemperature", "Module8MaxTemperature", "Module1MinTemperature", "Module2MinTemperature", "Module3MinTemperature", "Module4MinTemperature", "Module5MinTemperature", "Module6MinTemperature", "Module7MinTemperature", "Module8MinTemperature", "Module1AvgVoltage", "Module2AvgVoltage", "Module3AvgVoltage", "Module4AvgVoltage", "Module5AvgVoltage", "Module6AvgVoltage", "Module7AvgVoltage", "Module8AvgVoltage", "Module1AvgTemperature", "Module2AvgTemperature", "Module3AvgTemperature", "Module4AvgTemperature", "Module5AvgTemperature", "Module6AvgTemperature", "Module7AvgTemperature", "Module8AvgTemperature", "ResponseHeartbeat", "LeviInstruction", "LowPressureSensor", "HighPressureSensor", "levi_hems_gap_a", "levi_hems_gap_b", "levi_hems_gap_c", "levi_hems_gap_d", "levi_ems_gap_a", "levi_ems_gap_b", "levi_ems_gap_c", "levi_ems_gap_d", "levi_hems_current_a1", "levi_hems_current_a2", "levi_hems_current_b1", "levi_hems_current_b2", "levi_hems_current_c1", "levi_hems_current_c2", "levi_hems_current_d1", "levi_hems_current_d2", "levi_ems_current_ab", "levi_ems_current_cd", "levi_hems_airgap", "levi_hems_pitch", "levi_hems_roll", "levi_ems_offset_ab", "levi_ems_offset_cd", "levi_hems_power", "levi_ems_power", "levi_volt_min", "levi_volt_max", "levi_volt_avg", "BrakingCommDebug", "BrakingSignalDebug", "BrakingBoolDebug", "BrakingRearmDebug", "PropGPIODebug", "ReceivedCan", "SendingCANEvent"]; // Not touched by auto-gen diff --git a/gs/src/routes/+layout.svelte b/gs/src/routes/+layout.svelte index 47abf41bf..c7f41d9ef 100644 --- a/gs/src/routes/+layout.svelte +++ b/gs/src/routes/+layout.svelte @@ -347,13 +347,13 @@ gdd.stores.registerStore("FSMState", 0); - gdd.start(100); + gdd.start(50); initializeStores(); - onDestroy(() => { + onDestroy(async () => { GrandDataDistributor.getInstance().kill(); - unlisten(); + (await unlisten)(); }) From 8a57370ae5fbca466a33f137fdd0bbc3f301b77e Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 18:31:14 +0200 Subject: [PATCH 04/41] fix types --- gs/station/build.rs | 1 + gs/station/src/backend.rs | 12 ++++++++---- gs/station/src/connect/handle_incoming_data.rs | 10 ++++++---- gs/station/src/connect/mod.rs | 15 +++++++++------ gs/station/src/connect/queueing.rs | 10 ++++------ gs/station/src/connect/tcp_reader.rs | 6 ++++-- gs/station/src/connect/tcp_writer.rs | 6 ++++-- gs/station/src/levi/mod.rs | 9 ++++++--- gs/station/src/levi/parse_input.rs | 8 ++++---- gs/station/src/levi/read_from_stdout.rs | 7 ++++--- gs/station/src/levi/write_to_stdin.rs | 7 +++++-- gs/station/src/main.rs | 6 ++++++ gs/station/src/tui/app.rs | 10 +++++++--- 13 files changed, 68 insertions(+), 39 deletions(-) diff --git a/gs/station/build.rs b/gs/station/build.rs index eda6bc425..a528817c8 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -5,6 +5,7 @@ use std::env; use std::fs; use std::path::Path; use std::path::PathBuf; + use anyhow::Result; use goose_utils::check_ids; use goose_utils::commands::generate_commands; diff --git a/gs/station/src/backend.rs b/gs/station/src/backend.rs index 4b7bd2b3d..fd9fe26bc 100644 --- a/gs/station/src/backend.rs +++ b/gs/station/src/backend.rs @@ -7,6 +7,10 @@ use tokio::task::AbortHandle; use crate::api::Message; use crate::Command; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageReceiver; +use crate::MessageSender; // /// Any frontend that interfaces with this backend needs to comply to this trait // pub trait Frontend { @@ -21,10 +25,10 @@ use crate::Command; pub struct Backend { pub server_handle: Option, pub levi_handle: Option<(AbortHandle, AbortHandle)>, - pub message_transmitter: tokio::sync::broadcast::Sender, - pub message_receiver: tokio::sync::broadcast::Receiver, - pub command_transmitter: tokio::sync::broadcast::Sender, - pub command_receiver: tokio::sync::broadcast::Receiver, + pub message_transmitter: MessageSender, + pub message_receiver: MessageReceiver, + pub command_transmitter: CommandSender, + pub command_receiver: CommandReceiver, pub log: Log, pub save_path: PathBuf, } diff --git a/gs/station/src/connect/handle_incoming_data.rs b/gs/station/src/connect/handle_incoming_data.rs index 94bc319b8..d2edb76fb 100644 --- a/gs/station/src/connect/handle_incoming_data.rs +++ b/gs/station/src/connect/handle_incoming_data.rs @@ -1,18 +1,20 @@ #![allow(clippy::single_match)] -use tokio::sync::broadcast::Sender; use crate::api::Datapoint; use crate::api::Message; -use crate::{Command, Info}; +use crate::Command; +use crate::CommandSender; use crate::Datatype; +use crate::Info; +use crate::MessageSender; use crate::COMMAND_HASH; use crate::DATA_HASH; use crate::EVENTS_HASH; pub async fn handle_incoming_data( data: Datapoint, - msg_sender: Sender, - cmd_sender: Sender, + msg_sender: MessageSender, + cmd_sender: CommandSender, ) -> anyhow::Result<()> { msg_sender.send(Message::Data(data.clone()))?; diff --git a/gs/station/src/connect/mod.rs b/gs/station/src/connect/mod.rs index 8c0ef1897..c7d04152b 100644 --- a/gs/station/src/connect/mod.rs +++ b/gs/station/src/connect/mod.rs @@ -10,11 +10,14 @@ use crate::api::gs_socket; use crate::api::Message; use crate::connect::tcp_reader::get_messages_from_tcp; use crate::connect::tcp_writer::transmit_commands_to_tcp; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageSender; pub async fn connect_main( - message_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_receiver: CommandReceiver, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { // Bind the listener to the address message_transmitter @@ -38,9 +41,9 @@ pub async fn connect_main( async fn process( socket: TcpStream, - message_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_receiver: CommandReceiver, + command_transmitter: CommandSender, ) { let (reader, writer) = socket.into_split(); let transmit = message_transmitter.clone(); diff --git a/gs/station/src/connect/queueing.rs b/gs/station/src/connect/queueing.rs index e3378d510..c15c2524d 100644 --- a/gs/station/src/connect/queueing.rs +++ b/gs/station/src/connect/queueing.rs @@ -1,11 +1,9 @@ use std::collections::VecDeque; -use tokio::sync::broadcast::Sender; - use crate::api::Datapoint; -use crate::api::Message; use crate::connect::handle_incoming_data::handle_incoming_data; -use crate::Command; +use crate::CommandSender; +use crate::MessageSender; /// # Unloads from the buffer and transmits any messages found /// ``` @@ -18,8 +16,8 @@ use crate::Command; /// ``` pub async fn parse( parsing_buffer: &mut VecDeque, - msg_sender: Sender, - cmd_sender: Sender, + msg_sender: MessageSender, + cmd_sender: CommandSender, ) -> anyhow::Result<()> { while let Some(p) = parsing_buffer.front() { if *p == 0xFF { diff --git a/gs/station/src/connect/tcp_reader.rs b/gs/station/src/connect/tcp_reader.rs index a90f2cd4b..afd047066 100644 --- a/gs/station/src/connect/tcp_reader.rs +++ b/gs/station/src/connect/tcp_reader.rs @@ -4,12 +4,14 @@ use tokio::io::AsyncReadExt; use tokio::net::tcp::OwnedReadHalf; use crate::api::Message; +use crate::CommandSender; +use crate::MessageSender; use crate::NETWORK_BUFFER_SIZE; pub async fn get_messages_from_tcp( mut reader: OwnedReadHalf, - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { let mut buffer = [0; { NETWORK_BUFFER_SIZE }]; let mut byte_queue: VecDeque = VecDeque::new(); diff --git a/gs/station/src/connect/tcp_writer.rs b/gs/station/src/connect/tcp_writer.rs index 827f485b0..cb4a66ef1 100644 --- a/gs/station/src/connect/tcp_writer.rs +++ b/gs/station/src/connect/tcp_writer.rs @@ -4,10 +4,12 @@ use tokio::net::tcp::OwnedWriteHalf; use crate::api::Message; use crate::api::Message::Error; use crate::Command; +use crate::CommandReceiver; +use crate::MessageSender; pub async fn transmit_commands_to_tcp( - mut command_receiver: tokio::sync::broadcast::Receiver, - status_transmitter: tokio::sync::broadcast::Sender, + mut command_receiver: CommandReceiver, + status_transmitter: MessageSender, mut writer: OwnedWriteHalf, ) -> anyhow::Result<()> { tokio::spawn(async move { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index 0bd46f7dc..d306c9ffe 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,12 +6,15 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageSender; use crate::LEVI_EXEC_PATH; pub fn levi_main( - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, + message_transmitter: MessageSender, + command_transmitter: CommandSender, + command_receiver: CommandReceiver, ) -> anyhow::Result<(AbortHandle, AbortHandle)> { let mut lcmd = tokio::process::Command::new(LEVI_EXEC_PATH); message_transmitter.send(Message::Info(format!("starting levi at {}", LEVI_EXEC_PATH)))?; diff --git a/gs/station/src/levi/parse_input.rs b/gs/station/src/levi/parse_input.rs index 90f30ac66..c6514f051 100644 --- a/gs/station/src/levi/parse_input.rs +++ b/gs/station/src/levi/parse_input.rs @@ -1,14 +1,14 @@ -use tokio::sync::broadcast::Sender; - use crate::api::Datapoint; use crate::api::Message; use crate::Command; +use crate::CommandSender; use crate::Datatype; +use crate::MessageSender; pub fn handle_line_from_levi( line: &String, - msg_send: Sender, - cmd_send: Sender, + msg_send: MessageSender, + cmd_send: CommandSender, ) -> anyhow::Result<()> { let params = line.split(':').collect::>(); diff --git a/gs/station/src/levi/read_from_stdout.rs b/gs/station/src/levi/read_from_stdout.rs index d8226af3a..92a16b9ec 100644 --- a/gs/station/src/levi/read_from_stdout.rs +++ b/gs/station/src/levi/read_from_stdout.rs @@ -2,14 +2,15 @@ use tokio::io::AsyncBufReadExt; use crate::api::Message; use crate::levi::parse_input::handle_line_from_levi; -use crate::Command; +use crate::CommandSender; +use crate::MessageSender; /// # Read from levi child stdout /// reads from the stdout of the levi child process, and sends the messages to the message_transmitter. pub async fn read_from_levi_child_stdout( stdout: tokio::process::ChildStdout, - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { let mut reader = tokio::io::BufReader::new(stdout); let mut line = String::new(); diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index 203dcd781..cee53be50 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,11 +1,14 @@ use tokio::io::AsyncWriteExt; +use crate::CommandReceiver; +use crate::MessageSender; + /// # Writing to levi's stdin /// when a command is sent to the broadcast channel, it is sent to levi's stdin. pub async fn write_to_levi_child_stdin( mut stdin: tokio::process::ChildStdin, - status_sender: tokio::sync::broadcast::Sender, - mut command_receiver: tokio::sync::broadcast::Receiver, + status_sender: MessageSender, + mut command_receiver: CommandReceiver, ) -> anyhow::Result<()> { loop { let cmd = command_receiver.recv().await?; diff --git a/gs/station/src/main.rs b/gs/station/src/main.rs index 270626eb5..9a8d0a049 100644 --- a/gs/station/src/main.rs +++ b/gs/station/src/main.rs @@ -1,6 +1,7 @@ // Prevents an additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use crate::api::Message; use crate::backend::Backend; #[cfg(feature = "backend")] use crate::frontend::app::tauri_main; @@ -18,6 +19,11 @@ pub mod tui; include!(concat!(env!("OUT_DIR"), "/config.rs")); +pub type CommandSender = tokio::sync::broadcast::Sender; +pub type CommandReceiver = tokio::sync::broadcast::Receiver; +pub type MessageSender = tokio::sync::broadcast::Sender; +pub type MessageReceiver = tokio::sync::broadcast::Receiver; + /// Entry point of the application #[tokio::main] async fn main() { diff --git a/gs/station/src/tui/app.rs b/gs/station/src/tui/app.rs index 9a32fb0c8..7dc92c8d7 100644 --- a/gs/station/src/tui/app.rs +++ b/gs/station/src/tui/app.rs @@ -2,8 +2,9 @@ use std::collections::BTreeMap; use ratatui::Frame; -use crate::api::{LocationSequence, state_to_string}; +use crate::api::state_to_string; use crate::api::Datapoint; +use crate::api::LocationSequence; use crate::api::Message; use crate::backend::Backend; use crate::tui::render::CmdRow; @@ -106,7 +107,10 @@ impl App { self.safe = false; }, x => { - self.logs.push((Message::Status(x), format!("[info: {} at {}]", datapoint.value, datapoint.timestamp))); + self.logs.push(( + Message::Status(x), + format!("[info: {} at {}]", datapoint.value, datapoint.timestamp), + )); }, }, Datatype::RoutePlan => { @@ -146,7 +150,7 @@ impl App { }, Datatype::ResponseHeartbeat => { self.special_data.insert(Datatype::ResponseHeartbeat, datapoint.timestamp); - } + }, x if self.special_data.keys().collect::>().contains(&&x) => { self.special_data.insert(x, datapoint.value); }, From b66debdb4f8526aeca906503f39b4b55a684c205 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 19:59:23 +0200 Subject: [PATCH 05/41] send data to levi and frontend --- app/build.rs | 79 +++++++++---------- config/config.toml | 11 +-- gs/station/build.rs | 39 +++++---- gs/station/src/api.rs | 10 ++- gs/station/src/backend.rs | 1 + .../src/connect/handle_incoming_data.rs | 3 +- gs/station/src/connect/mod.rs | 4 +- gs/station/src/data/mod.rs | 1 + gs/station/src/data/process.rs | 37 +++++++++ gs/station/src/frontend/commands.rs | 3 +- gs/station/src/levi/mod.rs | 4 +- gs/station/src/levi/parse_input.rs | 5 +- gs/station/src/levi/write_to_stdin.rs | 47 ++++++++--- gs/station/src/main.rs | 1 + 14 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 gs/station/src/data/mod.rs create mode 100644 gs/station/src/data/process.rs diff --git a/app/build.rs b/app/build.rs index 19bc5ce11..d14e68437 100644 --- a/app/build.rs +++ b/app/build.rs @@ -27,7 +27,6 @@ struct GS { ip: [u8; 4], force: bool, port: u16, - // udp_port: u16, buffer_size: usize, timeout: u64, heartbeat: u64, @@ -37,21 +36,21 @@ struct GS { struct Pod { net: NetConfig, internal: InternalConfig, - bms: Bms, + comm: Comm, } #[derive(Debug, Deserialize)] -struct Bms { - lv_ids: Vec, - hv_ids: Vec, +struct Comm { + bms_lv_ids: Vec, + bms_hv_ids: Vec, gfd_ids: Vec, } #[derive(Debug, Deserialize)] struct NetConfig { - ip: [u8; 4], - port: u16, - udp_port: u16, + // ip: [u8; 4], + // port: u16, + // udp_port: u16, mac_addr: [u8; 6], keep_alive: u64, } @@ -69,12 +68,6 @@ pub const COMMANDS_PATH: &str = "../config/commands.toml"; pub const EVENTS_PATH: &str = "../config/events.toml"; fn main() -> Result<()> { - // if cfg!(debug_assertions) { - // env::set_var("DEFMT_LOG", "trace"); - // } else { - // env::set_var("DEFMT_LOG", "off"); - // } - let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("config.rs"); @@ -117,21 +110,23 @@ fn configure_ip(config: &Config) -> String { } fn configure_pod(config: &Config) -> String { + // format!( + // "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.port + // ) + // + &*format!( + // "pub static POD_UDP_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.udp_port + // ) + format!( - "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.port - ) + &*format!( - "pub static POD_UDP_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.udp_port - ) + &*format!( "pub static POD_MAC_ADDRESS: [u8;6] = [{},{},{},{},{},{}];\n", config.pod.net.mac_addr[0], config.pod.net.mac_addr[1], @@ -139,8 +134,8 @@ fn configure_pod(config: &Config) -> String { config.pod.net.mac_addr[3], config.pod.net.mac_addr[4], config.pod.net.mac_addr[5] - ) + &*format!("pub const KEEP_ALIVE: u64 = {};\n", config.pod.net.keep_alive) - + &*format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) + ) + &format!("pub const KEEP_ALIVE: u64 = {};\n", config.pod.net.keep_alive) + + &format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) } fn configure_internal(config: &Config) -> String { @@ -149,20 +144,20 @@ fn configure_internal(config: &Config) -> String { + &*format!("pub const CAN_QUEUE_SIZE: usize = {};\n", config.pod.internal.can_queue_size) + &*format!( "pub const LV_IDS: [u16;{}] = [{}];\n", - config.pod.bms.lv_ids.len(), - config.pod.bms.lv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config.pod.comm.bms_lv_ids.len(), + config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") ) + &*format!( "pub const HV_IDS: [u16;{}] = [{}];\n", - config.pod.bms.hv_ids.len(), - config.pod.bms.hv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config.pod.comm.bms_hv_ids.len(), + config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") ) + &*format!( "pub const GFD_IDS: [u16;{}] = [{}];\n", - config.pod.bms.gfd_ids.len(), + config.pod.comm.gfd_ids.len(), config .pod - .bms + .comm .gfd_ids .iter() .map(|x| x.to_string()) @@ -171,14 +166,14 @@ fn configure_internal(config: &Config) -> String { ) + &*format!( "pub const BATTERY_GFD_IDS: [u16;{}] = [{},{},{}];\n", - config.pod.bms.lv_ids.len() - + config.pod.bms.hv_ids.len() - + config.pod.bms.gfd_ids.len(), - config.pod.bms.lv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), - config.pod.bms.hv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), + config.pod.comm.bms_lv_ids.len() + + config.pod.comm.bms_hv_ids.len() + + config.pod.comm.gfd_ids.len(), + config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), + config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), config .pod - .bms + .comm .gfd_ids .iter() .map(|x| x.to_string()) diff --git a/config/config.toml b/config/config.toml index bf531b17c..7598116f4 100644 --- a/config/config.toml +++ b/config/config.toml @@ -2,7 +2,6 @@ ip = [192,168,1,3] force = true port = 6942 -udp_port = 6943 buffer_size = 1460 # this is the MAXIMUM size of messages transmitted, in bytes. timeout = 1500 # this is the timeout for the socket, in milliseconds. heartbeat = 1000 # how often to send a keep_alive heartbeat, in milliseconds. @@ -14,9 +13,6 @@ shortcut_channel = "shortcut_channel" levi_exec_path = "C:/Users/kikoa/RustroverProjects/Helios_III/gs/station/Levi/windows-x86_64-debug/PmpGettingStartedCs.exe" [pod.net] -ip = [192, 168, 0, 199] -port = 17034 -udp_port = 17035 mac_addr = [0x00, 0x1e, 0x67, 0x4c, 0x5c, 0x3e] keep_alive = 1000 # keep alive interval, in milliseconds. @@ -25,11 +21,12 @@ event_queue_size = 128 data_queue_size = 256 can_queue_size = 128 -[pod.bms] -lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1BC, 0x1DC, 0x1FC, 0x29C,0x221] -hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] +[pod.comm] +bms_lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1BC, 0x1DC, 0x1FC, 0x29C,0x221] +bms_hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] +levi_requested_data = ["Localisation", "PropulsionCurrent"] [[Info]] label = "ServerStarted" diff --git a/gs/station/build.rs b/gs/station/build.rs index a528817c8..59da158bf 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -22,12 +22,18 @@ struct Config { #[derive(Debug, Deserialize)] struct Pod { - net: NetConfig, + // net: NetConfig, + comm: CommConfig, } +// #[derive(Debug, Deserialize)] +// struct NetConfig { +// // ip: [u8; 4], +// // port: u16, +// } + #[derive(Debug, Deserialize)] -struct NetConfig { - ip: [u8; 4], - port: u16, +struct CommConfig { + levi_requested_data: Vec, } #[derive(Debug, Deserialize)] @@ -35,7 +41,6 @@ struct GS { ip: [u8; 4], force: bool, port: u16, - // udp_port: u16, buffer_size: usize, timeout: u64, heartbeat: u64, @@ -86,20 +91,22 @@ fn main() -> Result<()> { fn configure_gs(config: &Config) -> String { // format!("pub fn gs_socket() -> std::net::SocketAddr {{ std::net::SocketAddr::new(std::net::IpAddr::from([{},{},{},{}]),{}) }}\n", config.gs.ip[0], config.gs.ip[1], config.gs.ip[2], config.gs.ip[3], config.gs.port) - format!( - "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.port - ) + &*format!("pub const NETWORK_BUFFER_SIZE: usize = {};\n", config.gs.buffer_size) - + &*format!("pub const IP_TIMEOUT: u64 = {};\n", config.gs.timeout) - + &*format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) - + &*format!( + // format!( + // "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.port + // ) + &* + format!("pub const NETWORK_BUFFER_SIZE: usize = {};\n", config.gs.buffer_size) + + &format!("pub const IP_TIMEOUT: u64 = {};\n", config.gs.timeout) + + &format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) + + &format!( "pub const LEVI_EXEC_PATH: &str = \"{}\";\n", config.gs.levi_exec_path.to_str().unwrap() ) + + &format!("\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", config.pod.comm.levi_requested_data.len(), config.pod.comm.levi_requested_data.iter().map(|x| format!("Datatype::{x}, ")).collect::()) } fn configure_channels(config: &Config) -> String { diff --git a/gs/station/src/api.rs b/gs/station/src/api.rs index 18e097f4d..f7de956ca 100644 --- a/gs/station/src/api.rs +++ b/gs/station/src/api.rs @@ -36,9 +36,17 @@ impl Datapoint { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ProcessedData { + pub datatype: Datatype, + pub value: f64, + pub timestamp: u64, + pub style: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum Message { - Data(Datapoint), + Data(ProcessedData), Status(Info), Info(String), Warning(String), diff --git a/gs/station/src/backend.rs b/gs/station/src/backend.rs index fd9fe26bc..57f3e5c13 100644 --- a/gs/station/src/backend.rs +++ b/gs/station/src/backend.rs @@ -91,6 +91,7 @@ impl Backend { self.message_transmitter.clone(), self.command_transmitter.clone(), self.command_receiver.resubscribe(), + self.message_receiver.resubscribe(), ) { Ok(lh) => { self.levi_handle = Some(lh); diff --git a/gs/station/src/connect/handle_incoming_data.rs b/gs/station/src/connect/handle_incoming_data.rs index d2edb76fb..102c15d58 100644 --- a/gs/station/src/connect/handle_incoming_data.rs +++ b/gs/station/src/connect/handle_incoming_data.rs @@ -2,6 +2,7 @@ use crate::api::Datapoint; use crate::api::Message; +use crate::data::process::process; use crate::Command; use crate::CommandSender; use crate::Datatype; @@ -16,7 +17,7 @@ pub async fn handle_incoming_data( msg_sender: MessageSender, cmd_sender: CommandSender, ) -> anyhow::Result<()> { - msg_sender.send(Message::Data(data.clone()))?; + msg_sender.send(Message::Data(process(&data)))?; match data.datatype { Datatype::LeviInstruction => { diff --git a/gs/station/src/connect/mod.rs b/gs/station/src/connect/mod.rs index c7d04152b..d04d4946e 100644 --- a/gs/station/src/connect/mod.rs +++ b/gs/station/src/connect/mod.rs @@ -29,7 +29,7 @@ pub async fn connect_main( // The second item contains the IP and port of the new connection. let (socket, client_addr) = listener.accept().await?; message_transmitter.send(Message::Info(format!("New connection from: {}", client_addr)))?; - process( + process_stream( socket, message_transmitter.clone(), command_receiver.resubscribe(), @@ -39,7 +39,7 @@ pub async fn connect_main( } } -async fn process( +async fn process_stream( socket: TcpStream, message_transmitter: MessageSender, command_receiver: CommandReceiver, diff --git a/gs/station/src/data/mod.rs b/gs/station/src/data/mod.rs new file mode 100644 index 000000000..80fe812c8 --- /dev/null +++ b/gs/station/src/data/mod.rs @@ -0,0 +1 @@ +pub mod process; diff --git a/gs/station/src/data/process.rs b/gs/station/src/data/process.rs new file mode 100644 index 000000000..989e94c20 --- /dev/null +++ b/gs/station/src/data/process.rs @@ -0,0 +1,37 @@ +use crate::api::Datapoint; +use crate::api::ProcessedData; +use crate::Datatype; +use crate::ValueCheckResult; + +/// Preprocessing data from the pod before sending to the frontend +pub fn process(datapoint: &Datapoint) -> ProcessedData { + let style = match datapoint.datatype.check_bounds(datapoint.value) { + ValueCheckResult::Fine => "".to_string(), + ValueCheckResult::Warn => "warning".to_string(), + ValueCheckResult::Error => "error".to_string(), + ValueCheckResult::BrakeNow => "critical".to_string(), + }; + let x = datapoint.value as f64; + let value = match datapoint.datatype { + Datatype::BatteryEstimatedChargeHigh | Datatype::BatteryEstimatedChargeLow => x / 100.0, + Datatype::TotalBatteryVoltageHigh | Datatype::TotalBatteryVoltageLow => x / 100.0 - 2.0, + Datatype::BatteryCurrentLow => x / 10.0 + 150.0, + Datatype::BatteryCurrentHigh => x / 10.0 + 10.0, + + Datatype::BrakingCommDebug => x * 3.3 / 65535.0, + + Datatype::IMDVoltageDetails => { + if datapoint.value == 65535 { + 0.0 + } else { + x * 0.005 * 110.0 / 250.0 + } + }, + + Datatype::Localisation => x * 1.6, + + _ => x, + }; + + ProcessedData { datatype: datapoint.datatype, value, timestamp: datapoint.timestamp, style } +} diff --git a/gs/station/src/frontend/commands.rs b/gs/station/src/frontend/commands.rs index ba48ddb99..7bcc06a6b 100644 --- a/gs/station/src/frontend/commands.rs +++ b/gs/station/src/frontend/commands.rs @@ -6,6 +6,7 @@ use tauri::State; use crate::api::Datapoint; use crate::api::Message; +use crate::api::ProcessedData; use crate::backend::Backend; use crate::frontend::BackendState; use crate::frontend::BACKEND; @@ -48,7 +49,7 @@ pub fn generate_test_data() -> Vec { #[macro_export] #[allow(unused)] #[tauri::command] -pub fn unload_buffer(state: State) -> Vec { +pub fn unload_buffer(state: State) -> Vec { let mut data_buffer = state.data_buffer.lock().unwrap(); let mut datapoints = Vec::new(); for msg in data_buffer.iter() { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index d306c9ffe..51630bdd5 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,7 +6,7 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; -use crate::CommandReceiver; +use crate::{CommandReceiver, MessageReceiver}; use crate::CommandSender; use crate::MessageSender; use crate::LEVI_EXEC_PATH; @@ -15,6 +15,7 @@ pub fn levi_main( message_transmitter: MessageSender, command_transmitter: CommandSender, command_receiver: CommandReceiver, + message_receiver: MessageReceiver, ) -> anyhow::Result<(AbortHandle, AbortHandle)> { let mut lcmd = tokio::process::Command::new(LEVI_EXEC_PATH); message_transmitter.send(Message::Info(format!("starting levi at {}", LEVI_EXEC_PATH)))?; @@ -33,6 +34,7 @@ pub fn levi_main( stdin, transmitter.clone(), command_receiver, + message_receiver, ) .await { diff --git a/gs/station/src/levi/parse_input.rs b/gs/station/src/levi/parse_input.rs index c6514f051..a7066cba8 100644 --- a/gs/station/src/levi/parse_input.rs +++ b/gs/station/src/levi/parse_input.rs @@ -1,5 +1,6 @@ use crate::api::Datapoint; use crate::api::Message; +use crate::data::process::process; use crate::Command; use crate::CommandSender; use crate::Datatype; @@ -29,11 +30,11 @@ pub fn handle_line_from_levi( }, "DATA" if params.len() > 2 => { if let Ok(x) = params[2].trim().replace(',', ".").parse::() { - msg_send.send(Message::Data(Datapoint::new( + msg_send.send(Message::Data(process(&Datapoint::new( Datatype::from_str(params[1]), x.to_bits(), chrono::offset::Local::now().timestamp() as u64, - )))?; + ))))?; } else { msg_send.send(Message::Warning(format!( "Levi data not a number: {:?}", diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index cee53be50..efcc0f06f 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,6 +1,9 @@ use tokio::io::AsyncWriteExt; +use tokio::sync::broadcast::error::TryRecvError; +use crate::LEVI_REQUESTED_DATA; -use crate::CommandReceiver; +use crate::{CommandReceiver, MessageReceiver}; +use crate::api::Message; use crate::MessageSender; /// # Writing to levi's stdin @@ -9,15 +12,41 @@ pub async fn write_to_levi_child_stdin( mut stdin: tokio::process::ChildStdin, status_sender: MessageSender, mut command_receiver: CommandReceiver, + mut message_receiver: MessageReceiver, ) -> anyhow::Result<()> { loop { - let cmd = command_receiver.recv().await?; - stdin.write_all(format!("{}\n", cmd.to_str()).as_bytes()).await?; - stdin.flush().await?; - status_sender.send(crate::api::Message::Info(format!( - "wrote command {:?} to levi stdin: <{:?}>", - cmd, - cmd.to_str().as_bytes() - )))?; + match command_receiver.try_recv() { + Ok(cmd) => { + stdin.write_all(format!("{}\n", cmd.to_str()).as_bytes()).await?; + stdin.flush().await?; + status_sender.send(Message::Info(format!( + "wrote command {:?} to levi stdin: <{:?}>", + cmd, + cmd.to_str().as_bytes() + )))?; + } + Err(TryRecvError::Closed) => { + status_sender.send(Message::Error("command_receiver channel closed".into()))?; + break; + } + _ => {} + } + match message_receiver.try_recv() { + Ok(msg) => match msg { + Message::Data(d) if LEVI_REQUESTED_DATA.contains(&d.datatype) => { + + stdin.write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()).await?; + stdin.flush().await?; + + } + _ => {} + } + Err(TryRecvError::Closed) => { + status_sender.send(Message::Error("message_receiver channel closed".into()))?; + break; + } + _ => {} + } } + Ok(()) } diff --git a/gs/station/src/main.rs b/gs/station/src/main.rs index 9a8d0a049..617f9aabc 100644 --- a/gs/station/src/main.rs +++ b/gs/station/src/main.rs @@ -11,6 +11,7 @@ use crate::tui::tui_main; pub mod api; mod backend; pub mod connect; +mod data; #[cfg(feature = "backend")] mod frontend; mod levi; From cf8e2fb477fda675170dfc640cbef14a3f34fa0a Mon Sep 17 00:00:00 2001 From: Sjoerd Homburg Date: Fri, 12 Jul 2024 19:15:24 +0200 Subject: [PATCH 06/41] Added commands for retrievng data and added localization integration --- ...ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx | Bin 0 -> 53684 bytes .../Levi/.vs/PmpGettingStartedCs/v17/.suo | Bin 57856 -> 57856 bytes gs/station/Levi/Levitation.cs | 89 +++++++++++++++++- .../obj/x64/debug/PmpGettingStartedCs.exe | Bin 36352 -> 38400 bytes .../obj/x64/debug/PmpGettingStartedCs.pdb | Bin 77312 -> 81408 bytes .../PmpGettingStartedCs.exe | Bin 36352 -> 38400 bytes .../PmpGettingStartedCs.pdb | Bin 77312 -> 81408 bytes 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx diff --git a/gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx b/gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..1922797331865b5240d643b74e5cd560b641caf2 GIT binary patch literal 53684 zcma&P34A0~)&Acgy9hJDC@5+u2B;pAN_S>5GjxU_)m68-)sKoMLJTtM7$!5smS|9MV5|0FN3zt6{>bbagG<=k`6 zUF&wz}DOwvyON zQfs_prN&BQD|4;PSXp9axs`RSJheKJ)k&;QZgp2!-MZB^RyVdLTx+stty^c!l&u-Z zni;V+R;*1U2P`|_fZ72Ec(J`tZd;aZ_1jj}wp`n)+m>frzHJ%XYS>oGwgTG0%Omzc)vlE7s{B;!p|U+xv6q+a zmmS5<7+v)#mYQ(KJjUE7asQ?^alHj{S4wHtxm?Ad{52gVK} zJIL%Hw}YM?x^`&n_LQBp?Ig03%ubD+#&%}xEVr|colV(!%gzHkkL^6QJCWUW%jJ=> zW0f7R>@>>GSlLO-PF@~aRvtNUnN?mkIJm4@S>~q8lFoq#j`UmW`nzVJVhvO(1J&|C zwSS;m8CbqzV8yC|74E?B%7Imtfz{SP{h$Fa9x(Yp(izCYfsHFsg4ORVlfE-3|C~YN zR4k`bb}EijtvEx&&amaIw47Czv)XbFvK+VWxSr$sjvF{`;<#PMopkDfQ|~xl%ke_T ziybdEK{CIM^Lr(;Zy9X3+Hp-F&bf40`dP2?x#OU^^acPYtGKFv|zK^}%jjvC0*z zzhc!Y)=0&gsgy@5&a#SAslcJb6=!9|SygdXSDb??6{}JyS1SD#ysDMTP^BtAD=O9D z%22Peyi!@QqOzh<86K*5^3$pG(#j^QvdO7zs#P}is#bs1s#UF-YI&sURH}|!b+W3{ ztyZjRr7VSs#_G^Ob;zkMAF8f!s>4=wW~AC{R5w}GP5sqP&XCnVWYvbOnW6H?(7^H` z#~pH#AtxJhxm27?#dgDGyhq_Pya@Ygl^X%2mUrIouc+-c%mmG_cZ| zSy>)gIkaNss)3be#mY_PRo2X^^2n-z%Bq3ts!C;5MM`L_TD@|$HMrV3WVJQ3dgahT z*33a>SZ2&x#i_ZIwR&3f;+pT){Geu{S`gJjw-&~=FsrrOwRT)fO)c%zGE>X4TAtRr zrna$C+gKf0Hn?WcTQiugIb?E;7q3a$YqESz)>+dv2V2g;R`p=3bFkGtxVvKQ;F`6A zZRtB}*JOuGwq2{=wPs}QaLXgEd^a*~)O4fVji%hh!aR{AZj!l4Pv%ZH zb==f*Q{PREn>O8a%uORVP2Dte)7(v`-K^|p{ch&CnQ^l*H_P1Iar26s*WBE7bI;9< zo44FNaP!#Bb2p!I^PbxY-A?3oUAJr8ZquE>Lg!A}?quLj+3r-`?HRWhxV_NrWp1zQ z_BPh7{<<|&FOSp*Jh<7(>Q1*_wd&P!z3SAf%j-i{eW+X?a_R?}y6e@wxbEe3W7mye zH;uXp>n5!SBlRGx2T?sV^)RT1aXp->$8kOG)KgPW<9g=RbGP0J>Yc3K$vvyzvu3>V zh)koNlX*_pt6E;Q>{T6a$nu8D-jLkLi1%ciNi&E;#aW)4Nwq!CcwWczEzfs6S;LL( znX+f(zTuh3Gl^$1uQB1ZEU#sIt$s|&(o-eK>Un|d1r2Y^^2W;EnB$FQ z-dM*AT`x3V7RztD{<_HTd45ke zKxJdqj5Q;B6l0f-Jz(sLv8%=&GWK#~4;y=>u~!+}MPrK9wlQ`?HV_7z2H7acE+IE| z*OW(0dB!ZOngL@R%Q&ua>S%eXq9Y*<;{>v5%1UXRaak8-MKn$>i=1&Lj5BGfJu}oZ zD=afSU{q z4P*S8@jc@k<2Q`oGJYVQ62BNfm--ujo$-4TXH46e#F)%9T+;|lBQ%ZJGzUyGH7(Z! zkqIpmmQC1?tA>SwZ+$pVwbCQD3~nJhP1 z$7B;Gn>M*+a@*vN$w!RLAGv38-{i*R4U@M_9+*5dd1Ug~K+P2Mqi*W~L= z-ZLFzI)Ukyv7hR?rfW=B#Mn&4X0j|hEHmkvNzY6gGntqv+e~?8Dl^kFW?f`vMoiB& zy}Idn((8>hORr&iq3K1Y7n@#Ydb#O!&8CrtRc=@#vKluARyPLfjX~2`zOvzZ4L6X* zpy768mT4H%XjqMg*Jz}TM%Dm*1TrT zZ`OilEp67a=D}9eO`7$vX{@HPo2J|}wWjf#Mna@3q)n4GP2M!!rs*}Crr8XdEvMPa zn}KNtQL`P(O4y8oW+XSAX5va`Y^HHDvzuADnK{iYX=ZsdZ#6qXv(s*N(&lu~oStc} zi<&*R*{e5uUbE*nd#2fIG<#vo>Tg-KmNnC|D=oX)vWHr>-?D>N*=dzWTKyKLVeC-5 zty){w)Rr5y+*B5@mfOYLY{Hglw@lPBsq~>%tJVs_RuHv2(JgF#~|Xj(z@fS_py&2rG}51LM(z7tOc%|v_=G<(=Vgnkg3 zAPj;q48k~Q+dL6YOiGBrWk4AORxWbWMgJ(%=E^BWUL*J zMe$fRGL}t`bq*fuB%#$GTD8y`39XsX4njK*okr-S@_Z6HT}*Yd&#KkKS{T;aVJ!=5 zUD@8jwQdx;S?K1nn+aVBoDA!-36Mpo9QysCpW(?HYeP9S%RHx@3L zL}egK`$VCMLzBpi78=PVQ%pBBlcAZGksq2(VIvKjY1oRfX9;BGmlaQ@-Y5(*aahFwerg6Xw0JW5OvboN~gcS~%r}Q+_zr2&aN@svS-x z;Zz<@b;H9<*z>}kT*I)pv2B;zw%fMbZO3ane%lG!PS|#$wv)DtR(fIT@Ty7ESYxOl-s5s51nQN`&r|+P1H77+vIIC(Ka*f zM$itzcF=AIQ9HEoWYBhHchHXWcHC_zPCFUFE?Cy3q}fh_b`rLexSizfR931+J8QPH zpq=&F9jD!?wmU=ZPT1}SvSn^hr0prUJ=JJWVC2># z*TbBLeS90sCbomJz3W8275R4Lmm_}!J9pp5ii-VE82RnUk0XCP^0Ua7^~s8ijm6eP zvOq>AlNBp66Ooz3C~w43BaNC?)U=~!IchplvlcZ+qUOO-vySz-*^Zi7)XbyibfhzB z6hu*wM`Lz0=0s!dXe^S>7lm;YPDyJ?TqGSWj-oh?;w*~uD4vLtPBiXB<0DaKM{+ih z$&D=gGNZ&%mPA<|Wt}LuqTG)1a+LQ+xfA8fqr4X7hv0D}_ho~CxhD^!JdW}-%Cji% zMV&C}w4+WIb=|0IqA5G-c~Q?qJvqH9$F>!hM`C?YjB6&Y1@e3t>%(DO%i>xd*SfLm z$FA)4W$)|8o)>#2_JY_Oi+vOOf$Sq=KaKs4Y!2c^BW?t7)5OhY+?1O%4t$vz<3mk6 zCL^{ShYq%!7`@FH<4s0a9*4cSZO85YxLuFiCT=(4c3XN_+|J^57iVvg9Y^Ij>W?EQ zj%sl<5=RHe(OPWb(TYZzM?4!xv3N>q9!HX^hk+X>ah%K1U7Xl)GJyTQY@m`fPO>dDl$T)4sX&k3%oMv&F$LUm@u8Y%NoS8VwRL*x3)1Me8F>4d!VS8x8#6+^&lYLtv`=_Q!f-nhknL3j&kO?ShCrMOG;wXvJ zB+im}B1ys|5hod}UE->=Bd2;vW|AyOvM|Z=B%78sEy<^nPCe;_NheP_on$JNC*Y)4 zPkLU`^OK%Qdf~WZ#+_u`NynXRyb+9N!FZm^c6GcXOJX^-`crEpwPsRlQ)=6(UB&~n zY-8;pwZqhIr*@j!Sy~>!Q?*l1_2>=TTPH}JFm<97hh?dph^2b|mFj_4T65*#BdvvL zt)13VIoC)>mZh#IM+T|vhTKq|=TkRI-8^+CQg=$9*JY!mdnFj4Nh~U2i6YcmOj_kj9>jqBK4q!65 zvjo|>qf1(8R!%b~&1z{jg1vkun~OZnI%zhY=2n{9Y3`(X1$%tigGm3)(=^Z0yp!g= zR2~{S5%&JwAf2+)DJPv;n@+juRFF>3r0b$o&NF&-c`QhKjkFi0y>4ceGpm+aGji^n z+2zc3u^!raRxW4d{;WKLQw^t{IUXJ$oCY?VI0+rYnyJT~nGKdvx=2ft!&83MwZF3QRdb&x0$)^%osLf&Sb9Y zC0Q@cdQwg~w`#dHl3O$Ku|aN^bGwq;)!g>+0B1L_)0AC|oxmJBmlJs$O*wwLE{%a#SJnsGJVSbAB~9L-Ld^&))KY zm76d(Z7g(nb`@92!{lUc*2!aD-WbRmZr(`ofkUgDxB783-WtJinmmch#12(A)Flf_<7urRZ12Nj3H?j%(kh_dU;xw zb9b3X@LY?b9_DGBrxHGuXI74}A-&wiBX8EgZas_hEYGv)9OcPCbn{Mv;V6&7-8i2z z`BW&=YCc`fr-$~8I-O>#6ZoAVlqVc{ovho*dY#jeP?1huPY|1K6 z+18X@o^qN~YX_%XZ_4$jeuc03h#rsT9E zNvG0aDl5wrFqH>Wc|0Y3t7{H(^TUjNm`M){qG`*Ww!CS}Ok0gr=# z?##%tUZv8jtmsv|o@@2oa?kDWxlYfm_S~VKTgQW@+wHlNy}H$_mwWa8Uft={tMbW7 zPtK%0tLK$_UVqPXdS12X4dK+#i!nucot|&?{BqBCdVaO%5A}RG!?t?H>G3$PjV&q` zA34#HN7%I2bbBq+Yjt~pk8^H3>B&QF+-tkNcF=3by(sR*jb0M;5?Lu-Ii8dquB>x7 z1d<2eEWwD$I=wu`0eM%Bh2q{}rnj-u+gROL*|f2`Y2(nQjmtM}T(N26@TQF`H*K=| zH`1ODMy^L{)FS#g-4&Zew};NNvE#gupR-h1m1zS#}`RlS1aPdxRM4e_a`054Q?|81{Y_FwuLjaIbZ3zdjD$4p%P`sROE zujJ$tPCWYLQ`R57VV%2S!_mhbdF+|N@vl3fQ2IPy|3$lJryjZCsFT;9hz|6B>h=Ho z=hpF}FYHi2f2#t&S%EjYD~R8&0P&j_+sb=K$~y^>W@pMTA$MW;BFc-&-Gq37(6>9e zhY&9i`d%t;#J`O4UX))h#0y0HE94E^@Jk@b*C%g;?;~%>`%+#??nmw~#0y0F1LTdm z9Z0#K91ud!Amxe>FA({v@<#b9A52n19@+essLXVL*(l_J{eV8X8 z2jrpTm=NhBdE*5_MpGhvO1UG%3q*KV-q1Tqc}j>Ei2Up1jdU~QYsj7u`HmouV)$!? z$cK3a`HmsilW!p3C`7+Gk@898$t31BZYT1aCtoM*0injTZ=g?}H#O5b^JqH`0HAyo9_| zi1e4s8|gkoUM1XK-Z#h_@i)mE`9Dg2Oo;yYNy;}1k?vEJZ>9Vh%D0i9B|k^rPTnEJ z3q(1eC%+(sU3ZgT79##Da`JW^IO+GKg3q(9F;8rRW#O>A$e-XJ2xh=UJxjnf9 zxg)s~`C@Ws@+IUhLfE^A@?s%gAZ~XfcNZev5+T}sFNVLI;d_&>BKt^7C_klr$))6e z1&in$TdQYKaa96M7a$i=8ZAR zZN^8G6UyV1Ga>YM$tlJkMtMdEy*=^>#ve%@#qgua*OTjo@b__)PasbsHwaPgX_U_p z%1`M`hMz^gnLJl0Kc%TyY?*_^@k~fhbB|j!a{!a)|?@uxORw49%hVpHUze9-hcQX7ghJRUzcKxOh z`R-%`6T%#^3Ou}@9&iVA%s2ur2I7b4EZeiFY-C^-{kW` z*t4}Lk=xRXgpjwRygj)Cxg)s~`C@Ws@+IUh=r zIZdu3XUNx(J@Rn!2=YksDDt)B(d6sM*OSMP>&Z8eZzPW;k0XyKPY}va=_JZ0lN-oW z$WzJF$kWL)$TP_|ksHag$W7$gYGFS&)hfV_}=A9)dZG5LO>{FE-Cd@1G2$q$krX8e`pRpiy=N62f0@>9Bw z^7Z5m1n-9!1S!M_O6fBs7OA3~&ihVpYH5+i&YA<}P4ZbxoU z?m+G+#0x~emoR)6a#x|KE-fZ^CzlA3elNBd%0~<3r*sVE_2e7KH@A?mS_@+Kkd zJDc(><#Wh$g*X#>C*^kuq31o6-z$`#(uL&341d26_4*LQuVVPsm$qARi>ZOMXv?bU&p0 zF!>1iBl5@OPspE=j|%YuQT}5>*!MW)pOe2}_^&Acn*0s-jWBl$y`6<1O z@}A^gd8 zA;fsSkn!(h{Ke!47=8(PDR~)rIeCQ;dOsvYyIdudpVGB*fqmDJ*ONDpHwxva^gon8 zE`**>QofnIh2ghS{+$jDHV#0mCm8Lf=IU|9}uL5cw_SRUts*56g#p!|>!_5CU3N6E*?p9!J&=j1OL{wvDAq5NC&35Ne(h;shK z@IMP-$KNRbT?oBTG5i^ZKTH0Le2)A#`8BlE|j0rUUGq7`WU_sX_5O1k#9fB`;!Ll~r{l-@2E*z-;y(!EEBbX&*^h0ymt@**M9eSqV2OO<=-zvIS-KE6{4K)k>4kOKt4qNkbGE(e)}^a?EekJe@i|gg#O=? ze`NTR&Z8eZzPWu;sv5U$C1aACy*zS zCy^(U8^}|HDE~Cdr&B(Y+{o~=$W7$goy_kcRR!HAU{ujL5OsBkzXXgOx`1eonIxtMt+_A2Ki0$Uh+QjTjaON`^oQ+50DQE z@mfgO^*zRapZo#&kP!X#M?$3g3B!L%K1x1D{)~K_{J9YMeo6UPN}D8Eby{d$uN1(pi+xCTAIbo)CK8!SHvI?-rt*EtD@L zFA^gC#pL@LehKAEg~)dW!#~9E4+~+}M<`z-M7rxK-ylRiZesZVF#O}>C&*6F=lf9r6M4LGrsol=FQd^!$+F50j5D{>S7` z$;ZgY$zPDa5~BQHlfPm36O?~P{+|2;UC3R@MdV^~HzEAHgz`(tmyvr4 zq4(vKUqS9ozLISem{(yCf6~(NBM9e=7HBSe7z9$I-cAhgq~B#Q-#of z2IVs;pGA2Sc{cfGA?o!uA?!Vm;qPGh7KUF)UL=J64+v52CFG^#W#r{T>0+giP`*Zp zeAf$6&W#MeNeKUZobo3qe@ck-pAjPcXBq!F@^*%Qp1hOccae9KUnIXoep!g}z9xj; zZ;WYf5g{a4glusg0CO43$2$AkI%BNF4lk%G=pGA2S zc{ammg{aqC7`|DE7YMy?BhMqx7sCE`3X$(Ul;2Bk5hCA(l;1~QL|!b!3xuBc3!&#y z%9oLslUE3#??aS7OkPP|MP5yQguI5lmb^}ga&8nt-$xn#F(KOJ79ra4bBw=(@t-HZ zK;B8-C4`*w~NdI-p-=KW25O#f=@^={j0Qn&KT_Nmygz}Hb9}AKH zQ6cpIjPfspSU-Nt@ZVAXlMwa&hYy$q z>3c%xJ%Zs!l1GuRW&G=eDDOBS+V^C}Zy-+*qJ7^)&NBQQ@?0Ux*-XBTJdg43AkQb? zNxqAGw-7H7^>`2YUUG{N`Y)vXKFSvhQO`>lekpkwc{zCn`9UH4brt2S$&U!3|60n| z38D8!$~Q6oV+{WU<(nCQ3;8LAf12`VgwXRj%D0nuke?^NK;9`tJKRJ0tK`?nuan;( zze(OJM7{?pf0yzjLe%TW?L6J%{0MA>T^AoqUH7<-CjXd&n(9WA!#^y9zN;x; z!}x2->&WZL8^{}l@Y6?y$p3NjW`=)C2zzcNKP|)yM84Y?{#nYmQ@(@zJoyD7#@kmY z-y=kMUl*dD_X**@2N?e#`CTE(`99?zkPk8ZVakt?KN3RkPZ|Fh`M41M{|U;!7ouK& z5Mn(1mGa-nzYCH6Im-VQ!j95Q3T{nqBZQu9$?X`v1Gy8qvk>)LEJXU<$lb|3$R*@U z$(NCP3SsA~D8E_=f9@}YUA7SQK9KSKXQQXV3gGkloxN^%vsnmmZCkt5_9 zA@r^#M}@G*W4zDs2H9eGKprYYKZq#DWI~RUDVdSE5ao7-C~u1LG~?F^q4#jgN03Jf zk?*yXkEZ;3a=j4wjw4TE_y!@`?-cS>@-*^v@(l7!@=fGM@+@)_c{cfGa#o1)H&cEq z`8FZ?!MiBGn|zND^}m4fg_JKAqMjdM_$3U#obnYy=)0QX*OE7o9~Gj$9}_~)$0>h; z^36i@>(4O!Hp-tDqJMsw;a?%|A-^hwzOR$tB<~}?EkwEZQ+|N_t`K@3q5NaYKNlkX zFUVh#zaoE4{)YUm5O)8O@{>Z?^JmI`5yCJ3r2I7FpB18Ao~OLkE=9RplP?k?-;U(2 zLg-!0_}zqPrzJv^|8mN&Wc;gyNZ&_!A0g81Paeql0meH*=&Mp5V*Cmr@*O0Ey$3V= z5Fz|DDnveyvd{Pi*Gj$qI@>xHw)p1b1A=t+)Tcee47w2 z5PmpchGyibsy6ygOU{AThN@>ArkLX`Iz^0VaaLX>l-5c%(7_!lXEiSkz{-$Q;?h~o-cggQD{2|IeBp)UpA%7%``M(lk{QaKse<1%z zK1u$G{4@C%@~`CI$iE9w?o;H`LhK8++O;TWdm;R^gAnO&Y9)8_Ao5DCcA3#~J*@+pS@lYE+d zhJ02CJ*7p3o)-yG{i-!Qq7 zTt%)X4-!Jp2<0`D*9!4kNa#C+=|+VZZw(>J3xpVlZN^6oAE%rOVgD56!-U9xxDfR@ zhCGq+Cy^(U8^}}0Q_0iF)5$Z)Gs!oR8_BbT@b5W7*!xx?>_3n3?;y`-_`4~;hkUOP z{;$p-op4>$l`7`oyAzllKa(^k|=s!;|-S333?+-%Q_h%u}{f+T| zC;ve{MgEg~ntVoxe9uw-H~Bn?FC>E>w-Tbf7g64Z+?L_nQ{I8xk=%*#JCnOGd{=T2 zxtQEdi2A&g^2^9Q$-RW=FZ)oo$bHGBx2TgLDM$$rK=WQE~Xa){w8$d%-3 zvPP~U*OH?`l;=@4WK#$~w#a}ylpG^NvQ0*0OeW+wnUa|h_3cpZk`v@4;}4@eO|Bzn z7~i9OxDYQ8b|1m`qsXHf{yOsY&Y9)8_Ao zk1n!hO)`pDyi52Ss?sPf$X|#WhCepczW~=?moKtii6U>% zA~hRwbml}?wV*Urun6YjD@aQCzKGJS#-b2>y-QJAkyi`Sx*-C8++MO(soYRK?)E^~ z9=OBbLlu`L$bml#D=oq$k@tceTGd&E?J?&oz03k#^M=X9txrE<`)-T;DY}pE0X4T``C?YL164|tF_*%!(R;bYg z!nBs^Q;jH+K-J;$E_F}_zF>x`ATCBWZSJk~4vG7aWCzp;-xgEa26y;-_RdKAvtf(Z5QIPz|W_<{kUwrCIY5xWQl6=8By8bvmBqPB^y%T(qkA3aC3`sG{+-Cu+lMQ|zPVLaC}hPiqdX+VEC6^vfV)#WHN)2+6WG5|6_04%rJB@1A7S!q& z(>e@NzZ8QBMQUZV$Eq3ldrg|AvFag|asFIHAMRXvFBoO2PrIJFa|aZ(8R@i#so{&E zPlu^?q-|9*lIZ%OQ_wFEc8g}yZl*aP>d@E$q82pY@0TJ>MeWZz7B)*zF}l@5vq-W8 zVLHxdVcOPc1s!1OA)N}ApderMA+`luk~!}dQ$564c`xWCqeyoV@@j*secI+)b2SKc zDXA@5n(i6!eGElc-Whpy2x$Xod#J;-zDuC~DOHb)Dphaj%zC&4m5QE-bXOvY7OCB* zXfn-7}bIh ztKtRFl516Qx4=mFs;G?IaKncr+I_a5`{<;<6k%GAQMp+#K6K*IRa{5>A{}5zQW%bG z>J2SJZNZ<6vPj&i6N@A&s(o{%X&=%qS}biaXx{g5U!lHmF|z64)E$|6qgbAhaEq!} zA7~fVi`q9|?te09F0YoWeMqM{EppzIg;mg}HJ|S##Soe=2>MjPENs!{F0`m=Y5|gH z?0k_$XPviC@8;^EZB%#?T8iqUNR7}oDqM_M-Rf#YAChPpqlnPE`F*=C;Il}gM&|In zE^WGJ&>fFjl^{tG3oZMg>)CVJipf%fiiJ<7VSF3%!p9|u+GEd$g3Y#OVBZd98B z{a(k+F-WXV)cu-1C~EuaR#G>bFu63YBL%m*k5xZwwb1yX`Qpj|B5>;7pAGG z%iRrHU!+sF7q&oDU+LqBHnOj^go5Jn5aP2_6Zc^=|7H3`g6x2qZW>h@wD+Jac^O~w2-*Y%2rZ*|jL^K1?C5wyT1y3Xva zE*`7E7JI;xvnXR$-n0tYbla#Khv#9gHmE9{ zp8&MJy8Ayy>kBPD^j!-Dr>b8NVGwpZV)1oUg(`jUybS67jw+~9^_4CY-&cJoQwKyJ zvgzYeRc=b-$Z;vue;o?$(!_GZx*=?nh9TYE$h&ARNip(}S5GFi^)*)8s1KHo%KL>2 z#yAw{TBf5>H!&YXI_Ph%{Gr|_kn9#4Aa&0a3B>tkmbOv(hEU+1(_Vx-KHj2vV zB&}Wa7?DajyoN78D0zsjp-9!&hrHS{3G(WI(B}$#yHGI#3LhY^{?7&4s*gj9IwD7e z)=2xLP6c|Xs1I065nGXWF|}wCofz+idiB8)-05niiw4q>fkypj} zi{{lFx~OkP^>0$Y%-0;I^&xCj-X-lrvk(`|0&tFwigOS#KPt2ex)@%o^}t1&Q77|a zF_fTUSzLy@aa`1v7wEehd5!uWNeryg2H097(E9koqM`%45SMCca#PYK+XgviH8G6T zY#OGn(|)g_Uiu^jcMqr=)bRoZDr$4PP=zl*{6F&xJgHvO!K5?I)>T7i$^X(mLWbR?E=- zqLYVC!=sXwQ}VCVv#Pn;Ra_*VRjZ&%d+a5+)5BKvhIWty2CCPP zw(zrx+Bb@JgDQN9YOzUGw=Yur^rD>~cg5}oYGzeLj>F|$8iy_o)1{+0n9&F%#@FAJ zmZFSeL?Q7AYOdq5@GkUek!qUyz(trY1lnWuqP-)MXMrqA6TIKdv&5l)HMvT_^Q$pzK^i5VBXpI&a>hJvZ6Pkg2j3%H{1#0u2`+0 zrFcF<^>;(!QFWc%6iuPMT|2+}PY=)bMB<&`HSPDha;hWrZoyF^(rJ&?UBM^}*M4vw z$|&3f3+4~Xw6ki*TyRi^#G~@&0SWF3>tVtC3Q@EIVs)X?dZ<-8&FKT%#p)crpsKn( zPhg}rsCIANQ>&51l#5tZrCnZk4?2EzgzCP%52`eqww7)$)xP2pMSTiO&w(m+=N@P` zHCLz7V$8rQ?GZ)iM>h4ij=@p5shAGc8|q1f=~#H1-0AhkZ9#qh(1SPmLpPcKpit+W08OEL zm4B$=>P{rlU8!~pJrU7CSNKKCz(pTSbfzgfFcj!uFP`Y3MYrdVsb*AIZMhwyE(EGs zp98e2eXvUXqW;s(1iqNK^e#jcJ3*AIzSl9WE2j=CJ?)177cPbn)il+oA8hJ?I0p%J z9=aN~82AAH!&>2D^`GjyQVl}lFCm?-etmLN($TFhQ8S7W4@-+}JaR0Bi2r7+_!Ljq zOLdqIzxQecDx+PrcsxWljV-z!nnG8IKDjAO)6Ssg7N9W$3s}kOSX2Td1Evj$-CVGw25``q-=sF8&V|enucS#p8(X#~(x)xGbVuCz@-F5peasj|m^w_S(mf&SygLi^SHf#WzlU?Q|7omtQ5`8d&1o;uefBJD z*14qU@*)+>yVhLCua2!dpczHHaC*?@)?Ic2 zxn0}MXoU7yTy&Dr{-@R#a}I1N_SD)&FiQsx{(HEhM`(@I5k+2Ak4i*HOU9u#%tX$AFBMvv-r zy4ivx#Sl_`uy4UHY#{amM6~2xa#chU7p2W_tnpv!6>jq7rg;8U`_xx6$fliD-LAH1 zkJWvhe!6}hlsP(1y3Scqz=F0(@0pLy-w#?^?{aAG%~!a{u`H@V!kgv zOF%YlWKCDJDy+x5h*0}TQsLi@3d9*qDe-fuSkL#sW%oRfn&A5=+;2jva4XE`b9U3>Vsn8L$6c6=>D{D z4hq-tq1~)_s#K-gWGJ!^NfxV-@P@8nIy-9O1WAey`Q@fqb}mNZ!cy4sf@H;A|FOb?;!F@l-i@$gYb7^ar`0ONwzNpq zqI2!E@Q%NYOd_0>mzuXi)|Ik9&2yfdBfU znB{f)$2ablo>hIYUe#-NJOrYSBVDr88`^iZDWIh2&)S=`(mL4nPIs#3q1;)dE4+b% zMp00)>_EM0E?OE2wfdv*n$E@d%T2Mx(UUg(-$JD)prF{ts(mN}U$R_6F3wF*rPHC> zqV1-d_4BBIL%|lP(j`G35H#Hq)rY*rL*0J!_w8neP@_Uu4rtFX$F1 zvKT_hfeZ`UcO`sqgQ`M0?ecwcQ*3`7M5y)JWX1A{bUNqg%sSsy_D5o0bKo*R`4-&= zVLBseHVtzTX5?LLsB{X~VW6wHPV2>_I;T{phgrBzSMklTWmMh^4j^VB>I9`#EjF`I zP>6arC;CvJ)0}!po1$23RExF|?zBTC=HRHCs_Q+T%Rfr=HZ(bZptkp>3|}br8=#YH2H|K}(>Z=o@lV(mnMSn1=s+ zt8^1I7rkz-Zrb^^d+Uy3v!vh6?B-^p+yIYwud&TR$9%_M&1L3`^c7P z1E`3LZkcqEn?>Pjb5+ynz((Wg(ouYxh9p{#VqhbkP7m|monIV&qxFS?`3HP8ve@;( zjN*fTq*MFU@KJbeGYTr)1T%D4>2EmeM_a|AiK@p%H;~0<8=}tlx?Q+HlSojp!F^#8 zL}*@pWYij+k1#z#FWd=RZiaVt+0n~35LL5&)Oaq+Sd7d3La)89xXd|GXJGvm1)YL) z4C=I`qe3sm%s)4dbodqH1HPKyp=h?EeW6}=8CN06m0DliX@jbzx_IF~t}VS1hN~8R zKGHp$rn?dmtvO%qAyRR4)rYX6W#+8X=|(rg>gV}iy8Q)g(Y1Sik2xwq|7V>>x?&2_ z94I_ML1^+~0x-DKzNfy4@-!U#$!jsGohkgOU1tUTkKeMF;T(>OYiGOn4|s zwN#)*Ke5w|QWeduz2rB@p@%;@D{n?bF{V*aVbGk{=4a937fz9+n9Q|W$h$j=gwEpp zWif8GeP@wGHS6B2ST&GNJ*h)dJEqQXI{&HWr6^pxXmLUcEyXW?&HDgabjv=A(u!^d z(~9i^a_9y_8+nQP59#pT`z8J48#fC+O3^t?i`0hF0XB*pHC$B7{9r=R|2b-c&6d^| zciN!Ez=p!Th@Sd&q~LcJ!zMct_ma}lE}#1`D)KPJ%s-LO<_ zytI}QRCxR~C|7-dAHuW&RExHZR#m;WSks~0S)|jxn?Umz%Fr%~6h%kZIYO=1j;ZQ3 zhhAKyE5eXhSJP31B@pXSvs9~vyFOg5(K6;Dw4dtQGm0?%xVqTsBi*RH7oIvpEDG0} zBZn@evxq2KAKA3#x(F}$6)5CT$Lf)a_ETNs^#Hr5G&4Py?q|O)R-7sgd)s#qVAsLOpJ9r#(@d$5#;r=~n1J9d~G(&!P%C z5Vd`^0p{CCd*V~jQap>pp!ugU)U?<~=(MESw8^w9_n|VQ$Z-qGNaW4_hkQ#l77;qO zbk5cp{cwb7Eq9yqkcz6l*!5|(aM9^Y%g|w}g=-(z<+x~bq|-2+k?`MNEjX0WB8xsG z;l(qFK1DBhc0mq3^gJJx(K2*LtNIQ@HeD8V%xpombWG1r1zNc7WVF|*sGs)fa8Z4F zDcT&BuBy#&lP)&1a#PIST2<|YeMq9-D1QAGUTeWVRj;b{fU0YtWqyx||K+>*Z4pgZ z9N($UNQZZ^O6r7Mj9L_=L#CQ-pxK>*CgrgE!JdhVgLey5h8KvpQE5Thw>#Jd~Ko2vu9{OvNqo|hlux;i_ zE2b9YxK!SSv*+hE-Qnp}@Pa<6yT19REe_b>4c+Elpar3f1b4-N&@hpT9;@9}*HyJj z8@bqE%+0Y{xc2rPp$h-mchQzLs8V-oUe)4B(1M+ej`(72)N+yf9N4G(L~ST-3N7~+ zsJ?s!xR{G1dWfYJ+=6VnHJzV)b&=C_TDWS_(WvdV!~b)B)|Q#`fp$XmJ^pXk(m&B! zg|ku18(@Zhgj1}DNHXv256}6z7)Ma5>557tR!4yLV(k`(K*8gP(Du-cwIk}9`3sHX+Qynoszv!k{MUBvfPiv&hiLO0W*s?RqxCUB^ z2@2vYnnG>Szgg}>tagh&#HzmeMRQbw7JOo&KJcMnA6#bTy}TV%V%BZwc;}AWUa7oey3Z2KCF4L_2!l z|JT^vgIab~0UQrHDIzH{l7WaIZ^9HQEHb59cqhJr39)!10Y!)mVqvS2GUR4_LP#|c z?VlmWkjfwtRD{tkK}aR`VihYZE79}+t;;=k-b^Hcb7p_6wf0(TuaCX=**7n}3>#U* z#Mr2;+C}f-+JcyCQ7ni3!6lp7-$c)l7U1ZaeXcPB#erXt8GKIj2;CTnjuB!g<-Bh8 zD;daE-$2v}SPA)gEwDSAr2DFH*BpAM(Ht+HR$56Nhk-SgBdmjaG={)!BEy*k%VfT9 z%^%Un;(4*>b>jX&s*B4zz@8@;q)Q;kUs&9*Q8%$xYP4b7#2C-$m4 zs*TeBTsGBREcstD`u*x;%4>hKg@Qe!XqY^jN2p4g)zAlKu3qzIq|@+*I3UsqKu5Y*mRG zXA`M6m5zHE+;)W9V^VtmW{jKUYp7J+#^iQ+b}Jbj33JB50ufq1g(1N8l2$UZvgO4neY(=|`tGPy!Go3BZ z#NGIrfIQzc8*&?kmxH?-nzSw4;A%c0)-iJ#@Y{P%MP)G&S3faZfy_s#;MPT@JV+Y{ zi^|58l(y-Fi?d1kbC9=L+Rq=OF>jm9IaeXaUYKnd%)OvTL9MzNQ*Mc_1s zzR`<0V(8xn9XQuGcEV$=Fw`PAb}~>4BHOf?JdNIP87`81Pvtezzej z9NJ6uk0mPFZbI8E&+G`^#7pFB-)(*>382{EW!T_|7X$1sZO-x2GyBN3Y4B=8`L8JV zx{$Ir%QKwv9@bHfGrZv=fOJURp-uSc2BQkaMo>|CxA^f!U< zL7p<(*FemN9R>q->>w!ET-?BYFFXq^NIg;;BV;)f1Z&$Y&rXw7E!qaRDm!pA^xdik zlCf#`CN;(Bre1CKAF2j7M`9-@`6HqW>}b3{Z$1%jmaCEClJeU%6NPdmv$_s6+rv62 zg0662TwnVHcyVLU_EbjvxR-E|8jv`k(=|q%;UGbXzzzVwQMA(du9zV{6UXwq7T~rWU*0 z?UVCWQOU@x?H{PQ{gEKW6~ee+=Zmb@UwGfQV<;Wz8=%uMOZ>lkH6??|v-_ z-M6quTz?MCw_Xn8gJ$8$DBG&C-J79V9h%i=_p>_Nn-~Y@yY6hnB0%!Dm%>Q2`s!)D zEEdtxRXXwW#BaqthAi%GM_Rr_`GgYb3YE}436qI%Yk6i1f7=)cRNB^oiaF;CA?ysP z(x#W*xQ}$h?(UUV{9Yi}dd(P##qN1ByMJOYH1fv_ z#GFtaMd0ypS^Jn-y~Bo%sw=zv1Mr~LODsgmHVG}HmQkYHB2eI~Yejh^#c(LC8 zF8w52oN~XfG+N}&FK2!ohEE?Z3^`~#ij6hk2_xqNUi5JKNxVCHGM(i0o251uAvQ4( zX<`;naleh#qsI8s@{A6gOpH<2=7(|6=5QcQ^1O-iRk;w^HcM^RIbxmI{;$Sx2q=p? z1F$<;1N_h9h21ldb%#3_FeC8gWMf?NFXOTj{HWg9{#TlnLK+joA9VTfz^Kh-Xrdkw zT+z4GWyR|~yjNnP@Ac)`?X34M>4fxmbx1%H>J#B^Or9DjJ=@Rx|9y|ZW8$BV z0Et{o;H$4jkFP$3Xs_7^RJs<1a6<(72q`~GJV@#>J`pK26F-?fZ8XHef#10qw6wn) z$i$qzMTC0a4=-W}2eWP7^E~+@HYQx}-TwVO$c0KG_4Ady40&@8!qAC2@nw(Ust>!L zTvi8qn}idGlx9`6H0^|`m!D4(<{HU0UO{VddLy`Tje2Ks60mKqw7p$wbI5Tq;3GUo zU=||UBK6bd8JoneP<0$IAN+m#`$fQKG*rjUtk?Xc!L6bEFm6foxzP^*8Of{fWzq|!$w=p zEN*vswGv(~RT>jDzf~=3asO}x&+iRl(r*;qG#NjN7n?$)lunkWnMN-DcnaGZBLa70 z&FXgmM1TS-tYs4=Xtt`&DfC{`=ux zIjaatR5B(Ih=lR)rtxA!x_@OW$Tr--ekX~ZLyuU!SoV!dV+WP&FkFYezSQo!(p($s zlmp~n@swM!-GeW;$st(c8Jd;Wt1)RvlAoq9$GA3yV|9Tf(81LvvE8}Z-{SOoFO+91 z(*%eD7!s*pugwyI&y2yDH56=3Uw@q+w(N*kXoV@H$>UbrhQpX$k z3r)5nsvL0se?F6hjEgW97~60~<&9EAebN=j$#GT(t7FBRr8aTnI{`mZFWX?T7n*p{ z);&g5$ZBlM6&#vv>(iq6SZG4~i(BFjm6lNMJ!YW*T8{=9D1gCdw zH)tYQP&q=PSi*oqV5(730-^&in>x_*0)n-)zH_xo(AIDxVy+%AYKlL(+o}pFe#wyF zqOwv(<>An%WG&&~+-T?-2%73~(-uHN z;N&>)!7w(At~*#O literal 0 HcmV?d00001 diff --git a/gs/station/Levi/.vs/PmpGettingStartedCs/v17/.suo b/gs/station/Levi/.vs/PmpGettingStartedCs/v17/.suo index 121a8524db0e3b7c322e0b968e8e70a481241dc6..d52ec51ad16fb21929b4e18259ac2f43949d50b2 100644 GIT binary patch delta 1993 zcmcIlYfRf!6z~1_U!d?WE#cvKjMY#&rUjvt0%~N?I-s!8D&pq5VKtgjlF2H4_kz6?h7QaKtg8J#^{X9#7)V}qOP6;En&&xn5YNBMEM~C@%91{^L#A-wXA_|d#Aloz???LFp zd^3(}1Z76c()br*{eUAeBO3)dh_#4R#5%)) zA-2FeSX#wj-X1>JX^`@iE9R*8m1$?`jc}B1O<*5&W@&S7T8z6J}5l&XNuv zJeoQcqukg^=>kb<=TrgGF4-p3sbsNXNkppHh60h_RM~{P5nfshk4q2*BrB8d!wD@; zw3L>rj+$Gs9Ti{mn~61oM7>0f8?oV@lmje~{}0~MqBpt_dG#Jv!@KmGn2J$LQj11H z#nAzh5fTqsm=MUYh_pWUoW?awI~j|I?xH;`7gLEA0v~%A(?M6(n{Y{IhKsXF-1gz; z?7yx6)4Jkir@)szEG`~zD&46lSu}*|y6z~`RWR-2LuWne=9f#Z>V8VQq13O2v&~Zd zWaYuiz;8$Q^!~mc#wL?D9OQ|<(hy$JidMd+Sk#rQVqo9L#>-RC}F>1Ni7-aqjMM&_a z!?pL*-~)RBR5)57yCEI4J_-I9(LjQufVBg&>tXKQG?=_txU7|Ci&RCWW4SIH7|#A+bekf z#uo+tLjCcGky+$DFZHm%&hZ3zwa>(F^d*XGo#vAKQkOA5*Hm2Clxr$1Y07mPOSk5_ z%qEx9+~_Rc+GLKF9f#hbYIyEQ7N`d=z+3$``1x2C7<@nQs-v}xI}evIes$nkIUG5* znasF)@M-z7{bO%Wr&#EpSR~ySPlV@IUpVYMcMlKCDJD1gq6E5b%S4YPJ$_bj9zNrB zTO}hhg^0*8g2!!_h$&tvTyWRNkdSEUYPW=6C;BKNQt1#fg9xI)NV^>D!~Ncqd*v>f zh3`Ar{ePO2YtIBFf0c0C<@DRcJ6$XPlo&p_bId#8)9&u#%v{;}=tpHHb;jkSUq9)d z@0;8En!Pq~V)FVf`+hz)QO>yV)UD|E4C8`-hQXX82QnS$vBp9}!BWS;###P1yGp?} delta 1996 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

X7l2XUESlm9DTN&FK!O8KAWsFsfb&O4nt&HuA_y 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

>4e}J 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 delta 12853 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 Ia#V=VUwo3xz0hffq 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

X7l2XUESlm9DTN&FK!O8KAWsFsfb&O4nt&HuA_y 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

>4e}J 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 delta 12853 zcmZvj34Bh++Q848yon_8MnV$tMs^{B2qKB4A+gq06fMP-RFHm_AhZ;DskLn^dGJNn-Ds;twHLPwT1Bs__PVtFXutoQa}sfX--((3JkLDy%*&GBNmLRbkduS1ZF^KdIfWo^=z-E4I7mo>~>#>#niSym-@7>!W)! zH@5y|oqy@aQ=50czU=p1c~7a`(LG%Wv!_+6tR54)ZSRy>r{U>{UtgZ_Plz6!IL|_uS5om%lPvs7=zMRdUE? zvWDKyijkd{UGiwF7In2t(+Dtr#brlA_Y^tMD&M>(Mg9aswdAQB-uha-prZkHb5mq! z_h$YjNGYC8u_rOM`xv!Qq9+alhJ-@qnr-#VqK^f-xTJq(f z3KcBHgOiEN2iI4%Wy|1fRU}^zUS_@(C=-V?_PiBnU#lp?FMK|II!fqGKi&?M9YeBI zoSYx>sPb##8^w$f4RuKxO7;K;CH#n008C}%`5I`%Cno|s$sx4&g@ zzxai_un44#wvS?XswbUSme$odIcQwuL_$xBq&+-LLM8>ulMhFi+0ImTC2mY!An2o$ zX?S%T^uz53^N@8VU7$bg3Twk|FdBA;F|Y@02z$a7NtxM6Js?HIg|cO42lbGgC7vv?k9JTqWGJy$Rz2EDEtW5cmr3NT4&2oP zW_2*v*OsNTvdrge%Lib4ZTWLndvkAXX+OKY`Fd@cK0DWZueR(1AJ>-a;Pcv&H7D0R zO&&N$9=Jpv_=UW(+`!-Ho9@2E-mEYGD09n(vS?+G+`R)e42@+r9PmVjkh=AUlKU6!T%W!kcA<(BQl5ptfmp~NoFR>?AWd2iKB zHWIg%pO*IsY#(Y@>U*a)HeoAVcV`)C;kp&0^)tZXv0=s2d0z9J62`qnrTq#f6y=Sc zQZ#j{9^?JOhu1fZ=9JTA@}$MeMY1zLGp!bq9-yi`=QOZguF)CJ@i3kr2$f;0JSpRF zaFw95A8;zu&NraDOwiBT{jz3N6Y2kOQ*n2wm8+ZO3NZX4$Y~U2h8b!dGqzo6Yo|LK zoJt?k8P4$t<*r3alrJ|;yU=Z{G7r>GJjZc8GC0Q!Y(_@O9A8v}&3IeO;GnZdGP~*P z<}nh&4N*@;%9~jMm4|$Ypftm)ygMh^^h^Qw-S_{wM~Kdi~eXmUwC*||2*W9Z?9 zqo(1b7e^4DW9!BcHJiV?4k8Ki+0zG#+oyR8^a?F3>Y7&Z^a@ zW5bH)VCZcq1J~U?HSL`<`CGhgccO-Q8Rp9x%7Jxpp`-D(855n2YRY% zC~g!8SzYeISgYEc1jEQdrK`PZSNj60OjNpBGtQ}Yf3o9qfvDl%Q*Rud1fIHnq$@q~~ak z(b}zZ%%i5?otliXMWtP)O_@?Ov6Qaa&s7GkE2W$+^Lw1^*fb_`9I6sjy2_*kyOBw< zVk4u_a}l9mkvpDTU3+SR)6Y}Swaj1M7!AiiY*_JQ)IK8N_URomX?$tH*rMv%KfzHB z<#d@53DRrx(oh$Zb|$KHR5udjht27s8&GAS(pCPMAW2*F6AJu!0} zw_(MLF!yqStn;nc7-;o)auwIhvDda<>iv6qXaeGs1U>DCC&{M252U^S`FlddX{FTM zH_e*Mtb1kd3o)#%+%1hZ)|-!R;sdhXR%RMG)yhI`DfdqaV+BCAUV5<#&^ne|?nXvx zwdD*eUu&CkVfE{jcIw48-koAJt=25YF5aES>94Y3t%<*ctuA+>ky5SoS^l)Pv5{7- zT`#S+#=GN;q-xFcG6GF9*RIOncFW(m$ond5FYtLCuwYDuUZdJuE zSx&h?-WEoyYAtJ=i7&;uTQkR08UAJ&GK_m`7B@olIyYRa+Z(yn#^m*Veed>x71 z7Vqw0bgH(AlCxW5WcfB%Xw6o}Sq`=Oj%KE48)yJ%t8uGKYtWLlm(Z+C?@ zF`88yceFjDb~k51Th*^FGGKfB9oMtB%tlkQo`IIbt2nHAm5135QQvW?3#_Jc*L&kx z)>l4wpQ#Bg9-}d*84lj78lpvRBL3#Oo}h{x7>E z_2m$~xYMs0{a+4K+oa&-W@?X=6Tha7Dt5Z$BsMJau&LM?rarRD?nJ+MXNX?zS+6K$ zoYEO}Tm4s)Spn*6tJO7|e9`jq)y^=zD7>;WP2I2*qjtF@XIF?`8)~%(=#krKW$M#6Tx!Y@7K?p}+gL_#WAxPS5VM1A^fpHMa%^|J zJIQEVeLj21Ei^T+?*OZCNu?{a<|8}Qc7pnjYdlKkR>n)j?g-ga8GXm-Fv)V0u*Vfz zv(pdTZcyK0JjXIVig5x%OVzbrXd9!xW0WbEvwPy*HE#-*vU}t27_rt`4!wKt>;%hP zeY#NJ(TSI2#oqYPR90|RHM0VKb8nox=A(ST@|dyD6v{$E#| z^|HPZw7TXvdeZWI2G2E5ILm3>R@8U2^@8R3Zd}wfX0@$7Yjw>L^}6LbWPg}mfXD4m zV*#GGkFjFEOUCUFG2Nk770dUBu^QjFzZom-BgDxv;Fb70#;j&C8%?&+q51^1){g4l zLwz;J+xC{jaILNyaCbY(Rbi@0gUi{t<^~RK&X~WG>uhaTE^bf9D zXDe&XjmkJp=!#w+4>-x1qC%l%6OPjPeny#5Kq3@HA&i8hVKgj)4d58) zfn#9`90yZjF>C>~!*n+=iUEyTd4NigGAqy7o0KYPpN+brMmTkWD7%&McN;8+yP$Ts8?HdEgca~zDDYFb5}tzW z`gkwF)$khh!N1^B&~)+21k3zLJWav{pMiDYT38pZgY}`VFdA+|ZU8sIrtmqK2{*%R zxCQ3G=V3eeci0ua0DHi#un*M!`ois;G;co=JIELbUxvkSCmauV!3l6ToCqu747dl* zf_vdys2$FOuOdGN4?uyh!S(QUxDmbqH^DdIbMP%Lns*0@w^8hZ@4$UfJKPW7M}7r< z01v?r;pgxW{1P69|9~IClkj8s9sC6T0FS_bLGAA{Jcj%eJPvQc&-G2?_P1U{ywUIr z2Am@H56{59@GR^P&%pukJ2()24@bbV z^CU)*_yHC{?QjhI5qT{97kn6AhV$T0Z~?pm7s8+6B6t;6z+d1h_$z!GYJbnb>&R>2 z4Y&>d2KV@L{(mR&9*RHUXYhZaeo?*&KZm#A3HT@c7XAg#L00JA3s6^Z5wgeUy#)Q? z?=S%Jns3yCwII8L-ayDszBdR4!-xRRe{B-=QLwe|Z3wl)Sjg7CHx7owbXXU*gY{rn z$dgHdo2tPf{G_RGApVFNe^dSDrhfsaFeBJh@L1-}t^E1<4eU?TELm;^V& zWcV^{1b0JzpYT?~#&8d80^fqE@E~joKZM%fA=m=>FieN1U`zNt%z$MVNVFnx5w?bx zU>kT1X2Kh=ExZY}!&|T&@}IChbk{PB4lovWgsCtWHiMmDbJ!WSfO#+n=EDxKi{kme zi-dNZ3ww~!3HF5dz`Nl<*bCkZd&5Dn5BwYK3kzXCI2vj{MQ|YU7*4z(SMfO-Rf^u@XKF zSHXR7H9QV|@Cu}IZ!pu<8rT><1M}fpI1;XhGvTvvHQWI8w73!KX>b$#5pIS7jI~=} zQW=RCNc4nTp`KOfop&XC2_Ask;90l>{tjP;;fyFdVNAAd5ERBwPkh z!4>c{tZ>NE#CmwwIP$abpX6_VUqO9`e+~D;Z{R!dTlhXa13!TJ;b-2p{C;j2e#TiP zo=^LkdVi1Re2mo2lA{K*P++p0;oH= z$dUEqe}nv`@HePi{X5(O|A4HfjQ@qKnv9!}Rg+bo)sf|wR{I&Zl>GWxzcPD2QC06V zhGWB;!#?=QN}o9ewGTG7yzwafU?SwH_a-@P0(BdkIx=&O!4LCZ<{G0GYzG5j4;Tao zz+lse^A09a8^sW)9Sn!sF{3NHFkc_N$7C}x{g|h$4oY-J7}RGh9J0nY>Oy@z>p^`z zBcQ&Xnyh#&r%yZbMi_~H6O4i{LB=fat1ueA3md=>p$D?uv&w%;V=ad)&x{y3cI;lg zQK_HaIIMK5;$bLEfcoi8l%dCCOg1XDQ6tAF#W8B^7&Vct$78w;PqZJlMikQr6Ra`F zN*M+NspY;I3`Ex))`2Y`&!~|OlVM8<`aIcWe=_~^q_S2hnY)bEP`4!u)`QtF9`Y0C z(pFHP?l$lq^qFuVYzqg&yWkLbH@pw(Dd>J!0EfZ}a2T8chr?NL1mqqrGe(l&?Z_Af zm%swZ%ZyP7_08K4>Ps~m>PuAw1&)F1;aI32wPLsp{twi1oNn-&a6Ix`Z~{CEC&FW} z1TxR`J5gco^s78~qOo6D1ZSx^p$}m+VHRN}VJG1b;d{bOLIQJc4q-Tx;aLLWj2VKHGd;dR1E!c~I)#Uho^lQ1gYsBO$5v6ApY{I0w#RdG77!SW#H zI4-iDjP7wJGk6s8_&S(pVQ4H zadP~hL(~Xq`c-_;c_(kO4EQR(^#Dg&MdiAhJgU^3;kLQg;bdOmwZ4Hzc7HY9Jls&y zzK%<3OJB5h3lp;D5rSqAk)}I9rhL7+*LhBz7MD0?Z#a!Da4KbvvGP|rc3U02UQOz9 z1>Dq{d;MhKH|yI+I%c;x+ghFCnE5;IPdV;qJ8srE#Rt@q*59T#D5`2rq+R@Qw9URU z_uKkGTb#mo%f@f(o3XXzz_*Rft+8_M+bpwbym-!}H(24+%hJQDf0|PyTnf(AugeT# zY0p=+Dp)q1$u$qxlV8t_Y5$H>cB4~yTj%`8I&J?w#x8x!X=JQ(-hprk4YaD%&mN<@PJEccB`pJ%$AnrG9cfLv= zV#}u(NVI-3K{GN=%AjBiDW|)8oo-Eb_=|JnsQR|o zV^KDbIOW?p<@D>muC;|zZknUN148G`-s~4k#GHJd($$cdJQVd)4O)otsM=xcn&XwxNTBii& zoX58V`Iqy}f;O^HBeU|O%Ttv3c08Xv9#j=qo15>%SEgTm+E0GF79uD9-N5zwkwb^*V>!y3T4x^G&852e5z3(e_Ts9a|$KxKj1+brZHL`*C>($|7mUJ zjFBtAL=UZOHX(8nW~{4%^7V>$uO9KQ!7(>l-M?RF1h(5fSV$Qo{8~9$w$q zK2&*pjY3pdaGawY7wwxEs^ZG@HwXSkMpgz#cDDX~&@R-mf!6b-L;r%(aZ|OvspZVp zJN7yHw}0DSe-~ic-&;I(V!?Py+%9A1Ic3`2E)&ww@YSxP8hA+DZbSjQ1&-7C4Sh4~ zs3gz)n)!vd^IfsNedL$9c+jdl@!;(W=39E*i7Asxrj4IkJZWNeN9;VOQKN5nT+fRB z#?+#Tqq`QA(w3@@+j_^o=yn_U)!#Jy(eF5Pr&ZB>{ zugcHKxb1CaqAxd`9Bcek1~>dGsBg~gl~qbDD#s>z8%N|KWS9Mo&``WhAQ}Ss;z9EYAN^kiBe%o4f2hsull%KW%FHKyH+{f zvh%xT_;Mpuqy4Ara|%_8?~MkkK3|afLOsgkAI#Ig*B9lJiTO{BVx=t}aKwYQc-Rq-+2Tn@JY$O&9Pwvc zyzYp9+M<7`-F>$&F`4d1IATLvOm@U(w%FPc+gqZ49xr02aerH#!I&Ijsr|d&PTqezIsSH1zt`3At%z08E>b7#Qu@0lC#m-l4*CjW zRJ1>-(aNr2L;}AV*tVGrhfZZZZzuIzW+U6cOY*p#9GIl<>R(cMw1*}6uB7s47u;4A zJF1e~sz)4^{-Sw^RnY$nk}-DjD#>%j}-wi)*H$^t*|n|5rk= zFR_`b7of$VR#uu>n6kfDGnM38ouOROn=Z{6qcgLz{_48=n}@o+^!kyYGrf1H{S7nJ zH9zy2XkXn-)r&7FeIqkfR3b0k#wWay=dfJtO!^G-Q7ww=U5DiaKi}Rgl^p&!Nmhya zXFL6qow0QPkxccRN}NJ^GNFW^r}|RDG{SVk!-PkCE3#CRU0wSq{}qT8`@?h8&;I)p Ia#V= Date: Fri, 12 Jul 2024 20:04:47 +0200 Subject: [PATCH 07/41] levi datatypes --- config/datatypes.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/datatypes.toml b/config/datatypes.toml index b9c276870..e7a1190cd 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -833,6 +833,14 @@ id = 0x156 name = "levi_volt_avg" id = 0x157 +[[Datatype]] +name = "levi_location" +id = 0x117 + +[[Datatype]] +name = "levi_speed" +id = 0x116 + [[Datatype]] name = "BrakingCommDebug" id = 0x158 From aa5e69c1edbe365e9c31c8ed2cedab56a7910ff6 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:20:49 +0200 Subject: [PATCH 08/41] levi data existence checks --- config/config.toml | 2 +- gs/station/build.rs | 28 ++++++++++++++++++++++++-- gs/station/src/levi/mod.rs | 3 ++- gs/station/src/levi/write_to_stdin.rs | 29 ++++++++++++++------------- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/config/config.toml b/config/config.toml index 7598116f4..9ce6397d9 100644 --- a/config/config.toml +++ b/config/config.toml @@ -26,7 +26,7 @@ bms_lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1 bms_hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] -levi_requested_data = ["Localisation", "PropulsionCurrent"] +levi_requested_data = ["Localisation", "PropulsionCurrent", "AccelerationX", "AccelerationY", "AccelerationZ"] [[Info]] label = "ServerStarted" diff --git a/gs/station/build.rs b/gs/station/build.rs index 59da158bf..1d27891e8 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -71,11 +71,13 @@ fn main() -> Result<()> { content.push_str(&configure_gs(&config)); content.push_str(&configure_gs_ip(config.gs.ip, config.gs.port, config.gs.force)?); - content.push_str(&generate_datatypes(DATATYPES_PATH, true)?); + let dt = generate_datatypes(DATATYPES_PATH, true)?; + content.push_str(&dt); content.push_str(&generate_commands(COMMANDS_PATH, false)?); content.push_str(&generate_events(EVENTS_PATH, false)?); content.push_str(&configure_channels(&config)); content.push_str(&goose_utils::info::generate_info(CONFIG_PATH, true)?); + content.push_str(&levi_req_data(&config, dt)?); fs::write(dest_path.clone(), content).unwrap_or_else(|_| { panic!("Couldn't write to {}! Build failed.", dest_path.to_str().unwrap()); @@ -89,6 +91,29 @@ fn main() -> Result<()> { Ok(()) } +fn levi_req_data(config: &Config, dt: String) -> Result { + for data in config.pod.comm.levi_requested_data.iter() { + if !dt.contains(data) { + return Err(anyhow::anyhow!( + "Data type {:?} not found in datatypes.toml + Check that the (case-sensitive) spelling is correct, and that the datatype exists.", + data + )); + } + } + Ok(format!( + "\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", + config.pod.comm.levi_requested_data.len(), + config + .pod + .comm + .levi_requested_data + .iter() + .map(|x| format!("Datatype::{x}, ")) + .collect::() + )) +} + fn configure_gs(config: &Config) -> String { // format!("pub fn gs_socket() -> std::net::SocketAddr {{ std::net::SocketAddr::new(std::net::IpAddr::from([{},{},{},{}]),{}) }}\n", config.gs.ip[0], config.gs.ip[1], config.gs.ip[2], config.gs.ip[3], config.gs.port) // format!( @@ -106,7 +131,6 @@ fn configure_gs(config: &Config) -> String { "pub const LEVI_EXEC_PATH: &str = \"{}\";\n", config.gs.levi_exec_path.to_str().unwrap() ) - + &format!("\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", config.pod.comm.levi_requested_data.len(), config.pod.comm.levi_requested_data.iter().map(|x| format!("Datatype::{x}, ")).collect::()) } fn configure_channels(config: &Config) -> String { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index 51630bdd5..a3cd82e10 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,8 +6,9 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; -use crate::{CommandReceiver, MessageReceiver}; +use crate::CommandReceiver; use crate::CommandSender; +use crate::MessageReceiver; use crate::MessageSender; use crate::LEVI_EXEC_PATH; diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index efcc0f06f..547ee7756 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,10 +1,11 @@ use tokio::io::AsyncWriteExt; use tokio::sync::broadcast::error::TryRecvError; -use crate::LEVI_REQUESTED_DATA; -use crate::{CommandReceiver, MessageReceiver}; use crate::api::Message; +use crate::CommandReceiver; +use crate::MessageReceiver; use crate::MessageSender; +use crate::LEVI_REQUESTED_DATA; /// # Writing to levi's stdin /// when a command is sent to the broadcast channel, it is sent to levi's stdin. @@ -24,28 +25,28 @@ pub async fn write_to_levi_child_stdin( cmd, cmd.to_str().as_bytes() )))?; - } + }, Err(TryRecvError::Closed) => { status_sender.send(Message::Error("command_receiver channel closed".into()))?; break; - } - _ => {} + }, + _ => {}, } match message_receiver.try_recv() { Ok(msg) => match msg { Message::Data(d) if LEVI_REQUESTED_DATA.contains(&d.datatype) => { - - stdin.write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()).await?; - stdin.flush().await?; - - } - _ => {} - } + stdin + .write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()) + .await?; + stdin.flush().await?; + }, + _ => {}, + }, Err(TryRecvError::Closed) => { status_sender.send(Message::Error("message_receiver channel closed".into()))?; break; - } - _ => {} + }, + _ => {}, } } Ok(()) From 0ccaea4728f862857be41ad20da32b47be3ed00b Mon Sep 17 00:00:00 2001 From: guusback <144220691+guusback@users.noreply.github.com> Date: Fri, 12 Jul 2024 20:02:44 +0200 Subject: [PATCH 09/41] Update datatypes.toml added the boundaries such that the temps and pressures will be orange/red at the right values Signed-off-by: guusback <144220691+guusback@users.noreply.github.com> --- config/datatypes.toml | 44 ++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/config/datatypes.toml b/config/datatypes.toml index e7a1190cd..6ec16a9f1 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -336,63 +336,75 @@ upper = 80000 # (e-2) degrees [[Datatype]] name = "Temp_HEMS_1" id = 0x20a -upper = 10000 # (e-2) degrees +lower = {err = 0 }# (e-2) degrees # the idea here is that we blink red if we read below 0 (sensor disconnect) +upper = { warn = 6000, err = 8000 }# (e-2) degrees [[Datatype]] name = "Temp_HEMS_2" id = 0x20b -upper = 10000 # (e-2) degrees +lower = {err = 0 }# (e-2) degrees +upper = { warn = 6000, err = 8000 }# (e-2) degrees [[Datatype]] name = "Temp_HEMS_3" id = 0x20c -upper = 10000 # (e-2) degrees +lower = {err = 0 }# (e-2) degrees +upper = { warn = 6000, err = 8000 }# (e-2) degrees [[Datatype]] name = "Temp_HEMS_4" id = 0x20d -upper = 10000 # (e-2) degrees - +lower = {err = 0 }# (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "Temp_Motor_1" id = 0x20e -upper = 10000 # (e-2) degrees +lower = {err = 0 } +upper = { warn = 6000, err = 8000 } [[Datatype]] name = "Temp_Motor_2" id = 0x20f -upper = 10000 # (e-2) degrees +lower = {err = 0 } # (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "Ambient_presure" id = 0x220 # no limit necessary +lower = {warn = 90, err = 50} #(e-2) bar +upper = { warn = 120, err = 200 } #(e-2) bar [[Datatype]] name = "Ambient_temp" id = 0x224 -# no limit necessary +lower = {err = 0} # (e-2) degrees +upper = {err = 5000 } # (e-2) degrees [[Datatype]] name = "Temp_EMS_1" id = 0x222 -upper = 10000 # (e-2) degrees +lower = {err = 0 } # (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "Temp_EMS_2" id = 0x223 -upper = 10000 # (e-2) degrees +lower = {err = 0 } # (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "Temp_EMS_3" id = 0x225 -upper = 10000 # (e-2) degrees +lower = {err = 0 } # (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "Temp_EMS_4" id = 0x226 -upper = 10000 # (e-2) degrees +lower = {err = 0 } # (e-2) degrees +upper = { warn = 6000, err = 8000 } # (e-2) degrees [[Datatype]] name = "SingleCellVoltageHigh_1" @@ -709,17 +721,19 @@ id = 0x777 [[Datatype]] name = "LowPressureSensor" id = 0x127 -# lower = 40 #bar -# upper = 52 #bar +lower = {warn = 40, err = 35 } #(e-2) bar +upper = { warn = 52, err = 56} #(e-2) bar [[Datatype]] name = "HighPressureSensor" id = 0x128 # lower = 80 #bar # upper = 180 #bar - +lower = {warn = 90, err = 70 } #(e-2) bar +upper = { warn = 170, err = 180} #(e-2) bar # SJOERD LEVITATION DATATYPES +#Sjoerd needs to add or tell us limits here [[Datatype]] name = "levi_hems_gap_a" From ce41ff0052dd365e299a6245c7b0a53b1d6c3c0d Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:38:43 +0200 Subject: [PATCH 10/41] Update Cargo.toml Create "full" feature flag --- gs/station/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gs/station/Cargo.toml b/gs/station/Cargo.toml index 449263054..bdea90793 100644 --- a/gs/station/Cargo.toml +++ b/gs/station/Cargo.toml @@ -49,6 +49,8 @@ anyhow = "1.0.86" default = ["backend"] tui = ["crossterm", "ratatui"] backend = ["tauri"] +full = ["backend", "tui"] + # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # DO NOT REMOVE!! From 02ee8511063976299c0a3423372816b2e8bb131c Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:37:14 +0200 Subject: [PATCH 11/41] Update CI Update rust.yml --- .github/workflows/rust.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e46b3e6ca..f46ffaa90 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,13 +10,24 @@ env: CARGO_TERM_COLOR: always jobs: - build: + app: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build - run: rustup target add thumbv7em-none-eabihf && cd app && cargo build --verbose + run: rustup target add thumbv7em-none-eabihf && cd app && cargo build # - name: Run tests # run: cargo test --verbose + + station: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cd gs/station && cargo build --features full + - name: Run tests + run: cd gs/station cargo test --features full From 963808aa7d3f0965086298ea686644453baf31ed Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:44:17 +0200 Subject: [PATCH 12/41] Fix CI Tauri issues --- .github/workflows/rust.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f46ffaa90..864230032 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,6 +27,14 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: install dependencies (ubuntu only) + if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. + - name: Build run: cd gs/station && cargo build --features full - name: Run tests From 8343f53ea08d1865b26ad7cac36c0bb483ab5141 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:47:05 +0200 Subject: [PATCH 13/41] Fix ci? --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 864230032..6318d7a75 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -35,6 +35,28 @@ jobs: sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: lts/* + cache-dependency-path: ./gs/ + cache: 'npm' + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. + targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './station -> target' + + - name: install frontend dependencies + # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too. + run: npm install + - name: Build run: cd gs/station && cargo build --features full - name: Run tests From 47d0a4d7faf7ccee8525efb7bfbe60471fcbec0b Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:48:29 +0200 Subject: [PATCH 14/41] Npm oops --- .github/workflows/rust.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6318d7a75..8013fb310 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -51,11 +51,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './station -> target' - - - name: install frontend dependencies - # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too. - run: npm install + workspaces: './station -> target' - name: Build run: cd gs/station && cargo build --features full From 3bca500cd81ac7395015a65a0fba619888d720f2 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:52:06 +0200 Subject: [PATCH 15/41] Fix ci?? --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8013fb310..ff59390df 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,7 +32,9 @@ jobs: if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev + export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH + # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. - name: setup node From 0c34fd4baa5462795c236c4b7bba65e77e5fe543 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:53:57 +0200 Subject: [PATCH 16/41] Fix ci??? --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ff59390df..731bc20d0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,7 +32,7 @@ jobs: if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev && export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. From b4ede8f73626a92fd6dfd579181c6d336d9089c9 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:55:10 +0200 Subject: [PATCH 17/41] Fix ci???? --- .github/workflows/rust.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 731bc20d0..df4d41c41 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,12 +28,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: install dependencies (ubuntu only) - if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. + - name: install dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev && - export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev && export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. From d5875ba0fd3bf3ed5a50305b25dde49c0743aa66 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 10:54:50 +0200 Subject: [PATCH 18/41] Fix ci????? Signed-off-by: Andreas Tsatsanis --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index df4d41c41..0b383fb30 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,7 +31,7 @@ jobs: - name: install dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk3-dev && export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev && export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. From 553c8d71ed37ae526db7e1ef64a1d4a098366dd5 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 11:00:05 +0200 Subject: [PATCH 19/41] Fix ci?????? Signed-off-by: Andreas Tsatsanis --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0b383fb30..0e955a2a6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,8 +30,10 @@ jobs: - name: install dependencies run: | + sudo add-apt-repository ppa:some/ppa sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev && export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev + export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. From 24a2a707b083d8b9b5591ed8838c948d6b249c2d Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 11:02:31 +0200 Subject: [PATCH 20/41] Fix ci??????? Signed-off-by: Andreas Tsatsanis --- .github/workflows/rust.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0e955a2a6..8fe9fa841 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: - name: install dependencies run: | - sudo add-apt-repository ppa:some/ppa + echo "deb http://archive.ubuntu.com/ubuntu focal-updates main" >> /etc/apt/sources.list sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH @@ -43,17 +43,6 @@ jobs: node-version: lts/* cache-dependency-path: ./gs/ cache: 'npm' - - - name: install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. - targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - - - name: Rust cache - uses: swatinem/rust-cache@v2 - with: - workspaces: './station -> target' - name: Build run: cd gs/station && cargo build --features full From 6164fc9b083622ba5b96fd5610b561b7b7632708 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 11:03:24 +0200 Subject: [PATCH 21/41] fix ci Signed-off-by: Andreas Tsatsanis --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8fe9fa841..bde70a9c5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: - name: install dependencies run: | - echo "deb http://archive.ubuntu.com/ubuntu focal-updates main" >> /etc/apt/sources.list + sudo echo "deb http://archive.ubuntu.com/ubuntu focal-updates main" >> /etc/apt/sources.list sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH From 862c2493b7d01c91736c067257a4b6efa41252ea Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 11:05:50 +0200 Subject: [PATCH 22/41] fix ci Signed-off-by: Andreas Tsatsanis --- .github/workflows/rust.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bde70a9c5..96cffa1d4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,9 +30,8 @@ jobs: - name: install dependencies run: | - sudo echo "deb http://archive.ubuntu.com/ubuntu focal-updates main" >> /etc/apt/sources.list sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgdk-3-dev + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk-3-dev export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. From 6e51c509ba737b87c86be7567a42aa7549b72072 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 11:14:32 +0200 Subject: [PATCH 23/41] fix ci! --- .github/workflows/rust.yml | 3 ++- app/src/core/states/emergency_braking.rs | 1 + app/src/core/states/exit.rs | 5 ++++- app/src/core/states/levitating.rs | 2 +- app/src/core/states/moving_end_ls.rs | 6 +++++- app/src/core/states/moving_end_st.rs | 7 ++++++- app/src/core/states/moving_ls_cv.rs | 23 ++++++++++++++++++----- app/src/core/states/moving_ls_st.rs | 13 +++++++++++-- app/src/core/states/moving_st.rs | 10 +++++++--- config/config.toml | 4 ++++ config/events.toml | 10 +++++----- gs/station/src/tests/backend.rs | 2 +- util/src/shared/routes.rs | 10 +++++++--- 13 files changed, 72 insertions(+), 24 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 96cffa1d4..1b8fe61ce 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,4 +46,5 @@ jobs: - name: Build run: cd gs/station && cargo build --features full - name: Run tests - run: cd gs/station cargo test --features full + run: cd gs/station && cargo test --features full + diff --git a/app/src/core/states/emergency_braking.rs b/app/src/core/states/emergency_braking.rs index 9cda23f48..cdd5fbd03 100644 --- a/app/src/core/states/emergency_braking.rs +++ b/app/src/core/states/emergency_braking.rs @@ -22,6 +22,7 @@ impl Fsm { self.status.brakes_armed = false; self.status.route_set = false; self.status.speeds_set = false; + self.status.levitating = false; error!("------ Emergency Braking!! ------"); warn!("Emergency Braking!!!"); diff --git a/app/src/core/states/exit.rs b/app/src/core/states/exit.rs index 670b082a7..d5538e0c1 100644 --- a/app/src/core/states/exit.rs +++ b/app/src/core/states/exit.rs @@ -13,8 +13,11 @@ impl Fsm { BRAKE = true; } self.peripherals.hv_peripherals.power_hv_off(); - self.status.brakes_armed = false; self.peripherals.propulsion_controller.disable(); + self.status.brakes_armed = false; + self.status.route_set = false; + self.status.speeds_set = false; + self.status.levitating = false; info!("In exit state..."); } diff --git a/app/src/core/states/levitating.rs b/app/src/core/states/levitating.rs index d23063bfe..6a9ada8b7 100644 --- a/app/src/core/states/levitating.rs +++ b/app/src/core/states/levitating.rs @@ -17,7 +17,7 @@ impl Fsm { }, Event::LeviLandingEvent => { - transit!(self, State::Idle); + transit!(self, State::HVOn); }, _ => { diff --git a/app/src/core/states/moving_end_ls.rs b/app/src/core/states/moving_end_ls.rs index 07f2a4a0a..74ea90b24 100644 --- a/app/src/core/states/moving_end_ls.rs +++ b/app/src/core/states/moving_end_ls.rs @@ -4,7 +4,7 @@ use crate::core::finite_state_machine::Fsm; use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; -use crate::transit; +use crate::{Info, transit}; use crate::Event; impl Fsm { @@ -36,9 +36,13 @@ impl Fsm { }, _ => { info!("Invalid configuration!"); + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::Exit); }, }, + Event::LeviLandingEvent => { + transit!(self, State::HVOn); + }, _ => { info!("The current state ignores {}", event.to_str()); }, diff --git a/app/src/core/states/moving_end_st.rs b/app/src/core/states/moving_end_st.rs index e01737952..9a97c541d 100644 --- a/app/src/core/states/moving_end_st.rs +++ b/app/src/core/states/moving_end_st.rs @@ -4,7 +4,7 @@ use crate::core::finite_state_machine::Fsm; use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; -use crate::transit; +use crate::{Info, transit}; use crate::Event; impl Fsm { @@ -27,6 +27,7 @@ impl Fsm { _ => { info!("Invalid configuration!"); + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::Exit); }, }, @@ -40,9 +41,13 @@ impl Fsm { }, _ => { info!("Invalid configuration!"); + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::Exit); }, }, + Event::LeviLandingEvent => { + transit!(self, State::HVOn); + }, _ => { info!("The current state ignores {}", event.to_str()); }, diff --git a/app/src/core/states/moving_ls_cv.rs b/app/src/core/states/moving_ls_cv.rs index bfc87e52d..5ad463050 100644 --- a/app/src/core/states/moving_ls_cv.rs +++ b/app/src/core/states/moving_ls_cv.rs @@ -4,32 +4,45 @@ use crate::core::finite_state_machine::Fsm; use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; -use crate::transit; +use crate::{Command, Info, transit}; use crate::Event; impl Fsm { pub fn entry_ls_cv(&mut self) { - todo!(); + self.peripherals.propulsion_controller.set_speed(self.route.current_speed()); } pub async fn react_mv_ls_cv(&mut self, event: Event) { match event { - Event::LaneSwitchEndedC => match self.route.next_position() { + Event::LaneSwitchEnded => match self.route.next_position() { Location::ForwardC => { info!("Entering straight track after curved lane-switch!"); - self.send_levi_cmd(crate::Command::ls0(0)).await; + self.send_levi_cmd(Command::ls0(0)).await; transit!(self, State::EndST); }, + + Location::BackwardsA => { + info!("Entering straight track after curved lane-switch!"); + self.send_levi_cmd(Command::ls0(0)).await; + transit!(self, State::MovingST); + }, + Location::StopAndWait => { self.peripherals.propulsion_controller.stop(); - self.send_levi_cmd(crate::Command::ls0(0)).await; + self.send_levi_cmd(Command::ls0(0)).await; + self.send_levi_cmd(Command::LeviPropulsionStop(0)).await; transit!(self, State::Levitating); }, + _ => { info!("Invalid configuration!"); + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::EndST); }, }, + Event::LeviLandingEvent => { + transit!(self, State::HVOn); + }, _ => { info!("The current state ignores {}", event.to_str()); }, diff --git a/app/src/core/states/moving_ls_st.rs b/app/src/core/states/moving_ls_st.rs index bc8551b40..7f04bcce5 100644 --- a/app/src/core/states/moving_ls_st.rs +++ b/app/src/core/states/moving_ls_st.rs @@ -4,7 +4,7 @@ use crate::core::finite_state_machine::Fsm; use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; -use crate::transit; +use crate::{Info, transit}; use crate::Event; impl Fsm { @@ -16,7 +16,12 @@ impl Fsm { self.send_levi_cmd(crate::Command::ls0(0)).await; match event { - Event::LaneSwitchEndedB => match self.route.next_position() { + Event::LaneSwitchEnded => match self.route.next_position() { + Location::BackwardsA => { + info!("Exiting a straight run LS!"); + transit!(self, State::MovingST); + }, + Location::ForwardB => { info!("Exiting a straight run LS!"); transit!(self, State::EndST); @@ -29,9 +34,13 @@ impl Fsm { _ => { info!("Invalid configuration!"); + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::Exit); }, }, + Event::LeviLandingEvent => { + transit!(self, State::HVOn); + }, _ => { info!("The current state ignores {}", event.to_str()); }, diff --git a/app/src/core/states/moving_st.rs b/app/src/core/states/moving_st.rs index 574d59bfd..e578a1c5b 100644 --- a/app/src/core/states/moving_st.rs +++ b/app/src/core/states/moving_st.rs @@ -49,7 +49,7 @@ impl Fsm { }, _ => { error!("Invalid configuration!"); - self.log(Info::InvalidRouteConfiguration).await; + self.log(Info::InvalidRouteConfigurationAbortingRun).await; transit!(self, State::EmergencyBraking); }, } @@ -59,11 +59,15 @@ impl Fsm { #[cfg(debug_assertions)] info!("Braking point reached"); self.peripherals.propulsion_controller.stop(); - self.send_levi_cmd(crate::Command::ls0(0)).await; - self.send_levi_cmd(crate::Command::LeviPropulsionStop(0)).await; + self.send_levi_cmd(Command::ls0(0)).await; + self.send_levi_cmd(Command::LeviPropulsionStop(0)).await; transit!(self, State::Levitating); }, + Event::LeviLandingEvent => { + transit!(self, State::HVOn); + }, + _ => { info!("The current state ignores {}", event.to_str()); }, diff --git a/config/config.toml b/config/config.toml index 9ce6397d9..78b9d6be3 100644 --- a/config/config.toml +++ b/config/config.toml @@ -125,3 +125,7 @@ colour = "green" label = "EventsHashPassed" colour = "green" +[[Info]] +label = "InvalidRouteConfigurationAbortingRun" +colour = "red" + diff --git a/config/events.toml b/config/events.toml index c7f1e5cd4..41e2419f5 100644 --- a/config/events.toml +++ b/config/events.toml @@ -165,14 +165,14 @@ id = 0x504 priority = 0 [[Event]] -name = "LaneSwitchEndedB" +name = "LaneSwitchEnded" id = 0x505 priority = 0 -[[Event]] -name = "LaneSwitchEndedC" -id = 0x507 -priority = 0 +# [[Event]] +# name = "LaneSwitchEndedC" +# id = 0x507 +# priority = 0 [[Event]] name = "EndOfTrackReached" diff --git a/gs/station/src/tests/backend.rs b/gs/station/src/tests/backend.rs index dd3b6e4a9..dfba85bb3 100644 --- a/gs/station/src/tests/backend.rs +++ b/gs/station/src/tests/backend.rs @@ -14,5 +14,5 @@ fn import_procedures() { assert_eq!(example[2], "DH08.PROC.SC.x"); assert_eq!(example[3], "Kiko\nKiril\n"); assert_eq!(example[4], "Andreas\n"); - assert_eq!(example[5], "

1. I refuse to elaborate.


\n

2. :)


\n

3. if in trouble just call me


\n

just text is also fine in a procedure.


"); + assert_eq!(example[5], "

1. I refuse to elaborate.

\n

2. :)

\n

3. if in trouble just call me

\n

just text is also fine in a procedure.

"); } diff --git a/util/src/shared/routes.rs b/util/src/shared/routes.rs index ef758274a..3f8af9b7c 100644 --- a/util/src/shared/routes.rs +++ b/util/src/shared/routes.rs @@ -455,8 +455,12 @@ mod tests { }; let s_bytes: u64 = route.speeds.clone().into(); let r_bytes: u64 = route.positions.into(); - panic!("Speeds: {}\nPositions: {}", s_bytes, r_bytes); - assert!(s_bytes > 0); - assert!(r_bytes > 0); + // panic!("Speeds: {}\nPositions: {}", s_bytes, r_bytes); + + let mut r = Route::default(); + r.speeds_from(s_bytes); + r.positions_from(r_bytes); + + assert_eq!(r, route); } } From 539baf6900e9a9a041419df0616aeee29e1e7bdd Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 12:13:43 +0200 Subject: [PATCH 24/41] dodge a bullet --- .github/workflows/rust.yml | 13 +------------ app/src/core/states/hv_on.rs | 1 + app/src/core/states/levitating.rs | 1 + app/src/core/states/mod.rs | 13 ++++++++++++- app/src/core/states/moving_ls_cv.rs | 3 --- app/src/core/states/moving_st.rs | 3 --- config/config.toml | 10 +++++++++- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1b8fe61ce..9b1663056 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,8 +18,6 @@ jobs: - uses: actions/checkout@v3 - name: Build run: rustup target add thumbv7em-none-eabihf && cd app && cargo build - # - name: Run tests - # run: cargo test --verbose station: @@ -34,17 +32,8 @@ jobs: sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk-3-dev export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH - # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. - - - name: setup node - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache-dependency-path: ./gs/ - cache: 'npm' - - name: Build run: cd gs/station && cargo build --features full - - name: Run tests + - name: Test run: cd gs/station && cargo test --features full diff --git a/app/src/core/states/hv_on.rs b/app/src/core/states/hv_on.rs index 1e815e9dc..016634503 100644 --- a/app/src/core/states/hv_on.rs +++ b/app/src/core/states/hv_on.rs @@ -39,6 +39,7 @@ impl Fsm { } self.enter_moving().await; + self.set_ls_mode().await; }, _ => { diff --git a/app/src/core/states/levitating.rs b/app/src/core/states/levitating.rs index 6a9ada8b7..f4132af67 100644 --- a/app/src/core/states/levitating.rs +++ b/app/src/core/states/levitating.rs @@ -14,6 +14,7 @@ impl Fsm { Event::RunStarting => { self.send_levi_cmd(Command::LeviPropulsionStart(0)).await; self.enter_moving().await; + self.set_ls_mode().await; }, Event::LeviLandingEvent => { diff --git a/app/src/core/states/mod.rs b/app/src/core/states/mod.rs index acbdde6e2..15bdae6ec 100644 --- a/app/src/core/states/mod.rs +++ b/app/src/core/states/mod.rs @@ -18,7 +18,7 @@ mod moving { use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; - use crate::{Datatype, transit}; + use crate::{Command, Datatype, transit}; use crate::Info; impl Fsm { @@ -39,5 +39,16 @@ mod moving { }, } } + + /// Only used in ForwardA, BackwardB, BackwardC positions. + /// Tells levi which LS mode to use. + pub async fn set_ls_mode(&mut self) { + self.log(Info::SettingLSMode).await; + match self.route.peek_next_position() { + Location::LaneSwitchStraight => self.send_levi_cmd(Command::ls1(0)).await, + Location::LaneSwitchCurved => self.send_levi_cmd(Command::ls2(0)).await, + _ => self.send_levi_cmd(Command::ls0(0)).await, + } + } } } diff --git a/app/src/core/states/moving_ls_cv.rs b/app/src/core/states/moving_ls_cv.rs index 5ad463050..3161fbc1d 100644 --- a/app/src/core/states/moving_ls_cv.rs +++ b/app/src/core/states/moving_ls_cv.rs @@ -17,19 +17,16 @@ impl Fsm { Event::LaneSwitchEnded => match self.route.next_position() { Location::ForwardC => { info!("Entering straight track after curved lane-switch!"); - self.send_levi_cmd(Command::ls0(0)).await; transit!(self, State::EndST); }, Location::BackwardsA => { info!("Entering straight track after curved lane-switch!"); - self.send_levi_cmd(Command::ls0(0)).await; transit!(self, State::MovingST); }, Location::StopAndWait => { self.peripherals.propulsion_controller.stop(); - self.send_levi_cmd(Command::ls0(0)).await; self.send_levi_cmd(Command::LeviPropulsionStop(0)).await; transit!(self, State::Levitating); }, diff --git a/app/src/core/states/moving_st.rs b/app/src/core/states/moving_st.rs index e578a1c5b..954873543 100644 --- a/app/src/core/states/moving_st.rs +++ b/app/src/core/states/moving_st.rs @@ -25,20 +25,17 @@ impl Fsm { #[cfg(debug_assertions)] info!("Entering a lane switch!"); // self.peripherals.propulsion_controller.turn_off(); - self.send_levi_cmd(Command::ls1(0)).await; transit!(self, State::MovingLSST); }, Location::LaneSwitchCurved => { #[cfg(debug_assertions)] info!("Entering a lane switch!"); - self.send_levi_cmd(Command::ls2(0)).await; transit!(self, State::MovingLSCV); }, Location::StopAndWait => { #[cfg(debug_assertions)] info!("Stopping and waiting"); self.peripherals.propulsion_controller.stop(); - self.send_levi_cmd(Command::ls0(0)).await; self.send_levi_cmd(Command::LeviPropulsionStop(0)).await; transit!(self, State::Levitating); }, diff --git a/config/config.toml b/config/config.toml index 78b9d6be3..7e6e72a60 100644 --- a/config/config.toml +++ b/config/config.toml @@ -79,7 +79,7 @@ colour = "green" [[Info]] label = "Unsafe" colour = "magenta" - +[] [[Info]] label = "EnablePropulsionGpio" colour = "yellow" @@ -129,3 +129,11 @@ colour = "green" label = "InvalidRouteConfigurationAbortingRun" colour = "red" +[[Info]] +label = "SettingLSMode" +colour = "green" + +[[Info]] +label = "PodNotLevitating" +colour = "red" + From f1231ad09ce1c8b8fb29076efbd5898aa56e76b1 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Sat, 13 Jul 2024 13:36:34 +0200 Subject: [PATCH 25/41] refactor: use the new datatype struct --- gs/docs/GDD.md | 7 +- gs/docs/api/apiGDD.md | 2 +- gs/src/lib/components/BottomBar.svelte | 18 +- gs/src/lib/components/FSM.svelte | 2 +- gs/src/lib/components/Localiser.svelte | 4 - gs/src/lib/components/generic/Store.svelte | 18 +- gs/src/lib/panels/VitalsPanel.svelte | 6 +- gs/src/lib/panels/tabs/BatteriesTab.svelte | 4 +- gs/src/lib/panels/tabs/LocationTab.svelte | 4 +- gs/src/lib/panels/tabs/PneumaticsTab.svelte | 4 +- gs/src/lib/panels/tabs/RunInitTab.svelte | 2 +- gs/src/lib/stores/state.ts | 9 +- gs/src/lib/types.ts | 5 +- gs/src/lib/util/GrandDataDistributor.ts | 41 ++-- gs/src/routes/+layout.svelte | 216 ++++++++++---------- 15 files changed, 174 insertions(+), 168 deletions(-) diff --git a/gs/docs/GDD.md b/gs/docs/GDD.md index c4247293b..a7f02c3cd 100644 --- a/gs/docs/GDD.md +++ b/gs/docs/GDD.md @@ -82,11 +82,12 @@ After registering a store to listen to and defining its type, one can later subs front-end! This is done using the `$storeName` syntax. For example, if you have a store named `BatteryBalanceLow`: ```sveltehtml +

{$lvBattery}

diff --git a/gs/docs/api/apiGDD.md b/gs/docs/api/apiGDD.md index bb021cf49..c992c0f3b 100644 --- a/gs/docs/api/apiGDD.md +++ b/gs/docs/api/apiGDD.md @@ -42,7 +42,7 @@ the stores can only have names of this type. This is ensured by the `NamedDataty |-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| | `registerStore(name: NamedDatatype, initial: T, dataConvFun?: (data: bigint, current: T) => T): void` | Register a store with the given name, initial value, and an optional process function. | | `updateStore(name: NamedDatatype, data: bigint)` | Update a store with the given name and data. | -| `getStore(name: NamedDatatype):Writable` | Get a store with the given name. (Which is any of `NamedDatatype` | +| `getWritable(name: NamedDatatype):Writable` | Get a store with the given name. (Which is any of `NamedDatatype` | ## Store diff --git a/gs/src/lib/components/BottomBar.svelte b/gs/src/lib/components/BottomBar.svelte index 4b75aa56e..64243045a 100644 --- a/gs/src/lib/components/BottomBar.svelte +++ b/gs/src/lib/components/BottomBar.svelte @@ -1,6 +1,7 @@ -
+ + + + +

Delft Hyperloop: Helios III

Current state: {$fsmState}

diff --git a/gs/src/lib/components/FSM.svelte b/gs/src/lib/components/FSM.svelte index 7f36cba00..5e9de45ae 100644 --- a/gs/src/lib/components/FSM.svelte +++ b/gs/src/lib/components/FSM.svelte @@ -21,7 +21,7 @@ const storeManager = GrandDataDistributor.getInstance().stores; - const fsmState = storeManager.getStore("FSMState"); + const fsmState = storeManager.getWritable("FSMState"); function turn_on(state:SVGGElement) { if (!state || !state.style) return; diff --git a/gs/src/lib/components/Localiser.svelte b/gs/src/lib/components/Localiser.svelte index 3b4765659..5d5e2d153 100644 --- a/gs/src/lib/components/Localiser.svelte +++ b/gs/src/lib/components/Localiser.svelte @@ -2,7 +2,6 @@ import {onMount} from "svelte"; import util from "$lib/util/util"; import {inputEmerg} from "$lib/stores/state"; - import {GrandDataDistributor} from "$lib"; export let max:number = 13000; export let loc:number = 1000; /* should be < 16000 */ @@ -91,9 +90,6 @@ emergYPosition = progress_straight.getPointAtLength(emergPosition).y; } } - - const storeManager = GrandDataDistributor.getInstance().stores; - const velocity = storeManager.getStore("Velocity");
diff --git a/gs/src/lib/components/generic/Store.svelte b/gs/src/lib/components/generic/Store.svelte index 9231be4d2..61156d8ba 100644 --- a/gs/src/lib/components/generic/Store.svelte +++ b/gs/src/lib/components/generic/Store.svelte @@ -4,21 +4,7 @@ export let datatype: NamedDatatype; const store = GrandDataDistributor.getInstance().stores.getStore(datatype); - - // instead of this shit, fetch the - const lower_error_threshold = 0; - const lower_warn_threshold = 0.1; - const upper_warn_threshold = 1.1; - const upper_error_threshold = 1.2; - - let colourClass = ""; - $: if ($store < lower_error_threshold || $store > upper_error_threshold) { - colourClass = "text-error-500"; - } else if ($store < lower_warn_threshold || $store > upper_warn_threshold) { - colourClass = "text-warning-500"; - } else { - colourClass = ""; - } + const writable = store.writable; -{$store} \ No newline at end of file +{$writable} \ No newline at end of file diff --git a/gs/src/lib/panels/VitalsPanel.svelte b/gs/src/lib/panels/VitalsPanel.svelte index c635e21f2..5599e4ce1 100644 --- a/gs/src/lib/panels/VitalsPanel.svelte +++ b/gs/src/lib/panels/VitalsPanel.svelte @@ -8,8 +8,8 @@ let width: number; const storeManager = GrandDataDistributor.getInstance().stores; - const lvBattery = storeManager.getStore("BatteryEstimatedChargeLow"); - const hvBattery = storeManager.getStore("BatteryEstimatedChargeHigh"); + const lvBattery = storeManager.getWritable("BatteryEstimatedChargeLow"); + const hvBattery = storeManager.getWritable("BatteryEstimatedChargeHigh"); let tableTempsArr: any[][]; let tableArr2: any[][]; @@ -36,7 +36,7 @@ ["IMD Voltage", DE.IMDVOLTAGEDETAILS, "Insulation+", DE.INSULATIONPOSITIVE], ] - const location = storeManager.getStore("Localisation"); + const location = storeManager.getWritable("Localisation"); const toastStore = getToastStore(); diff --git a/gs/src/lib/panels/tabs/BatteriesTab.svelte b/gs/src/lib/panels/tabs/BatteriesTab.svelte index 69edaa059..4a5ce40d5 100644 --- a/gs/src/lib/panels/tabs/BatteriesTab.svelte +++ b/gs/src/lib/panels/tabs/BatteriesTab.svelte @@ -9,8 +9,8 @@ TileGrid, ToggleCommand } from "$lib"; const storeManager = GrandDataDistributor.getInstance().stores; - const lvBattery = storeManager.getStore("BatteryEstimatedChargeLow"); - const hvBattery = storeManager.getStore("BatteryEstimatedChargeHigh"); + const lvBattery = storeManager.getWritable("BatteryEstimatedChargeLow"); + const hvBattery = storeManager.getWritable("BatteryEstimatedChargeHigh"); const avg1Temp = "Module1AvgTemperature", max1Temp = "Module1MaxTemperature", min1Temp = "Module1MinTemperature", avg1Vol = "Module1AvgVoltage", max1Vol = "Module1MaxVoltage", min1Vol = "Module1MinVoltage", diff --git a/gs/src/lib/panels/tabs/LocationTab.svelte b/gs/src/lib/panels/tabs/LocationTab.svelte index 74290e073..9110f1b70 100644 --- a/gs/src/lib/panels/tabs/LocationTab.svelte +++ b/gs/src/lib/panels/tabs/LocationTab.svelte @@ -1,5 +1,5 @@
diff --git a/gs/src/lib/panels/tabs/PneumaticsTab.svelte b/gs/src/lib/panels/tabs/PneumaticsTab.svelte index 897a38951..010595f34 100644 --- a/gs/src/lib/panels/tabs/PneumaticsTab.svelte +++ b/gs/src/lib/panels/tabs/PneumaticsTab.svelte @@ -3,8 +3,8 @@ import {DatatypeEnum as DE} from "$lib/namedDatatypeEnum"; const storeManager = GrandDataDistributor.getInstance().stores; - const lowPressure = storeManager.getStore("LowPressureSensor"); - const highPressure = storeManager.getStore("HighPressureSensor"); + const lowPressure = storeManager.getWritable("LowPressureSensor"); + const highPressure = storeManager.getWritable("HighPressureSensor"); $: pressureTable = [ ["Low Pressure", DE.LOWPRESSURESENSOR], diff --git a/gs/src/lib/panels/tabs/RunInitTab.svelte b/gs/src/lib/panels/tabs/RunInitTab.svelte index 1092f61e2..8473f8abc 100644 --- a/gs/src/lib/panels/tabs/RunInitTab.svelte +++ b/gs/src/lib/panels/tabs/RunInitTab.svelte @@ -12,7 +12,7 @@ const storeManager = GrandDataDistributor.getInstance().stores; - const state = storeManager.getStore("FSMState"); + const state = storeManager.getWritable("FSMState"); let tableArr2:any[][]; $: tableArr2 = [ diff --git a/gs/src/lib/stores/state.ts b/gs/src/lib/stores/state.ts index 95b05be08..d146c3439 100644 --- a/gs/src/lib/stores/state.ts +++ b/gs/src/lib/stores/state.ts @@ -2,6 +2,12 @@ import { writable, type Writable } from 'svelte/store'; import {RunMode} from "$lib/types"; import {PlotBuffer} from "$lib"; +export enum ErrorStatus { + SAFE, + WARNING, + UNSAFE, +} + export const detailTabSet: Writable = writable(0); export const inputSpeed: Writable = writable(50); export const inputEmerg: Writable = writable(-1); @@ -12,4 +18,5 @@ export const details_pane: Writable = writable(80) export const chartStore = writable(new Map()); -export const serverStatus: Writable = writable(false); \ No newline at end of file +export const serverStatus: Writable = writable(false); +export const bigErrorStatus: Writable = writable(ErrorStatus.SAFE); diff --git a/gs/src/lib/types.ts b/gs/src/lib/types.ts index cc6139c03..25bfe1772 100644 --- a/gs/src/lib/types.ts +++ b/gs/src/lib/types.ts @@ -13,8 +13,9 @@ export const NamedDatatypeValues = ["YourNewDatatypeName", "DefaultDatatype", "P */ export type Datapoint = { datatype: NamedDatatype, - value: bigint, + value: number, timestamp: number, + style: string } /** @@ -30,7 +31,7 @@ export const EventChannel = { /** * Function to convert data received at DATAPOINT.value to a given type */ -export type dataConvFun = (data: bigint, old:T) => T; +export type dataConvFun = (data: number, old:T) => T; /** * BMS Module Voltage diff --git a/gs/src/lib/util/GrandDataDistributor.ts b/gs/src/lib/util/GrandDataDistributor.ts index 3bdeedbac..fb84ff884 100644 --- a/gs/src/lib/util/GrandDataDistributor.ts +++ b/gs/src/lib/util/GrandDataDistributor.ts @@ -1,7 +1,6 @@ import {invoke} from "@tauri-apps/api/tauri"; import {get, type Writable, writable} from "svelte/store"; -import {type dataConvFun, type Datapoint, EventChannel, type NamedDatatype} from "$lib/types"; -import {emit} from "@tauri-apps/api/event"; +import {type dataConvFun, type Datapoint, type NamedDatatype} from "$lib/types"; /** * The GrandDataDistributor class is responsible for fetching data from the backend @@ -85,7 +84,7 @@ export class GrandDataDistributor { protected processData(data: Datapoint[]) { data.forEach((datapoint) => { // emit(EventChannel.INFO, `Datapoint received: ${datapoint.datatype} - ${datapoint.value}`); - this.StoreManager.updateStore(datapoint.datatype, datapoint.value); + this.StoreManager.updateStore(datapoint.datatype, datapoint.style, datapoint.value); }); } @@ -114,23 +113,28 @@ class StoreManager { * @param processFunction - the function to process the data */ public registerStore(name: NamedDatatype, initial: T, processFunction?: dataConvFun) { - // if (this.stores.has(name)) throw new Error(`Store with name ${name} already exists`); - this.stores.set(name, new Store(initial, processFunction)); + this.stores.set(name, new Store(initial, '', processFunction)); } /** * Update a store * @param name - the name of the store + * @param style * @param data - the data to update the store with */ - public updateStore(name: NamedDatatype, data: bigint) { + public updateStore(name: NamedDatatype, style:string, data: number) { const store = this.stores.get(name); - if (store) store.set(data); + if (store) store.set(data, style); } - public getStore(name: NamedDatatype):Writable { + public getWritable(name: NamedDatatype):Writable { if (!this.stores.has(name)) throw new Error(`Store with name ${name} does not exist`); - return this.stores.get(name)!.getWritable; + return this.stores.get(name)!.writable; + } + + public getStore(name: NamedDatatype):Store { + if (!this.stores.has(name)) throw new Error(`Store with name ${name} does not exist`); + return this.stores.get(name)!; } } @@ -141,18 +145,25 @@ class StoreManager { */ class Store { private readonly processFunction: dataConvFun; - private readonly writable: Writable; + private readonly _writable: Writable; + private _style: string; - constructor(initial:T, processFunction: dataConvFun = (data) => data.valueOf() as unknown as T) { - this.writable = writable(initial); + constructor(initial:T, style:string, processFunction: dataConvFun = (data) => data.valueOf() as unknown as T) { + this._writable = writable(initial); this.processFunction = processFunction; + this._style = style; } - public set(data: bigint) { + public set(data: number, style: string) { this.writable.set(this.processFunction(data, get(this.writable))); + this._style = style; + } + + public get writable():Writable { + return this._writable; } - public get getWritable():Writable { - return this.writable; + public get style():string { + return this._style; } } diff --git a/gs/src/routes/+layout.svelte b/gs/src/routes/+layout.svelte index c7f41d9ef..4b922eb87 100644 --- a/gs/src/routes/+layout.svelte +++ b/gs/src/routes/+layout.svelte @@ -6,9 +6,8 @@ PlotBuffer, StrokePresets, TitleBar, - tempParse, - voltParse, - addEntryToChart, u64ToDouble, pressureParse, sensorParse + addEntryToChart, + u64ToDouble, } from "$lib"; import {initializeStores, Modal, Toast} from '@skeletonlabs/skeleton'; import {chartStore} from "$lib/stores/state"; @@ -88,87 +87,83 @@ /////////////////////////////////////////////////////// let gdd = GrandDataDistributor.getInstance(); - gdd.stores.registerStore("BatteryEstimatedChargeHigh", 0.0, data => Number(data) / 100); - gdd.stores.registerStore("BatteryEstimatedChargeLow", 0.0, data => Number(data) / 100); - - gdd.stores.registerStore("Module1AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module1MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module1MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module1AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module1MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module1MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module2AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module2MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module2MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module2AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module2MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module2MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module3AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module3MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module3MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module3AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module3MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module3MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module4AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module4MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module4MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module4AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module4MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module4MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module5AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module5MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module5MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module5AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module5MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module5MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module6AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module6MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module6MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module6AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module6MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module6MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module7AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module7MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module7MinTemperature", 0.0, tempParse) - gdd.stores.registerStore("Module7AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module7MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module7MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("Module8AvgTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module8MaxTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module8MinTemperature", 0.0, tempParse); - gdd.stores.registerStore("Module8AvgVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module8MaxVoltage", "0.0", voltParse); - gdd.stores.registerStore("Module8MinVoltage", "0.0", voltParse); - - gdd.stores.registerStore("BatteryMinVoltageHigh", "0.0", voltParse); - gdd.stores.registerStore("BatteryMaxVoltageHigh", "0.0", voltParse); - gdd.stores.registerStore("BatteryVoltageHigh", "0.0", voltParse); - - gdd.stores.registerStore("BatteryMinTemperatureHigh", 0.0, tempParse); - gdd.stores.registerStore("BatteryMaxTemperatureHigh", 0.0, tempParse); - gdd.stores.registerStore("BatteryTemperatureHigh", 0.0, tempParse); - - gdd.stores.registerStore("BatteryTemperatureLow", 0.0, tempParse) - gdd.stores.registerStore("BatteryMinTemperatureLow", 0.0, tempParse) - gdd.stores.registerStore("BatteryMaxTemperatureLow", 0.0, tempParse) - - gdd.stores.registerStore("BatteryVoltageLow", "0.0", voltParse) - gdd.stores.registerStore("BatteryMinVoltageLow", "0.0", voltParse) - gdd.stores.registerStore("BatteryMaxVoltageLow", "0.0", voltParse) - - gdd.stores.registerStore("TotalBatteryVoltageHigh", 0.0, data => { - return Number(data) / 100 - 2; - }) - gdd.stores.registerStore("TotalBatteryVoltageLow", 0.0, data => { - return Number(data) / 100 - 2; - }) + gdd.stores.registerStore("BatteryEstimatedChargeHigh", 0.0); + gdd.stores.registerStore("BatteryEstimatedChargeLow", 0.0); + + gdd.stores.registerStore("Module1AvgTemperature", 0.0); + gdd.stores.registerStore("Module1MaxTemperature", 0.0); + gdd.stores.registerStore("Module1MinTemperature", 0.0); + gdd.stores.registerStore("Module1AvgVoltage", 0.0); + gdd.stores.registerStore("Module1MaxVoltage", 0.0); + gdd.stores.registerStore("Module1MinVoltage", 0.0); + + gdd.stores.registerStore("Module2AvgTemperature", 0.0); + gdd.stores.registerStore("Module2MaxTemperature", 0.0); + gdd.stores.registerStore("Module2MinTemperature", 0.0); + gdd.stores.registerStore("Module2AvgVoltage", 0.0); + gdd.stores.registerStore("Module2MaxVoltage", 0.0); + gdd.stores.registerStore("Module2MinVoltage", 0.0); + + gdd.stores.registerStore("Module3AvgTemperature", 0.0); + gdd.stores.registerStore("Module3MaxTemperature", 0.0); + gdd.stores.registerStore("Module3MinTemperature", 0.0); + gdd.stores.registerStore("Module3AvgVoltage", 0.0); + gdd.stores.registerStore("Module3MaxVoltage", 0.0); + gdd.stores.registerStore("Module3MinVoltage", 0.0); + + gdd.stores.registerStore("Module4AvgTemperature", 0.0); + gdd.stores.registerStore("Module4MaxTemperature", 0.0); + gdd.stores.registerStore("Module4MinTemperature", 0.0); + gdd.stores.registerStore("Module4AvgVoltage", 0.0); + gdd.stores.registerStore("Module4MaxVoltage", 0.0); + gdd.stores.registerStore("Module4MinVoltage", 0.0); + + gdd.stores.registerStore("Module5AvgTemperature", 0.0); + gdd.stores.registerStore("Module5MaxTemperature", 0.0); + gdd.stores.registerStore("Module5MinTemperature", 0.0); + gdd.stores.registerStore("Module5AvgVoltage", 0.0); + gdd.stores.registerStore("Module5MaxVoltage", 0.0); + gdd.stores.registerStore("Module5MinVoltage", 0.0); + + gdd.stores.registerStore("Module6AvgTemperature", 0.0); + gdd.stores.registerStore("Module6MaxTemperature", 0.0); + gdd.stores.registerStore("Module6MinTemperature", 0.0); + gdd.stores.registerStore("Module6AvgVoltage", 0.0); + gdd.stores.registerStore("Module6MaxVoltage", 0.0); + gdd.stores.registerStore("Module6MinVoltage", 0.0); + + gdd.stores.registerStore("Module7AvgTemperature", 0.0); + gdd.stores.registerStore("Module7MaxTemperature", 0.0); + gdd.stores.registerStore("Module7MinTemperature", 0.0) + gdd.stores.registerStore("Module7AvgVoltage", 0.0); + gdd.stores.registerStore("Module7MaxVoltage", 0.0); + gdd.stores.registerStore("Module7MinVoltage", 0.0); + + gdd.stores.registerStore("Module8AvgTemperature", 0.0); + gdd.stores.registerStore("Module8MaxTemperature", 0.0); + gdd.stores.registerStore("Module8MinTemperature", 0.0); + gdd.stores.registerStore("Module8AvgVoltage", 0.0); + gdd.stores.registerStore("Module8MaxVoltage", 0.0); + gdd.stores.registerStore("Module8MinVoltage", 0.0); + + gdd.stores.registerStore("BatteryMinVoltageHigh", 0.0); + gdd.stores.registerStore("BatteryMaxVoltageHigh", 0.0); + gdd.stores.registerStore("BatteryVoltageHigh", 0.0); + + gdd.stores.registerStore("BatteryMinTemperatureHigh", 0.0); + gdd.stores.registerStore("BatteryMaxTemperatureHigh", 0.0); + gdd.stores.registerStore("BatteryTemperatureHigh", 0.0); + + gdd.stores.registerStore("BatteryTemperatureLow", 0.0) + gdd.stores.registerStore("BatteryMinTemperatureLow", 0.0) + gdd.stores.registerStore("BatteryMaxTemperatureLow", 0.0) + + gdd.stores.registerStore("BatteryVoltageLow", 0.0) + gdd.stores.registerStore("BatteryMinVoltageLow", 0.0) + gdd.stores.registerStore("BatteryMaxVoltageLow", 0.0) + + gdd.stores.registerStore("TotalBatteryVoltageHigh", 0.0) + gdd.stores.registerStore("TotalBatteryVoltageLow", 0.0) gdd.stores.registerStore("BatteryCurrentLow", 0.0, data => { const curr = Number(data) / 10; @@ -210,19 +205,19 @@ gdd.stores.registerStore("PropulsionVoltage", 0); gdd.stores.registerStore("PropulsionVRefInt", 0); - gdd.stores.registerStore("BrakeTemperature", 0, pressureParse); - gdd.stores.registerStore("BrakePressure", 0,pressureParse); + gdd.stores.registerStore("BrakeTemperature", 0); + gdd.stores.registerStore("BrakePressure", 0); /////////////////////////////////////////////////////////////// //////////////////// REGISTER GYROSCOPE /////////////////////// /////////////////////////////////////////////////////////////// - gdd.stores.registerStore("GyroscopeX", 0, sensorParse); - gdd.stores.registerStore("GyroscopeY", 0, sensorParse); - gdd.stores.registerStore("GyroscopeZ", 0, sensorParse); - gdd.stores.registerStore("AccelerationX", 0, sensorParse); - gdd.stores.registerStore("AccelerationY", 0, sensorParse); - gdd.stores.registerStore("AccelerationZ", 0, sensorParse); + gdd.stores.registerStore("GyroscopeX", 0); + gdd.stores.registerStore("GyroscopeY", 0); + gdd.stores.registerStore("GyroscopeZ", 0); + gdd.stores.registerStore("AccelerationX", 0); + gdd.stores.registerStore("AccelerationY", 0); + gdd.stores.registerStore("AccelerationZ", 0); /////////////////////////////////////////////////////////////// /////////////////// REGISTER TEMPERATURES ///////////////////// @@ -275,15 +270,15 @@ ///////////////////// REGISTER LEVI DATA ////////////////////// /////////////////////////////////////////////////////////////// - gdd.stores.registerStore("levi_hems_gap_a", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_hems_gap_b", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_hems_gap_c", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_hems_gap_d", 0.0, u64ToDouble) + gdd.stores.registerStore("levi_hems_gap_a", 0.0) + gdd.stores.registerStore("levi_hems_gap_b", 0.0) + gdd.stores.registerStore("levi_hems_gap_c", 0.0) + gdd.stores.registerStore("levi_hems_gap_d", 0.0) - gdd.stores.registerStore("levi_ems_gap_a", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_ems_gap_b", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_ems_gap_c", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_ems_gap_d", 0.0, u64ToDouble) + gdd.stores.registerStore("levi_ems_gap_a", 0.0) + gdd.stores.registerStore("levi_ems_gap_b", 0.0) + gdd.stores.registerStore("levi_ems_gap_c", 0.0) + gdd.stores.registerStore("levi_ems_gap_d", 0.0) gdd.stores.registerStore("levi_hems_current_a1", 0.0, data => addEntryToChart(hemsCurrentChart, data, 1)) gdd.stores.registerStore("levi_hems_current_a2", 0.0, data => addEntryToChart(hemsCurrentChart, data, 2)) @@ -312,20 +307,20 @@ return curr; }) - gdd.stores.registerStore("levi_hems_power", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_ems_power", 0.0, u64ToDouble) + gdd.stores.registerStore("levi_hems_power", 0.0) + gdd.stores.registerStore("levi_ems_power", 0.0) - gdd.stores.registerStore("levi_volt_min", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_volt_max", 0.0, u64ToDouble) - gdd.stores.registerStore("levi_volt_avg", 0.0, u64ToDouble) + gdd.stores.registerStore("levi_volt_min", 0.0) + gdd.stores.registerStore("levi_volt_max", 0.0) + gdd.stores.registerStore("levi_volt_avg", 0.0) /////////////////////////////////////////////////////////////// ///////////////////////// PNEUMATICS ////////////////////////// /////////////////////////////////////////////////////////////// - gdd.stores.registerStore("LowPressureSensor", 0, pressureParse); - gdd.stores.registerStore("HighPressureSensor", 0, pressureParse); - gdd.stores.registerStore("BrakingCommDebug", 0, data => (Number(data) * 3.3) / 65535); + gdd.stores.registerStore("LowPressureSensor", 0); + gdd.stores.registerStore("HighPressureSensor", 0); + gdd.stores.registerStore("BrakingCommDebug", 0); gdd.stores.registerStore("BrakingSignalDebug", 0) gdd.stores.registerStore("BrakingRearmDebug", 0) @@ -336,10 +331,7 @@ gdd.stores.registerStore("InsulationOriginal", 0); gdd.stores.registerStore("InsulationPositive", 0); gdd.stores.registerStore("InsulationNegative", 0); - gdd.stores.registerStore("IMDVoltageDetails", "0.0", data => { - const curr = Number(data); - return curr === 65535 ? "0" : (curr * 0.005*110/250).toString(); - }); + gdd.stores.registerStore("IMDVoltageDetails", 0); /////////////////////////////////////////////////////////////// ///////////////// REGISTER META & ADDITIONAL ////////////////// From 0bedd7d36593939c80a8f85cd45a660538fd8e81 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Sat, 13 Jul 2024 13:45:25 +0200 Subject: [PATCH 26/41] fix: types --- gs/src/lib/panels/tabs/RunInitTab.svelte | 6 ------ gs/src/lib/types.ts | 12 ++++++------ gs/src/lib/util/DecodeBMS.ts | 24 ++++-------------------- gs/src/lib/util/parsers.ts | 11 +++++------ gs/src/routes/+layout.svelte | 11 ++++------- 5 files changed, 19 insertions(+), 45 deletions(-) diff --git a/gs/src/lib/panels/tabs/RunInitTab.svelte b/gs/src/lib/panels/tabs/RunInitTab.svelte index 8473f8abc..6574704c2 100644 --- a/gs/src/lib/panels/tabs/RunInitTab.svelte +++ b/gs/src/lib/panels/tabs/RunInitTab.svelte @@ -72,9 +72,3 @@
- - diff --git a/gs/src/lib/types.ts b/gs/src/lib/types.ts index 25bfe1772..2c0907a66 100644 --- a/gs/src/lib/types.ts +++ b/gs/src/lib/types.ts @@ -89,14 +89,14 @@ export type Log = { } export enum RouteStep { - STRAIGHT_START = 'StraightStart', + FORWARD_A = 'ForwardA', + FORWARD_B = 'ForwardB', + FORWARD_C = 'ForwardC', + BACKWARD_A = 'BackwardsA', + BACKWARD_B = 'BackwardsB', + BACKWARD_C = 'BackwardsC', LANE_SWITCH_STRAIGHT = 'LaneSwitchStraight', LANE_SWITCH_CURVED = 'LaneSwitchCurved', - STRAIGHT_END_TRACK = 'StraightEndTrack', - LANE_SWITCH_END_TRACK = 'LaneSwitchEndTrack', - STRAIGHT_BACKWARDS = 'StraightBackwards', - STOP_AND_WAIT = 'StopAndWait', - BRAKE_HERE = 'BrakeHere' } export type Procedure = { diff --git a/gs/src/lib/util/DecodeBMS.ts b/gs/src/lib/util/DecodeBMS.ts index ff4a2b9b0..6cc9317b6 100644 --- a/gs/src/lib/util/DecodeBMS.ts +++ b/gs/src/lib/util/DecodeBMS.ts @@ -4,7 +4,7 @@ import type {dataConvFun, BMSDiagnostic, BMSEvent, BmsModuleVoltage} from "$lib/ * DATATYPE BMS MODULE VOLTAGE * @param data - the data to be converted received at the DATAPOINT.value */ -export const moduleVoltage: dataConvFun = (data: bigint) => { +export const moduleVoltage: dataConvFun = (data: number) => { let id = BigInt(data) >> BigInt(48); let voltage = BigInt(data) & BigInt(0x0000FFFFFFFFFFFF); let max = ((BigInt(voltage) & BigInt(0x000000000000FFFF)) + BigInt(200)) * BigInt(0.1); @@ -18,7 +18,7 @@ export const moduleVoltage: dataConvFun = (data: bigint) => { * DATATYPE BMS MODULE TEMPERATURE * @param data - the data to be converted received at the DATAPOINT.value */ -export const moduleTemperature = (data: bigint): BmsModuleVoltage => { +export const moduleTemperature = (data: number): BmsModuleVoltage => { let id = BigInt(data) >> BigInt(48); let temperature = BigInt(data) & BigInt(0x0000FFFFFFFFFFFF); let max = BigInt(temperature) & BigInt(0x000000000000FFFF) - BigInt(100); @@ -27,28 +27,12 @@ export const moduleTemperature = (data: bigint): BmsModuleVoltage => { return {id, max, min, avg}; } - - - - - - - - - - - - - - - - /** * DATATYPE DIAGNOSTIC * @param data * @constructor */ -export const BMSDiagnosticTranslation: dataConvFun = (data: bigint): BMSDiagnostic => { +export const BMSDiagnosticTranslation: dataConvFun = (data: number): BMSDiagnostic => { let possibleErrors = ["Under-voltage – some cell is below critical minimum voltage", "Over-voltage – some cell is above critical maximum volta", "Discharge Over-current - discharge current (negative current) exceeds the critical discharge current setting", @@ -71,7 +55,7 @@ export const BMSDiagnosticTranslation: dataConvFun = (data: bigin let errors = []; let important: number[] = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 19, 20, 21, 24, 25, 26, 27, 28, 29] for (let i of important) { - if ((data & BigInt(1)) << BigInt(i)) { + if ((data & 1) << i) { errors.push(possibleErrors[i]); } } diff --git a/gs/src/lib/util/parsers.ts b/gs/src/lib/util/parsers.ts index 18adead03..8e27052f3 100644 --- a/gs/src/lib/util/parsers.ts +++ b/gs/src/lib/util/parsers.ts @@ -4,18 +4,17 @@ import {detailTabSet} from "$lib"; import {invoke} from "@tauri-apps/api/tauri"; const MAX_VALUE = 4_294_967_295; -const tempParse: dataConvFun = (data: bigint) => { +const tempParse: dataConvFun = (data: number) => { return Number(data) - 100; } -const voltParse: dataConvFun = (data: bigint) => { +const voltParse: dataConvFun = (data: number) => { return Number(data) === 200 ? "INVALID" : (Number(data) / 100).toString(); } -const addEntryToChart = (chart: PlotBuffer, data: bigint, index: number) => { - const curr = u64ToDouble(data); - chart.addEntry(index, curr); - return curr; +const addEntryToChart = (chart: PlotBuffer, data: number, index: number) => { + chart.addEntry(index, data); + return data; } const u64ToDouble = (u64: bigint): number => { diff --git a/gs/src/routes/+layout.svelte b/gs/src/routes/+layout.svelte index 4b922eb87..e8ab56103 100644 --- a/gs/src/routes/+layout.svelte +++ b/gs/src/routes/+layout.svelte @@ -7,7 +7,6 @@ StrokePresets, TitleBar, addEntryToChart, - u64ToDouble, } from "$lib"; import {initializeStores, Modal, Toast} from '@skeletonlabs/skeleton'; import {chartStore} from "$lib/stores/state"; @@ -297,14 +296,12 @@ gdd.stores.registerStore("levi_hems_pitch", 0.0, data => addEntryToChart(rolPitchChart, data, 2)) gdd.stores.registerStore("levi_ems_offset_ab", 0.0, data => { - const curr: number = u64ToDouble(data); - hoffChart.addEntry(1, curr); - return curr; + hoffChart.addEntry(1, data); + return data; }) gdd.stores.registerStore("levi_ems_offset_cd", 0.0, data => { - const curr = u64ToDouble(data); - hoffChart.addEntry(2, curr); - return curr; + hoffChart.addEntry(2, data); + return data; }) gdd.stores.registerStore("levi_hems_power", 0.0) From 7aedae3cff01bd7960a1c40eaf19051e4bb961f1 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Sat, 13 Jul 2024 14:29:20 +0200 Subject: [PATCH 27/41] fix: types --- gs/src/lib/panels/LogsPanel.svelte | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/gs/src/lib/panels/LogsPanel.svelte b/gs/src/lib/panels/LogsPanel.svelte index c75d7312a..4a2d4c0c3 100644 --- a/gs/src/lib/panels/LogsPanel.svelte +++ b/gs/src/lib/panels/LogsPanel.svelte @@ -2,11 +2,14 @@ @@ -122,12 +122,8 @@
- - {#if width > 550} - - {:else} - - {/if} + + @@ -159,36 +155,24 @@
- - -
- -
+ +
- -
+ +
- - - - - - - - - - - - - - - - - + + {#if width > 550} + + {:else} + + {/if} + + {/if} diff --git a/gs/src/lib/stores/state.ts b/gs/src/lib/stores/state.ts index a046311a5..95b05be08 100644 --- a/gs/src/lib/stores/state.ts +++ b/gs/src/lib/stores/state.ts @@ -2,7 +2,7 @@ import { writable, type Writable } from 'svelte/store'; import {RunMode} from "$lib/types"; import {PlotBuffer} from "$lib"; -export const detailTabSet: Writable = writable(1); +export const detailTabSet: Writable = writable(0); export const inputSpeed: Writable = writable(50); export const inputEmerg: Writable = writable(-1); export const inputTurn: Writable = writable(RunMode.ShortRun); From 9a7900ee377bb666caea1eb5e956ee3d27018ec0 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Thu, 11 Jul 2024 21:15:02 +0200 Subject: [PATCH 30/41] fix: reshuffle the insulation in the table --- gs/src/lib/panels/VitalsPanel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gs/src/lib/panels/VitalsPanel.svelte b/gs/src/lib/panels/VitalsPanel.svelte index 8065c21a8..c6ed4aa7b 100644 --- a/gs/src/lib/panels/VitalsPanel.svelte +++ b/gs/src/lib/panels/VitalsPanel.svelte @@ -77,7 +77,7 @@ $: tableArr2 = [ ["Insulation", $ins, "Insulation-", $insn], - ["Insulation+", $insp, "IMD Voltage", $imdv], + ["IMD Voltage", $imdv, "Insulation+", $insp], ] const location = storeManager.getStore("Localisation"); From bf2c615de05b1ac3210a5344db09177bdab4fe79 Mon Sep 17 00:00:00 2001 From: Zakrok09 Date: Thu, 11 Jul 2024 22:25:41 +0200 Subject: [PATCH 31/41] feat: everything is a limitable state --- gs/src/lib/components/generic/Store.svelte | 24 +++ gs/src/lib/components/generic/Table.svelte | 5 + gs/src/lib/index.ts | 2 + gs/src/lib/namedDatatypeEnum.ts | 198 ++++++++++++++++++++ gs/src/lib/panels/VitalsPanel.svelte | 82 ++------ gs/src/lib/panels/tabs/BatteriesTab.svelte | 135 +++++-------- gs/src/lib/panels/tabs/LeviTab.svelte | 41 ++-- gs/src/lib/panels/tabs/PneumaticsTab.svelte | 8 +- gs/src/lib/panels/tabs/RunInitTab.svelte | 27 +-- gs/src/lib/types.ts | 1 + gs/src/routes/+layout.svelte | 6 +- 11 files changed, 320 insertions(+), 209 deletions(-) create mode 100644 gs/src/lib/components/generic/Store.svelte create mode 100644 gs/src/lib/namedDatatypeEnum.ts diff --git a/gs/src/lib/components/generic/Store.svelte b/gs/src/lib/components/generic/Store.svelte new file mode 100644 index 000000000..9231be4d2 --- /dev/null +++ b/gs/src/lib/components/generic/Store.svelte @@ -0,0 +1,24 @@ + + +{$store} \ No newline at end of file diff --git a/gs/src/lib/components/generic/Table.svelte b/gs/src/lib/components/generic/Table.svelte index 033ef6a2c..c6343f639 100644 --- a/gs/src/lib/components/generic/Table.svelte +++ b/gs/src/lib/components/generic/Table.svelte @@ -1,4 +1,7 @@ diff --git a/gs/src/lib/panels/tabs/PneumaticsTab.svelte b/gs/src/lib/panels/tabs/PneumaticsTab.svelte index 542978891..897a38951 100644 --- a/gs/src/lib/panels/tabs/PneumaticsTab.svelte +++ b/gs/src/lib/panels/tabs/PneumaticsTab.svelte @@ -1,13 +1,17 @@ diff --git a/gs/src/lib/panels/tabs/RunInitTab.svelte b/gs/src/lib/panels/tabs/RunInitTab.svelte index 29c083711..1092f61e2 100644 --- a/gs/src/lib/panels/tabs/RunInitTab.svelte +++ b/gs/src/lib/panels/tabs/RunInitTab.svelte @@ -8,33 +8,20 @@ SpeedsInput, GrandDataDistributor, Chart } from "$lib"; import {getModalStore, type ModalComponent} from "@skeletonlabs/skeleton"; + import {DatatypeEnum} from "$lib/namedDatatypeEnum"; const storeManager = GrandDataDistributor.getInstance().stores; - const accelX = storeManager.getStore("AccelerationX") - const accelY = storeManager.getStore("AccelerationY") - const accelZ = storeManager.getStore("AccelerationZ") - const gyroX = storeManager.getStore("GyroscopeX") - const gyroY = storeManager.getStore("GyroscopeY") - const gyroZ = storeManager.getStore("GyroscopeZ") - const state = storeManager.getStore("FSMState"); - // const mainpcb_connected = storeManager.getStore(""); - // const propulsion_connected = storeManager.getStore("PropulsionVRefInt"); - // const levitation_connected = storeManager.getStore(""); - // const mainpcb_connected = storeManager.getStore(""); - // const mainpcb_connected = storeManager.getStore(""); - - let tableArr2:any[][]; $: tableArr2 = [ - ["Acceleration X", $accelX], - ["Acceleration Y", $accelY], - ["Acceleration Z", $accelZ], - ["Gyroscope X", $gyroX], - ["Gyroscope Y", $gyroY], - ["Gyroscope Z", $gyroZ], + ["Acceleration X", DatatypeEnum.ACCELERATIONX], + ["Acceleration Y", DatatypeEnum.ACCELERATIONY], + ["Acceleration Z", DatatypeEnum.ACCELERATIONZ], + ["Gyroscope X", DatatypeEnum.GYROSCOPEX], + ["Gyroscope Y", DatatypeEnum.GYROSCOPEY], + ["Gyroscope Z", DatatypeEnum.GYROSCOPEZ], ] const modalStore = getModalStore(); diff --git a/gs/src/lib/types.ts b/gs/src/lib/types.ts index 6057419b7..cc6139c03 100644 --- a/gs/src/lib/types.ts +++ b/gs/src/lib/types.ts @@ -4,6 +4,7 @@ export const NamedCommandValues:NamedCommand[] = ["DefaultCommand", "Heartbeat", /*AUTO GENERATED USING npm run generate:datatypes */ export type NamedDatatype = "YourNewDatatypeName" | "DefaultDatatype" | "PropulsionVoltage" | "PropulsionCurrent" | "PropulsionVRefInt" | "LevitationTemperature" | "AccelerationX" | "AccelerationY" | "AccelerationZ" | "GyroscopeX" | "GyroscopeY" | "GyroscopeZ" | "IMDGeneralInfo" | "IMDIsolationDetails" | "IMDVoltageDetails" | "InsulationNegative" | "InsulationPositive" | "InsulationOriginal" | "DefaultBMSLow" | "DiagnosticBMSLow" | "DefaultBMSHigh" | "DiagnosticBMSHigh" | "BatteryVoltageLow" | "BatteryVoltageHigh" | "TotalBatteryVoltageLow" | "TotalBatteryVoltageHigh" | "BatteryTemperatureLow" | "BatteryTemperatureHigh" | "BatteryBalanceLow" | "BatteryBalanceHigh" | "SingleCellVoltageLow" | "SingleCellTemperatureLow" | "ChargeStateLow" | "ChargeStateHigh" | "BatteryCurrentLow" | "BatteryCurrentHigh" | "BatteryEnergyParamsLow" | "BatteryEnergyParamsHigh" | "BatteryMaxVoltageLow" | "BatteryEstimatedChargeLow" | "BatteryMinTemperatureLow" | "BatteryMaxTemperatureLow" | "BatteryMinBalancingLow" | "BatteryMaxBalancingLow" | "BatteryMinVoltageLow" | "BatteryMinVoltageHigh" | "BatteryMaxVoltageHigh" | "BatteryEstimatedChargeHigh" | "BatteryMinTemperatureHigh" | "BatteryMaxTemperatureHigh" | "BatteryMinBalancingHigh" | "BatteryMaxBalancingHigh" | "BatteryEventLow" | "BatteryEventHigh" | "BrakeTemperature" | "PropulsionSpeed" | "FSMState" | "FSMEvent" | "EndOfTrackTriggered" | "Localisation" | "Velocity" | "Acceleration" | "Direction" | "BrakePressure" | "UnknownCanId" | "Info" | "Presure_VB" | "Average_Temp_VB_Bottom" | "Average_Temp_VB_top" | "Temp_HEMS_1" | "Temp_HEMS_2" | "Temp_HEMS_3" | "Temp_HEMS_4" | "Temp_Motor_1" | "Temp_Motor_2" | "Ambient_presure" | "Ambient_temp" | "Temp_EMS_1" | "Temp_EMS_2" | "Temp_EMS_3" | "Temp_EMS_4" | "SingleCellVoltageHigh_1" | "SingleCellTemperatureHigh_1" | "SingleCellVoltageHigh_2" | "SingleCellTemperatureHigh_2" | "SingleCellVoltageHigh_3" | "SingleCellTemperatureHigh_3" | "SingleCellVoltageHigh_4" | "SingleCellTemperatureHigh_4" | "SingleCellVoltageHigh_5" | "SingleCellTemperatureHigh_5" | "SingleCellVoltageHigh_6" | "SingleCellTemperatureHigh_6" | "SingleCellVoltageHigh_7" | "SingleCellTemperatureHigh_7" | "SingleCellVoltageHigh_8" | "SingleCellTemperatureHigh_8" | "SingleCellVoltageHigh_9" | "SingleCellTemperatureHigh_9" | "SingleCellVoltageHigh_10" | "SingleCellTemperatureHigh_10" | "SingleCellVoltageHigh_11" | "SingleCellTemperatureHigh_11" | "SingleCellVoltageHigh_12" | "SingleCellTemperatureHigh_12" | "SingleCellVoltageHigh_13" | "SingleCellTemperatureHigh_13" | "SingleCellVoltageHigh_14" | "SingleCellTemperatureHigh_14" | "Module1MaxVoltage" | "Module2MaxVoltage" | "Module3MaxVoltage" | "Module4MaxVoltage" | "Module5MaxVoltage" | "Module6MaxVoltage" | "Module7MaxVoltage" | "Module8MaxVoltage" | "Module1MinVoltage" | "Module2MinVoltage" | "Module3MinVoltage" | "Module4MinVoltage" | "Module5MinVoltage" | "Module6MinVoltage" | "Module7MinVoltage" | "Module8MinVoltage" | "Module1MaxTemperature" | "Module2MaxTemperature" | "Module3MaxTemperature" | "Module4MaxTemperature" | "Module5MaxTemperature" | "Module6MaxTemperature" | "Module7MaxTemperature" | "Module8MaxTemperature" | "Module1MinTemperature" | "Module2MinTemperature" | "Module3MinTemperature" | "Module4MinTemperature" | "Module5MinTemperature" | "Module6MinTemperature" | "Module7MinTemperature" | "Module8MinTemperature" | "Module1AvgVoltage" | "Module2AvgVoltage" | "Module3AvgVoltage" | "Module4AvgVoltage" | "Module5AvgVoltage" | "Module6AvgVoltage" | "Module7AvgVoltage" | "Module8AvgVoltage" | "Module1AvgTemperature" | "Module2AvgTemperature" | "Module3AvgTemperature" | "Module4AvgTemperature" | "Module5AvgTemperature" | "Module6AvgTemperature" | "Module7AvgTemperature" | "Module8AvgTemperature" | "ResponseHeartbeat" | "LeviInstruction" | "LowPressureSensor" | "HighPressureSensor" | "levi_hems_gap_a" | "levi_hems_gap_b" | "levi_hems_gap_c" | "levi_hems_gap_d" | "levi_ems_gap_a" | "levi_ems_gap_b" | "levi_ems_gap_c" | "levi_ems_gap_d" | "levi_hems_current_a1" | "levi_hems_current_a2" | "levi_hems_current_b1" | "levi_hems_current_b2" | "levi_hems_current_c1" | "levi_hems_current_c2" | "levi_hems_current_d1" | "levi_hems_current_d2" | "levi_ems_current_ab" | "levi_ems_current_cd" | "levi_hems_airgap" | "levi_hems_pitch" | "levi_hems_roll" | "levi_ems_offset_ab" | "levi_ems_offset_cd" | "levi_hems_power" | "levi_ems_power" | "levi_volt_min" | "levi_volt_max" | "levi_volt_avg" | "BrakingCommDebug" | "BrakingSignalDebug" | "BrakingBoolDebug" | "BrakingRearmDebug" | "PropGPIODebug" | "ReceivedCan" | "SendingCANEvent" +export const NamedDatatypeValues = ["YourNewDatatypeName", "DefaultDatatype", "PropulsionVoltage", "PropulsionCurrent", "PropulsionVRefInt", "LevitationTemperature", "AccelerationX", "AccelerationY", "AccelerationZ", "GyroscopeX", "GyroscopeY", "GyroscopeZ", "IMDGeneralInfo", "IMDIsolationDetails", "IMDVoltageDetails", "InsulationNegative", "InsulationPositive", "InsulationOriginal", "DefaultBMSLow", "DiagnosticBMSLow", "DefaultBMSHigh", "DiagnosticBMSHigh", "BatteryVoltageLow", "BatteryVoltageHigh", "TotalBatteryVoltageLow", "TotalBatteryVoltageHigh", "BatteryTemperatureLow", "BatteryTemperatureHigh", "BatteryBalanceLow", "BatteryBalanceHigh", "SingleCellVoltageLow", "SingleCellTemperatureLow", "ChargeStateLow", "ChargeStateHigh", "BatteryCurrentLow", "BatteryCurrentHigh", "BatteryEnergyParamsLow", "BatteryEnergyParamsHigh", "BatteryMaxVoltageLow", "BatteryEstimatedChargeLow", "BatteryMinTemperatureLow", "BatteryMaxTemperatureLow", "BatteryMinBalancingLow", "BatteryMaxBalancingLow", "BatteryMinVoltageLow", "BatteryMinVoltageHigh", "BatteryMaxVoltageHigh", "BatteryEstimatedChargeHigh", "BatteryMinTemperatureHigh", "BatteryMaxTemperatureHigh", "BatteryMinBalancingHigh", "BatteryMaxBalancingHigh", "BatteryEventLow", "BatteryEventHigh", "BrakeTemperature", "PropulsionSpeed", "FSMState", "FSMEvent", "EndOfTrackTriggered", "Localisation", "Velocity", "Acceleration", "Direction", "BrakePressure", "UnknownCanId", "Info", "Presure_VB", "Average_Temp_VB_Bottom", "Average_Temp_VB_top", "Temp_HEMS_1", "Temp_HEMS_2", "Temp_HEMS_3", "Temp_HEMS_4", "Temp_Motor_1", "Temp_Motor_2", "Ambient_presure", "Ambient_temp", "Temp_EMS_1", "Temp_EMS_2", "Temp_EMS_3", "Temp_EMS_4", "SingleCellVoltageHigh_1", "SingleCellTemperatureHigh_1", "SingleCellVoltageHigh_2", "SingleCellTemperatureHigh_2", "SingleCellVoltageHigh_3", "SingleCellTemperatureHigh_3", "SingleCellVoltageHigh_4", "SingleCellTemperatureHigh_4", "SingleCellVoltageHigh_5", "SingleCellTemperatureHigh_5", "SingleCellVoltageHigh_6", "SingleCellTemperatureHigh_6", "SingleCellVoltageHigh_7", "SingleCellTemperatureHigh_7", "SingleCellVoltageHigh_8", "SingleCellTemperatureHigh_8", "SingleCellVoltageHigh_9", "SingleCellTemperatureHigh_9", "SingleCellVoltageHigh_10", "SingleCellTemperatureHigh_10", "SingleCellVoltageHigh_11", "SingleCellTemperatureHigh_11", "SingleCellVoltageHigh_12", "SingleCellTemperatureHigh_12", "SingleCellVoltageHigh_13", "SingleCellTemperatureHigh_13", "SingleCellVoltageHigh_14", "SingleCellTemperatureHigh_14", "Module1MaxVoltage", "Module2MaxVoltage", "Module3MaxVoltage", "Module4MaxVoltage", "Module5MaxVoltage", "Module6MaxVoltage", "Module7MaxVoltage", "Module8MaxVoltage", "Module1MinVoltage", "Module2MinVoltage", "Module3MinVoltage", "Module4MinVoltage", "Module5MinVoltage", "Module6MinVoltage", "Module7MinVoltage", "Module8MinVoltage", "Module1MaxTemperature", "Module2MaxTemperature", "Module3MaxTemperature", "Module4MaxTemperature", "Module5MaxTemperature", "Module6MaxTemperature", "Module7MaxTemperature", "Module8MaxTemperature", "Module1MinTemperature", "Module2MinTemperature", "Module3MinTemperature", "Module4MinTemperature", "Module5MinTemperature", "Module6MinTemperature", "Module7MinTemperature", "Module8MinTemperature", "Module1AvgVoltage", "Module2AvgVoltage", "Module3AvgVoltage", "Module4AvgVoltage", "Module5AvgVoltage", "Module6AvgVoltage", "Module7AvgVoltage", "Module8AvgVoltage", "Module1AvgTemperature", "Module2AvgTemperature", "Module3AvgTemperature", "Module4AvgTemperature", "Module5AvgTemperature", "Module6AvgTemperature", "Module7AvgTemperature", "Module8AvgTemperature", "ResponseHeartbeat", "LeviInstruction", "LowPressureSensor", "HighPressureSensor", "levi_hems_gap_a", "levi_hems_gap_b", "levi_hems_gap_c", "levi_hems_gap_d", "levi_ems_gap_a", "levi_ems_gap_b", "levi_ems_gap_c", "levi_ems_gap_d", "levi_hems_current_a1", "levi_hems_current_a2", "levi_hems_current_b1", "levi_hems_current_b2", "levi_hems_current_c1", "levi_hems_current_c2", "levi_hems_current_d1", "levi_hems_current_d2", "levi_ems_current_ab", "levi_ems_current_cd", "levi_hems_airgap", "levi_hems_pitch", "levi_hems_roll", "levi_ems_offset_ab", "levi_ems_offset_cd", "levi_hems_power", "levi_ems_power", "levi_volt_min", "levi_volt_max", "levi_volt_avg", "BrakingCommDebug", "BrakingSignalDebug", "BrakingBoolDebug", "BrakingRearmDebug", "PropGPIODebug", "ReceivedCan", "SendingCANEvent"]; // Not touched by auto-gen diff --git a/gs/src/routes/+layout.svelte b/gs/src/routes/+layout.svelte index 47abf41bf..c7f41d9ef 100644 --- a/gs/src/routes/+layout.svelte +++ b/gs/src/routes/+layout.svelte @@ -347,13 +347,13 @@ gdd.stores.registerStore("FSMState", 0); - gdd.start(100); + gdd.start(50); initializeStores(); - onDestroy(() => { + onDestroy(async () => { GrandDataDistributor.getInstance().kill(); - unlisten(); + (await unlisten)(); }) From c569cb34cbcc6f2e9bd870730dc518ad4d8de9f5 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 18:31:14 +0200 Subject: [PATCH 32/41] fix types --- gs/station/src/backend.rs | 12 ++++++++---- gs/station/src/connect/handle_incoming_data.rs | 7 ++++--- gs/station/src/connect/mod.rs | 15 +++++++++------ gs/station/src/connect/queueing.rs | 10 ++++------ gs/station/src/connect/tcp_reader.rs | 6 ++++-- gs/station/src/connect/tcp_writer.rs | 6 ++++-- gs/station/src/levi/mod.rs | 9 ++++++--- gs/station/src/levi/parse_input.rs | 8 ++++---- gs/station/src/levi/read_from_stdout.rs | 7 ++++--- gs/station/src/levi/write_to_stdin.rs | 7 +++++-- gs/station/src/main.rs | 6 ++++++ 11 files changed, 58 insertions(+), 35 deletions(-) diff --git a/gs/station/src/backend.rs b/gs/station/src/backend.rs index 4b7bd2b3d..fd9fe26bc 100644 --- a/gs/station/src/backend.rs +++ b/gs/station/src/backend.rs @@ -7,6 +7,10 @@ use tokio::task::AbortHandle; use crate::api::Message; use crate::Command; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageReceiver; +use crate::MessageSender; // /// Any frontend that interfaces with this backend needs to comply to this trait // pub trait Frontend { @@ -21,10 +25,10 @@ use crate::Command; pub struct Backend { pub server_handle: Option, pub levi_handle: Option<(AbortHandle, AbortHandle)>, - pub message_transmitter: tokio::sync::broadcast::Sender, - pub message_receiver: tokio::sync::broadcast::Receiver, - pub command_transmitter: tokio::sync::broadcast::Sender, - pub command_receiver: tokio::sync::broadcast::Receiver, + pub message_transmitter: MessageSender, + pub message_receiver: MessageReceiver, + pub command_transmitter: CommandSender, + pub command_receiver: CommandReceiver, pub log: Log, pub save_path: PathBuf, } diff --git a/gs/station/src/connect/handle_incoming_data.rs b/gs/station/src/connect/handle_incoming_data.rs index 11bde8d4a..cb408ba9c 100644 --- a/gs/station/src/connect/handle_incoming_data.rs +++ b/gs/station/src/connect/handle_incoming_data.rs @@ -1,19 +1,20 @@ #![allow(clippy::single_match)] -use tokio::sync::broadcast::Sender; use crate::api::Datapoint; use crate::api::Message; use crate::Command; use crate::Datatype; use crate::Info; +use crate::CommandSender; +use crate::MessageSender; use crate::COMMAND_HASH; use crate::DATA_HASH; use crate::EVENTS_HASH; pub async fn handle_incoming_data( data: Datapoint, - msg_sender: Sender, - cmd_sender: Sender, + msg_sender: MessageSender, + cmd_sender: CommandSender, ) -> anyhow::Result<()> { msg_sender.send(Message::Data(data.clone()))?; diff --git a/gs/station/src/connect/mod.rs b/gs/station/src/connect/mod.rs index 8c0ef1897..c7d04152b 100644 --- a/gs/station/src/connect/mod.rs +++ b/gs/station/src/connect/mod.rs @@ -10,11 +10,14 @@ use crate::api::gs_socket; use crate::api::Message; use crate::connect::tcp_reader::get_messages_from_tcp; use crate::connect::tcp_writer::transmit_commands_to_tcp; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageSender; pub async fn connect_main( - message_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_receiver: CommandReceiver, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { // Bind the listener to the address message_transmitter @@ -38,9 +41,9 @@ pub async fn connect_main( async fn process( socket: TcpStream, - message_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_receiver: CommandReceiver, + command_transmitter: CommandSender, ) { let (reader, writer) = socket.into_split(); let transmit = message_transmitter.clone(); diff --git a/gs/station/src/connect/queueing.rs b/gs/station/src/connect/queueing.rs index e3378d510..c15c2524d 100644 --- a/gs/station/src/connect/queueing.rs +++ b/gs/station/src/connect/queueing.rs @@ -1,11 +1,9 @@ use std::collections::VecDeque; -use tokio::sync::broadcast::Sender; - use crate::api::Datapoint; -use crate::api::Message; use crate::connect::handle_incoming_data::handle_incoming_data; -use crate::Command; +use crate::CommandSender; +use crate::MessageSender; /// # Unloads from the buffer and transmits any messages found /// ``` @@ -18,8 +16,8 @@ use crate::Command; /// ``` pub async fn parse( parsing_buffer: &mut VecDeque, - msg_sender: Sender, - cmd_sender: Sender, + msg_sender: MessageSender, + cmd_sender: CommandSender, ) -> anyhow::Result<()> { while let Some(p) = parsing_buffer.front() { if *p == 0xFF { diff --git a/gs/station/src/connect/tcp_reader.rs b/gs/station/src/connect/tcp_reader.rs index a90f2cd4b..afd047066 100644 --- a/gs/station/src/connect/tcp_reader.rs +++ b/gs/station/src/connect/tcp_reader.rs @@ -4,12 +4,14 @@ use tokio::io::AsyncReadExt; use tokio::net::tcp::OwnedReadHalf; use crate::api::Message; +use crate::CommandSender; +use crate::MessageSender; use crate::NETWORK_BUFFER_SIZE; pub async fn get_messages_from_tcp( mut reader: OwnedReadHalf, - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { let mut buffer = [0; { NETWORK_BUFFER_SIZE }]; let mut byte_queue: VecDeque = VecDeque::new(); diff --git a/gs/station/src/connect/tcp_writer.rs b/gs/station/src/connect/tcp_writer.rs index 827f485b0..cb4a66ef1 100644 --- a/gs/station/src/connect/tcp_writer.rs +++ b/gs/station/src/connect/tcp_writer.rs @@ -4,10 +4,12 @@ use tokio::net::tcp::OwnedWriteHalf; use crate::api::Message; use crate::api::Message::Error; use crate::Command; +use crate::CommandReceiver; +use crate::MessageSender; pub async fn transmit_commands_to_tcp( - mut command_receiver: tokio::sync::broadcast::Receiver, - status_transmitter: tokio::sync::broadcast::Sender, + mut command_receiver: CommandReceiver, + status_transmitter: MessageSender, mut writer: OwnedWriteHalf, ) -> anyhow::Result<()> { tokio::spawn(async move { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index 0bd46f7dc..d306c9ffe 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,12 +6,15 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; +use crate::CommandReceiver; +use crate::CommandSender; +use crate::MessageSender; use crate::LEVI_EXEC_PATH; pub fn levi_main( - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, - command_receiver: tokio::sync::broadcast::Receiver, + message_transmitter: MessageSender, + command_transmitter: CommandSender, + command_receiver: CommandReceiver, ) -> anyhow::Result<(AbortHandle, AbortHandle)> { let mut lcmd = tokio::process::Command::new(LEVI_EXEC_PATH); message_transmitter.send(Message::Info(format!("starting levi at {}", LEVI_EXEC_PATH)))?; diff --git a/gs/station/src/levi/parse_input.rs b/gs/station/src/levi/parse_input.rs index 90f30ac66..c6514f051 100644 --- a/gs/station/src/levi/parse_input.rs +++ b/gs/station/src/levi/parse_input.rs @@ -1,14 +1,14 @@ -use tokio::sync::broadcast::Sender; - use crate::api::Datapoint; use crate::api::Message; use crate::Command; +use crate::CommandSender; use crate::Datatype; +use crate::MessageSender; pub fn handle_line_from_levi( line: &String, - msg_send: Sender, - cmd_send: Sender, + msg_send: MessageSender, + cmd_send: CommandSender, ) -> anyhow::Result<()> { let params = line.split(':').collect::>(); diff --git a/gs/station/src/levi/read_from_stdout.rs b/gs/station/src/levi/read_from_stdout.rs index d8226af3a..92a16b9ec 100644 --- a/gs/station/src/levi/read_from_stdout.rs +++ b/gs/station/src/levi/read_from_stdout.rs @@ -2,14 +2,15 @@ use tokio::io::AsyncBufReadExt; use crate::api::Message; use crate::levi::parse_input::handle_line_from_levi; -use crate::Command; +use crate::CommandSender; +use crate::MessageSender; /// # Read from levi child stdout /// reads from the stdout of the levi child process, and sends the messages to the message_transmitter. pub async fn read_from_levi_child_stdout( stdout: tokio::process::ChildStdout, - message_transmitter: tokio::sync::broadcast::Sender, - command_transmitter: tokio::sync::broadcast::Sender, + message_transmitter: MessageSender, + command_transmitter: CommandSender, ) -> anyhow::Result<()> { let mut reader = tokio::io::BufReader::new(stdout); let mut line = String::new(); diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index 203dcd781..cee53be50 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,11 +1,14 @@ use tokio::io::AsyncWriteExt; +use crate::CommandReceiver; +use crate::MessageSender; + /// # Writing to levi's stdin /// when a command is sent to the broadcast channel, it is sent to levi's stdin. pub async fn write_to_levi_child_stdin( mut stdin: tokio::process::ChildStdin, - status_sender: tokio::sync::broadcast::Sender, - mut command_receiver: tokio::sync::broadcast::Receiver, + status_sender: MessageSender, + mut command_receiver: CommandReceiver, ) -> anyhow::Result<()> { loop { let cmd = command_receiver.recv().await?; diff --git a/gs/station/src/main.rs b/gs/station/src/main.rs index 270626eb5..9a8d0a049 100644 --- a/gs/station/src/main.rs +++ b/gs/station/src/main.rs @@ -1,6 +1,7 @@ // Prevents an additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use crate::api::Message; use crate::backend::Backend; #[cfg(feature = "backend")] use crate::frontend::app::tauri_main; @@ -18,6 +19,11 @@ pub mod tui; include!(concat!(env!("OUT_DIR"), "/config.rs")); +pub type CommandSender = tokio::sync::broadcast::Sender; +pub type CommandReceiver = tokio::sync::broadcast::Receiver; +pub type MessageSender = tokio::sync::broadcast::Sender; +pub type MessageReceiver = tokio::sync::broadcast::Receiver; + /// Entry point of the application #[tokio::main] async fn main() { From 96c1123fd76fe2ba21a6de48933fb2cad29b084f Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 19:59:23 +0200 Subject: [PATCH 33/41] send data to levi and frontend --- app/build.rs | 79 +++++++++---------- config/config.toml | 11 +-- gs/station/build.rs | 39 +++++---- gs/station/src/api.rs | 10 ++- gs/station/src/backend.rs | 1 + .../src/connect/handle_incoming_data.rs | 3 +- gs/station/src/connect/mod.rs | 4 +- gs/station/src/data/mod.rs | 1 + gs/station/src/data/process.rs | 37 +++++++++ gs/station/src/frontend/commands.rs | 3 +- gs/station/src/levi/mod.rs | 4 +- gs/station/src/levi/parse_input.rs | 5 +- gs/station/src/levi/write_to_stdin.rs | 47 ++++++++--- gs/station/src/main.rs | 1 + 14 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 gs/station/src/data/mod.rs create mode 100644 gs/station/src/data/process.rs diff --git a/app/build.rs b/app/build.rs index 96e92f188..8b4b1fb9d 100644 --- a/app/build.rs +++ b/app/build.rs @@ -28,7 +28,6 @@ struct GS { ip: [u8; 4], force: bool, port: u16, - // udp_port: u16, buffer_size: usize, timeout: u64, heartbeat: u64, @@ -38,21 +37,21 @@ struct GS { struct Pod { net: NetConfig, internal: InternalConfig, - bms: Bms, + comm: Comm, } #[derive(Debug, Deserialize)] -struct Bms { - lv_ids: Vec, - hv_ids: Vec, +struct Comm { + bms_lv_ids: Vec, + bms_hv_ids: Vec, gfd_ids: Vec, } #[derive(Debug, Deserialize)] struct NetConfig { - ip: [u8; 4], - port: u16, - udp_port: u16, + // ip: [u8; 4], + // port: u16, + // udp_port: u16, mac_addr: [u8; 6], keep_alive: u64, } @@ -70,12 +69,6 @@ pub const COMMANDS_PATH: &str = "../config/commands.toml"; pub const EVENTS_PATH: &str = "../config/events.toml"; fn main() -> Result<()> { - // if cfg!(debug_assertions) { - // env::set_var("DEFMT_LOG", "trace"); - // } else { - // env::set_var("DEFMT_LOG", "off"); - // } - let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("config.rs"); @@ -118,21 +111,23 @@ fn configure_ip(config: &Config) -> String { } fn configure_pod(config: &Config) -> String { + // format!( + // "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.port + // ) + // + &*format!( + // "pub static POD_UDP_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.udp_port + // ) + format!( - "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.port - ) + &*format!( - "pub static POD_UDP_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.udp_port - ) + &*format!( "pub static POD_MAC_ADDRESS: [u8;6] = [{},{},{},{},{},{}];\n", config.pod.net.mac_addr[0], config.pod.net.mac_addr[1], @@ -140,8 +135,8 @@ fn configure_pod(config: &Config) -> String { config.pod.net.mac_addr[3], config.pod.net.mac_addr[4], config.pod.net.mac_addr[5] - ) + &*format!("pub const KEEP_ALIVE: u64 = {};\n", config.pod.net.keep_alive) - + &*format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) + ) + &format!("pub const KEEP_ALIVE: u64 = {};\n", config.pod.net.keep_alive) + + &format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) } fn configure_internal(config: &Config) -> String { @@ -150,20 +145,20 @@ fn configure_internal(config: &Config) -> String { + &*format!("pub const CAN_QUEUE_SIZE: usize = {};\n", config.pod.internal.can_queue_size) + &*format!( "pub const LV_IDS: [u16;{}] = [{}];\n", - config.pod.bms.lv_ids.len(), - config.pod.bms.lv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config.pod.comm.bms_lv_ids.len(), + config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") ) + &*format!( "pub const HV_IDS: [u16;{}] = [{}];\n", - config.pod.bms.hv_ids.len(), - config.pod.bms.hv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config.pod.comm.bms_hv_ids.len(), + config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") ) + &*format!( "pub const GFD_IDS: [u16;{}] = [{}];\n", - config.pod.bms.gfd_ids.len(), + config.pod.comm.gfd_ids.len(), config .pod - .bms + .comm .gfd_ids .iter() .map(|x| x.to_string()) @@ -172,14 +167,14 @@ fn configure_internal(config: &Config) -> String { ) + &*format!( "pub const BATTERY_GFD_IDS: [u16;{}] = [{},{},{}];\n", - config.pod.bms.lv_ids.len() - + config.pod.bms.hv_ids.len() - + config.pod.bms.gfd_ids.len(), - config.pod.bms.lv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), - config.pod.bms.hv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), + config.pod.comm.bms_lv_ids.len() + + config.pod.comm.bms_hv_ids.len() + + config.pod.comm.gfd_ids.len(), + config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), + config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), config .pod - .bms + .comm .gfd_ids .iter() .map(|x| x.to_string()) diff --git a/config/config.toml b/config/config.toml index f5cb222bc..e26934931 100644 --- a/config/config.toml +++ b/config/config.toml @@ -2,7 +2,6 @@ ip = [192,168,1,3] force = true port = 6942 -udp_port = 6943 buffer_size = 1460 # this is the MAXIMUM size of messages transmitted, in bytes. timeout = 1500 # this is the timeout for the socket, in milliseconds. heartbeat = 1000 # how often to send a keep_alive heartbeat, in milliseconds. @@ -14,9 +13,6 @@ shortcut_channel = "shortcut_channel" levi_exec_path = "C:/Users/kikoa/RustroverProjects/Helios_III/gs/station/Levi/windows-x86_64-debug/PmpGettingStartedCs.exe" [pod.net] -ip = [192, 168, 0, 199] -port = 17034 -udp_port = 17035 mac_addr = [0x00, 0x1e, 0x67, 0x4c, 0x5c, 0x3e] keep_alive = 1000 # keep alive interval, in milliseconds. @@ -25,11 +21,12 @@ event_queue_size = 128 data_queue_size = 256 can_queue_size = 128 -[pod.bms] -lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1BC, 0x1DC, 0x1FC, 0x29C,0x221] -hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] +[pod.comm] +bms_lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1BC, 0x1DC, 0x1FC, 0x29C,0x221] +bms_hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] +levi_requested_data = ["Localisation", "PropulsionCurrent"] [[Info]] label = "ServerStarted" diff --git a/gs/station/build.rs b/gs/station/build.rs index a528817c8..59da158bf 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -22,12 +22,18 @@ struct Config { #[derive(Debug, Deserialize)] struct Pod { - net: NetConfig, + // net: NetConfig, + comm: CommConfig, } +// #[derive(Debug, Deserialize)] +// struct NetConfig { +// // ip: [u8; 4], +// // port: u16, +// } + #[derive(Debug, Deserialize)] -struct NetConfig { - ip: [u8; 4], - port: u16, +struct CommConfig { + levi_requested_data: Vec, } #[derive(Debug, Deserialize)] @@ -35,7 +41,6 @@ struct GS { ip: [u8; 4], force: bool, port: u16, - // udp_port: u16, buffer_size: usize, timeout: u64, heartbeat: u64, @@ -86,20 +91,22 @@ fn main() -> Result<()> { fn configure_gs(config: &Config) -> String { // format!("pub fn gs_socket() -> std::net::SocketAddr {{ std::net::SocketAddr::new(std::net::IpAddr::from([{},{},{},{}]),{}) }}\n", config.gs.ip[0], config.gs.ip[1], config.gs.ip[2], config.gs.ip[3], config.gs.port) - format!( - "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", - config.pod.net.ip[0], - config.pod.net.ip[1], - config.pod.net.ip[2], - config.pod.net.ip[3], - config.pod.net.port - ) + &*format!("pub const NETWORK_BUFFER_SIZE: usize = {};\n", config.gs.buffer_size) - + &*format!("pub const IP_TIMEOUT: u64 = {};\n", config.gs.timeout) - + &*format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) - + &*format!( + // format!( + // "pub static POD_IP_ADDRESS: ([u8;4],u16) = ([{},{},{},{}],{});\n", + // config.pod.net.ip[0], + // config.pod.net.ip[1], + // config.pod.net.ip[2], + // config.pod.net.ip[3], + // config.pod.net.port + // ) + &* + format!("pub const NETWORK_BUFFER_SIZE: usize = {};\n", config.gs.buffer_size) + + &format!("pub const IP_TIMEOUT: u64 = {};\n", config.gs.timeout) + + &format!("pub const HEARTBEAT: u64 = {};\n", config.gs.heartbeat) + + &format!( "pub const LEVI_EXEC_PATH: &str = \"{}\";\n", config.gs.levi_exec_path.to_str().unwrap() ) + + &format!("\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", config.pod.comm.levi_requested_data.len(), config.pod.comm.levi_requested_data.iter().map(|x| format!("Datatype::{x}, ")).collect::()) } fn configure_channels(config: &Config) -> String { diff --git a/gs/station/src/api.rs b/gs/station/src/api.rs index 18e097f4d..f7de956ca 100644 --- a/gs/station/src/api.rs +++ b/gs/station/src/api.rs @@ -36,9 +36,17 @@ impl Datapoint { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ProcessedData { + pub datatype: Datatype, + pub value: f64, + pub timestamp: u64, + pub style: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum Message { - Data(Datapoint), + Data(ProcessedData), Status(Info), Info(String), Warning(String), diff --git a/gs/station/src/backend.rs b/gs/station/src/backend.rs index fd9fe26bc..57f3e5c13 100644 --- a/gs/station/src/backend.rs +++ b/gs/station/src/backend.rs @@ -91,6 +91,7 @@ impl Backend { self.message_transmitter.clone(), self.command_transmitter.clone(), self.command_receiver.resubscribe(), + self.message_receiver.resubscribe(), ) { Ok(lh) => { self.levi_handle = Some(lh); diff --git a/gs/station/src/connect/handle_incoming_data.rs b/gs/station/src/connect/handle_incoming_data.rs index cb408ba9c..43d3cf79f 100644 --- a/gs/station/src/connect/handle_incoming_data.rs +++ b/gs/station/src/connect/handle_incoming_data.rs @@ -2,6 +2,7 @@ use crate::api::Datapoint; use crate::api::Message; +use crate::data::process::process; use crate::Command; use crate::Datatype; use crate::Info; @@ -16,7 +17,7 @@ pub async fn handle_incoming_data( msg_sender: MessageSender, cmd_sender: CommandSender, ) -> anyhow::Result<()> { - msg_sender.send(Message::Data(data.clone()))?; + msg_sender.send(Message::Data(process(&data)))?; match data.datatype { Datatype::LeviInstruction => { diff --git a/gs/station/src/connect/mod.rs b/gs/station/src/connect/mod.rs index c7d04152b..d04d4946e 100644 --- a/gs/station/src/connect/mod.rs +++ b/gs/station/src/connect/mod.rs @@ -29,7 +29,7 @@ pub async fn connect_main( // The second item contains the IP and port of the new connection. let (socket, client_addr) = listener.accept().await?; message_transmitter.send(Message::Info(format!("New connection from: {}", client_addr)))?; - process( + process_stream( socket, message_transmitter.clone(), command_receiver.resubscribe(), @@ -39,7 +39,7 @@ pub async fn connect_main( } } -async fn process( +async fn process_stream( socket: TcpStream, message_transmitter: MessageSender, command_receiver: CommandReceiver, diff --git a/gs/station/src/data/mod.rs b/gs/station/src/data/mod.rs new file mode 100644 index 000000000..80fe812c8 --- /dev/null +++ b/gs/station/src/data/mod.rs @@ -0,0 +1 @@ +pub mod process; diff --git a/gs/station/src/data/process.rs b/gs/station/src/data/process.rs new file mode 100644 index 000000000..989e94c20 --- /dev/null +++ b/gs/station/src/data/process.rs @@ -0,0 +1,37 @@ +use crate::api::Datapoint; +use crate::api::ProcessedData; +use crate::Datatype; +use crate::ValueCheckResult; + +/// Preprocessing data from the pod before sending to the frontend +pub fn process(datapoint: &Datapoint) -> ProcessedData { + let style = match datapoint.datatype.check_bounds(datapoint.value) { + ValueCheckResult::Fine => "".to_string(), + ValueCheckResult::Warn => "warning".to_string(), + ValueCheckResult::Error => "error".to_string(), + ValueCheckResult::BrakeNow => "critical".to_string(), + }; + let x = datapoint.value as f64; + let value = match datapoint.datatype { + Datatype::BatteryEstimatedChargeHigh | Datatype::BatteryEstimatedChargeLow => x / 100.0, + Datatype::TotalBatteryVoltageHigh | Datatype::TotalBatteryVoltageLow => x / 100.0 - 2.0, + Datatype::BatteryCurrentLow => x / 10.0 + 150.0, + Datatype::BatteryCurrentHigh => x / 10.0 + 10.0, + + Datatype::BrakingCommDebug => x * 3.3 / 65535.0, + + Datatype::IMDVoltageDetails => { + if datapoint.value == 65535 { + 0.0 + } else { + x * 0.005 * 110.0 / 250.0 + } + }, + + Datatype::Localisation => x * 1.6, + + _ => x, + }; + + ProcessedData { datatype: datapoint.datatype, value, timestamp: datapoint.timestamp, style } +} diff --git a/gs/station/src/frontend/commands.rs b/gs/station/src/frontend/commands.rs index ba48ddb99..7bcc06a6b 100644 --- a/gs/station/src/frontend/commands.rs +++ b/gs/station/src/frontend/commands.rs @@ -6,6 +6,7 @@ use tauri::State; use crate::api::Datapoint; use crate::api::Message; +use crate::api::ProcessedData; use crate::backend::Backend; use crate::frontend::BackendState; use crate::frontend::BACKEND; @@ -48,7 +49,7 @@ pub fn generate_test_data() -> Vec { #[macro_export] #[allow(unused)] #[tauri::command] -pub fn unload_buffer(state: State) -> Vec { +pub fn unload_buffer(state: State) -> Vec { let mut data_buffer = state.data_buffer.lock().unwrap(); let mut datapoints = Vec::new(); for msg in data_buffer.iter() { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index d306c9ffe..51630bdd5 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,7 +6,7 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; -use crate::CommandReceiver; +use crate::{CommandReceiver, MessageReceiver}; use crate::CommandSender; use crate::MessageSender; use crate::LEVI_EXEC_PATH; @@ -15,6 +15,7 @@ pub fn levi_main( message_transmitter: MessageSender, command_transmitter: CommandSender, command_receiver: CommandReceiver, + message_receiver: MessageReceiver, ) -> anyhow::Result<(AbortHandle, AbortHandle)> { let mut lcmd = tokio::process::Command::new(LEVI_EXEC_PATH); message_transmitter.send(Message::Info(format!("starting levi at {}", LEVI_EXEC_PATH)))?; @@ -33,6 +34,7 @@ pub fn levi_main( stdin, transmitter.clone(), command_receiver, + message_receiver, ) .await { diff --git a/gs/station/src/levi/parse_input.rs b/gs/station/src/levi/parse_input.rs index c6514f051..a7066cba8 100644 --- a/gs/station/src/levi/parse_input.rs +++ b/gs/station/src/levi/parse_input.rs @@ -1,5 +1,6 @@ use crate::api::Datapoint; use crate::api::Message; +use crate::data::process::process; use crate::Command; use crate::CommandSender; use crate::Datatype; @@ -29,11 +30,11 @@ pub fn handle_line_from_levi( }, "DATA" if params.len() > 2 => { if let Ok(x) = params[2].trim().replace(',', ".").parse::() { - msg_send.send(Message::Data(Datapoint::new( + msg_send.send(Message::Data(process(&Datapoint::new( Datatype::from_str(params[1]), x.to_bits(), chrono::offset::Local::now().timestamp() as u64, - )))?; + ))))?; } else { msg_send.send(Message::Warning(format!( "Levi data not a number: {:?}", diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index cee53be50..efcc0f06f 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,6 +1,9 @@ use tokio::io::AsyncWriteExt; +use tokio::sync::broadcast::error::TryRecvError; +use crate::LEVI_REQUESTED_DATA; -use crate::CommandReceiver; +use crate::{CommandReceiver, MessageReceiver}; +use crate::api::Message; use crate::MessageSender; /// # Writing to levi's stdin @@ -9,15 +12,41 @@ pub async fn write_to_levi_child_stdin( mut stdin: tokio::process::ChildStdin, status_sender: MessageSender, mut command_receiver: CommandReceiver, + mut message_receiver: MessageReceiver, ) -> anyhow::Result<()> { loop { - let cmd = command_receiver.recv().await?; - stdin.write_all(format!("{}\n", cmd.to_str()).as_bytes()).await?; - stdin.flush().await?; - status_sender.send(crate::api::Message::Info(format!( - "wrote command {:?} to levi stdin: <{:?}>", - cmd, - cmd.to_str().as_bytes() - )))?; + match command_receiver.try_recv() { + Ok(cmd) => { + stdin.write_all(format!("{}\n", cmd.to_str()).as_bytes()).await?; + stdin.flush().await?; + status_sender.send(Message::Info(format!( + "wrote command {:?} to levi stdin: <{:?}>", + cmd, + cmd.to_str().as_bytes() + )))?; + } + Err(TryRecvError::Closed) => { + status_sender.send(Message::Error("command_receiver channel closed".into()))?; + break; + } + _ => {} + } + match message_receiver.try_recv() { + Ok(msg) => match msg { + Message::Data(d) if LEVI_REQUESTED_DATA.contains(&d.datatype) => { + + stdin.write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()).await?; + stdin.flush().await?; + + } + _ => {} + } + Err(TryRecvError::Closed) => { + status_sender.send(Message::Error("message_receiver channel closed".into()))?; + break; + } + _ => {} + } } + Ok(()) } diff --git a/gs/station/src/main.rs b/gs/station/src/main.rs index 9a8d0a049..617f9aabc 100644 --- a/gs/station/src/main.rs +++ b/gs/station/src/main.rs @@ -11,6 +11,7 @@ use crate::tui::tui_main; pub mod api; mod backend; pub mod connect; +mod data; #[cfg(feature = "backend")] mod frontend; mod levi; From 6614f78585dd9c31d6286e778526ecb1868ecb59 Mon Sep 17 00:00:00 2001 From: Sjoerd Homburg Date: Fri, 12 Jul 2024 19:15:24 +0200 Subject: [PATCH 34/41] Added commands for retrievng data and added localization integration --- ...ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx | Bin 0 -> 53684 bytes .../Levi/.vs/PmpGettingStartedCs/v17/.suo | Bin 57856 -> 57856 bytes gs/station/Levi/Levitation.cs | 89 +++++++++++++++++- .../obj/x64/debug/PmpGettingStartedCs.exe | Bin 36352 -> 38400 bytes .../obj/x64/debug/PmpGettingStartedCs.pdb | Bin 77312 -> 81408 bytes .../PmpGettingStartedCs.exe | Bin 36352 -> 38400 bytes .../PmpGettingStartedCs.pdb | Bin 77312 -> 81408 bytes 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx diff --git a/gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx b/gs/station/Levi/.vs/PmpGettingStartedCs/FileContentIndex/ec1b092a-754e-46c9-8782-d52c5b582b3e.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..1922797331865b5240d643b74e5cd560b641caf2 GIT binary patch literal 53684 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 literal 0 HcmV?d00001 diff --git a/gs/station/Levi/.vs/PmpGettingStartedCs/v17/.suo b/gs/station/Levi/.vs/PmpGettingStartedCs/v17/.suo index 121a8524db0e3b7c322e0b968e8e70a481241dc6..d52ec51ad16fb21929b4e18259ac2f43949d50b2 100644 GIT binary patch delta 1993 zcmcIlYfRf!6z~1_U!d?WE#cvKjMY#&rUjvt0%~N?I-s!8D&pq5VKtgjlF2H4_kz6?h7QaKtg8J#^{X9#7)V}qOP6;En&&xn5YNBMEM~C@%91{^L#A-wXA_|d#Aloz???LFp zd^3(}1Z76c()br*{eUAeBO3)dh_#4R#5%)) zA-2FeSX#wj-X1>JX^`@iE9R*8m1$?`jc}B1O<*5&W@&S7T8z6J}5l&XNuv zJeoQcqukg^=>kb<=TrgGF4-p3sbsNXNkppHh60h_RM~{P5nfshk4q2*BrB8d!wD@; zw3L>rj+$Gs9Ti{mn~61oM7>0f8?oV@lmje~{}0~MqBpt_dG#Jv!@KmGn2J$LQj11H z#nAzh5fTqsm=MUYh_pWUoW?awI~j|I?xH;`7gLEA0v~%A(?M6(n{Y{IhKsXF-1gz; z?7yx6)4Jkir@)szEG`~zD&46lSu}*|y6z~`RWR-2LuWne=9f#Z>V8VQq13O2v&~Zd zWaYuiz;8$Q^!~mc#wL?D9OQ|<(hy$JidMd+Sk#rQVqo9L#>-RC}F>1Ni7-aqjMM&_a z!?pL*-~)RBR5)57yCEI4J_-I9(LjQufVBg&>tXKQG?=_txU7|Ci&RCWW4SIH7|#A+bekf z#uo+tLjCcGky+$DFZHm%&hZ3zwa>(F^d*XGo#vAKQkOA5*Hm2Clxr$1Y07mPOSk5_ z%qEx9+~_Rc+GLKF9f#hbYIyEQ7N`d=z+3$``1x2C7<@nQs-v}xI}evIes$nkIUG5* znasF)@M-z7{bO%Wr&#EpSR~ySPlV@IUpVYMcMlKCDJD1gq6E5b%S4YPJ$_bj9zNrB zTO}hhg^0*8g2!!_h$&tvTyWRNkdSEUYPW=6C;BKNQt1#fg9xI)NV^>D!~Ncqd*v>f zh3`Ar{ePO2YtIBFf0c0C<@DRcJ6$XPlo&p_bId#8)9&u#%v{;}=tpHHb;jkSUq9)d z@0;8En!Pq~V)FVf`+hz)QO>yV)UD|E4C8`-hQXX82QnS$vBp9}!BWS;###P1yGp?} delta 1996 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

X7l2XUESlm9DTN&FK!O8KAWsFsfb&O4nt&HuA_y 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

>4e}J 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 delta 12853 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 Ia#V=VUwo3xz0hffq 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

X7l2XUESlm9DTN&FK!O8KAWsFsfb&O4nt&HuA_y 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

>4e}J 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 delta 12853 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 Ia#V= Date: Fri, 12 Jul 2024 20:04:47 +0200 Subject: [PATCH 35/41] levi datatypes --- config/datatypes.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/datatypes.toml b/config/datatypes.toml index 0c8a16f78..6ec16a9f1 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -847,6 +847,14 @@ id = 0x156 name = "levi_volt_avg" id = 0x157 +[[Datatype]] +name = "levi_location" +id = 0x117 + +[[Datatype]] +name = "levi_speed" +id = 0x116 + [[Datatype]] name = "BrakingCommDebug" id = 0x158 From 5c4174670f42be3f4cd366c21b4f318b66ada466 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Fri, 12 Jul 2024 20:20:49 +0200 Subject: [PATCH 36/41] levi data existence checks --- config/config.toml | 2 +- gs/station/build.rs | 28 ++++++++++++++++++++++++-- gs/station/src/levi/mod.rs | 3 ++- gs/station/src/levi/write_to_stdin.rs | 29 ++++++++++++++------------- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/config/config.toml b/config/config.toml index e26934931..6ca870d88 100644 --- a/config/config.toml +++ b/config/config.toml @@ -26,7 +26,7 @@ bms_lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1 bms_hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] -levi_requested_data = ["Localisation", "PropulsionCurrent"] +levi_requested_data = ["Localisation", "PropulsionCurrent", "AccelerationX", "AccelerationY", "AccelerationZ"] [[Info]] label = "ServerStarted" diff --git a/gs/station/build.rs b/gs/station/build.rs index 59da158bf..1d27891e8 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -71,11 +71,13 @@ fn main() -> Result<()> { content.push_str(&configure_gs(&config)); content.push_str(&configure_gs_ip(config.gs.ip, config.gs.port, config.gs.force)?); - content.push_str(&generate_datatypes(DATATYPES_PATH, true)?); + let dt = generate_datatypes(DATATYPES_PATH, true)?; + content.push_str(&dt); content.push_str(&generate_commands(COMMANDS_PATH, false)?); content.push_str(&generate_events(EVENTS_PATH, false)?); content.push_str(&configure_channels(&config)); content.push_str(&goose_utils::info::generate_info(CONFIG_PATH, true)?); + content.push_str(&levi_req_data(&config, dt)?); fs::write(dest_path.clone(), content).unwrap_or_else(|_| { panic!("Couldn't write to {}! Build failed.", dest_path.to_str().unwrap()); @@ -89,6 +91,29 @@ fn main() -> Result<()> { Ok(()) } +fn levi_req_data(config: &Config, dt: String) -> Result { + for data in config.pod.comm.levi_requested_data.iter() { + if !dt.contains(data) { + return Err(anyhow::anyhow!( + "Data type {:?} not found in datatypes.toml + Check that the (case-sensitive) spelling is correct, and that the datatype exists.", + data + )); + } + } + Ok(format!( + "\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", + config.pod.comm.levi_requested_data.len(), + config + .pod + .comm + .levi_requested_data + .iter() + .map(|x| format!("Datatype::{x}, ")) + .collect::() + )) +} + fn configure_gs(config: &Config) -> String { // format!("pub fn gs_socket() -> std::net::SocketAddr {{ std::net::SocketAddr::new(std::net::IpAddr::from([{},{},{},{}]),{}) }}\n", config.gs.ip[0], config.gs.ip[1], config.gs.ip[2], config.gs.ip[3], config.gs.port) // format!( @@ -106,7 +131,6 @@ fn configure_gs(config: &Config) -> String { "pub const LEVI_EXEC_PATH: &str = \"{}\";\n", config.gs.levi_exec_path.to_str().unwrap() ) - + &format!("\npub const LEVI_REQUESTED_DATA: [Datatype; {}] = [{}];\n", config.pod.comm.levi_requested_data.len(), config.pod.comm.levi_requested_data.iter().map(|x| format!("Datatype::{x}, ")).collect::()) } fn configure_channels(config: &Config) -> String { diff --git a/gs/station/src/levi/mod.rs b/gs/station/src/levi/mod.rs index 51630bdd5..a3cd82e10 100644 --- a/gs/station/src/levi/mod.rs +++ b/gs/station/src/levi/mod.rs @@ -6,8 +6,9 @@ use anyhow::anyhow; use tokio::task::AbortHandle; use crate::api::Message; -use crate::{CommandReceiver, MessageReceiver}; +use crate::CommandReceiver; use crate::CommandSender; +use crate::MessageReceiver; use crate::MessageSender; use crate::LEVI_EXEC_PATH; diff --git a/gs/station/src/levi/write_to_stdin.rs b/gs/station/src/levi/write_to_stdin.rs index efcc0f06f..547ee7756 100644 --- a/gs/station/src/levi/write_to_stdin.rs +++ b/gs/station/src/levi/write_to_stdin.rs @@ -1,10 +1,11 @@ use tokio::io::AsyncWriteExt; use tokio::sync::broadcast::error::TryRecvError; -use crate::LEVI_REQUESTED_DATA; -use crate::{CommandReceiver, MessageReceiver}; use crate::api::Message; +use crate::CommandReceiver; +use crate::MessageReceiver; use crate::MessageSender; +use crate::LEVI_REQUESTED_DATA; /// # Writing to levi's stdin /// when a command is sent to the broadcast channel, it is sent to levi's stdin. @@ -24,28 +25,28 @@ pub async fn write_to_levi_child_stdin( cmd, cmd.to_str().as_bytes() )))?; - } + }, Err(TryRecvError::Closed) => { status_sender.send(Message::Error("command_receiver channel closed".into()))?; break; - } - _ => {} + }, + _ => {}, } match message_receiver.try_recv() { Ok(msg) => match msg { Message::Data(d) if LEVI_REQUESTED_DATA.contains(&d.datatype) => { - - stdin.write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()).await?; - stdin.flush().await?; - - } - _ => {} - } + stdin + .write_all(format!("data:{:?}:{}\n", d.datatype, d.value).as_bytes()) + .await?; + stdin.flush().await?; + }, + _ => {}, + }, Err(TryRecvError::Closed) => { status_sender.send(Message::Error("message_receiver channel closed".into()))?; break; - } - _ => {} + }, + _ => {}, } } Ok(()) From 33c65a8336171696f8390e00ffa664429a393452 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 15:17:51 +0200 Subject: [PATCH 37/41] heartbeats in config --- app/build.rs | 19 ++++++++++++++++++- config/config.toml | 7 +++++++ config/datatypes.toml | 8 ++++++++ util/src/shared/routes.rs | 1 + 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/build.rs b/app/build.rs index 8b4b1fb9d..6ce7e24b5 100644 --- a/app/build.rs +++ b/app/build.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] +use anyhow::anyhow; extern crate serde; @@ -10,6 +11,7 @@ use anyhow::Result; use goose_utils::check_ids; use goose_utils::ip::configure_gs_ip; use serde::Deserialize; +use std::collections::BTreeMap; /* BUILD CONFIGURATION @@ -38,6 +40,7 @@ struct Pod { net: NetConfig, internal: InternalConfig, comm: Comm, + heartbeats: BTreeMap, } #[derive(Debug, Deserialize)] @@ -84,9 +87,11 @@ fn main() -> Result<()> { content.push_str(&configure_pod(&config)); content.push_str(&configure_internal(&config)); content.push_str(&goose_utils::commands::generate_commands(COMMANDS_PATH, true)?); - content.push_str(&goose_utils::datatypes::generate_datatypes(DATATYPES_PATH, false)?); + let dt = goose_utils::datatypes::generate_datatypes(DATATYPES_PATH, false)?; + content.push_str(&dt); content.push_str(&goose_utils::events::generate_events(EVENTS_PATH, true)?); content.push_str(&goose_utils::info::generate_info(CONFIG_PATH, false)?); + content.push_str(&configure_heartbeats(&config, &dt)?); // content.push_str(&*can::main(&id_list)); fs::write(dest_path.clone(), content).unwrap_or_else(|e| { @@ -105,6 +110,18 @@ fn main() -> Result<()> { Ok(()) } +fn configure_heartbeats(config: &Config, dt: &str) -> Result { + let mut x = format!("\npub const HEARTBEATS_LEN: usize = {};\npub const HEARTBEATS: [(Datatype, u64); HEARTBEATS_LEN] = [", config.pod.heartbeats.len()); + for (key, val) in &config.pod.heartbeats { + if !dt.contains(key) { + return Err(anyhow!("\n\nFound heartbeat for non-existing datatype: {:?}\nYou can only add a timeout for datatypes present in /config/datatypes.toml (check your spelling)\n", key)); + } + x.push_str(&format!("(Datatype::{}, {}), ", key, val)); + } + x.push_str("];\n"); + Ok(x) +} + fn configure_ip(config: &Config) -> String { format!("pub const NETWORK_BUFFER_SIZE: usize = {};\n", config.gs.buffer_size) + &*format!("pub const IP_TIMEOUT: u64 = {};\n", config.gs.timeout) diff --git a/config/config.toml b/config/config.toml index 6ca870d88..4655d003f 100644 --- a/config/config.toml +++ b/config/config.toml @@ -28,6 +28,13 @@ gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] levi_requested_data = ["Localisation", "PropulsionCurrent", "AccelerationX", "AccelerationY", "AccelerationZ"] +[pod.heartbeats] +# syntax: +# = +LocalisationHeartbeat = 1000 +# SensorHubHeartbeat = 1000 + + [[Info]] label = "ServerStarted" colour = "Green" diff --git a/config/datatypes.toml b/config/datatypes.toml index 6ec16a9f1..0fba9e81a 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -919,3 +919,11 @@ id = 0x16c name = "NextPositionDebug" id = 0x16d +[[Datatype]] +name = "LocalisationHeartbeat" +id = 0x16e + +[[Datatype]] +name = "SensorHubHeartbeat" +id = 0x16f + diff --git a/util/src/shared/routes.rs b/util/src/shared/routes.rs index 333505a0d..e72d8558c 100644 --- a/util/src/shared/routes.rs +++ b/util/src/shared/routes.rs @@ -124,6 +124,7 @@ impl defmt::Format for Route { } } +#[allow(dead_code)] impl Route { pub fn speeds_from(&mut self, val: u64) { self.speeds = LocationSpeedMap::from(val); } From e6f5f96df7934cfebf8a7d944774afa313b01ed5 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 18:07:33 +0200 Subject: [PATCH 38/41] initial heartbeats implementation --- app/build.rs | 38 ++++++++++++-- app/src/core/data/mod.rs | 45 +++++++++++++++- config/config.toml | 5 ++ config/datatypes.toml | 4 ++ planning/graveyard/lamp.rs | 104 +++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 planning/graveyard/lamp.rs diff --git a/app/build.rs b/app/build.rs index 6ce7e24b5..113de2e02 100644 --- a/app/build.rs +++ b/app/build.rs @@ -3,6 +3,7 @@ use anyhow::anyhow; extern crate serde; +use std::collections::BTreeMap; use std::env; use std::fs; use std::path::Path; @@ -11,7 +12,6 @@ use anyhow::Result; use goose_utils::check_ids; use goose_utils::ip::configure_gs_ip; use serde::Deserialize; -use std::collections::BTreeMap; /* BUILD CONFIGURATION @@ -163,12 +163,26 @@ fn configure_internal(config: &Config) -> String { + &*format!( "pub const LV_IDS: [u16;{}] = [{}];\n", config.pod.comm.bms_lv_ids.len(), - config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config + .pod + .comm + .bms_lv_ids + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ") ) + &*format!( "pub const HV_IDS: [u16;{}] = [{}];\n", config.pod.comm.bms_hv_ids.len(), - config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", ") + config + .pod + .comm + .bms_hv_ids + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ") ) + &*format!( "pub const GFD_IDS: [u16;{}] = [{}];\n", @@ -187,8 +201,22 @@ fn configure_internal(config: &Config) -> String { config.pod.comm.bms_lv_ids.len() + config.pod.comm.bms_hv_ids.len() + config.pod.comm.gfd_ids.len(), - config.pod.comm.bms_lv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), - config.pod.comm.bms_hv_ids.iter().map(|x| x.to_string()).collect::>().join(", "), + config + .pod + .comm + .bms_lv_ids + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", "), + config + .pod + .comm + .bms_hv_ids + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", "), config .pod .comm diff --git a/app/src/core/data/mod.rs b/app/src/core/data/mod.rs index 5f749bc69..f03e4e186 100644 --- a/app/src/core/data/mod.rs +++ b/app/src/core/data/mod.rs @@ -5,13 +5,19 @@ mod batteries; mod sources; -use crate::DataReceiver; + +use embassy_time::{Duration, Instant}; +use heapless::Vec; +use crate::{DataReceiver, HEARTBEATS, HEARTBEATS_LEN, Info}; use crate::DataSender; use crate::Datapoint; use crate::Datatype; use crate::Event; use crate::EventSender; use crate::ValueCheckResult; + +type HB = Vec<(Datatype, Duration, Instant), { HEARTBEATS_LEN }>; + /// ## Individual handling of datapoints /// A lot of the subsystems on the pod use their own "encoding" for data. /// In order to make a reasonable matching between semantic meaning of @@ -22,6 +28,9 @@ pub async fn data_middle_step( outgoing: DataSender, event_sender: EventSender, ) -> ! { + let mut hb = HB::new(); + let hb_dt = HEARTBEATS.iter().map(|x| x.0).collect::>(); + loop { let data = incoming.receive().await; @@ -39,12 +48,44 @@ pub async fn data_middle_step( outgoing.send(value_critical(data.datatype, data.value)).await; }, } - // 2. check specific data types + // 2. check heartbeats + let mut seen = !hb_dt.contains(&data.datatype); + + for (dt, out, last) in hb.iter_mut() { + if !seen && *dt == data.datatype { + seen = true; + *last = Instant::now(); + } else { + if last.elapsed() > *out { + event_sender.send(Event::EmergencyBraking).await; + outgoing.send(Datapoint::new(Datatype::HeartbeatExpired, dt.to_id() as u64, Instant::now().as_ticks())).await; + } + } + } + if !seen { + match hb.push((data.datatype, timeout(data.datatype), Instant::now())) { + Ok(_) => {}, + Err(_) => outgoing.send(Datapoint::new(Datatype::Info, Info::lamp_error_unreachable.to_idx(), Instant::now().as_ticks())).await, + } + } + + // 3. check for special cases outgoing.send(data).await; } } +fn timeout(dt: Datatype) -> Duration { + for (d, t) in HEARTBEATS { + if d == dt { + return Duration::from_millis(t); + } + } + Duration::from_millis(0) // This is unreachable, + // but as to not panic we return zero timeout. + // Since this will always be expired, it will always cause emergency braking +} + fn value_warning(dt: Datatype, v: u64) -> Datapoint { Datapoint::new(Datatype::ValueWarning, dt.to_id() as u64, v) } diff --git a/config/config.toml b/config/config.toml index 4655d003f..8a1042ea6 100644 --- a/config/config.toml +++ b/config/config.toml @@ -144,3 +144,8 @@ colour = "green" label = "PodNotLevitating" colour = "red" +[[Info]] +label = "lamp_error_unreachable" +colour = "magenta" + + diff --git a/config/datatypes.toml b/config/datatypes.toml index 0fba9e81a..c1d391091 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -927,3 +927,7 @@ id = 0x16e name = "SensorHubHeartbeat" id = 0x16f +[[Datatype]] +name = "HeartbeatExpired" +id = 0x7fd + diff --git a/planning/graveyard/lamp.rs b/planning/graveyard/lamp.rs new file mode 100644 index 000000000..f1ed48dec --- /dev/null +++ b/planning/graveyard/lamp.rs @@ -0,0 +1,104 @@ +//! # LinearAttributeMAp (lamp) +//! Mapping a key to a constant attribute and a mutable value, with `O(n)` for all operations. + +use heapless::Vec; + +pub enum MapError { + KeyNotFound, + MapFull, +} + +pub trait AttributedMap { + fn init(&mut self, key: A, attr: B) -> Result<(), MapError>; + fn insert(&mut self, key: A, val: C) -> Result, MapError>; + fn set(&mut self, key: A, attr: B, val: C) -> Result<(), MapError>; + fn get(&self, key: &A) -> Result<(&B, Option<&C>), MapError>; +} + +pub trait Initialisable { + fn init() -> Self; +} + +pub struct LinearAttributeMap { + buffer: Vec<(A, B, Option), N>, +} + +impl LinearAttributeMap { + pub fn new() -> Self { Self { buffer: Vec::new() } } + + fn set_inner(&mut self, key: A, attr: B, val: Option) -> Result<(), MapError> { + match self.buffer.iter_mut().find(|(k, _, _)| k == &key) { + None => match self.buffer.push((key, attr, val)) { + Ok(_) => Ok(()), + Err(_) => Err(MapError::MapFull), + }, + Some(x) => { + *x = (key, attr, val); + Ok(()) + }, + } + } +} + +impl AttributedMap for LinearAttributeMap { + fn init(&mut self, key: A, attr: B) -> Result<(), MapError> { self.set_inner(key, attr, None) } + + fn insert(&mut self, key: A, value: C) -> Result, MapError> { + match self.buffer.iter_mut().find(|(k, _, _)| k == &key) { + None => match self.buffer.push((key, B::init(), Some(value))) { + Ok(_) => Ok(None), + Err(_) => Err(MapError::MapFull), + }, + Some((_, _, c)) => { + match c { + None => { + *c = Some(value); + Ok(None) + } + Some(x) => { + let r = core::mem::replace(x, value); + Ok(Some(r)) + } + } + }, + } + } + + fn set(&mut self, key: A, attr: B, val: C) -> Result<(), MapError> { + self.set_inner(key, attr, Some(val)) + } + + fn get(&self, key: &A) -> Result<(&B, Option<&C>), MapError> { + for (k, b, v) in self.buffer.iter() { + if k == key { + return Ok((b, v.as_ref())); + } + } + Err(MapError::KeyNotFound) + } +} + +impl<'a, A: Eq, B: Initialisable, C, const N: usize> IntoIterator for &'a LinearAttributeMap { + type Item = (&'a A, &'a B, Option<&'a C>); + type IntoIter = core::iter::Map< + core::slice::Iter<'a, (A, B, Option)>, + fn(&(A, B, Option)) -> (&A, &B, Option<&C>), + >; + + fn into_iter(self) -> Self::IntoIter { + self.buffer.iter().map(|(a, b, c)| (a, b, c.as_ref())) + } +} + +// Iterate through all the heartbeats and check that none are expired, send an EmergencyBraking event if something has expired, +// async fn check_heartbeats(hb: &HB, event_sender: EventSender, data_sender: DataSender) { +// let now = Instant::now(); +// for (dt, out, last) in hb.into_iter() { +// if let Some(l) = last { +// if l.elapsed() > *out { +// event_sender.send(Event::EmergencyBraking).await; +// data_sender.send(Datapoint::new(Datatype::HeartbeatExpired, dt.to_id() as u64, now.as_ticks())).await; +// } +// } +// } +// } \ No newline at end of file From 3502ddcb657ce5f29eb573c12af18c0e9655729f Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 13 Jul 2024 18:44:51 +0200 Subject: [PATCH 39/41] heartbeats fixed --- app/src/core/communication/tcp.rs | 3 ++ app/src/core/data/mod.rs | 11 +++-- config/commands.toml | 4 ++ config/config.toml | 4 +- gs/station/src/tui/app.rs | 70 +++++++++++++++---------------- 5 files changed, 49 insertions(+), 43 deletions(-) diff --git a/app/src/core/communication/tcp.rs b/app/src/core/communication/tcp.rs index a676cc532..3bfd37d38 100644 --- a/app/src/core/communication/tcp.rs +++ b/app/src/core/communication/tcp.rs @@ -245,6 +245,9 @@ pub async fn tcp_connection_handler( .send(Event::from_id((e & 0xFFFF) as u16, Some(69420))) .await; }, + Command::CreateDatapoint(x) => { + data_sender.send(Datapoint::new(Datatype::from_id(x as u16), x, Instant::now().as_ticks())).await; + } Command::SystemReset(_) => { #[cfg(debug_assertions)] info!("[tcp] SystemReset command received"); diff --git a/app/src/core/data/mod.rs b/app/src/core/data/mod.rs index f03e4e186..0d6794e30 100644 --- a/app/src/core/data/mod.rs +++ b/app/src/core/data/mod.rs @@ -16,7 +16,7 @@ use crate::Event; use crate::EventSender; use crate::ValueCheckResult; -type HB = Vec<(Datatype, Duration, Instant), { HEARTBEATS_LEN }>; +type HB = Vec<(Datatype, Duration, Option), { HEARTBEATS_LEN }>; /// ## Individual handling of datapoints /// A lot of the subsystems on the pod use their own "encoding" for data. @@ -54,16 +54,15 @@ pub async fn data_middle_step( for (dt, out, last) in hb.iter_mut() { if !seen && *dt == data.datatype { seen = true; - *last = Instant::now(); - } else { - if last.elapsed() > *out { + *last = Some(Instant::now()); + } else if last.is_some_and(|l| l.elapsed() > *out) { event_sender.send(Event::EmergencyBraking).await; outgoing.send(Datapoint::new(Datatype::HeartbeatExpired, dt.to_id() as u64, Instant::now().as_ticks())).await; - } + *last = None; } } if !seen { - match hb.push((data.datatype, timeout(data.datatype), Instant::now())) { + match hb.push((data.datatype, timeout(data.datatype), None)) { Ok(_) => {}, Err(_) => outgoing.send(Datapoint::new(Datatype::Info, Info::lamp_error_unreachable.to_idx(), Instant::now().as_ticks())).await, } diff --git a/config/commands.toml b/config/commands.toml index 40a2b9c19..5eef78b85 100644 --- a/config/commands.toml +++ b/config/commands.toml @@ -177,6 +177,10 @@ id = 0x65 name = "SetCurrentSpeed" id = 0x5a +[[Command]] +name = "CreateDatapoint" +id = 0x5b + [[Command]] name = "EmitEvent" id = 0x7A0 diff --git a/config/config.toml b/config/config.toml index 8a1042ea6..f421c1101 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,5 +1,5 @@ [gs] -ip = [192,168,1,3] +ip = [192,168,1,2] force = true port = 6942 buffer_size = 1460 # this is the MAXIMUM size of messages transmitted, in bytes. @@ -31,7 +31,7 @@ levi_requested_data = ["Localisation", "PropulsionCurrent", "AccelerationX", "Ac [pod.heartbeats] # syntax: # = -LocalisationHeartbeat = 1000 +LocalisationHeartbeat = 1000 # SensorHubHeartbeat = 1000 diff --git a/gs/station/src/tui/app.rs b/gs/station/src/tui/app.rs index 7dc92c8d7..075d8bd21 100644 --- a/gs/station/src/tui/app.rs +++ b/gs/station/src/tui/app.rs @@ -27,7 +27,7 @@ pub struct App { pub cmds: Vec, pub cur_state: String, pub last_heartbeat: String, - pub special_data: BTreeMap, + pub special_data: BTreeMap, pub backend: Backend, pub safe: bool, } @@ -46,36 +46,36 @@ impl App { cur_state: "None Yet".to_string(), last_heartbeat: "None Yet".to_string(), special_data: BTreeMap::from([ - (Datatype::InsulationNegative, 0), - (Datatype::InsulationPositive, 0), - (Datatype::InsulationOriginal, 0), - (Datatype::IMDVoltageDetails, 0), - (Datatype::IMDIsolationDetails, 0), - (Datatype::IMDGeneralInfo, 0), - (Datatype::Average_Temp_VB_top, 0), - (Datatype::GyroscopeX, 0), - (Datatype::GyroscopeY, 0), - (Datatype::GyroscopeZ, 0), - (Datatype::LowPressureSensor, 0), - (Datatype::BatteryCurrentHigh, 0), - (Datatype::BatteryTemperatureHigh, 0), - (Datatype::TotalBatteryVoltageLow, 0), - (Datatype::TotalBatteryVoltageHigh, 0), - (Datatype::SingleCellVoltageLow, 0), - (Datatype::BatteryMaxBalancingLow, 0), - (Datatype::Localisation, 0), - (Datatype::Velocity, 0), - (Datatype::LowPressureSensor, 0), - (Datatype::HighPressureSensor, 0), - (Datatype::PropulsionCurrent, 0), - (Datatype::PropulsionVoltage, 0), - (Datatype::PropulsionSpeed, 0), - (Datatype::PropulsionVRefInt, 0), - (Datatype::BrakingCommDebug, 0), - (Datatype::BrakingSignalDebug, 42), - (Datatype::BrakingBoolDebug, 42), - (Datatype::BrakingRearmDebug, 42), - (Datatype::PropGPIODebug, 42), + (Datatype::InsulationNegative, 0.0), + (Datatype::InsulationPositive, 0.0), + (Datatype::InsulationOriginal, 0.0), + (Datatype::IMDVoltageDetails, 0.0), + (Datatype::IMDIsolationDetails, 0.0), + (Datatype::IMDGeneralInfo, 0.0), + (Datatype::Average_Temp_VB_top, 0.0), + (Datatype::GyroscopeX, 0.0), + (Datatype::GyroscopeY, 0.0), + (Datatype::GyroscopeZ, 0.0), + (Datatype::LowPressureSensor, 0.0), + (Datatype::BatteryCurrentHigh, 0.0), + (Datatype::BatteryTemperatureHigh, 0.0), + (Datatype::TotalBatteryVoltageLow, 0.0), + (Datatype::TotalBatteryVoltageHigh, 0.0), + (Datatype::SingleCellVoltageLow, 0.0), + (Datatype::BatteryMaxBalancingLow, 0.0), + (Datatype::Localisation, 0.0), + (Datatype::Velocity, 0.0), + (Datatype::LowPressureSensor, 0.0), + (Datatype::HighPressureSensor, 0.0), + (Datatype::PropulsionCurrent, 0.0), + (Datatype::PropulsionVoltage, 0.0), + (Datatype::PropulsionSpeed, 0.0), + (Datatype::PropulsionVRefInt, 0.0), + (Datatype::BrakingCommDebug, 0.0), + (Datatype::BrakingSignalDebug, 42.0), + (Datatype::BrakingBoolDebug, 42.0), + (Datatype::BrakingRearmDebug, 42.0), + (Datatype::PropGPIODebug, 42.0), ]), backend, safe: true, @@ -117,14 +117,14 @@ impl App { self.logs.push(( Message::Info(format!( "Route: \n{}\ncurrently at {}", - LocationSequence::from(datapoint.value), + LocationSequence::from(datapoint.value.round() as u64), datapoint.timestamp, )), timestamp(), )); }, Datatype::FSMState => { - self.cur_state = state_to_string(datapoint.value).to_string(); + self.cur_state = state_to_string(datapoint.value.round() as u64).to_string(); self.logs.push(( Message::Warning(format!( "State is now: {:?}", @@ -135,7 +135,7 @@ impl App { self.logs.push((Message::Data(datapoint), timestamp())) }, Datatype::FSMEvent => { - if datapoint.value == Event::Heartbeating.to_id() as u64 { + if (datapoint.value - Event::Heartbeating.to_id() as f64).abs() <= f64::EPSILON * 2.0 { self.last_heartbeat = timestamp(); } else if self .special_data @@ -149,7 +149,7 @@ impl App { } }, Datatype::ResponseHeartbeat => { - self.special_data.insert(Datatype::ResponseHeartbeat, datapoint.timestamp); + self.special_data.insert(Datatype::ResponseHeartbeat, datapoint.timestamp as f64); }, x if self.special_data.keys().collect::>().contains(&&x) => { self.special_data.insert(x, datapoint.value); From cb6b55cff67ad2c45d87eecad834d8e57eaa8a37 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 14 Jul 2024 14:16:34 +0200 Subject: [PATCH 40/41] prevent tcp blocking from full mpmc queues --- app/.cargo/config.toml | 2 +- app/build.rs | 4 +- app/src/core/communication/can.rs | 25 +-- app/src/core/communication/mod.rs | 7 + app/src/core/communication/tcp.rs | 172 +++++++++--------- .../core/controllers/battery_controller.rs | 136 +++++--------- .../core/controllers/breaking_controller.rs | 104 ++--------- .../finite_state_machine_peripherals.rs | 29 ++- app/src/core/controllers/hv_controller.rs | 2 - .../core/controllers/propulsion_controller.rs | 27 +-- app/src/core/data/mod.rs | 53 +++--- app/src/core/states/mod.rs | 2 +- app/src/main.rs | 22 ++- app/src/pconfig.rs | 120 +++++++++++- config/commands.toml | 4 + config/config.toml | 17 +- config/datatypes.toml | 8 + gs/station/.clippy.toml | 1 + gs/station/build.rs | 6 +- .../src/connect/handle_incoming_data.rs | 8 + gs/station/src/connect/tcp_writer.rs | 15 +- gs/station/src/frontend/app.rs | 14 +- gs/station/src/main.rs | 1 + gs/station/src/tui/app.rs | 20 ++ gs/station/src/tui/interactions.rs | 18 +- gs/station/src/tui/render.rs | 26 +-- util/src/datatypes.rs | 2 +- util/src/lib.rs | 13 +- 28 files changed, 447 insertions(+), 411 deletions(-) create mode 100644 gs/station/.clippy.toml diff --git a/app/.cargo/config.toml b/app/.cargo/config.toml index acc7acb4b..45219d0ba 100644 --- a/app/.cargo/config.toml +++ b/app/.cargo/config.toml @@ -7,4 +7,4 @@ target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) [env] #DEFMT_LOG = "off" -DEFMT_LOG = "trace" +DEFMT_LOG = "error" diff --git a/app/build.rs b/app/build.rs index 113de2e02..49188d264 100644 --- a/app/build.rs +++ b/app/build.rs @@ -9,7 +9,7 @@ use std::fs; use std::path::Path; use anyhow::Result; -use goose_utils::check_ids; +use goose_utils::check_config; use goose_utils::ip::configure_gs_ip; use serde::Deserialize; @@ -80,7 +80,7 @@ fn main() -> Result<()> { let mut content = String::from("//@generated\n"); - let _ = check_ids(DATATYPES_PATH, COMMANDS_PATH, EVENTS_PATH); + content.push_str(&check_config(DATATYPES_PATH, COMMANDS_PATH, EVENTS_PATH, CONFIG_PATH)?); content.push_str(&configure_ip(&config)); content.push_str(&configure_gs_ip(config.gs.ip, config.gs.port, config.gs.force)?); diff --git a/app/src/core/communication/can.rs b/app/src/core/communication/can.rs index 4620f0beb..f6deefb01 100644 --- a/app/src/core/communication/can.rs +++ b/app/src/core/communication/can.rs @@ -13,9 +13,9 @@ use crate::core::communication::Datapoint; use crate::core::controllers::battery_controller::ground_fault_detection_isolation_details; use crate::core::controllers::battery_controller::ground_fault_detection_voltage_details; use crate::core::controllers::can_controller::CanTwoUtils; -use crate::pconfig::bytes_to_u64; +use crate::pconfig::{bytes_to_u64, send_event}; use crate::pconfig::id_as_value; -use crate::CanReceiver; +use crate::{CanReceiver, send_data}; use crate::CanSender; use crate::DataSender; use crate::Datatype; @@ -53,21 +53,13 @@ pub async fn can_receiving_handler( let mut error_counter = 0u64; let mut gfd_counter = 0u64; loop { - // #[cfg(debug_assertions)] match bus.read().await { Ok(envelope) => { error_counter = 0; let (frame, timestamp) = envelope.parts(); - // frame.header().format(); let id = id_as_value(frame.id()); #[cfg(debug_assertions)] - data_sender - .send(Datapoint::new( - Datatype::ReceivedCan, - id as u64, - bytes_to_u64(frame.data()), - )) - .await; + send_data!(data_sender, Datatype::ReceivedCan, id as u64, bytes_to_u64(frame.data())); #[cfg(debug_assertions)] info!("[CAN ({})] received frame: id={:?} data={:?}", bus_nr, id, frame.data()); if DATA_IDS.contains(&id) { @@ -125,17 +117,12 @@ pub async fn can_receiving_handler( .await; } } else if EVENT_IDS.contains(&id) { - event_sender.send(Event::from_id(id, Some(69420))).await; // since we are never supposed to change the speed through the can bus (and run config is the only event with an actual value), i want a magic number that i can filter out from the run config handler just to make sure the pod doesn't do something stupid + // since we are never supposed to change the speed through the can bus (and run config is the only event with an actual value), i want a magic number that i can filter out from the run config handler just to make sure the pod doesn't do something stupid + send_event(event_sender, Event::from_id(id, Some(69420))); } else { #[cfg(debug_assertions)] info!("[CAN ({})] unknown ID: {:?}", bus_nr, id); - data_sender - .send(Datapoint::new( - Datatype::UnknownCanId, - id as u64, - bytes_to_u64(frame.data()), - )) - .await; + send_data!(data_sender, Datatype::UnknownCanId, id as u64, bytes_to_u64(frame.data())); } }, Err(e) => { diff --git a/app/src/core/communication/mod.rs b/app/src/core/communication/mod.rs index f2ad62443..f40335da7 100644 --- a/app/src/core/communication/mod.rs +++ b/app/src/core/communication/mod.rs @@ -1,3 +1,4 @@ +use defmt::Formatter; use crate::Datatype; pub mod can; @@ -33,3 +34,9 @@ impl Datapoint { bytes } } + +impl defmt::Format for Datapoint { + fn format(&self, fmt: Formatter) { + defmt::write!(fmt, "Datapoint {{ datatype: {:?}, value: {:?}, timestamp: {:?} }}", self.datatype, self.value, self.timestamp) + } +} diff --git a/app/src/core/communication/tcp.rs b/app/src/core/communication/tcp.rs index 111dfd413..10770e11c 100644 --- a/app/src/core/communication/tcp.rs +++ b/app/src/core/communication/tcp.rs @@ -6,21 +6,24 @@ use embassy_net::Stack; use embassy_stm32::eth::generic_smi::GenericSMI; use embassy_stm32::eth::Ethernet; use embassy_stm32::peripherals::ETH; +use embassy_sync::channel::TrySendError; use embassy_time::Instant; use embassy_time::Timer; use embedded_io_async::Write; +use embedded_io_async::WriteReady; use heapless::Deque; use panic_probe as _; use crate::core::communication::Datapoint; -use crate::pconfig::embassy_socket_from_config; -use crate::Command; +use crate::pconfig::{embassy_socket_from_config, queue_data, queue_event, send_event, ticks}; +use crate::{Command, Info, send_data}; use crate::DataReceiver; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; use crate::COMMAND_HASH; +use crate::CONFIG_HASH; use crate::DATA_HASH; use crate::EVENTS_HASH; use crate::GS_IP_ADDRESS; @@ -39,7 +42,7 @@ pub async fn tcp_connection_handler( data_receiver: DataReceiver, data_sender: DataSender, ) -> ! { - let mut last_valid_timestamp = embassy_time::Instant::now().as_millis(); + let mut last_valid_timestamp = Instant::now().as_millis(); // info!("------------------------------------------------ TCP Connection Handler Started! ------------------------------------------"); stack.wait_config_up().await; @@ -52,68 +55,57 @@ pub async fn tcp_connection_handler( // let mut socket: TcpSocket = // TcpSocket::new(stack, unsafe { &mut rx_buffer }, unsafe { &mut tx_buffer }); + let mut rx_buffer: [u8; NETWORK_BUFFER_SIZE] = [0u8; { NETWORK_BUFFER_SIZE }]; + let mut tx_buffer: [u8; NETWORK_BUFFER_SIZE] = [0u8; { NETWORK_BUFFER_SIZE }]; + + let mut socket: TcpSocket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + let mut buf = [0; { NETWORK_BUFFER_SIZE }]; 'netstack: loop { - // info!("====================================================Connecting to ground station______________________________"); - let mut rx_buffer: [u8; NETWORK_BUFFER_SIZE] = [0u8; { NETWORK_BUFFER_SIZE }]; - let mut tx_buffer: [u8; NETWORK_BUFFER_SIZE] = [0u8; { NETWORK_BUFFER_SIZE }]; - - let mut socket: TcpSocket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + info!("[netstack] loop starting"); match socket.connect(gs_addr).await { Ok(_) => { - last_valid_timestamp = embassy_time::Instant::now().as_millis(); + last_valid_timestamp = Instant::now().as_millis(); }, Err(e) => { - let d = embassy_time::Instant::now().as_millis() - last_valid_timestamp; + let d = Instant::now().as_millis() - last_valid_timestamp; error!("[tcp:stack] error connecting to gs: {:?} (diff={})", e, d); if d > IP_TIMEOUT { - event_sender.send(Event::ConnectionLossEvent).await; + send_event(event_sender, Event::ConnectionLossEvent); } - Timer::after_millis(500).await; + Timer::after_millis(100).await; continue 'netstack; }, } - event_sender.send(Event::ConnectionEstablishedEvent).await; - data_sender - .send(Datapoint::new(Datatype::CommandHash, COMMAND_HASH, Instant::now().as_ticks())) - .await; - data_sender - .send(Datapoint::new(Datatype::DataHash, DATA_HASH, Instant::now().as_ticks())) - .await; - data_sender - .send(Datapoint::new(Datatype::EventsHash, EVENTS_HASH, Instant::now().as_ticks())) - .await; - // let mut connection = client.connect(gs_addr).await.unwrap(); - // info!("----------------------------------------------------------------Connected to ground station=========================="); + send_event(event_sender, Event::ConnectionEstablishedEvent); - // socket.set_keep_alive(Some(Duration::from_millis(KEEP_ALIVE))); - // socket.set_timeout(Some(Duration::from_millis(IP_TIMEOUT))); + // Handshake + // Exchange hashes of the configuration files + // in order to confirm that the exchanged ids are correct + queue_data(data_sender, Datatype::CommandHash, COMMAND_HASH).await; + queue_data(data_sender, Datatype::EventsHash, EVENTS_HASH).await; + queue_data(data_sender, Datatype::DataHash, DATA_HASH).await; + queue_data(data_sender, Datatype::ConfigHash, CONFIG_HASH).await; - /*// let (mut tcp_reader,mut tcp_writer) = unsafe { socket.split() }; - // spawn the writer task: it will take messages from the channel and send them over the TCP connection - // x.spawn(ground_station_message_dispatcher(tcp_writer, data_receiver.clone())).unwrap();*/ - #[cfg(debug_assertions)] - match socket.write(b"aaaaaaaaaaaaaaa0").await { - Ok(_) => { - info!("]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]Data sent successfully") - }, - Err(e) => { - info!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Failed to send data: {:?}", e) - }, - } + // Begin relying on the frontend + queue_data(data_sender, Datatype::FrontendHeartbeating, 0).await; + let mut parsing_buffer = Deque::::new(); // loop to receive data from the TCP connection 'connection: loop { Timer::after_millis(1).await; - if !socket.may_recv() || !socket.may_send() { + if !socket.may_recv() + || !socket.may_send() + || Instant::now().as_millis() - last_valid_timestamp > IP_TIMEOUT + { error!("[tcp] may_recv: connection closed"); break 'connection; } if socket.can_recv() { - last_valid_timestamp = embassy_time::Instant::now().as_millis(); + last_valid_timestamp = Instant::now().as_millis(); let n = socket.read(&mut buf).await.unwrap_or(420000); if n == 42000 { error!("[tcp] Failed to read from socket"); @@ -121,7 +113,7 @@ pub async fn tcp_connection_handler( } if n == 0 { info!("[tcp] Connection closed by ground station.."); - event_sender.send(Event::ConnectionLossEvent).await; + send_event(event_sender, Event::ConnectionLossEvent); break 'connection; } #[cfg(debug_assertions)] @@ -147,15 +139,15 @@ pub async fn tcp_connection_handler( info!("[tcp] Command received: {:?}", cmd); match cmd { Command::EmergencyBrake(_) => { - event_sender.send(Event::EmergencyBraking).await; + send_event(event_sender, Event::EmergencyBraking); #[cfg(debug_assertions)] info!("[tcp] EmergencyBrake command received!!"); if let Err(e) = socket .write_all( &Datapoint::new( Datatype::Info, - crate::Info::EmergencyBrakeReceived.to_idx(), - embassy_time::Instant::now().as_ticks(), + Info::EmergencyBrakeReceived.to_idx(), + ticks(), ) .as_bytes(), ) @@ -177,92 +169,84 @@ pub async fn tcp_connection_handler( Command::launch(_) => { #[cfg(debug_assertions)] info!("[tcp] Levitate command received"); - event_sender.send(Event::LeviLaunchingEvent).await; + send_event(event_sender, Event::LeviLaunchingEvent); }, Command::land(_) => { #[cfg(debug_assertions)] info!("[tcp] StopLevitating command received"); - event_sender.send(Event::LeviLandingEvent).await; + send_event(event_sender, Event::LeviLandingEvent); }, Command::SetRoute(x) => { #[cfg(debug_assertions)] info!("[tcp] Configure command received"); - event_sender.send(Event::SettingRoute(x)).await; + send_event(event_sender, Event::SettingRoute(x)); }, Command::SetSpeeds(x) => { #[cfg(debug_assertions)] info!("[tcp] Configure command received"); - event_sender.send(Event::SettingSpeeds(x)).await; + send_event(event_sender, Event::SettingSpeeds(x)); }, Command::SetOverrides(x) => { - event_sender.send(Event::SettingOverrides(x)).await; + send_event(event_sender, Event::SettingOverrides(x)); }, Command::SetCurrentSpeed(x) => { #[cfg(debug_assertions)] info!("[tcp] SetCurrentSpeed command received"); - event_sender.send(Event::SetCurrentSpeedCommand(x)).await; + send_event(event_sender, Event::SetCurrentSpeedCommand(x)); }, Command::StartRun(_) => { #[cfg(debug_assertions)] info!("[tcp] Start Run command received"); - event_sender.send(Event::RunStarting).await; + send_event(event_sender, Event::RunStarting); }, Command::ContinueRun(_) => { #[cfg(debug_assertions)] info!("[tcp] Start Run command received"); - event_sender.send(Event::ContinueRunEvent).await; + send_event(event_sender, Event::ContinueRunEvent); }, Command::Shutdown(_) => { #[cfg(debug_assertions)] info!("[tcp] Shutdown command received"); - event_sender.send(Event::ExitEvent).await; + send_event(event_sender, Event::ExitEvent); }, Command::StartHV(_) => { #[cfg(debug_assertions)] info!("[tcp] StartHV command received"); - event_sender.send(Event::TurnOnHVCommand).await; + send_event(event_sender, Event::TurnOnHVCommand); }, Command::StopHV(_) => { #[cfg(debug_assertions)] info!("[tcp] StopHV command received"); - event_sender.send(Event::TurnOffHVCommand).await; - // TODO: no turn off HV exists?? + send_event(event_sender, Event::TurnOffHVCommand); }, Command::DcOn(_) => { #[cfg(debug_assertions)] info!("[tcp] DcOn command received"); - event_sender.send(Event::DcTurnedOn).await; + send_event(event_sender, Event::DcTurnedOn); }, Command::DcOff(_) => { #[cfg(debug_assertions)] info!("[tcp] DcOff command received"); - event_sender.send(Event::DcTurnedOff).await; + send_event(event_sender, Event::DcTurnedOff); }, Command::EmitEvent(e) => { #[cfg(debug_assertions)] info!("[tcp] EmitEvent command received"); - event_sender - .send(Event::from_id((e & 0xFFFF) as u16, Some(69420))) - .await; + send_event(event_sender, Event::from_id((e & 0xFFFF) as u16, Some(69420))); + }, Command::CreateDatapoint(x) => { - data_sender - .send(Datapoint::new( - Datatype::from_id(x as u16), - x, - Instant::now().as_ticks(), - )) - .await; + send_data!(data_sender, Datatype::from_id(x as u16), x); }, Command::SystemReset(_) => { #[cfg(debug_assertions)] info!("[tcp] SystemReset command received"); - event_sender.send(Event::SystemResetCommand).await; + send_event(event_sender, Event::SystemResetCommand); }, Command::FinishRunConfig(_) => { #[cfg(debug_assertions)] info!("[tcp] FinishRunConfig command received"); - event_sender.send(Event::RunConfigCompleteEvent).await; + send_event(event_sender, Event::RunConfigCompleteEvent); }, Command::Heartbeat(x) => { #[cfg(debug_assertions)] @@ -285,10 +269,13 @@ pub async fn tcp_connection_handler( }, } }, + Command::FrontendHeartbeat(x) => { + send_data!(data_sender, Datatype::FrontendHeartbeating, x); + }, Command::ArmBrakes(_) => { #[cfg(debug_assertions)] info!("[tcp] ArmBrakesCommand command received"); - event_sender.send(Event::ArmBrakesCommand).await; + queue_event(event_sender, Event::ArmBrakesCommand).await; }, _ => {}, // TODO: DELETE THIS } @@ -299,21 +286,34 @@ pub async fn tcp_connection_handler( } } - if let Ok(data) = data_receiver.try_receive() { - let data = data.as_bytes(); - #[cfg(debug_assertions)] - info!("[tcp:mpmc] Sending data: {:?}", data); - match socket.write_all(&data).await { - Ok(_) => { - #[cfg(debug_assertions)] - info!("[tcp:socket] Data written successfully"); - }, - Err(e) => { - error!("[tcp:socket] Failed to write data: {:?}", e); - }, - } - } // the else case is of empty MPMC channel queue, - // which triggers very often, so we ignore it. + match socket.write_ready() { + Ok(x) => { + if x { + if let Ok(data) = data_receiver.try_receive() { + let data = data.as_bytes(); + #[cfg(debug_assertions)] + info!("[tcp:mpmc] Sending data: {:?}", data); + match socket.write_all(&data).await { + Ok(_) => { + #[cfg(debug_assertions)] + info!("[tcp:socket] Data written successfully"); + }, + Err(e) => { + error!("[tcp:socket] Failed to write data: {:?}", e); + }, + } + } // the else case is of empty MPMC channel queue, + // which triggers very often, so we ignore it. + } else { + error!("[tcp] socket not writeable"); + Timer::after_millis(500).await; + } + }, + Err(y) => { + error!("[tcp] error getting socket write status: {:?}", y); + Timer::after_millis(500).await; + }, + } } // if this is reached, it means that the connection was dropped info!("D:"); diff --git a/app/src/core/controllers/battery_controller.rs b/app/src/core/controllers/battery_controller.rs index 5292fec9d..8191cf945 100644 --- a/app/src/core/controllers/battery_controller.rs +++ b/app/src/core/controllers/battery_controller.rs @@ -1,8 +1,8 @@ -use defmt::debug; +use defmt::{debug, trace}; use defmt::info; use crate::core::communication::Datapoint; -use crate::pconfig::bytes_to_u64; +use crate::pconfig::{bytes_to_u64, queue_data, queue_dp}; use crate::DataSender; use crate::Datatype; use crate::EventSender; @@ -68,28 +68,15 @@ pub struct GroundFaultDetection {} pub async fn ground_fault_detection_isolation_details( data: &[u8], data_sender: DataSender, - timestamp: u64, + t: u64, ) { let negative_insulation_resistance = ((data[1] as u64) << 8) | (data[0] as u64); - data_sender - .send(Datapoint::new( - Datatype::InsulationNegative, - negative_insulation_resistance, - timestamp, - )) - .await; + queue_dp(data_sender, Datatype::InsulationNegative, negative_insulation_resistance, t).await; let positive_insulation_resistance = ((data[3] as u64) << 8) | (data[2] as u64); - data_sender - .send(Datapoint::new( - Datatype::InsulationPositive, - positive_insulation_resistance, - timestamp, - )) - .await; + queue_dp(data_sender, Datatype::InsulationPositive, positive_insulation_resistance, t).await; + let original_measurement_counter = data[4] as u64 | ((data[5] as u64) << 8); - data_sender - .send(Datapoint::new(Datatype::InsulationOriginal, original_measurement_counter, timestamp)) - .await; + queue_dp(data_sender, Datatype::InsulationOriginal, original_measurement_counter, t).await; } pub async fn ground_fault_detection_voltage_details( @@ -98,7 +85,7 @@ pub async fn ground_fault_detection_voltage_details( timestamp: u64, ) { let hv_voltage = ((data[1] as u64) << 8) | (data[0] as u64); - data_sender.send(Datapoint::new(Datatype::IMDVoltageDetails, hv_voltage, timestamp)).await; + queue_dp(data_sender, Datatype::IMDVoltageDetails, hv_voltage, timestamp).await; } //===============BMS===============// @@ -111,51 +98,37 @@ impl BatteryController { _sender: DataSender, timestamp: u64, ) { - debug!("Here BMS"); + trace!("Here BMS"); match Datatype::from_id(id) { Datatype::DefaultBMSLow | Datatype::DefaultBMSHigh => { self.default_bms_startup_info(data, timestamp).await; - info!("Default BMS"); + debug!("Default BMS"); }, Datatype::BatteryVoltageLow | Datatype::BatteryVoltageHigh => { self.battery_voltage_overall_bms(data, timestamp).await; - info!("Battery Voltage") + debug!("Battery Voltage") }, Datatype::DiagnosticBMSLow | Datatype::DiagnosticBMSHigh => { self.diagnostic_bms(data, timestamp).await; - info!("Diagnostic BMS") + debug!("Diagnostic BMS") }, Datatype::BatteryTemperatureLow | Datatype::BatteryTemperatureHigh => { self.overall_temperature_bms(data, timestamp).await; - info!("Battery Temperature") + debug!("Battery Temperature") }, Datatype::BatteryBalanceLow | Datatype::BatteryBalanceHigh => { self.overall_balancing_status_bms(data, timestamp).await; - info!("Battery Balancing") + debug!("Battery Balancing") }, Datatype::ChargeStateLow | Datatype::ChargeStateHigh => { self.state_of_charge_bms(data, timestamp).await; - info!("Charge State") + debug!("Charge State") }, _x if Datatype::SingleCellTemperatureLow.to_id() == id || (Datatype::SingleCellTemperatureHigh_1.to_id() <= id && Datatype::SingleCellTemperatureHigh_14.to_id() >= id) => { debug!("Individual Temperature"); - // if (id - Datatype::SingleCellTemperatureHigh_1.to_id() != 0) { - // self.receive_single_cell_id = true; - // } - // - // if (self.receive_single_cell_id) { - // self.single_cell_id = 0; - // self.module_buffer = [0; 14]; - // self.current_number_of_cells = 0; - // self.receive_single_cell_id = false; - // } else if (Datatype::SingleCellTemperatureLow.to_id() == id) { - // // self.overall_temperature_bms(&*Self::single_cell_low_process(data).await,timestamp); - // } else { - // self.individual_temperature_bms(data, timestamp).await; - // } if self.number_of_temp >= 13 { self.number_of_temp = 0; let mut i = 0; @@ -183,7 +156,7 @@ impl BatteryController { self.number_of_temp += 1; } - info!("Individual Temperature") + debug!("Individual Temperature") }, _x if Datatype::SingleCellVoltageLow.to_id() == id || (Datatype::SingleCellVoltageHigh_1.to_id() <= id @@ -205,14 +178,6 @@ impl BatteryController { i += 1; } } - // if (id - Datatype::SingleCellVoltageHigh_1.to_id() != 0) { - // self.receive_single_cell_id = true; - // } - // if (self.receive_single_cell_id) { - // self.single_cell_id = 0; - // self.module_buffer = [0; 14]; - // self.current_number_of_cells = 0; - // self.receive_single_cell_id = false; if Datatype::SingleCellVoltageLow.to_id() == id { // self.overall_temperature_bms(&*Self::single_cell_low_process(data).await,timestamp); } else { @@ -225,14 +190,14 @@ impl BatteryController { self.number_of_volt += 1; } - info!("Individual Voltage") + debug!("Individual Voltage") }, Datatype::BatteryEventLow | Datatype::BatteryEventHigh => { self.event_bms(data, timestamp).await; - info!("Battery Event") + debug!("Battery Event") }, x => { - info!("Ignored BMS id: {:?}", x.to_id()); + debug!("Ignored BMS: {:?} (id={:?})", x, x.to_id()); }, } } @@ -241,12 +206,12 @@ impl BatteryController { // let mut msg: u64 = 0; let dt = if self.high_voltage { Datatype::BatteryEventHigh } else { Datatype::BatteryEventLow }; - self.data_sender.send(Datapoint::new(dt, bytes_to_u64(data), timestamp)).await; + queue_dp(self.data_sender, dt, bytes_to_u64(data), timestamp).await; } pub async fn battery_voltage_overall_bms(&mut self, data: &[u8], timestamp: u64) { let min_cell_voltage = data[0] as u64 + 200; //VOLTAGE scaled by 100 - //Should be a decimal number so multiplying it by 100 is not such a bad idea + //Should be a decimal number so multiplying it by 100 is not such a bad idea let max_cell_voltage = data[1] as u64 + 200; //VOLTAGE let avg_cell_voltage = data[0] as u64 + 200; //VOLTAGE let total_pack_voltage = (((data[5] as u64) << 24) @@ -274,18 +239,10 @@ impl BatteryController { } else { Datatype::TotalBatteryVoltageLow }; - self.data_sender - .send(Datapoint::new(battery_voltage_dt, avg_cell_voltage, timestamp)) - .await; - self.data_sender - .send(Datapoint::new(battery_voltage_min, min_cell_voltage, timestamp)) - .await; - self.data_sender - .send(Datapoint::new(battery_voltage_max, max_cell_voltage, timestamp)) - .await; - self.data_sender - .send(Datapoint::new(total_battery_voltage_dt, total_pack_voltage, timestamp)) - .await; + queue_dp(self.data_sender, battery_voltage_dt, avg_cell_voltage, timestamp).await; + queue_dp(self.data_sender, battery_voltage_min, min_cell_voltage, timestamp).await; + queue_dp(self.data_sender, battery_voltage_max, max_cell_voltage, timestamp).await; + queue_dp(self.data_sender, total_battery_voltage_dt, total_pack_voltage, timestamp).await; } pub async fn default_bms_startup_info(&mut self, data: &[u8], timestamp: u64) { @@ -307,7 +264,7 @@ impl BatteryController { for (i, &x) in data.iter().enumerate() { msg |= (x as u64) << (i * 8); } - self.data_sender.send(Datapoint::new(dt, msg, timestamp)).await; + queue_dp(self.data_sender, dt, msg, timestamp).await; } pub async fn state_of_charge_bms(&mut self, data: &[u8], timestamp: u64) { @@ -327,11 +284,9 @@ impl BatteryController { Datatype::BatteryEstimatedChargeLow }; - self.data_sender.send(Datapoint::new(battery_current_dt, current, timestamp)).await; - self.data_sender.send(Datapoint::new(charge_state_dt, state_of_charge, timestamp)).await; - self.data_sender - .send(Datapoint::new(estimated_charge_dt, estimated_charge, timestamp)) - .await; + queue_dp(self.data_sender, battery_current_dt, current, timestamp).await; + queue_dp(self.data_sender, charge_state_dt, state_of_charge, timestamp).await; + queue_dp(self.data_sender, estimated_charge_dt, estimated_charge, timestamp).await; } pub async fn overall_temperature_bms(&mut self, data: &[u8], timestamp: u64) { @@ -355,13 +310,12 @@ impl BatteryController { Datatype::BatteryMaxTemperatureLow }; - self.data_sender.send(Datapoint::new(battery_temp_dt, avg_temp, timestamp)).await; - self.data_sender.send(Datapoint::new(battery_temp_min, min_temp, timestamp)).await; - self.data_sender.send(Datapoint::new(battery_temp_max, max_temp, timestamp)).await; + queue_dp(self.data_sender, battery_temp_dt, avg_temp, timestamp).await; + queue_dp(self.data_sender, battery_temp_min, min_temp, timestamp).await; + queue_dp(self.data_sender, battery_temp_max, max_temp, timestamp).await; } - #[allow(dead_code)] - pub async fn individual_temperature_bms(&mut self, data: &[u8], timestamp: u64) { + /*pub async fn individual_temperature_bms(&mut self, data: &[u8], timestamp: u64) { for &x in data.iter() { if self.single_cell_id < 8 { self.module_buffer[self.current_number_of_cells] = x as u64; @@ -380,19 +334,16 @@ impl BatteryController { break; } } - } + }*/ async fn send_module_temp(&mut self, timestamp: u64) { let module_id = self.single_cell_id; let (min_temp, max_temp, avg_temp) = Self::module_data_calculation(self.module_buffer).await; - // min_temp = if (min_temp - 100) < 0 { 0 } else { min_temp - 100 }; - // max_temp = if (max_temp - 100) < 0 { 0 } else { max_temp - 100 }; - // avg_temp = if (avg_temp - 100) < 0 { 0 } else { avg_temp - 100 }; let (avg_temp_dt, min_temp_dt, max_temp_dt) = Self::match_temp(module_id).await; - self.data_sender.send(Datapoint::new(avg_temp_dt, avg_temp, timestamp)).await; - self.data_sender.send(Datapoint::new(min_temp_dt, min_temp, timestamp)).await; - self.data_sender.send(Datapoint::new(max_temp_dt, max_temp, timestamp)).await; + queue_dp(self.data_sender, avg_temp_dt, avg_temp, timestamp).await; + queue_dp(self.data_sender, min_temp_dt, min_temp, timestamp).await; + queue_dp(self.data_sender, max_temp_dt, max_temp, timestamp).await; } async fn send_module_voltage(&mut self, timestamp: u64) { @@ -400,9 +351,9 @@ impl BatteryController { let (min_voltage, max_voltage, avg_voltage) = Self::module_data_calculation(self.module_buffer).await; let (avg_voltage_dt, min_voltage_dt, max_voltage_dt) = Self::match_voltage(module_id).await; - self.data_sender.send(Datapoint::new(avg_voltage_dt, avg_voltage + 200, timestamp)).await; - self.data_sender.send(Datapoint::new(min_voltage_dt, min_voltage + 200, timestamp)).await; - self.data_sender.send(Datapoint::new(max_voltage_dt, max_voltage + 200, timestamp)).await; + queue_dp(self.data_sender, avg_voltage_dt, avg_voltage + 200, timestamp).await; + queue_dp(self.data_sender, min_voltage_dt, min_voltage + 200, timestamp).await; + queue_dp(self.data_sender, max_voltage_dt, max_voltage + 200, timestamp).await; } pub async fn match_temp(id: u16) -> (Datatype, Datatype, Datatype) { @@ -525,7 +476,6 @@ impl BatteryController { (min, max, avg) } - #[allow(dead_code)] pub async fn individual_voltages_bms(&mut self, data: &[u8], timestamp: u64) { for &x in data.iter() { if self.single_cell_id < 8 { @@ -565,8 +515,8 @@ impl BatteryController { } else { Datatype::BatteryMaxBalancingLow }; - self.data_sender.send(Datapoint::new(balancing_dt, avg_cell_balancing, timestamp)).await; - self.data_sender.send(Datapoint::new(balancing_min, min_cell_balancing, timestamp)).await; - self.data_sender.send(Datapoint::new(balancing_max, max_cell_balancing, timestamp)).await; + queue_dp(self.data_sender, balancing_dt, avg_cell_balancing, timestamp).await; + queue_dp(self.data_sender, balancing_min, min_cell_balancing, timestamp).await; + queue_dp(self.data_sender, balancing_max, max_cell_balancing, timestamp).await; } } diff --git a/app/src/core/controllers/breaking_controller.rs b/app/src/core/controllers/breaking_controller.rs index 34ccc7d9d..f538fb7fc 100644 --- a/app/src/core/controllers/breaking_controller.rs +++ b/app/src/core/controllers/breaking_controller.rs @@ -13,11 +13,12 @@ use embassy_time::Instant; use embassy_time::Timer; use crate::core::communication::Datapoint; -use crate::try_spawn; +use crate::{send_data, try_spawn}; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; +use crate::pconfig::{queue_event}; pub static mut BRAKE: bool = false; @@ -35,25 +36,15 @@ pub async fn control_braking_heartbeat( data_sender: DataSender, mut braking_signal: Output<'static>, ) { - // pub async fn control_braking_heartbeat(sender: EventSender, mut braking_heartbeat: SimplePwm<'static, TIM16>) { info!("----------------- Start Braking Heartbeat! -----------------"); let mut booting = true; let mut last_timestamp = Instant::now(); loop { Timer::after_micros(10).await; - // if adc.read(&mut pf12) > 30000 { - // braking_signal.set_low(); - // // info!("------------ BRAKE ! ------------"); - // } else if unsafe { !BRAKE } { braking_signal.set_high(); - // braking_heartbeat.set_duty(Channel::Ch1, braking_heartbeat.get_max_duty()/2); } else { braking_signal.set_low(); - - // braking_heartbeat.set_duty(Channel::Ch1, 0); - // sender.send(Event::EmergencyBrakeCommand).await; - // info!("------------ BRAKE ! ------------"); } if booting { sender.send(Event::BootingCompleteEvent).await; @@ -61,44 +52,12 @@ pub async fn control_braking_heartbeat( } if Instant::now().duration_since(last_timestamp) > Duration::from_millis(1000) { match braking_signal.get_output_level() { - Level::Low => { - data_sender - .send(Datapoint::new( - Datatype::BrakingSignalDebug, - 0, - Instant::now().as_ticks(), - )) - .await; - }, - Level::High => { - data_sender - .send(Datapoint::new( - Datatype::BrakingSignalDebug, - 1, - Instant::now().as_ticks(), - )) - .await; - }, + Level::Low => send_data!(data_sender, Datatype::BrakingSignalDebug, 0), + Level::High => send_data!(data_sender, Datatype::BrakingSignalDebug, 1), } match unsafe { BRAKE } { - true => { - data_sender - .send(Datapoint::new( - Datatype::BrakingBoolDebug, - 1, - Instant::now().as_ticks(), - )) - .await; - }, - false => { - data_sender - .send(Datapoint::new( - Datatype::BrakingBoolDebug, - 0, - Instant::now().as_ticks(), - )) - .await; - }, + true => send_data!(data_sender, Datatype::BrakingBoolDebug, 1), + false => send_data!(data_sender, Datatype::BrakingBoolDebug, 0), } last_timestamp = Instant::now(); } @@ -119,9 +78,7 @@ async fn read_braking_communication( let is_activated = v < 25000; // when braking comm goes low, someone else triggered brakes (eg big red button) if edge && is_activated { edge = false; // braking comm value is low, so we don't brake until it goes high again - if unsafe { !DISABLE_BRAKING_COMMUNICATION } { - event_sender.send(Event::EmergencyBraking).await; - } + if unsafe { !DISABLE_BRAKING_COMMUNICATION } { queue_event(event_sender, Event::EmergencyBraking).await; } Timer::after_millis(1000).await; } if !edge && !is_activated { @@ -130,13 +87,7 @@ async fn read_braking_communication( } Timer::after_micros(10).await; if Instant::now().duration_since(last_timestamp) > Duration::from_millis(500) { - data_sender - .send(Datapoint::new( - Datatype::BrakingCommDebug, - v as u64, - Instant::now().as_ticks(), - )) - .await; + send_data!(data_sender, Datatype::BrakingCommDebug, v as u64); last_timestamp = Instant::now(); } } @@ -159,27 +110,7 @@ impl BrakingController { // If we want to keep it alive we send a 10khz digital clock signal let braking_rearm: Output = Output::new(pg1, Level::High, Speed::Low); // <--- To keep the breaks not rearmed we send a 1, if we want to arm the breaks we send a 0 - //let mut led: Output = Output::new(pb0, Level::High, Speed::Low); - // let mut led2 : Output = Output::new(pd5,Level::High,Speed::Low); - //led.set_high(); - //info!("set led on pb0 to high"); - // let mut pwm = SimplePwm::new( - // ptime, - // Some(PwmPin::new_ch1(pb8, OutputType::PushPull)), - // None, - // None, - // None, - // khz(30), - // Default::default(), - // ); - // let braking_communication = Input::new(pf12, Pull::None); // <--- If its HIGH it means that breaks are rearmed, if its low it , means we are breaking - // Finally if we set the heartbeat to LOW, and we still receive a 1 is basically means we are crashing so lets actually make use of the crashing state - - // let mut braking_communication = Adc::new(); - // braking_communication - let braking_signal = Output::new(pb8, Level::High, Speed::Low); - // pwm.enable(Channel::Ch1); // VGA ground let _ = Output::new(pd5, Level::Low, Speed::Low); @@ -199,23 +130,10 @@ impl BrakingController { pub async fn arm_breaks(&mut self) { self.braking_rearm.set_low(); - self.data_sender - .send(Datapoint::new(Datatype::BrakingRearmDebug, 0, Instant::now().as_ticks())) - .await; - Timer::after_micros(10).await; + send_data!(self.data_sender, Datatype::BrakingRearmDebug, 0); + Timer::after_micros(50).await; // braking pcb only takes an instant to rearm the brakes self.braking_rearm.set_high(); - self.data_sender - .send(Datapoint::new(Datatype::BrakingRearmDebug, 1, Instant::now().as_ticks())) - .await; - - // let time_stamp = Instant::now(); - // while (Instant::now() - time_stamp) < Duration::from_millis(100) { - // if self.braking_communication.is_high() { - // self.brakes_extended = true; - // return true; - // } - // } - // false + send_data!(self.data_sender, Datatype::BrakingRearmDebug, 1); } #[allow(dead_code)] diff --git a/app/src/core/controllers/finite_state_machine_peripherals.rs b/app/src/core/controllers/finite_state_machine_peripherals.rs index 0846dbdaf..6cdde6ca0 100644 --- a/app/src/core/controllers/finite_state_machine_peripherals.rs +++ b/app/src/core/controllers/finite_state_machine_peripherals.rs @@ -84,9 +84,6 @@ impl FSMPeripherals { ) .await; - // let mut b = Output::new(p.PB0, Level::High, Speed::High); - // b.set_high(); - debug!("creating can controller"); // the can controller configures both buses and then spawns all 4 read/write tasks. let can_controller = CanController::new( @@ -120,7 +117,18 @@ impl FSMPeripherals { .await; // the propulsion controller spawns tasks for reading current and voltage, and holds functions for setting the speed through the DAC - // let propulsion_controller = PropulsionController::new(); + let propulsion_controller = PropulsionController::new( + *x, + i.data_sender, + i.event_sender, + p.PA4, + p.DAC1, + p.ADC2, + p.PA5, + p.PA6, + p.PB1, + ) + .await; debug!("peripherals initialised."); // return this struct back to the FSM @@ -136,18 +144,7 @@ impl FSMPeripherals { dc_dc: Output::new(p.PD2, Level::Low, Speed::Low), }, red_led: Output::new(p.PB14, Level::Low, Speed::High), - propulsion_controller: PropulsionController::new( - *x, - i.data_sender, - i.event_sender, - p.PA4, - p.DAC1, - p.ADC2, - p.PA5, - p.PA6, - p.PB1, - ) - .await, + propulsion_controller, led_controller, } } diff --git a/app/src/core/controllers/hv_controller.rs b/app/src/core/controllers/hv_controller.rs index 6cf2f6db5..8238b22fa 100644 --- a/app/src/core/controllers/hv_controller.rs +++ b/app/src/core/controllers/hv_controller.rs @@ -27,8 +27,6 @@ impl HVPeripherals { #[cfg(debug_assertions)] info!("HV Powered on"); info!("HV Powered on"); - info!("HV Powered on"); - info!("HV Powered on"); } pub fn power_hv_off(&mut self) { diff --git a/app/src/core/controllers/propulsion_controller.rs b/app/src/core/controllers/propulsion_controller.rs index e6db20577..010eaaa56 100644 --- a/app/src/core/controllers/propulsion_controller.rs +++ b/app/src/core/controllers/propulsion_controller.rs @@ -17,12 +17,13 @@ use embassy_time::Instant; use embassy_time::Timer; use crate::core::communication::Datapoint; -use crate::try_spawn; +use crate::{send_data, try_spawn}; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; + pub struct PropulsionController { pub speed_set_pin: embassy_stm32::dac::DacCh1<'static, DAC1>, pub enable_pin: Output<'static>, @@ -94,29 +95,15 @@ pub async fn read_prop_adc( mut pa5: PA5, mut pa6: PA6, ) { + Timer::after_millis(5000).await; loop { let v_ref_int = adc.read_internal(&mut v_ref_int_channel); let v = adc.read(&mut pa5) as u64; let i = adc.read(&mut pa6) as u64; - // #[cfg(debug_assertions)] - // defmt::info!( - // "Propulsion:\n\t voltage (pc0): {} \n\t current (pf3): {} \n\t reference: {}\n", - // v, i, v_ref_int - // ); - data_sender - .send(Datapoint::new(Datatype::PropulsionVoltage, v, Instant::now().as_ticks())) - .await; - data_sender - .send(Datapoint::new(Datatype::PropulsionCurrent, i, Instant::now().as_ticks())) - .await; - data_sender - .send(Datapoint::new( - Datatype::PropulsionVRefInt, - v_ref_int as u64, - Instant::now().as_ticks(), - )) - .await; + send_data!(data_sender, Datatype::PropulsionVoltage, v; 1000); + send_data!(data_sender, Datatype::PropulsionCurrent, i; 1000); + send_data!(data_sender, Datatype::PropulsionVRefInt, v_ref_int as u64; 1000); - Timer::after_millis(250).await; + Timer::after_millis(100).await; } } diff --git a/app/src/core/data/mod.rs b/app/src/core/data/mod.rs index 0eb58436d..094b2f4f8 100644 --- a/app/src/core/data/mod.rs +++ b/app/src/core/data/mod.rs @@ -10,7 +10,7 @@ use embassy_time::Duration; use embassy_time::Instant; use heapless::Vec; -use crate::DataReceiver; +use crate::{DataReceiver, send_data}; use crate::DataSender; use crate::Datapoint; use crate::Datatype; @@ -20,6 +20,7 @@ use crate::Info; use crate::ValueCheckResult; use crate::HEARTBEATS; use crate::HEARTBEATS_LEN; +use crate::pconfig::{queue_event, ticks}; type HB = Vec<(Datatype, Duration, Option), { HEARTBEATS_LEN }>; @@ -42,15 +43,11 @@ pub async fn data_middle_step( // 1. check thresholds match data.datatype.check_bounds(data.value) { ValueCheckResult::Fine => {}, - ValueCheckResult::Warn => { - outgoing.send(value_warning(data.datatype, data.value)).await; - }, - ValueCheckResult::Error => { - outgoing.send(value_error(data.datatype, data.value)).await; - }, + ValueCheckResult::Warn => send_data!(outgoing, Datatype::ValueWarning, data.datatype.to_id() as u64, data.value), + ValueCheckResult::Error => send_data!(outgoing, Datatype::ValueError, data.datatype.to_id() as u64, data.value), ValueCheckResult::BrakeNow => { - event_sender.send(Event::ValueOutOfBounds).await; - outgoing.send(value_critical(data.datatype, data.value)).await; + queue_event(event_sender, Event::ValueOutOfBounds).await; + send_data!(outgoing, Datatype::ValueCausedBraking, data.datatype.to_id() as u64, data.value); }, } // 2. check heartbeats @@ -66,7 +63,7 @@ pub async fn data_middle_step( .send(Datapoint::new( Datatype::HeartbeatExpired, dt.to_id() as u64, - Instant::now().as_ticks(), + ticks(), )) .await; *last = None; @@ -76,13 +73,7 @@ pub async fn data_middle_step( match hb.push((data.datatype, timeout(data.datatype), None)) { Ok(_) => {}, Err(_) => { - outgoing - .send(Datapoint::new( - Datatype::Info, - Info::lamp_error_unreachable.to_idx(), - Instant::now().as_ticks(), - )) - .await + send_data!(outgoing, Datatype::Info, Info::lamp_error_unreachable.to_idx()); }, } } @@ -100,18 +91,18 @@ fn timeout(dt: Datatype) -> Duration { } } Duration::from_millis(0) // This is unreachable, - // but as to not panic we return zero timeout. - // Since this will always be expired, it will always cause emergency braking -} - -fn value_warning(dt: Datatype, v: u64) -> Datapoint { - Datapoint::new(Datatype::ValueWarning, dt.to_id() as u64, v) -} - -fn value_error(dt: Datatype, v: u64) -> Datapoint { - Datapoint::new(Datatype::ValueError, dt.to_id() as u64, v) -} - -fn value_critical(dt: Datatype, v: u64) -> Datapoint { - Datapoint::new(Datatype::ValueCausedBraking, dt.to_id() as u64, v) + // but as to not panic we return zero timeout. + // Since this will always be expired, it will always cause emergency braking } +// +// fn value_warning(dt: Datatype, v: u64) -> Datapoint { +// Datapoint::new(Datatype::ValueWarning, dt.to_id() as u64, v) +// } +// +// fn value_error(dt: Datatype, v: u64) -> Datapoint { +// Datapoint::new(Datatype::ValueError, dt.to_id() as u64, v) +// } +// +// fn value_critical(dt: Datatype, v: u64) -> Datapoint { +// Datapoint::new(Datatype::ValueCausedBraking, dt.to_id() as u64, v) +// } diff --git a/app/src/core/states/mod.rs b/app/src/core/states/mod.rs index d5fd6337d..7b415c761 100644 --- a/app/src/core/states/mod.rs +++ b/app/src/core/states/mod.rs @@ -18,7 +18,7 @@ mod moving { use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; - use crate::transit; + use crate::{send_data, transit}; use crate::Command; use crate::Datatype; use crate::Info; diff --git a/app/src/main.rs b/app/src/main.rs index ba47e3606..5d7452c08 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -13,9 +13,8 @@ )] #![deny(clippy::async_yields_async)] #![deny(rustdoc::broken_intra_doc_links)] -// #[warn(unused_must_use)] +#[warn(unused_must_use)] -// Import absolutely EVERYTHING use ::core::borrow::Borrow; use defmt::*; @@ -61,13 +60,18 @@ bind_interrupts!(struct CanTwoInterrupts { FDCAN2_IT1 => can::IT1InterruptHandler; }); -/// Custom Data types----------------------- +// Custom Data types----------------------- + +/// A transmitter for the [`Datapoint`] MPMC [`DATA_QUEUE`] type DataSender = embassy_sync::channel::Sender<'static, NoopRawMutex, Datapoint, { DATA_QUEUE_SIZE }>; +/// A receiver for the [`Datapoint`] MPMC [`DATA_QUEUE`] type DataReceiver = embassy_sync::channel::Receiver<'static, NoopRawMutex, Datapoint, { DATA_QUEUE_SIZE }>; +/// A transmitter for the [`Event`] MPMC [`EVENT_QUEUE`] type EventSender = embassy_sync::priority_channel::Sender<'static, NoopRawMutex, Event, Max, { EVENT_QUEUE_SIZE }>; +/// A receiver for the [`Event`] MPMC [`EVENT_QUEUE`] type EventReceiver = embassy_sync::priority_channel::Receiver< 'static, NoopRawMutex, @@ -75,25 +79,33 @@ type EventReceiver = embassy_sync::priority_channel::Receiver< Max, { EVENT_QUEUE_SIZE }, >; +/// A transmitter for the [`can::frame::Frame`] MPMC [`CAN_ONE_QUEUE`]/[`CAN_TWO_QUEUE`] type CanSender = embassy_sync::channel::Sender<'static, NoopRawMutex, can::frame::Frame, { CAN_QUEUE_SIZE }>; +/// A receiver for the [`can::frame::Frame`] MPMC [`CAN_ONE_QUEUE`]/[`CAN_TWO_QUEUE`] type CanReceiver = embassy_sync::channel::Receiver<'static, NoopRawMutex, can::frame::Frame, { CAN_QUEUE_SIZE }>; -/// Static Allocations - MPMC queues +// Static Allocations - MPMC queues +/// The allocation for [`Event`]-[`embassy_sync::channel`] static EVENT_QUEUE: StaticCell> = StaticCell::new(); + +/// The allocation for [`Datapoint`]-[`embassy_sync::channel`] static DATA_QUEUE: StaticCell> = StaticCell::new(); - +/// The allocation for a [`Datapoint`]-[`embassy_sync::channel`] static PARSED_DATA_QUEUE: StaticCell> = StaticCell::new(); +/// The allocation for [`can::frame::Frame`]-[`embassy_sync::channel`] static CAN_ONE_QUEUE: StaticCell> = StaticCell::new(); +/// The allocation for [`can::frame::Frame`]-[`embassy_sync::channel`] static CAN_TWO_QUEUE: StaticCell> = StaticCell::new(); +/// Util struct for initialising [`FSMPeripherals`] pub struct InternalMessaging { event_sender: EventSender, data_sender: DataSender, diff --git a/app/src/pconfig.rs b/app/src/pconfig.rs index 46615c3d4..9358c751b 100644 --- a/app/src/pconfig.rs +++ b/app/src/pconfig.rs @@ -1,4 +1,4 @@ -use defmt::info; +use defmt::{error, info}; use embassy_net::IpAddress::Ipv4; use embassy_net::IpEndpoint; use embassy_net::Ipv4Address; @@ -7,10 +7,13 @@ use embassy_stm32::rcc; use embassy_stm32::rcc::Pll; use embassy_stm32::rcc::*; use embassy_stm32::Config; +use embassy_time::{Instant, Timer}; use embedded_can::ExtendedId; use embedded_nal_async::Ipv4Addr; use embedded_nal_async::SocketAddr; use embedded_nal_async::SocketAddrV4; +use crate::{DataSender, Datatype, Event, EventSender}; +use crate::core::communication::Datapoint; #[inline] pub fn default_configuration() -> Config { @@ -143,3 +146,118 @@ macro_rules! try_spawn { } }; } + +pub fn ticks() -> u64 { + Instant::now().as_ticks() +} + + +/// Instantly sends an event through the MPMC queue. +/// * If the queue is full, the event will be discarded. +/// * This function will *not* block +pub fn send_event(event_sender: EventSender, event: Event) { + match event_sender.try_send(event) { + Ok(_) => {} + Err(e) => { + error!("[send] event channel full: {:?}", e) + } + } +} + +// /// Instantly sends a datapoint through the MPMC queue. +// /// * If the queue is full, the event will be discarded. +// /// * This function will *not* block. +// /// * The current timestamp will be used. +// /// * If a specific value for timestamp is needed, use [`send_dp`] instead. +// pub fn send_data(data_sender: DataSender, t: Datatype, data: u64) { +// match data_sender.try_send(Datapoint::new(t, data, ticks())) { +// Ok(_) => {} +// Err(e) => { +// error!("[send] data channel full: {:?}", e) +// } +// } +// } + +#[macro_export] +macro_rules! send_data { + ($data_sender:expr, $dtype:expr, $data:expr) => { + { + if let Err(e) = $data_sender.try_send( + Datapoint::new($dtype, $data, crate::pconfig::ticks()) + ) { + defmt::error!("[send] data channel full: {:?}", e); + } + } + }; + ($data_sender:expr, $dtype:expr, $data:expr, $timestamp:expr) => { + { + if let Err(e) = $data_sender.try_send( + Datapoint::new($dtype, $data, $timestamp) + ) { + defmt::error!("[send] data channel full: {:?}", e); + } + } + }; + ($data_sender:expr, $dtype:expr, $data:expr; $timeout:expr) => { + { + if let Err(e) = $data_sender.try_send( + Datapoint::new($dtype, $data, crate::pconfig::ticks()) + ) { + defmt::error!("[send] data channel full: {:?}", e); + Timer::after_millis($timeout).await; + } + } + }; + ($data_sender:expr, $dtype:expr, $data:expr, $timestamp:expr; $timeout:expr) => { + { + if let Err(e) = $data_sender.try_send( + Datapoint::new($dtype, $data, $timestamp) + ) { + defmt::error!("[send] data channel full: {:?}", e); + Timer::after_millis($timeout).await; + } + } + }; +} + +// /// Instantly sends a datapoint through the MPMC queue. +// /// * If the queue is full, the event will be discarded. +// /// * This function will *not* block. +// /// * You need to specify a value for the timestamp (doesn't have to be a timestamp) +// /// * If you just want a timestamp as the timestamp, use [`send_data`] instead. +// pub fn send_dp(data_sender: DataSender, t: Datatype, d: u64, p: u64) { +// match data_sender.try_send(Datapoint::new(t, d, p)) { +// Ok(_) => {} +// Err(e) => { +// error!("[send] data channel full: {:?}", e) +// } +// } +// } + +/// Block the current task in order to send an event through the MPMC queue. +/// * If there's space in the event queue, this will complete instantly +/// * Otherwise it will `await` until there's space. +/// * *If the event isn't critical and the current task +/// shouldn't block, please use [`send_event`] instead* +pub async fn queue_event(event_sender: EventSender, event: Event) { + event_sender.send(event).await +} + +/// Block the current task in order to send a datapoint through the MPMC queue. +/// * If there's space in the event queue, this will complete instantly +/// * Otherwise it will `await` until there's space. +/// * *If the event isn't critical and the current task +/// shouldn't block, please use [`send_event`] instead* +pub async fn queue_data(data_sender: DataSender, t: Datatype, data: u64) { + data_sender.send(Datapoint::new(t, data, ticks())).await +} + +/// Block the current task in order to send a datapoint through the MPMC queue. +/// * If there's space in the event queue, this will complete instantly +/// * Otherwise it will `await` until there's space. +/// * *If the event isn't critical and the current task +/// shouldn't block, please use [`send_event`] instead* +/// * If you don't need to specify a specific timestamp, use [`queue_data`] instead +pub async fn queue_dp(data_sender: DataSender, t: Datatype, d: u64, p: u64) { + data_sender.send(Datapoint::new(t, d, p)).await +} \ No newline at end of file diff --git a/config/commands.toml b/config/commands.toml index 5eef78b85..19ac98af8 100644 --- a/config/commands.toml +++ b/config/commands.toml @@ -6,6 +6,10 @@ id = 0x01 name = "Heartbeat" id = 0x042 +[[Command]] +name = "FrontendHeartbeat" +id = 0x043 + # LEVITATION COMMANDS [[Command]] diff --git a/config/config.toml b/config/config.toml index 78dab2220..ea5312c66 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,5 +1,5 @@ [gs] -ip = [192,168,1,2] +ip = [192,168,0,93] force = true port = 6942 buffer_size = 1460 # this is the MAXIMUM size of messages transmitted, in bytes. @@ -17,8 +17,8 @@ mac_addr = [0x00, 0x1e, 0x67, 0x4c, 0x5c, 0x3e] keep_alive = 1000 # keep alive interval, in milliseconds. [pod.internal] -event_queue_size = 128 -data_queue_size = 256 +event_queue_size = 256 +data_queue_size = 1024 can_queue_size = 128 [pod.comm] @@ -26,13 +26,14 @@ bms_lv_ids = [0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1 bms_hv_ids = [0x3A0, 0x3A1, 0x3A2, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3C0, 0x3E0, 0x400, 0x4A0, 0x425, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD,0x4A1, 0x4A3,0x4A4,0x4A5,0x4A6,0x4A7,0x4A8, 0x4A9,0x4AA,0x4AB,0x4AC,0x4AD] gfd_ids = [0x37,0x38,0x39] sensor_hub = [0x1b,0x1c,0x1d,0x15] -levi_requested_data = ["Localisation", "PropulsionCurrent", "AccelerationX", "AccelerationY", "AccelerationZ"] +levi_requested_data = ["Localisation", "PropulsionCurrent"] [pod.heartbeats] # syntax: # = -LocalisationHeartbeat = 1000 -# SensorHubHeartbeat = 1000 +LocalisationHeartbeat = 1500 +FrontendHeartbeating = 1500 +SensorHubHeartbeat = 1500 [[Info]] @@ -132,6 +133,10 @@ colour = "green" label = "EventsHashPassed" colour = "green" +[[Info]] +label = "ConfigHashPassed" +colour = "green" + [[Info]] label = "InvalidRouteConfigurationAbortingRun" colour = "red" diff --git a/config/datatypes.toml b/config/datatypes.toml index c1d391091..f5bc92b40 100644 --- a/config/datatypes.toml +++ b/config/datatypes.toml @@ -903,6 +903,10 @@ id = 0x168 name = "DataHash" id = 0x169 +[[Datatype]] +name = "ConfigHash" +id = 0x171 + [[Datatype]] name = "ValueError" id = 0x16a @@ -927,6 +931,10 @@ id = 0x16e name = "SensorHubHeartbeat" id = 0x16f +[[Datatype]] +name = "FrontendHeartbeating" +id = 0x170 + [[Datatype]] name = "HeartbeatExpired" id = 0x7fd diff --git a/gs/station/.clippy.toml b/gs/station/.clippy.toml new file mode 100644 index 000000000..6d5318e71 --- /dev/null +++ b/gs/station/.clippy.toml @@ -0,0 +1 @@ +absolute-paths-max-segments = 4 \ No newline at end of file diff --git a/gs/station/build.rs b/gs/station/build.rs index c2cf8be2a..04901cc2b 100644 --- a/gs/station/build.rs +++ b/gs/station/build.rs @@ -8,7 +8,7 @@ use std::path::Path; use std::path::PathBuf; use anyhow::Result; -use goose_utils::check_ids; +use goose_utils::check_config; use goose_utils::commands::generate_commands; use goose_utils::datatypes::generate_datatypes; use goose_utils::events::generate_events; @@ -64,12 +64,12 @@ fn main() -> Result<()> { let dest_path = Path::new(&out_dir).join("config.rs"); let gs_file = fs::read_to_string(CONFIG_PATH)?; - let _ = check_ids(DATATYPES_PATH, COMMANDS_PATH, EVENTS_PATH); - let config: Config = toml::from_str(&gs_file)?; let mut content = String::from("//@generated\n"); + content.push_str(&check_config(DATATYPES_PATH, COMMANDS_PATH, EVENTS_PATH, CONFIG_PATH)?); + content.push_str(&configure_gs(&config)); content.push_str(&configure_gs_ip(config.gs.ip, config.gs.port, config.gs.force)?); let dt = generate_datatypes(DATATYPES_PATH, true)?; diff --git a/gs/station/src/connect/handle_incoming_data.rs b/gs/station/src/connect/handle_incoming_data.rs index 102c15d58..5ddf82341 100644 --- a/gs/station/src/connect/handle_incoming_data.rs +++ b/gs/station/src/connect/handle_incoming_data.rs @@ -9,6 +9,7 @@ use crate::Datatype; use crate::Info; use crate::MessageSender; use crate::COMMAND_HASH; +use crate::CONFIG_HASH; use crate::DATA_HASH; use crate::EVENTS_HASH; @@ -44,6 +45,13 @@ pub async fn handle_incoming_data( msg_sender.send(Message::Status(Info::EventsHashPassed))?; } }, + Datatype::ConfigHash => { + if data.value != CONFIG_HASH { + msg_sender.send(Message::Error("Config hash mismatch".to_string()))?; + } else { + msg_sender.send(Message::Status(Info::ConfigHashPassed))?; + } + }, _ => {}, } diff --git a/gs/station/src/connect/tcp_writer.rs b/gs/station/src/connect/tcp_writer.rs index cb4a66ef1..85c1d9c20 100644 --- a/gs/station/src/connect/tcp_writer.rs +++ b/gs/station/src/connect/tcp_writer.rs @@ -48,15 +48,16 @@ pub async fn transmit_commands_to_tcp( match writer.write_all(&bytes).await { Ok(_) => { last_send_timestamp = std::time::Instant::now(); - status_transmitter - .send(crate::api::Message::Info(format!( - "[tcp] Sent command: {:?}", - command - ))) - .expect("messaging channel closed, cannot recover"); + // status_transmitter + // .send(Message::Info(format!( + // "[tcp] Sent command: {:?}", + // command + // ))) + // .expect("messaging channel closed, cannot recover"); }, Err(e) => { - eprintln!("Error sending command over tcp: {:?}", e); + // eprintln!("Error sending command over tcp: {:?}", e); + status_transmitter.send(Error(format!("Error sending command over tcp: {:?}", e))).expect("message channel closed"); break; }, } diff --git a/gs/station/src/frontend/app.rs b/gs/station/src/frontend/app.rs index 513fdbad5..bb8caf3de 100644 --- a/gs/station/src/frontend/app.rs +++ b/gs/station/src/frontend/app.rs @@ -1,8 +1,10 @@ use std::sync::Mutex; +use std::time::Duration; use tauri::GlobalShortcutManager; use tauri::Manager; use tauri::WindowEvent; +use tokio::time::sleep; use crate::api::Message; use crate::backend::Backend; @@ -14,6 +16,7 @@ use crate::INFO_CHANNEL; use crate::SHORTCUT_CHANNEL; use crate::STATUS_CHANNEL; use crate::WARNING_CHANNEL; +use crate::HEARTBEAT; pub fn tauri_main(backend: Backend) { println!("Starting tauri application"); @@ -41,8 +44,17 @@ pub fn tauri_main(backend: Backend) { BACKEND.replace(Mutex::new(backend)); } - // set up shortcuts + let s = app_handle.clone(); + + // set up heartbeat + tokio::spawn(async move { + loop { + s.emit_all(SHORTCUT_CHANNEL, "heartbeat").unwrap(); + sleep(Duration::from_millis(HEARTBEAT)).await; + } + }); + // set up shortcuts let s = app_handle.clone(); let shortcuts = app_handle.global_shortcut_manager(); diff --git a/gs/station/src/main.rs b/gs/station/src/main.rs index 617f9aabc..37220997f 100644 --- a/gs/station/src/main.rs +++ b/gs/station/src/main.rs @@ -1,5 +1,6 @@ // Prevents an additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#![warn(clippy::absolute_paths)] use crate::api::Message; use crate::backend::Backend; diff --git a/gs/station/src/tui/app.rs b/gs/station/src/tui/app.rs index 24045aae9..45c36c3df 100644 --- a/gs/station/src/tui/app.rs +++ b/gs/station/src/tui/app.rs @@ -1,4 +1,6 @@ use std::collections::BTreeMap; +use std::time::Duration; +use std::time::Instant; use ratatui::Frame; @@ -10,10 +12,12 @@ use crate::backend::Backend; use crate::tui::render::CmdRow; use crate::tui::timestamp; use crate::tui::Tui; +use crate::Command; use crate::Datatype; use crate::Event; use crate::Info; use crate::COMMANDS_LIST; +use crate::HEARTBEAT; #[allow(dead_code)] pub struct App { @@ -30,6 +34,9 @@ pub struct App { pub special_data: BTreeMap, pub backend: Backend, pub safe: bool, + pub last_sent_heartbeat: Instant, + pub received_bytes: u64, + pub throughput: f64, } impl App { @@ -46,6 +53,7 @@ impl App { cur_state: "None Yet".to_string(), last_heartbeat: "None Yet".to_string(), special_data: BTreeMap::from([ + (Datatype::FrontendHeartbeating, 0.0), (Datatype::InsulationNegative, 0.0), (Datatype::InsulationPositive, 0.0), (Datatype::InsulationOriginal, 0.0), @@ -79,6 +87,9 @@ impl App { ]), backend, safe: true, + last_sent_heartbeat: Instant::now(), + received_bytes: 0, + throughput: 0.0, } } @@ -96,6 +107,7 @@ impl App { fn receive_data(&mut self) { while let Ok(msg) = self.backend.message_receiver.try_recv() { + self.received_bytes = self.received_bytes.wrapping_add(20); self.backend.log_msg(&msg); match msg { Message::Data(datapoint) => match datapoint.datatype { @@ -167,6 +179,14 @@ impl App { }, } } + if self.last_sent_heartbeat.elapsed() > Duration::from_millis(HEARTBEAT) { + self.backend + .command_transmitter + .send(Command::FrontendHeartbeat(10)) + .expect("backend failed"); + self.last_sent_heartbeat = Instant::now(); + self.received_bytes = 0; + } } pub fn quit(&mut self) { diff --git a/gs/station/src/tui/interactions.rs b/gs/station/src/tui/interactions.rs index e9001b303..ccf3e3472 100644 --- a/gs/station/src/tui/interactions.rs +++ b/gs/station/src/tui/interactions.rs @@ -45,14 +45,14 @@ impl App { KeyCode::Esc => self.backend.send_command(crate::Command::EmergencyBrake(0)), KeyCode::Up => self.scroll_up(1), KeyCode::Down => self.scroll_down(1), - KeyCode::Char('k') | KeyCode::Char('j') => self.scroll_down(10), - KeyCode::Char('m') => self.scroll_down(10000), - KeyCode::Char('u') => self.scroll_up(10000), - KeyCode::Char('i') => self.scroll_up(10), + KeyCode::Char('j') => self.scroll_down(10), + KeyCode::Char('h') => self.scroll_down(10000), + KeyCode::Char('l') => self.scroll_up(10000), + KeyCode::Char('k') => self.scroll_up(10), KeyCode::Char('s') => { self.backend.start_server(); }, - KeyCode::Char('l') => self.backend.start_levi(), + KeyCode::Char('o') => self.backend.start_levi(), // KeyCode::Char('t') => self.logs.push((LogType::Warning, format!("{}: this is a testing goose",Util::Now()).parse()?)), KeyCode::Tab => { self.selected_row = (self.selected_row + 1) % self.cmds.len(); @@ -101,10 +101,10 @@ impl App { self.backend.send_command(Command::SetRoute(1822648539875311616)); self.backend.send_command(Command::SetSpeeds(14106055789030410752)); }, - KeyCode::Char('o') => { - self.backend.send_command(Command::SetRoute(8328165916070586159)); - self.backend.send_command(Command::SetSpeeds(46542390612732)); - }, + // KeyCode::Char('o') => { + // self.backend.send_command(Command::SetRoute(8328165916070586159)); + // self.backend.send_command(Command::SetSpeeds(46542390612732)); + // }, KeyCode::Char('w') => { self.logs.push((self.backend.save().unwrap(), timestamp())); }, diff --git a/gs/station/src/tui/render.rs b/gs/station/src/tui/render.rs index 7e8a0faeb..af49b72c5 100644 --- a/gs/station/src/tui/render.rs +++ b/gs/station/src/tui/render.rs @@ -1,3 +1,4 @@ +use std::ops::Div; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::block::*; @@ -15,9 +16,7 @@ pub struct CmdRow { } impl CmdRow { - pub fn to_row(&self) -> ratatui::widgets::Row { - ratatui::widgets::Row::new(vec![self.name.clone(), self.value.to_string()]) - } + pub fn to_row(&self) -> Row { Row::new(vec![self.name.clone(), self.value.to_string()]) } pub fn as_cmd(&self) -> Command { Command::from_string(self.name.trim(), self.value) } } @@ -39,22 +38,25 @@ impl Widget for &App { let title = Title::from(" Goose™ Ground Station ultimate ".light_green().bold()); let instructions = Title::from(Line::from(vec![ "Scroll Up".light_blue(), - " ".light_cyan().bold(), + " ".light_cyan().bold(), " –– ".set_style(safety_style), - "Scroll Down".light_blue(), - " ".light_cyan().bold(), + "Down".light_blue(), + " ".light_cyan().bold(), " –– ".set_style(safety_style), - "Scroll to End".light_blue(), - " ".light_cyan().bold(), + "to end".light_blue(), + " ".light_cyan().bold(), " –– ".set_style(safety_style), - "Emergency Brake".red(), + "Brake".red(), " ".light_red().bold(), " –– ".set_style(safety_style), - "Launch Station".light_green(), - " ".light_green().bold(), + "Launch (station, levi)".light_green(), + " ".light_green().bold(), " –– ".set_style(safety_style), "Quit".magenta(), - " ".light_magenta().bold(), + " ".light_magenta().bold(), + " ––– ".set_style(safety_style), + "throughput".green(), + format!(" <{:.2} bytes/s> ", (self.received_bytes as f64).div(self.last_sent_heartbeat.elapsed().as_secs_f64())).green(), " ––––––– ".set_style(safety_style), "timestamp:".light_blue(), format!(" <{}> ", timestamp()).light_blue(), diff --git a/util/src/datatypes.rs b/util/src/datatypes.rs index 2c466f0ad..8782d8146 100644 --- a/util/src/datatypes.rs +++ b/util/src/datatypes.rs @@ -228,7 +228,7 @@ impl Datatype {{ if drv { "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, PartialOrd, Ord)]" } else { - "#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]" + "#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, defmt::Format)]" }, enum_definitions, match_to_id, diff --git a/util/src/lib.rs b/util/src/lib.rs index a400ce73f..7c3c4b625 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -1,5 +1,9 @@ use std::cmp::min; use std::collections::HashSet; +use std::fs::read_to_string; +use std::hash::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; pub mod commands; pub mod datatypes; @@ -10,7 +14,7 @@ pub mod limits; mod shared; use anyhow::Result; -pub fn check_ids(dp: &str, cp: &str, ep: &str) -> Result<()> { +pub fn check_config(dp: &str, cp: &str, ep: &str, conf: &str) -> Result { let mut items = vec![]; let d = datatypes::get_data_items(dp)?; let c = commands::get_command_items(cp)?; @@ -68,7 +72,12 @@ pub fn check_ids(dp: &str, cp: &str, ep: &str) -> Result<()> { ); } } - Ok(()) + + let cs = read_to_string(conf)?; + let mut hasher = DefaultHasher::new(); + cs.hash(&mut hasher); + let hash = hasher.finish(); + Ok(format!("\npub const CONFIG_HASH: u64 = {};\n", hash)) } fn nearest_id(id: u16, ids: &[u16]) -> u16 { From 60afe790853e09e7053572ac2922de2bef0c428f Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 14 Jul 2024 14:24:27 +0200 Subject: [PATCH 41/41] fmt and clippy --- app/src/core/communication/can.rs | 20 ++++++++-- app/src/core/communication/mod.rs | 9 ++++- app/src/core/communication/tcp.rs | 18 ++++++--- .../core/controllers/battery_controller.rs | 9 +++-- .../core/controllers/breaking_controller.rs | 10 +++-- .../finite_state_machine_peripherals.rs | 2 +- .../core/controllers/propulsion_controller.rs | 6 +-- app/src/core/data/mod.rs | 34 +++++++++++------ app/src/core/states/mod.rs | 2 +- app/src/main.rs | 15 +------- app/src/pconfig.rs | 38 +++++++++---------- gs/station/src/connect/tcp_writer.rs | 4 +- gs/station/src/frontend/app.rs | 2 +- gs/station/src/tui/render.rs | 7 +++- 14 files changed, 103 insertions(+), 73 deletions(-) diff --git a/app/src/core/communication/can.rs b/app/src/core/communication/can.rs index f6deefb01..8bafca0db 100644 --- a/app/src/core/communication/can.rs +++ b/app/src/core/communication/can.rs @@ -13,9 +13,11 @@ use crate::core::communication::Datapoint; use crate::core::controllers::battery_controller::ground_fault_detection_isolation_details; use crate::core::controllers::battery_controller::ground_fault_detection_voltage_details; use crate::core::controllers::can_controller::CanTwoUtils; -use crate::pconfig::{bytes_to_u64, send_event}; +use crate::pconfig::bytes_to_u64; use crate::pconfig::id_as_value; -use crate::{CanReceiver, send_data}; +use crate::pconfig::send_event; +use crate::send_data; +use crate::CanReceiver; use crate::CanSender; use crate::DataSender; use crate::Datatype; @@ -59,7 +61,12 @@ pub async fn can_receiving_handler( let (frame, timestamp) = envelope.parts(); let id = id_as_value(frame.id()); #[cfg(debug_assertions)] - send_data!(data_sender, Datatype::ReceivedCan, id as u64, bytes_to_u64(frame.data())); + send_data!( + data_sender, + Datatype::ReceivedCan, + id as u64, + bytes_to_u64(frame.data()) + ); #[cfg(debug_assertions)] info!("[CAN ({})] received frame: id={:?} data={:?}", bus_nr, id, frame.data()); if DATA_IDS.contains(&id) { @@ -122,7 +129,12 @@ pub async fn can_receiving_handler( } else { #[cfg(debug_assertions)] info!("[CAN ({})] unknown ID: {:?}", bus_nr, id); - send_data!(data_sender, Datatype::UnknownCanId, id as u64, bytes_to_u64(frame.data())); + send_data!( + data_sender, + Datatype::UnknownCanId, + id as u64, + bytes_to_u64(frame.data()) + ); } }, Err(e) => { diff --git a/app/src/core/communication/mod.rs b/app/src/core/communication/mod.rs index f40335da7..29232df7a 100644 --- a/app/src/core/communication/mod.rs +++ b/app/src/core/communication/mod.rs @@ -1,4 +1,5 @@ use defmt::Formatter; + use crate::Datatype; pub mod can; @@ -37,6 +38,12 @@ impl Datapoint { impl defmt::Format for Datapoint { fn format(&self, fmt: Formatter) { - defmt::write!(fmt, "Datapoint {{ datatype: {:?}, value: {:?}, timestamp: {:?} }}", self.datatype, self.value, self.timestamp) + defmt::write!( + fmt, + "Datapoint {{ datatype: {:?}, value: {:?}, timestamp: {:?} }}", + self.datatype, + self.value, + self.timestamp + ) } } diff --git a/app/src/core/communication/tcp.rs b/app/src/core/communication/tcp.rs index 10770e11c..cb91f50d6 100644 --- a/app/src/core/communication/tcp.rs +++ b/app/src/core/communication/tcp.rs @@ -6,7 +6,6 @@ use embassy_net::Stack; use embassy_stm32::eth::generic_smi::GenericSMI; use embassy_stm32::eth::Ethernet; use embassy_stm32::peripherals::ETH; -use embassy_sync::channel::TrySendError; use embassy_time::Instant; use embassy_time::Timer; use embedded_io_async::Write; @@ -15,13 +14,19 @@ use heapless::Deque; use panic_probe as _; use crate::core::communication::Datapoint; -use crate::pconfig::{embassy_socket_from_config, queue_data, queue_event, send_event, ticks}; -use crate::{Command, Info, send_data}; +use crate::pconfig::embassy_socket_from_config; +use crate::pconfig::queue_data; +use crate::pconfig::queue_event; +use crate::pconfig::send_event; +use crate::pconfig::ticks; +use crate::send_data; +use crate::Command; use crate::DataReceiver; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; +use crate::Info; use crate::COMMAND_HASH; use crate::CONFIG_HASH; use crate::DATA_HASH; @@ -88,7 +93,6 @@ pub async fn tcp_connection_handler( queue_data(data_sender, Datatype::DataHash, DATA_HASH).await; queue_data(data_sender, Datatype::ConfigHash, CONFIG_HASH).await; - // Begin relying on the frontend queue_data(data_sender, Datatype::FrontendHeartbeating, 0).await; @@ -232,8 +236,10 @@ pub async fn tcp_connection_handler( Command::EmitEvent(e) => { #[cfg(debug_assertions)] info!("[tcp] EmitEvent command received"); - send_event(event_sender, Event::from_id((e & 0xFFFF) as u16, Some(69420))); - + send_event( + event_sender, + Event::from_id((e & 0xFFFF) as u16, Some(69420)), + ); }, Command::CreateDatapoint(x) => { send_data!(data_sender, Datatype::from_id(x as u16), x); diff --git a/app/src/core/controllers/battery_controller.rs b/app/src/core/controllers/battery_controller.rs index 8191cf945..b0b746695 100644 --- a/app/src/core/controllers/battery_controller.rs +++ b/app/src/core/controllers/battery_controller.rs @@ -1,8 +1,9 @@ -use defmt::{debug, trace}; -use defmt::info; +use defmt::debug; +use defmt::trace; use crate::core::communication::Datapoint; -use crate::pconfig::{bytes_to_u64, queue_data, queue_dp}; +use crate::pconfig::bytes_to_u64; +use crate::pconfig::queue_dp; use crate::DataSender; use crate::Datatype; use crate::EventSender; @@ -211,7 +212,7 @@ impl BatteryController { pub async fn battery_voltage_overall_bms(&mut self, data: &[u8], timestamp: u64) { let min_cell_voltage = data[0] as u64 + 200; //VOLTAGE scaled by 100 - //Should be a decimal number so multiplying it by 100 is not such a bad idea + //Should be a decimal number so multiplying it by 100 is not such a bad idea let max_cell_voltage = data[1] as u64 + 200; //VOLTAGE let avg_cell_voltage = data[0] as u64 + 200; //VOLTAGE let total_pack_voltage = (((data[5] as u64) << 24) diff --git a/app/src/core/controllers/breaking_controller.rs b/app/src/core/controllers/breaking_controller.rs index f538fb7fc..12933241c 100644 --- a/app/src/core/controllers/breaking_controller.rs +++ b/app/src/core/controllers/breaking_controller.rs @@ -12,13 +12,13 @@ use embassy_time::Duration; use embassy_time::Instant; use embassy_time::Timer; -use crate::core::communication::Datapoint; -use crate::{send_data, try_spawn}; +use crate::pconfig::queue_event; +use crate::send_data; +use crate::try_spawn; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; -use crate::pconfig::{queue_event}; pub static mut BRAKE: bool = false; @@ -78,7 +78,9 @@ async fn read_braking_communication( let is_activated = v < 25000; // when braking comm goes low, someone else triggered brakes (eg big red button) if edge && is_activated { edge = false; // braking comm value is low, so we don't brake until it goes high again - if unsafe { !DISABLE_BRAKING_COMMUNICATION } { queue_event(event_sender, Event::EmergencyBraking).await; } + if unsafe { !DISABLE_BRAKING_COMMUNICATION } { + queue_event(event_sender, Event::EmergencyBraking).await; + } Timer::after_millis(1000).await; } if !edge && !is_activated { diff --git a/app/src/core/controllers/finite_state_machine_peripherals.rs b/app/src/core/controllers/finite_state_machine_peripherals.rs index 6cdde6ca0..1bec19c7a 100644 --- a/app/src/core/controllers/finite_state_machine_peripherals.rs +++ b/app/src/core/controllers/finite_state_machine_peripherals.rs @@ -128,7 +128,7 @@ impl FSMPeripherals { p.PA6, p.PB1, ) - .await; + .await; debug!("peripherals initialised."); // return this struct back to the FSM diff --git a/app/src/core/controllers/propulsion_controller.rs b/app/src/core/controllers/propulsion_controller.rs index 010eaaa56..559316183 100644 --- a/app/src/core/controllers/propulsion_controller.rs +++ b/app/src/core/controllers/propulsion_controller.rs @@ -13,17 +13,15 @@ use embassy_stm32::peripherals::PA4; use embassy_stm32::peripherals::PA5; use embassy_stm32::peripherals::PA6; use embassy_stm32::peripherals::PB1; -use embassy_time::Instant; use embassy_time::Timer; -use crate::core::communication::Datapoint; -use crate::{send_data, try_spawn}; +use crate::send_data; +use crate::try_spawn; use crate::DataSender; use crate::Datatype; use crate::Event; use crate::EventSender; - pub struct PropulsionController { pub speed_set_pin: embassy_stm32::dac::DacCh1<'static, DAC1>, pub enable_pin: Output<'static>, diff --git a/app/src/core/data/mod.rs b/app/src/core/data/mod.rs index 094b2f4f8..89ff2e25c 100644 --- a/app/src/core/data/mod.rs +++ b/app/src/core/data/mod.rs @@ -10,7 +10,10 @@ use embassy_time::Duration; use embassy_time::Instant; use heapless::Vec; -use crate::{DataReceiver, send_data}; +use crate::pconfig::queue_event; +use crate::pconfig::ticks; +use crate::send_data; +use crate::DataReceiver; use crate::DataSender; use crate::Datapoint; use crate::Datatype; @@ -20,7 +23,6 @@ use crate::Info; use crate::ValueCheckResult; use crate::HEARTBEATS; use crate::HEARTBEATS_LEN; -use crate::pconfig::{queue_event, ticks}; type HB = Vec<(Datatype, Duration, Option), { HEARTBEATS_LEN }>; @@ -43,11 +45,23 @@ pub async fn data_middle_step( // 1. check thresholds match data.datatype.check_bounds(data.value) { ValueCheckResult::Fine => {}, - ValueCheckResult::Warn => send_data!(outgoing, Datatype::ValueWarning, data.datatype.to_id() as u64, data.value), - ValueCheckResult::Error => send_data!(outgoing, Datatype::ValueError, data.datatype.to_id() as u64, data.value), + ValueCheckResult::Warn => send_data!( + outgoing, + Datatype::ValueWarning, + data.datatype.to_id() as u64, + data.value + ), + ValueCheckResult::Error => { + send_data!(outgoing, Datatype::ValueError, data.datatype.to_id() as u64, data.value) + }, ValueCheckResult::BrakeNow => { queue_event(event_sender, Event::ValueOutOfBounds).await; - send_data!(outgoing, Datatype::ValueCausedBraking, data.datatype.to_id() as u64, data.value); + send_data!( + outgoing, + Datatype::ValueCausedBraking, + data.datatype.to_id() as u64, + data.value + ); }, } // 2. check heartbeats @@ -60,11 +74,7 @@ pub async fn data_middle_step( } else if last.is_some_and(|l| l.elapsed() > *out) { event_sender.send(Event::EmergencyBraking).await; outgoing - .send(Datapoint::new( - Datatype::HeartbeatExpired, - dt.to_id() as u64, - ticks(), - )) + .send(Datapoint::new(Datatype::HeartbeatExpired, dt.to_id() as u64, ticks())) .await; *last = None; } @@ -91,8 +101,8 @@ fn timeout(dt: Datatype) -> Duration { } } Duration::from_millis(0) // This is unreachable, - // but as to not panic we return zero timeout. - // Since this will always be expired, it will always cause emergency braking + // but as to not panic we return zero timeout. + // Since this will always be expired, it will always cause emergency braking } // // fn value_warning(dt: Datatype, v: u64) -> Datapoint { diff --git a/app/src/core/states/mod.rs b/app/src/core/states/mod.rs index 7b415c761..d5fd6337d 100644 --- a/app/src/core/states/mod.rs +++ b/app/src/core/states/mod.rs @@ -18,7 +18,7 @@ mod moving { use crate::core::finite_state_machine::State; use crate::core::fsm_status::Location; use crate::core::fsm_status::RouteUse; - use crate::{send_data, transit}; + use crate::transit; use crate::Command; use crate::Datatype; use crate::Info; diff --git a/app/src/main.rs b/app/src/main.rs index 5d7452c08..f22fb54c4 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -1,21 +1,8 @@ #![no_std] #![no_main] -#![allow( -// unused_must_use, -// unused_imports, -// unused_variables, -// unused_mut, -// dead_code, -// unreachable_code, -// unused_doc_comments, -// incomplete_features, - clippy::too_many_arguments -)] +#![allow(clippy::too_many_arguments)] #![deny(clippy::async_yields_async)] #![deny(rustdoc::broken_intra_doc_links)] -#[warn(unused_must_use)] - - use ::core::borrow::Borrow; use defmt::*; use defmt_rtt as _; diff --git a/app/src/pconfig.rs b/app/src/pconfig.rs index 9358c751b..70bc9d111 100644 --- a/app/src/pconfig.rs +++ b/app/src/pconfig.rs @@ -1,4 +1,5 @@ -use defmt::{error, info}; +use defmt::error; +use defmt::info; use embassy_net::IpAddress::Ipv4; use embassy_net::IpEndpoint; use embassy_net::Ipv4Address; @@ -7,13 +8,17 @@ use embassy_stm32::rcc; use embassy_stm32::rcc::Pll; use embassy_stm32::rcc::*; use embassy_stm32::Config; -use embassy_time::{Instant, Timer}; +use embassy_time::Instant; use embedded_can::ExtendedId; use embedded_nal_async::Ipv4Addr; use embedded_nal_async::SocketAddr; use embedded_nal_async::SocketAddrV4; -use crate::{DataSender, Datatype, Event, EventSender}; + use crate::core::communication::Datapoint; +use crate::DataSender; +use crate::Datatype; +use crate::Event; +use crate::EventSender; #[inline] pub fn default_configuration() -> Config { @@ -147,20 +152,17 @@ macro_rules! try_spawn { }; } -pub fn ticks() -> u64 { - Instant::now().as_ticks() -} - +pub fn ticks() -> u64 { Instant::now().as_ticks() } /// Instantly sends an event through the MPMC queue. /// * If the queue is full, the event will be discarded. /// * This function will *not* block pub fn send_event(event_sender: EventSender, event: Event) { match event_sender.try_send(event) { - Ok(_) => {} + Ok(_) => {}, Err(e) => { error!("[send] event channel full: {:?}", e) - } + }, } } @@ -183,7 +185,7 @@ macro_rules! send_data { ($data_sender:expr, $dtype:expr, $data:expr) => { { if let Err(e) = $data_sender.try_send( - Datapoint::new($dtype, $data, crate::pconfig::ticks()) + $crate::Datapoint::new($dtype, $data, $crate::pconfig::ticks()) ) { defmt::error!("[send] data channel full: {:?}", e); } @@ -192,7 +194,7 @@ macro_rules! send_data { ($data_sender:expr, $dtype:expr, $data:expr, $timestamp:expr) => { { if let Err(e) = $data_sender.try_send( - Datapoint::new($dtype, $data, $timestamp) + $crate::Datapoint::new($dtype, $data, $timestamp) ) { defmt::error!("[send] data channel full: {:?}", e); } @@ -201,20 +203,20 @@ macro_rules! send_data { ($data_sender:expr, $dtype:expr, $data:expr; $timeout:expr) => { { if let Err(e) = $data_sender.try_send( - Datapoint::new($dtype, $data, crate::pconfig::ticks()) + $crate::Datapoint::new($dtype, $data, $crate::pconfig::ticks()) ) { defmt::error!("[send] data channel full: {:?}", e); - Timer::after_millis($timeout).await; + embassy_time::Timer::after_millis($timeout).await; } } }; ($data_sender:expr, $dtype:expr, $data:expr, $timestamp:expr; $timeout:expr) => { { if let Err(e) = $data_sender.try_send( - Datapoint::new($dtype, $data, $timestamp) + $crate::core::communication::Datapoint::new($dtype, $data, $timestamp) ) { defmt::error!("[send] data channel full: {:?}", e); - Timer::after_millis($timeout).await; + embassy_time::Timer::after_millis($timeout).await; } } }; @@ -239,9 +241,7 @@ macro_rules! send_data { /// * Otherwise it will `await` until there's space. /// * *If the event isn't critical and the current task /// shouldn't block, please use [`send_event`] instead* -pub async fn queue_event(event_sender: EventSender, event: Event) { - event_sender.send(event).await -} +pub async fn queue_event(event_sender: EventSender, event: Event) { event_sender.send(event).await } /// Block the current task in order to send a datapoint through the MPMC queue. /// * If there's space in the event queue, this will complete instantly @@ -260,4 +260,4 @@ pub async fn queue_data(data_sender: DataSender, t: Datatype, data: u64) { /// * If you don't need to specify a specific timestamp, use [`queue_data`] instead pub async fn queue_dp(data_sender: DataSender, t: Datatype, d: u64, p: u64) { data_sender.send(Datapoint::new(t, d, p)).await -} \ No newline at end of file +} diff --git a/gs/station/src/connect/tcp_writer.rs b/gs/station/src/connect/tcp_writer.rs index 85c1d9c20..da957c88d 100644 --- a/gs/station/src/connect/tcp_writer.rs +++ b/gs/station/src/connect/tcp_writer.rs @@ -57,7 +57,9 @@ pub async fn transmit_commands_to_tcp( }, Err(e) => { // eprintln!("Error sending command over tcp: {:?}", e); - status_transmitter.send(Error(format!("Error sending command over tcp: {:?}", e))).expect("message channel closed"); + status_transmitter + .send(Error(format!("Error sending command over tcp: {:?}", e))) + .expect("message channel closed"); break; }, } diff --git a/gs/station/src/frontend/app.rs b/gs/station/src/frontend/app.rs index bb8caf3de..c83d2ac77 100644 --- a/gs/station/src/frontend/app.rs +++ b/gs/station/src/frontend/app.rs @@ -12,11 +12,11 @@ use crate::frontend::commands::*; use crate::frontend::BackendState; use crate::frontend::BACKEND; use crate::ERROR_CHANNEL; +use crate::HEARTBEAT; use crate::INFO_CHANNEL; use crate::SHORTCUT_CHANNEL; use crate::STATUS_CHANNEL; use crate::WARNING_CHANNEL; -use crate::HEARTBEAT; pub fn tauri_main(backend: Backend) { println!("Starting tauri application"); diff --git a/gs/station/src/tui/render.rs b/gs/station/src/tui/render.rs index af49b72c5..fec1ae4fd 100644 --- a/gs/station/src/tui/render.rs +++ b/gs/station/src/tui/render.rs @@ -1,4 +1,5 @@ use std::ops::Div; + use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::block::*; @@ -56,7 +57,11 @@ impl Widget for &App { " ".light_magenta().bold(), " ––– ".set_style(safety_style), "throughput".green(), - format!(" <{:.2} bytes/s> ", (self.received_bytes as f64).div(self.last_sent_heartbeat.elapsed().as_secs_f64())).green(), + format!( + " <{:.2} bytes/s> ", + (self.received_bytes as f64).div(self.last_sent_heartbeat.elapsed().as_secs_f64()) + ) + .green(), " ––––––– ".set_style(safety_style), "timestamp:".light_blue(), format!(" <{}> ", timestamp()).light_blue(),