STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
actuator_c.cpp
Go to the documentation of this file.
1/**
2 * Actuator Controller (actuator_c) state machine — ESP32 PCB
3 *
4 * Coordinates abort sequences. Uses DAQv2-Comms over Ethernet/UDP.
5 * Reference: Stream_ADC_Data for ethernet/heartbeat sending,
6 * Actuator_Testing for sensor data collection and sending.
7 */
8
9#include <Arduino.h>
10#include <Ethernet.h>
11#include <EthernetUdp.h>
12#include <SPI.h>
13#include <SPIFFS.h>
14#include <daq-protocol.h>
15#include <esp_mac.h>
16
17#include <cstring>
18#include <vector>
19
20#include "actuator_board_pins.h"
21#include "actuator_config.h"
22
23using namespace actuator_board_pins;
24
25//-----------------------------------------------------------------------------
26// Board identity and network (IP 192.168.2.XXX and board_id = XXX from SPIFFS)
27//-----------------------------------------------------------------------------
28static uint8_t board_id = BOARD_ID_DEFAULT;
29byte mac[6];
31IPAddress gateway(0, 0, 0, 0);
32IPAddress subnet(255, 255, 255, 0);
33IPAddress dns(192, 168, 2, 1);
34IPAddress serverIP(192, 168, 2, 20);
35const int udpListenPort = 5005;
36const int serverPort = 5006;
37EthernetUDP udp;
38
39//-----------------------------------------------------------------------------
40// State machine
41//-----------------------------------------------------------------------------
50
53static unsigned long last_server_heartbeat_ms = 0;
54static unsigned long state_enter_ms = 0;
55
56// Stored config from server (placeholder until full ACTUATOR_CONFIG parse
57// exists)
59 bool valid;
60 uint8_t raw[64];
61 size_t raw_len;
62};
63static StoredConfig stored_config = {false, {0}, 0};
64
65// Sensor data (like Actuator_Testing): current sense pins
66const uint8_t NUM_SENSORS = NUM_ACTUATORS;
67static unsigned long last_adc_read_ms = 0;
68
69// Actuator output state (0 = OFF, 1 = ON), like Actuator_Testing
71
72// Heartbeat at fixed interval (actuator_config.h: BOARD_HEARTBEAT_INTERVAL_MS)
73static unsigned long lastHeartbeatMillis = 0;
74
75//-----------------------------------------------------------------------------
76// Status LED: non-blocking blink count = state number (timing in
77// actuator_config.h)
78//-----------------------------------------------------------------------------
79enum class LedPhase { Idle, On, Off };
81static unsigned long led_cycle_start_ms = 0;
82static uint8_t led_blink_index = 0;
83static unsigned long led_phase_start_ms = 0;
86
88 switch (s) {
90 return 1;
92 return 2;
94 return 3;
96 return 4;
98 return 5;
100 return 6;
101 default:
102 return 1;
103 }
104}
105
106static void updateLedNonBlocking() {
107 const unsigned long now = millis();
108 const uint8_t nblinks = getStateNumber(state);
109 const int pin = Actuator_Board.LED;
110
111 if (state != last_led_state) {
114 led_cycle_start_ms = now;
115 }
116
117 switch (led_phase) {
118 case LedPhase::Idle:
119 digitalWrite(pin, LOW);
120 if ((now - led_cycle_start_ms) >= LED_CYCLE_MS) {
121 led_cycle_start_ms = now;
122 led_blink_index = 0;
123 digitalWrite(pin, HIGH);
125 led_phase_start_ms = now;
126 }
127 break;
128 case LedPhase::On:
129 if ((now - led_phase_start_ms) >= LED_BLINK_ON_MS) {
130 digitalWrite(pin, LOW);
132 led_phase_start_ms = now;
133 }
134 break;
135 case LedPhase::Off:
136 if ((now - led_phase_start_ms) >= LED_BLINK_OFF_MS) {
138 if (led_blink_index < nblinks) {
139 digitalWrite(pin, HIGH);
141 } else {
143 led_cycle_start_ms = now; // 5 s pause before next burst
144 }
145 led_phase_start_ms = now;
146 }
147 break;
148 }
149}
150
151//-----------------------------------------------------------------------------
152// Helpers: map our state to daq::BoardState for heartbeats
153//-----------------------------------------------------------------------------
172
173//-----------------------------------------------------------------------------
174// Send board heartbeat to server (Board ID + boardState), like Stream_ADC_Data
175//-----------------------------------------------------------------------------
176static void sendBoardHeartbeat() {
177 daq::BoardHeartbeatPacket hb;
178 memset(hb.firmware_hash, 0, sizeof(hb.firmware_hash));
179 hb.board_id = board_id;
180 hb.engine_state = daq::EngineState::SAFE;
181 hb.board_state = getBoardStateForHeartbeat();
182
185 sizeof(packetBuffer));
186 if (len == 0)
187 return;
188
189 udp.beginPacket(serverIP, serverPort);
190 udp.write(packetBuffer, len);
191 udp.endPacket();
192}
193
194//-----------------------------------------------------------------------------
195// Parse incoming UDP: read header and return packet type (or UNKNOWN)
196//-----------------------------------------------------------------------------
197static bool readPacketHeader(const uint8_t* buffer, size_t buffer_size,
198 daq::PacketHeader& header_out) {
199 if (buffer_size < sizeof(daq::PacketHeader))
200 return false;
201 memcpy(&header_out, buffer, sizeof(daq::PacketHeader));
202 return true;
203}
204
205//-----------------------------------------------------------------------------
206// Process one incoming packet: update last heartbeat time and return what was
207// received
208//-----------------------------------------------------------------------------
217
218static IncomingPacketKind processIncomingPacket(const uint8_t* buffer,
219 size_t len) {
220 daq::PacketHeader hdr;
221 if (!readPacketHeader(buffer, len, hdr))
223
224 switch (hdr.packet_type) {
226 daq::PacketHeader dummy;
227 daq::ServerHeartbeatPacket data;
228 if (daq::parse_server_heartbeat_packet(buffer, len, dummy, data)) {
230 millis(); // Only server heartbeat refreshes watchdog
232 }
234 }
236 // Store config data (TODO: full parse when layout is final)
237 stored_config.valid = true;
238 stored_config.raw_len = (len <= sizeof(stored_config.raw))
239 ? len
240 : sizeof(stored_config.raw);
241 memcpy(stored_config.raw, buffer, stored_config.raw_len);
250 daq::PacketHeader cmd_header;
251 std::vector<daq::ActuatorCommand> commands;
252 if (daq::parse_actuator_command_packet(buffer, len, cmd_header,
253 commands) &&
257 }
259 }
260 default:
262 }
263}
264
265//-----------------------------------------------------------------------------
266// Actuator commands from server (Active and Standard Abort only), like
267// Actuator_Testing
268//-----------------------------------------------------------------------------
270 const std::vector<daq::ActuatorCommand>& commands) {
271 for (const auto& cmd : commands) {
272 if (cmd.actuator_id < 1 || cmd.actuator_id > NUM_ACTUATORS)
273 continue;
274 int pin = getActuatorPin(cmd.actuator_id);
275 if (pin < 0)
276 continue;
277 uint8_t new_state = (cmd.actuator_state != 0) ? 1 : 0;
278 uint8_t idx = cmd.actuator_id - 1;
279 if (actuator_states[idx] != new_state) {
280 digitalWrite(pin, new_state ? HIGH : LOW);
281 actuator_states[idx] = new_state;
282 }
283 }
284}
285
286//-----------------------------------------------------------------------------
287// Sensor data: read current sense pins and send one packet (like
288// Actuator_Testing)
289//-----------------------------------------------------------------------------
292 for (uint8_t i = 0; i < NUM_SENSORS; i++) {
293 int pin = getCurrentSensePin(i);
294 if (pin < 0)
295 continue;
296 int adcValue = analogRead(pin);
297 float voltage = (static_cast<float>(adcValue) / 4095.0f) * 3.3f;
298 uint32_t voltage_bits;
299 memcpy(&voltage_bits, &voltage, sizeof(float));
300 chunk.add_datapoint(i, voltage_bits);
301 }
302 if (chunk.empty())
303 return;
304
306 std::vector<daq::SensorDataChunkCollection> chunks;
307 chunks.push_back(chunk);
308 size_t packetSize = daq::create_sensor_data_packet(
310 if (packetSize == 0)
311 return;
312
313 udp.beginPacket(serverIP, serverPort);
314 udp.write(packetBuffer, packetSize);
315 udp.endPacket();
316}
317
318//-----------------------------------------------------------------------------
319// Watchdog and sensor streaming helpers
320//-----------------------------------------------------------------------------
321static bool heartbeatTimedOut() {
323 return false; // No heartbeat received yet
325}
326
328 unsigned long now = millis();
330 last_adc_read_ms = now;
332 }
333}
334
335//-----------------------------------------------------------------------------
336// State handlers (each runs one tick, may transition)
337//-----------------------------------------------------------------------------
338
339// 1: Waiting for Server — heartbeat sent at fixed interval in loop; on config
340// -> Active
341static void run_WaitingForServer() {
342 // Incoming packets processed in loop(); transition to Active happens when
343 // Config received
344}
345
346// 2: Active — stream sensor data, watchdog -> Connection Loss; heartbeat at
347// fixed interval
355
356// 3: Standard Abort — Abort Done packet -> Abort Finished; watchdog -> No
357// Connection Abort; heartbeat at fixed interval
364
365// 4: No Connection Abort — complete abort sequence (TODO); after timeout ->
366// Abort Finished; heartbeat at fixed interval
373
374// 5: Abort Finished — Clear Abort packet -> Active; heartbeat at fixed interval
375static void run_AbortFinished() {
376 // ClearAbort transition applied in loop()
377}
378
379// 6: Connection Loss Detected — if server heartbeat received (in loop) ->
380// Active; else after grace time -> No Connection Abort
387
388//-----------------------------------------------------------------------------
389// Apply transitions that depend on received packet (called from loop after
390// processIncomingPacket)
391//-----------------------------------------------------------------------------
393 switch (state) {
395 if (kind == IncomingPacketKind::Config) {
398 }
399 break;
401 if (kind == IncomingPacketKind::Abort) {
404 }
405 break;
407 if (kind == IncomingPacketKind::AbortDone) {
410 }
411 break;
413 if (kind == IncomingPacketKind::ClearAbort) {
416 }
417 break;
422 }
423 break;
425 // No packet-driven transitions; 10s timer handles -> Abort Finished
426 break;
427 }
428}
429
430//-----------------------------------------------------------------------------
431// Pin setup and default states (like Actuator_Testing)
432//-----------------------------------------------------------------------------
433static void initializeActuators() {
434 Serial.println("Initializing actuators...");
435 for (int i = 1; i <= NUM_ACTUATORS; i++) {
436 int pin = getActuatorPin(i);
437 if (pin < 0) {
438 Serial.print("Warning: Invalid actuator ID ");
439 Serial.println(i);
440 continue;
441 }
442 pinMode(pin, OUTPUT);
443 digitalWrite(pin, LOW);
444 actuator_states[i - 1] = 0;
445 Serial.print("Actuator ");
446 Serial.print(i);
447 Serial.print(" (GPIO ");
448 Serial.print(pin);
449 Serial.println(") set to OFF");
450 }
451 Serial.println("All actuators initialized to OFF state");
452}
453
455 Serial.println("Initializing current sense pins...");
456 for (uint8_t i = 0; i < NUM_SENSORS; i++) {
457 int pin = getCurrentSensePin(i);
458 if (pin >= 0) {
459 pinMode(pin, INPUT);
460 Serial.print("Current sense sensor ");
461 Serial.print(i);
462 Serial.print(" (GPIO ");
463 Serial.print(pin);
464 Serial.println(") initialized");
465 }
466 }
467}
468
469//-----------------------------------------------------------------------------
470// Setup and loop
471//-----------------------------------------------------------------------------
472void setup() {
473 Serial.begin(115200);
474 Serial.println("Actuator Controller (actuator_c) starting...");
475
476 bool spiffs_ok = false;
477 // Mount read-only: do not format on fail, so we never overwrite burned
478 // value
479 if (SPIFFS.begin(false)) {
480 File f = SPIFFS.open(SPIFFS_BOARD_VALUE_PATH, "r");
481 if (f && f.available() >= 1) {
482 uint8_t b;
483 if (f.read(&b, 1) == 1) {
484 board_id = b;
485 staticIP = IPAddress(192, 168, 2, b);
486 spiffs_ok = true;
487 Serial.print("Board ID and IP from SPIFFS: ");
488 Serial.print(static_cast<unsigned>(board_id));
489 Serial.print(" / 192.168.2.");
490 Serial.println(static_cast<unsigned>(b));
491 }
492 }
493 if (f)
494 f.close();
495 SPIFFS.end();
496 }
497 if (!spiffs_ok)
498 Serial.println(
499 "SPIFFS read skipped or failed, using default board ID 1 / "
500 "192.168.2.1");
501
504
509
510 ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_ETH));
511 Serial.print("Generated unique MAC address: ");
512 for (int i = 0; i < 6; i++) {
513 if (i > 0)
514 Serial.print(":");
515 if (mac[i] < 0x10)
516 Serial.print("0");
517 Serial.print(mac[i], HEX);
518 }
519 Serial.println();
520
521 Serial.println("Initializing Ethernet...");
522 SPI.begin(Actuator_Board.ETH_SCLK, Actuator_Board.ETH_MISO,
523 Actuator_Board.ETH_MOSI, Actuator_Board.ETH_CS);
525 Ethernet.init(Actuator_Board.ETH_CS);
529 udp.begin(udpListenPort);
530
531 Serial.print("Ethernet initialized. IP: ");
532 Serial.println(Ethernet.localIP());
533 Serial.print("Link Status: ");
534 if (Ethernet.linkStatus() == LinkON) {
535 Serial.println("Connected");
536 } else if (Ethernet.linkStatus() == LinkOFF) {
537 Serial.println("Disconnected");
538 } else {
539 Serial.println("Unknown");
540 }
541 Serial.print("Listening for actuator commands on UDP port: ");
542 Serial.println(udpListenPort);
543
547
548 Serial.println("Setup complete. State: WaitingForServer");
549}
550
551void loop() {
553
554 // 1) Read all pending UDP packets and update heartbeat time / apply
555 // transitions
556 int packetSize = udp.parsePacket();
557 if (packetSize > 0) {
559 int bytesRead = udp.read(packetBuffer, sizeof(packetBuffer));
560 if (bytesRead > 0) {
561 IncomingPacketKind kind =
564 }
565 }
566
567 // 2) Run current state
568 switch (state) {
571 break;
573 run_Active();
574 break;
577 break;
580 break;
583 break;
586 break;
587 }
588
589 // Send board heartbeat at fixed interval (e.g. once per second)
590 unsigned long now = millis();
594 }
595
597}
static SerialStub Serial
Definition Arduino.h:128
#define INPUT
Definition Arduino.h:21
#define HIGH
Definition Arduino.h:19
void pinMode(int, int)
Definition Arduino.h:46
#define OUTPUT
Definition Arduino.h:22
unsigned long millis()
Definition Arduino.h:29
#define HEX
Definition Arduino.h:24
int analogRead(int)
Definition Arduino.h:53
#define LOW
Definition Arduino.h:20
void digitalWrite(int, int)
Definition Arduino.h:48
#define NO_CONNECTION_ABORT_DONE_MS
#define BOARD_ID_DEFAULT
#define ETHERNET_INIT_DELAY_MS
#define LED_CYCLE_MS
#define LOOP_DELAY_MS
#define CONNECTION_LOSS_GRACE_MS
#define SPIFFS_BOARD_VALUE_PATH
#define ETHERNET_BEGIN_DELAY_MS
#define HEARTBEAT_TIMEOUT_MS
#define LED_BLINK_OFF_MS
#define ADC_READ_INTERVAL_MS
#define BOARD_HEARTBEAT_INTERVAL_MS
#define LED_BLINK_ON_MS
#define ETHERNET_SPI_DELAY_MS
static EthernetClass Ethernet
Definition Ethernet.h:116
#define MAX_PACKET_SIZE
Definition main.h:6
static void readCurrentSensePinsAndSend()
static StoredConfig stored_config
static void run_Active()
static void run_WaitingForServer()
static void run_AbortFinished()
static void initializeCurrentSensePins()
static bool heartbeatTimedOut()
static void run_NoConnectionAbort()
void setup()
static void applyPacketTransition(IncomingPacketKind kind)
static void updateLedNonBlocking()
IPAddress dns(192, 168, 2, 1)
static uint8_t getStateNumber(ActuatorControllerState s)
IPAddress subnet(255, 255, 255, 0)
IPAddress staticIP(192, 168, 2, BOARD_ID_DEFAULT)
@ Off
Definition main.cpp:86
@ Idle
Definition main.cpp:86
static void initializeActuators()
static bool readPacketHeader(const uint8_t *buffer, size_t buffer_size, daq::PacketHeader &header_out)
IPAddress serverIP(192, 168, 2, 20)
static IncomingPacketKind processIncomingPacket(const uint8_t *buffer, size_t len)
static void run_ConnectionLossDetected()
static void sendBoardHeartbeat()
static void processActuatorCommands(const std::vector< daq::ActuatorCommand > &commands)
IPAddress gateway(0, 0, 0, 0)
static void run_StandardAbort()
static void streamSensorDataIfDue()
static daq::BoardState getBoardStateForHeartbeat()
void loop()
const int IPAddress
void delay(int ms)
static uint8_t chunk[64]
Definition board_uart9.c:12
std::vector< daq::SensorDataChunkCollection > chunks
Definition main.cpp:55
#define NUM_SENSORS
Definition main.cpp:46
static unsigned long lastHeartbeatMillis
Definition main.cpp:81
static unsigned long last_adc_read_ms
Definition main.cpp:79
static unsigned long led_cycle_start_ms
Definition main.cpp:88
static unsigned long last_server_heartbeat_ms
Definition main.cpp:59
static unsigned long led_phase_start_ms
Definition main.cpp:90
ActuatorControllerState
Definition main.cpp:45
const int serverPort
Definition main.cpp:38
static uint8_t led_blink_index
Definition main.cpp:89
const int udpListenPort
Definition main.cpp:37
IPAddress dns(192, 168, 2, 1)
IncomingPacketKind
Definition main.cpp:237
byte mac[6]
Definition main.cpp:31
IPAddress subnet(255, 255, 255, 0)
IPAddress staticIP(192, 168, 2, BOARD_ID_DEFAULT)
LedPhase
Definition main.cpp:86
@ Off
Definition main.cpp:86
@ Idle
Definition main.cpp:86
IPAddress serverIP(192, 168, 2, 20)
static uint8_t board_id
Definition main.cpp:30
static uint8_t actuator_states[NUM_ACTUATORS]
Definition main.cpp:80
EthernetUDP udp
Definition main.cpp:40
static ActuatorControllerState last_led_state
Definition main.cpp:91
IPAddress gateway(0, 0, 0, 0)
static ActuatorControllerState state
Definition main.cpp:57
static unsigned long state_enter_ms
Definition main.cpp:60
static LedPhase led_phase
Definition main.cpp:87
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]
Definition main.cpp:15
static osMessageQueueId_t commands
Definition idle_task.c:28
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
int getActuatorPin(uint8_t actuator_id)
int getCurrentSensePin(uint8_t actuator_id)
constexpr Layout Actuator_Board
size_t create_sensor_data_packet(const std::vector< SensorDataChunkCollection > &chunks, const uint8_t num_sensors, uint32_t timestamp_ms, uint8_t *buffer, size_t buffer_size)
Creates a complete Sensor Data packet in the provided buffer.
bool parse_server_heartbeat_packet(const uint8_t *buffer, size_t buffer_size, PacketHeader &header_out, ServerHeartbeatPacket &data_out)
Parses a Server Heartbeat packet from buffer.
size_t create_board_heartbeat_packet(const BoardHeartbeatPacket &data, uint32_t timestamp_ms, uint8_t *buffer, size_t buffer_size)
Creates a complete Board Heartbeat packet in the provided buffer.
BoardState
Defines the operational state of a board.
Definition DiabloEnums.h:31
bool parse_actuator_command_packet(const uint8_t *buffer, size_t buffer_size, PacketHeader &header_out, std::vector< ActuatorCommand > &commands_out)
Parses an Actuator Command packet from buffer.
uint8_t raw[64]
Represents a single data chunk with timestamp and sensor datapoints.