STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
PressureStateMachine.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <cstring>
7#include <iostream>
8#include <thread>
9
10#include "DiabloPacketUtils.h"
11#include "DiabloPackets.h"
14#include "db.hpp"
15#include "fsw/BoardTypeWire.hpp"
16
17using namespace vtable;
18using namespace vtable::builder;
19
20namespace fsw {
21namespace control {
22
37
41
42bool PressureStateMachine::initialize(std::shared_ptr<elodin::ElodinClient> elodin_client,
43 std::shared_ptr<config::BoardDiscovery> board_discovery,
44 const PressureThresholds& thresholds) {
45 if (!elodin_client || !elodin_client->is_connected()) {
46 std::cerr << "[PressureStateMachine] ERROR: Elodin client not connected" << std::endl;
47 return false;
48 }
49
50 elodin_client_ = elodin_client;
51 board_discovery_ = board_discovery;
52 thresholds_ = thresholds;
54 abort_requested_ = false;
55
56 // Get actuator board IP/port (from discovery or config fallback)
57 auto [board_ip, board_port] = getActuatorBoardAddress(ActuatorID::LOX_MAIN);
58 actuator_board_ip_ = board_ip;
59 actuator_board_port_ = board_port;
60
61 // Create UDP socket for sending commands (reused, like DiabloAvionics GUI)
62 //
63 // This is the one board-facing sender left unpinned to a NIC, because nothing constructs a
64 // PressureStateMachine — it is dead code, and pinning a socket that never opens is churn. If
65 // this class is ever revived, pass a bind address here: UDPSocket's sender ctor takes one, and
66 // fsw::net::resolveDaqBindAddress() (lib/include/net/DaqInterface.hpp) supplies it. Without
67 // that, commands leave on whatever interface the route table prefers, which on the shared apps
68 // box is not necessarily the board LAN.
69 command_socket_ = std::make_unique<daq_comms::transport::UDPSocket>(actuator_board_ip_,
71 if (!command_socket_->is_valid()) {
72 std::cerr << "[PressureStateMachine] WARNING: Failed to create UDP socket for "
73 << actuator_board_ip_ << ":" << actuator_board_port_ << " - "
74 << command_socket_->last_error() << std::endl;
75 std::cerr << "[PressureStateMachine] Commands will fail until socket is valid" << std::endl;
76 }
77
78 std::cout << "[PressureStateMachine] ✅ Initialized" << std::endl;
79 std::cout << "[PressureStateMachine] Actuator Board: " << actuator_board_ip_ << ":"
80 << actuator_board_port_ << std::endl;
81 std::cout << "[PressureStateMachine] GN2 Low Target: " << thresholds_.gn2_low_target_psi
82 << " PSI" << std::endl;
83 std::cout << "[PressureStateMachine] GN2 High Target: " << thresholds_.gn2_high_target_psi
84 << " PSI" << std::endl;
85 std::cout << "[PressureStateMachine] Fuel Target: " << thresholds_.fuel_target_psi << " PSI"
86 << std::endl;
87 std::cout << "[PressureStateMachine] OX Target: " << thresholds_.ox_target_psi << " PSI"
88 << std::endl;
89
90 // Register VTables with Elodin DB so PSM actuator command packets (0x5060-0x5066)
91 // and state transition packets (0x43) are accepted and stored.
93
94 return true;
95}
96
98 if (running_.load()) {
99 std::cerr << "[PressureStateMachine] WARNING: Already running" << std::endl;
100 return;
101 }
102
103 running_ = true;
105 std::cout << "[PressureStateMachine] ✅ Started state machine" << std::endl;
106}
107
109 if (!running_.load()) {
110 return;
111 }
112
113 running_ = false;
114 if (state_machine_thread_.joinable()) {
116 }
117 std::cout << "[PressureStateMachine] ✅ Stopped state machine" << std::endl;
118}
119
123
125 switch (current_state_.load()) {
127 return "DEBUG_STATE";
129 return "IDLE";
131 return "ARMED";
133 return "FUEL_FILL";
135 return "OX_FILL";
137 return "GN2_LOW_PRESS";
139 return "GN2_VENT";
141 return "FUEL_PRESS";
143 return "FUEL_VENT";
145 return "OX_PRESS";
147 return "OX_VENT";
149 return "GN2_HIGH_PRESS";
151 return "GN2_HIGH_VENT";
153 return "VENT";
155 return "CALIBRATE";
157 return "READY";
159 return "FIRE";
161 return "ABORT";
162 }
163 return "UNKNOWN";
164}
165
167 std::lock_guard<std::mutex> lock(state_mutex_);
168 SystemState current = current_state_.load();
169
170 // Validate transition
171 bool valid = false;
172 switch (current) {
174 valid =
175 (target_state == SystemState::DEBUG_STATE || target_state == SystemState::ARMED);
176 break;
178 valid = (target_state == SystemState::IDLE);
179 break;
181 valid = (target_state == SystemState::FUEL_FILL || target_state == SystemState::IDLE);
182 break;
183 // Add more valid transitions as needed
184 default:
185 // Allow transitions to IDLE or ABORT from any state
186 valid = (target_state == SystemState::IDLE || target_state == SystemState::ABORT);
187 break;
188 }
189
190 if (valid) {
191 current_state_ = target_state;
192 state_entry_time_ = std::chrono::steady_clock::now();
193 executeStateEntryActions(target_state);
194 std::cout << "[PressureStateMachine] ✅ Transitioned: " << getCurrentStateName() << " -> "
195 << getCurrentStateName() << std::endl;
196 return true;
197 }
198
199 std::cerr << "[PressureStateMachine] ❌ Invalid transition: " << getCurrentStateName() << " -> "
200 << static_cast<int>(target_state) << std::endl;
201 return false;
202}
203
205 abort_requested_ = true;
206 std::lock_guard<std::mutex> lock(state_mutex_);
208 state_entry_time_ = std::chrono::steady_clock::now();
210 std::cout << "[PressureStateMachine] 🚨 ABORT REQUESTED" << std::endl;
211}
212
217
219 auto last_pressure_read = std::chrono::steady_clock::now();
220
221 while (running_.load()) {
222 auto now = std::chrono::steady_clock::now();
223
224 // Read pressure data periodically
225 if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_pressure_read)
226 .count() >= pressure_read_period_.count()) {
228 last_pressure_read = now;
229 }
230
231 // Check transitions and execute state actions
234
235 // Sleep until next cycle
236 std::this_thread::sleep_for(state_machine_period_);
237 }
238}
239
241 if (!elodin_client_ || !elodin_client_->is_connected()) {
242 return;
243 }
244
245 // Read incoming packets from Elodin (non-blocking)
246 // Elodin sends data packets back over the TCP connection
247 // Packet format: [len(4)][type(1)][packet_id(2)][request_id(1)][padding(4)][payload...]
248
249 constexpr size_t MAX_PACKET_SIZE = 4096;
250 uint8_t packet_buffer[MAX_PACKET_SIZE];
251
252 // Try to read a packet (non-blocking)
253 ssize_t packet_len = elodin_client_->read_packet(packet_buffer, MAX_PACKET_SIZE);
254 if (packet_len <= 0) {
255 return; // No data available or error
256 }
257
258 // Parse packet header
259 uint32_t len = *reinterpret_cast<uint32_t*>(packet_buffer);
260 uint8_t type = packet_buffer[4];
261 uint16_t packet_id = (static_cast<uint16_t>(packet_buffer[5]) << 8) | packet_buffer[6];
262
263 // Check if this is a data packet (type 1 = TABLE)
264 if (type != 1) {
265 return; // Not a data packet
266 }
267
268 // Extract payload (skip 12-byte header)
269 const uint8_t* payload = packet_buffer + 12;
270 size_t payload_len = len - 12;
271
272 // Parse pressure sensor messages based on packet_id
273 // Match packet_id to sensor message IDs
274 std::lock_guard<std::mutex> lock(pressure_mutex_);
275
276 // Parse calibrated PT packets: [timestamp(8), channelId(1), padding(3), pressurePsi(float32,4),
277 // ...] Minimum payload size = 16 bytes to read the pressurePsi field.
278 constexpr size_t CAL_PT_MIN_LEN = 16;
279
280 if (packet_id == pt_hp_message_id_ || packet_id == pt_lp_message_id_) {
281 if (payload_len >= CAL_PT_MIN_LEN) {
282 float psi_f;
283 std::memcpy(&psi_f, payload + 12, sizeof(float));
284 latest_pressures_.gn2_pressure_psi = static_cast<double>(psi_f);
285 latest_pressures_.timestamp = std::chrono::steady_clock::now();
286 latest_pressures_.valid = true;
287 }
288 } else if (packet_id == pt_fup_message_id_ || packet_id == pt_fdp_message_id_) {
289 if (payload_len >= CAL_PT_MIN_LEN) {
290 float psi_f;
291 std::memcpy(&psi_f, payload + 12, sizeof(float));
292 latest_pressures_.fuel_pressure_psi = static_cast<double>(psi_f);
293 latest_pressures_.timestamp = std::chrono::steady_clock::now();
294 latest_pressures_.valid = true;
295 }
296 } else if (packet_id == pt_oup_message_id_ || packet_id == pt_odp_message_id_) {
297 if (payload_len >= CAL_PT_MIN_LEN) {
298 float psi_f;
299 std::memcpy(&psi_f, payload + 12, sizeof(float));
300 latest_pressures_.ox_pressure_psi = static_cast<double>(psi_f);
301 latest_pressures_.timestamp = std::chrono::steady_clock::now();
302 latest_pressures_.valid = true;
303 }
304 }
305}
306
308 std::lock_guard<std::mutex> lock(state_mutex_);
309 SystemState current = current_state_.load();
310
311 // Get latest pressure readings (need to unlock state_mutex_ first to avoid deadlock)
312 PressureReadings pressures;
313 {
314 std::lock_guard<std::mutex> pressure_lock(pressure_mutex_);
315 pressures = latest_pressures_;
316 }
317
318 if (!pressures.valid) {
319 return; // No valid pressure data yet
320 }
321
322 // Check abort condition
323 if (abort_requested_.load()) {
324 if (current != SystemState::ABORT) {
326 state_entry_time_ = std::chrono::steady_clock::now();
328 }
329 return;
330 }
331
332 // State-specific transition checks based on diagram
333 switch (current) {
335 if (pressures.gn2_pressure_psi > thresholds_.gn2_low_max_psi) {
336 // Transition to GN2_VENT if pressure too high
338 state_entry_time_ = std::chrono::steady_clock::now();
340 } else if (pressures.gn2_pressure_psi >= thresholds_.gn2_low_target_psi) {
341 // Transition to FUEL_PRESS when target reached
343 state_entry_time_ = std::chrono::steady_clock::now();
345 }
346 break;
347
349 if (pressures.gn2_pressure_psi > 3000.0) {
350 // Transition to VENT if pressure exceeds 3000 PSI
352 state_entry_time_ = std::chrono::steady_clock::now();
354 } else if (pressures.gn2_pressure_psi < thresholds_.gn2_low_max_psi) {
355 // Return to GN2_LOW_PRESS when pressure drops
357 state_entry_time_ = std::chrono::steady_clock::now();
359 }
360 break;
361
363 if (pressures.fuel_pressure_psi > thresholds_.fuel_max_psi) {
364 // Transition to FUEL_VENT if pressure too high
366 state_entry_time_ = std::chrono::steady_clock::now();
368 } else {
369 // Transition to OX_PRESS when fuel pressurization complete
370 // (In diagram, this happens after actions complete)
371 // For now, we'll transition when pressure is near target
372 if (std::abs(pressures.fuel_pressure_psi - thresholds_.fuel_target_psi) < 50.0) {
374 state_entry_time_ = std::chrono::steady_clock::now();
376 }
377 }
378 break;
379
381 if (pressures.fuel_pressure_psi > 850.0) {
382 // Transition to VENT if pressure exceeds 850 PSI
384 state_entry_time_ = std::chrono::steady_clock::now();
386 } else if (pressures.fuel_pressure_psi < thresholds_.fuel_min_psi) {
387 // Return to FUEL_PRESS when pressure drops
389 state_entry_time_ = std::chrono::steady_clock::now();
391 }
392 break;
393
395 if (pressures.ox_pressure_psi > thresholds_.ox_max_psi) {
396 // Transition to OX_VENT if pressure too high
398 state_entry_time_ = std::chrono::steady_clock::now();
400 } else {
401 // Transition to GN2_HIGH_PRESS when ox pressurization complete
402 if (std::abs(pressures.ox_pressure_psi - thresholds_.ox_target_psi) < 50.0) {
404 state_entry_time_ = std::chrono::steady_clock::now();
406 }
407 }
408 break;
409
411 if (pressures.ox_pressure_psi > 1100.0) {
412 // Transition to VENT if pressure exceeds 1100 PSI
414 state_entry_time_ = std::chrono::steady_clock::now();
416 } else if (pressures.ox_pressure_psi < thresholds_.ox_min_psi) {
417 // Return to OX_PRESS when pressure drops
419 state_entry_time_ = std::chrono::steady_clock::now();
421 }
422 break;
423
425 if (pressures.gn2_pressure_psi > thresholds_.gn2_high_max_psi) {
426 // Transition to GN2_HIGH_VENT if pressure too high
428 state_entry_time_ = std::chrono::steady_clock::now();
430 } else if (pressures.gn2_pressure_psi >= thresholds_.gn2_high_target_psi) {
431 // Transition to CALIBRATE when target reached
433 state_entry_time_ = std::chrono::steady_clock::now();
435 }
436 break;
437
439 if (pressures.gn2_pressure_psi > 4600.0) {
440 // Transition to VENT if pressure exceeds 4600 PSI
442 state_entry_time_ = std::chrono::steady_clock::now();
444 } else if (pressures.gn2_pressure_psi < thresholds_.gn2_high_max_psi) {
445 // Return to GN2_HIGH_PRESS when pressure drops
447 state_entry_time_ = std::chrono::steady_clock::now();
449 }
450 break;
451
452 default:
453 break;
454 }
455}
456
458 const SystemState from_state = current_state_.load();
459 std::cout << "[PressureStateMachine] Entering state: " << getCurrentStateName() << std::endl;
460
461 // Write state transition event to Elodin DB [0x43, 0x00]
462 if (elodin_client_ && elodin_client_->is_connected()) {
464 std::get<0>(st_msg.fields) = std::chrono::duration_cast<std::chrono::nanoseconds>(
465 std::chrono::system_clock::now().time_since_epoch())
466 .count();
467 std::get<1>(st_msg.fields) = static_cast<uint8_t>(from_state);
468 std::get<2>(st_msg.fields) = static_cast<uint8_t>(state);
469 std::get<3>(st_msg.fields) = (state == SystemState::ABORT) ? uint8_t(2)
470 : abort_requested_.load() ? uint8_t(2)
471 : uint8_t(0);
472 elodin_client_->publish(0x4300, st_msg);
473 }
474
475 switch (state) {
476 case SystemState::ARMED: {
477 // Actions: "mains Close", "all SOL close"
480 // Close all solenoids (vent and press valves)
485 break;
486 }
487
489 // Actions: "-FV OPEN", "-personnel fill tank, X L", "-personnel removes QD"
491 // Note: Personnel actions are manual, not automated
492 break;
493 }
494
496 // Actions: "-SOL FV CLOSE", "-SOL OV OPEN", "-personnel fill tank", "-personnel
497 // approves tank temp"
500 // Note: Personnel actions are manual
501 break;
502 }
503
505 // Actions: "-SOL FV PV CLOSE", "-press to 2000psi"
509 break;
510 }
511
513 // Action: "open PV"
515 break;
516 }
517
519 // Actions: "-SOL FV PV CLOSE", "-OPEN SOL FUP", "-CLOSE OUP", "-press to 1000psi"
524 break;
525 }
526
528 // Action: "open FV"
530 break;
531 }
532
534 // Actions: "-SOL FV PV AND OV CLOSE", "-CLOSE FUP", "-OPEN SOL OUP", "-press to
535 // 1000psi"
541 break;
542 }
543
545 // Action: "open OV"
547 break;
548 }
549
551 // Actions: "-SOL FV PV AND OV CLOSE", "-CLOSE OUP AND FUP", "-press to 4500psi"
557 // Continue pressurization to 4500 PSI
558 break;
559 }
560
562 // Action: "open PV"
564 break;
565 }
566
567 case SystemState::VENT: {
568 // Actions: "open FV and OV", "open pressurant vent"
572 break;
573 }
574
575 case SystemState::READY: {
576 // Actions: "-pressure reg starts"
577 // This would start a pressure regulator (not an actuator command)
578 std::cout << "[PressureStateMachine] Pressure regulator started" << std::endl;
579 break;
580 }
581
582 case SystemState::FIRE: {
583 // Actions: "-ignitor", "-main F and main O"
586 // Note: Ignitor would be a separate actuator
587 break;
588 }
589
590 case SystemState::ABORT: {
591 // Actions: "-stop pressure reg", "-fully open SOL FDP and BV ODP", "-open MAIN F and
592 // MAIN O", "-open FUP and OUP"
597 std::cout << "[PressureStateMachine] 🚨 ABORT: All valves opened for emergency venting"
598 << std::endl;
599 break;
600 }
601
602 default:
603 break;
604 }
605}
606
608 // Actions to perform while in state (monitoring, etc.)
609 // Most states don't have continuous actions, but we could add monitoring here
610}
611
613 float value) {
614 // Send to Elodin (for logging/monitoring)
615 if (elodin_client_ && elodin_client_->is_connected()) {
616 uint16_t message_id = actuatorToMessageID(actuator);
617 if (message_id != 0) {
619 std::get<0>(cmd_msg.fields) = std::chrono::duration_cast<std::chrono::nanoseconds>(
620 std::chrono::steady_clock::now().time_since_epoch())
621 .count();
622 std::get<1>(cmd_msg.fields) = static_cast<uint8_t>(actuator);
623 std::get<2>(cmd_msg.fields) = static_cast<uint8_t>(command);
624 std::get<3>(cmd_msg.fields) = value;
625 std::get<4>(cmd_msg.fields) = 1; // Status: ACK
626
627 elodin_client_->publish(message_id, cmd_msg);
628 }
629 }
630
631 // Send via UDP to board (actual command)
632 sendActuatorCommandUDP(actuator, command, value);
633}
634
636 float value) {
637 // Check if socket is valid (reused socket, like DiabloAvionics GUI)
638 if (!command_socket_ || !command_socket_->is_valid()) {
639 std::cerr << "[PressureStateMachine] ERROR: UDP socket not valid for actuator "
640 << static_cast<int>(actuator) << std::endl;
641 return;
642 }
643
644 // Convert CommandType to actuator_state (0=OFF, 1=ON)
645 // Matching DiabloAvionics GUI: actuator_state: 0 = OFF, non-zero = ON
646 uint8_t actuator_state = 0;
647 if (command == CommandType::OPEN || command == CommandType::PULSE) {
648 actuator_state = 1; // ON
649 } else if (command == CommandType::CLOSE) {
650 actuator_state = 0; // OFF
651 } else if (command == CommandType::SET_POSITION) {
652 actuator_state = (value > 0.5f) ? 1 : 0; // Threshold at 50%
653 }
654
655 // Map actuator ID to board channel (1-indexed, 1-10)
656 // Matching DiabloAvionics GUI: actuator_id: 1-10 (1-indexed)
657 // Actuator IDs map to channels: LOX_MAIN=1, FUEL_MAIN=2, LOX_VENT=3, etc.
658 uint8_t actuator_channel =
659 static_cast<uint8_t>(actuator) + 1; // Convert 0-indexed to 1-indexed
660
661 // Construct actuator command packet (matching DAQv2-Comms format exactly)
662 daq::ActuatorCommand cmd;
663 cmd.actuator_id = actuator_channel;
664 cmd.actuator_state = actuator_state;
665
666 std::vector<daq::ActuatorCommand> commands;
667 commands.push_back(cmd);
668
669 uint8_t buf[512];
670 uint32_t ts_ms = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
671 std::chrono::steady_clock::now().time_since_epoch())
672 .count() &
673 0xFFFFFFFFu);
674 size_t len = daq::create_actuator_command_packet(commands, ts_ms, buf, sizeof(buf));
675 std::vector<uint8_t> packet(buf, buf + len);
676 if (packet.empty()) {
677 std::cerr << "[PressureStateMachine] ERROR: Failed to construct actuator command packet"
678 << std::endl;
679 return;
680 }
681
682 // Send packet via UDP (reusing socket, like DiabloAvionics GUI)
683 // Matching: self.command_sock.sendto(packet, (self.device_ip, self.device_port))
684 ssize_t sent = command_socket_->send(packet.data(), packet.size());
685 if (sent == static_cast<ssize_t>(packet.size())) {
686 std::cout << "[PressureStateMachine] ✅ Sent UDP command to " << actuator_board_ip_ << ":"
687 << actuator_board_port_ << ": Actuator=" << static_cast<int>(actuator_channel)
688 << " -> " << (actuator_state ? "ON" : "OFF") << std::endl;
689 } else {
690 std::cerr << "[PressureStateMachine] ❌ Failed to send UDP command: sent " << sent << "/"
691 << packet.size() << " bytes" << " error: " << command_socket_->last_error()
692 << std::endl;
693 }
694}
695
696std::pair<std::string, uint16_t> PressureStateMachine::getActuatorBoardAddress(
697 ActuatorID actuator) const {
698 // Map actuator to board_id from config
699 // Actuators are on board_id 6 (from config_flight_daq.toml)
700 constexpr uint16_t ACTUATOR_BOARD_PORT = 5005; // Default UDP port for actuator commands
701
702 // Get board IP from board discovery
703 if (board_discovery_) {
704 auto boards = board_discovery_->get_discovered_boards();
705 for (const auto& board : boards) {
706 // Match board_id - check if this board has actuator sensors
707 // Board signature contains board_id, check if it matches ACTUATOR_BOARD_ID
708 // For now, check if board has actuator sensors or is actuator type
709 if (board.signature.board_type == fsw::daq_wire::kActuator) {
710 return {board.current_ip, board.port > 0 ? board.port : ACTUATOR_BOARD_PORT};
711 }
712 }
713 }
714
715 // Fallback: Use default IP range for actuators
716 // From config: actuators are on board_id 6, which maps to IP 192.168.2.206
717 // (assuming IP assignment: 192.168.2.200 + board_id)
718 return {"192.168.2.206", ACTUATOR_BOARD_PORT};
719}
720
722 auto it = actuator_message_ids_.find(actuator);
723 if (it != actuator_message_ids_.end()) {
724 return it->second;
725 }
726 return 0;
727}
728
730 if (!elodin_client_ || !elodin_client_->is_connected())
731 return;
732
733 // Local helper — mirrors ControllerService::send_msg but scoped to PSM
734 auto send_vtable_msg = [this](VTableMsg msg) {
735 auto buf = Msg(msg).encode_vec();
736 if (!buf.empty())
737 elodin_client_->send_msg({0, 0}, buf);
738 };
739
740 // Actuator command VTable: U64(8) + U8(1) + U8(1) + F32(4) + U8(1) = 15 bytes
741 auto make_act_vt = []() {
742 return builder::vtable({
743 raw_field(0, 8, schema(PrimType::U64(), {}, component("PSM.actuator.timestamp_ns"))),
744 raw_field(8, 1, schema(PrimType::U8(), {}, component("PSM.actuator.actuator_id"))),
745 raw_field(9, 1, schema(PrimType::U8(), {}, component("PSM.actuator.command_type"))),
746 raw_field(10, 4, schema(PrimType::F32(), {}, component("PSM.actuator.value"))),
747 raw_field(14, 1, schema(PrimType::U8(), {}, component("PSM.actuator.status"))),
748 });
749 };
750
751 for (uint8_t lo = 0x60; lo <= 0x66; ++lo) {
752 send_vtable_msg(
753 VTableMsg{.id = std::make_tuple(uint8_t(0x50), lo), .vtable = make_act_vt()});
754 }
755
756 // PSM state transition VTable [0x43, 0x00]: U64(8) + 3×U8 = 11 bytes
757 auto state_trans_vt = builder::vtable({
758 raw_field(0, 8, schema(PrimType::U64(), {}, component("CONTROLLER.state.timestamp_ns"))),
759 raw_field(8, 1, schema(PrimType::U8(), {}, component("CONTROLLER.state.from_state"))),
760 raw_field(9, 1, schema(PrimType::U8(), {}, component("CONTROLLER.state.to_state"))),
761 raw_field(10, 1, schema(PrimType::U8(), {}, component("CONTROLLER.state.reason"))),
762 });
763 send_vtable_msg(
764 VTableMsg{.id = std::make_tuple(uint8_t(0x43), uint8_t(0x00)), .vtable = state_trans_vt});
765
766 std::cout << "[PressureStateMachine] ✅ Registered PSM VTables (0x5060-0x5066, 0x43)"
767 << std::endl;
768}
769
770} // namespace control
771} // namespace fsw
#define MAX_PACKET_SIZE
Definition main.h:6
void readPressureData()
Read pressure data from Elodin.
void registerPSMVTables()
Register Elodin VTables for PSM actuator commands and state transitions.
std::chrono::milliseconds pressure_read_period_
bool requestTransition(SystemState target_state)
Request transition to a specific state (manual override).
uint16_t actuatorToMessageID(ActuatorID actuator) const
Convert actuator ID to message ID from config.
void stateMachineLoop()
Main state machine loop.
SystemState
System states matching the state machine diagram.
void sendActuatorCommand(ActuatorID actuator, CommandType command, float value=0.0f)
Send actuator command.
bool initialize(std::shared_ptr< elodin::ElodinClient > elodin_client, std::shared_ptr< config::BoardDiscovery > board_discovery, const PressureThresholds &thresholds)
Initialize the state machine.
std::string getCurrentStateName() const
Get current state name as string.
std::chrono::steady_clock::time_point state_entry_time_
std::atomic< SystemState > current_state_
CommandType
Command types for actuators.
void executeStateActions(SystemState state)
Execute state actions (while in state).
void executeStateEntryActions(SystemState state)
Execute state entry actions.
SystemState getCurrentState() const
Get current state.
std::map< ActuatorID, uint16_t > actuator_message_ids_
PressureReadings getLatestPressures() const
Get latest pressure readings.
std::chrono::milliseconds state_machine_period_
std::pair< std::string, uint16_t > getActuatorBoardAddress(ActuatorID actuator) const
Get board IP and port for actuator.
void start()
Start the state machine.
void sendActuatorCommandUDP(ActuatorID actuator, CommandType command, float value=0.0f)
Send actuator command via UDP to board.
ActuatorID
Actuator identifiers matching config.
std::shared_ptr< elodin::ElodinClient > elodin_client_
std::unique_ptr< daq_comms::transport::UDPSocket > command_socket_
std::shared_ptr< config::BoardDiscovery > board_discovery_
void checkTransitions()
Check transition conditions based on current state and pressures.
static ActuatorControllerState state
Definition main.cpp:57
@ IDLE
Definition gnss_frame.c:6
static osMessageQueueId_t commands
Definition idle_task.c:28
size_type count(const key_type &key) const
Definition json.hpp:19264
static osMutexId_t lock
Definition mode.c:11
CommsMessage< uint64_t, uint8_t, uint8_t, float, uint8_t > ActuatorCommandMessage
Actuator command message.
CommsMessage< uint64_t, uint8_t, uint8_t, uint8_t > ControllerStateTransitionMessage
PSM state transition event message [0x43, 0x00].
size_t create_actuator_command_packet(const std::vector< ActuatorCommand > &commands, uint32_t timestamp_ms, uint8_t *buffer, size_t buffer_size)
Creates a complete Actuator Command packet in the provided buffer.
constexpr uint8_t kActuator
Motorized ball valve position: raw ADC counts -> % open.
STL namespace.
std::tuple< Fields... > fields
std::string board(const std::string &name, const std::string &id, bool enabled=true)