STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1/**
2 * LC (Load Cell) Hotfire state machine — ESP32 PCB
3 *
4 * Same state machine: Waiting for Server → Active → Standalone Abort (and back
5 * to Active on clear). Sends heartbeats, streams load-cell (differential)
6 * sensor data to server or to actuator controller in abort. Uses LC_Board pin
7 * layout and BoardType::LOAD_CELL. Only connectors on ADC 1 (1, 2, 3, 6, 7).
8 * Differential read between pin 1 and pin 2 per connector.
9 * Uses shared SensorHotfireCore; board-specific: ADC init, collect_chunk,
10 * send_chunks_to.
11 */
12
13#include "main.h"
14
15#include <Arduino.h>
16#include <Ethernet.h>
17#include <EthernetUdp.h>
18#include <SPI.h>
19#include <daq-protocol.h>
20#include <esp_mac.h>
21
22#include <cstring>
23#include <vector>
24
25#include "STAR_ADS126X.h"
26
27// clang-format off
28// MUST be defined before sense_board_pins.h / connector_adc_map.h
29#define PINS_ACTIVE_LAYOUT sense_board_pins::LC_Board
30#include "sense_board_pins.h"
31#include "connector_adc_map.h"
32#include "adc_mappings.h"
33#include "SensorHotfireCore.h"
34#include "SensorSelfTest.h"
35#include "send_policy.h"
36// clang-format on
37
38using namespace sense_board_pins;
39
40//-----------------------------------------------------------------------------
41// ADC config — LC Board ADC 1 only; differential (pin 1 vs pin 2) per connector
42//-----------------------------------------------------------------------------
43// 38400 SPS + SINC5 is the part's fastest and noisiest setting (~1.5 uV RMS
44// input-referred at gain 32). Nothing on this board needs that bandwidth: the
45// load cell is a scale reading a fill over seconds. 400 SPS + SINC4 is ~16x
46// quieter for free, before any host-side filtering.
47#define FILTER ADS126X_SINC4
48#define DATA_RATE ADS126X_RATE_20
50#define GAIN ADS126X_GAIN_32
51#define NUM_CHANNELS 5
52
53static const uint8_t LC_ADC1_CONNECTORS[] = {1, 2, 3, 6, 7};
54
56SPIClass ADC_SPI(HSPI);
57std::vector<daq::SensorDataChunkCollection> dataChunks;
58
61
62static unsigned long g_last_sensor_packet_log_ms = 0;
63
64// Set to true to enable Serial output from core and board; false to disable.
65bool g_verbose = true;
66
67static void flush_cycles(int cycles) {
68 for (int i = 0; i < cycles; i++) {
69 while (digitalRead(Pins.ADC_DRDY_1) != LOW)
71 ads126x.readADC1();
72 }
73}
74
75static int last_mux_p = -1, last_mux_n = -1;
76
77static void collect_chunk_impl() {
78 const uint8_t active_count = coreState.stored_config.num_sensors;
79 if (active_count == 0)
80 return;
81 const uint8_t* active_ids = coreState.stored_config.sensor_ids;
82
84 for (uint8_t i = 0; i < active_count; i++) {
85 const uint8_t connector_id = active_ids[i];
86 const int ch1 = getAdcChannel(connector_id, 1);
87 const int ch2 = getAdcChannel(connector_id, 2);
88 if (ch1 < 0 || ch2 < 0)
89 continue;
90 const bool mux_changed = (ch1 != last_mux_p || ch2 != last_mux_n);
91 ads126x.setInputMux(static_cast<uint8_t>(ch1),
92 static_cast<uint8_t>(ch2));
93 last_mux_p = ch1;
94 last_mux_n = ch2;
96 // Only after an actual channel change does the digital filter still
97 // hold the previous input's samples. Flushing unconditionally throws
98 // away a perfectly good conversion every cycle, which halves the
99 // output rate when a single connector is active.
100 if (mux_changed)
102 while (digitalRead(Pins.ADC_DRDY_1) != LOW)
104 // DRDY asserts as the conversion register is being updated; reading
105 // immediately catches it mid-update and yields corrupted samples.
106 // LC_Calibration/PT_BOARD_Multi both pause here before reading.
108 const auto reading = ads126x.readADC1();
109 const uint32_t value =
110 reading.checksumValid ? static_cast<uint32_t>(reading.value) : 0u;
111 chunk.add_datapoint(connector_id, value);
112 }
113 if (chunk.size() == active_count &&
115 dataChunks.push_back(chunk);
116}
117
118static void send_chunks_to_impl(IPAddress dest_ip, int dest_port,
119 bool also_to_abort_controller,
120 IPAddress abort_controller_ip,
121 int abort_controller_port) {
122 if (dataChunks.empty())
123 return;
124 // A full LC chunk takes ~0.8 s at SINC4 / 20 SPS, so waiting for a whole
125 // packet of them would hold each reading back 7-12 s. Ship on age too.
127 dataChunks.front().timestamp, millis(),
129 return;
131 const uint8_t num_sensors = dataChunks[0].num_sensors;
132 size_t packetSize = daq::create_sensor_data_packet(
133 dataChunks, num_sensors, millis(), packetBuffer, sizeof(packetBuffer));
134 if (packetSize == 0) {
135 HF_VERBOSE("Send FAIL: create_sensor_data_packet returned 0 (n=");
136 HF_VERBOSE(dataChunks.size());
137 HF_VERBOSE(", buf=");
139 HF_VERBOSELN(")");
140 return;
141 }
142 coreState.udp.beginPacket(dest_ip, dest_port);
143 coreState.udp.write(packetBuffer, packetSize);
144 coreState.udp.endPacket();
145 HF_VERBOSE("Sent: sensor_data to ");
146 HF_VERBOSE(dest_ip);
147 HF_VERBOSE(":");
148 HF_VERBOSELN(dest_port);
149
150 unsigned long now = millis();
151 if (now - g_last_sensor_packet_log_ms >= 1000) {
153 HF_VERBOSELN("SENSOR_DATA contents:");
154 for (size_t i = 0; i < dataChunks.size(); ++i) {
155 const auto& chunk = dataChunks[i];
156 HF_VERBOSE(" chunk ");
157 HF_VERBOSE(i);
158 HF_VERBOSE(" ts=");
159 HF_VERBOSE(chunk.timestamp);
160 HF_VERBOSE(" :");
161 for (const auto& dp : chunk.datapoints) {
162 HF_VERBOSE(" (id=");
163 HF_VERBOSE(static_cast<unsigned>(dp.sensor_id));
164 HF_VERBOSE(", data=");
165 HF_VERBOSE(dp.data);
166 HF_VERBOSE(")");
167 }
169 }
170 }
171
172 if (also_to_abort_controller) {
173 coreState.udp.beginPacket(abort_controller_ip, abort_controller_port);
174 coreState.udp.write(packetBuffer, packetSize);
175 coreState.udp.endPacket();
176 HF_VERBOSE("Sent: sensor_data to ");
177 HF_VERBOSE(abort_controller_ip);
178 HF_VERBOSE(":");
179 HF_VERBOSELN(abort_controller_port);
180 }
181 dataChunks.clear();
182}
183
184static void init_adc_cb(void*) {
185 ADC_SPI.begin(Pins.ADC_SCLK, Pins.ADC_MISO, Pins.ADC_MOSI, Pins.ADC_CS_1);
186 ADC_SPI.setDataMode(SPI_MODE1);
187 pinMode(Pins.ADC_DRDY_1, INPUT);
188 ads126x.begin(Pins.ADC_CS_1, &ADC_SPI);
189 ads126x.stopADC1();
190 ads126x.setInputMux(static_cast<uint8_t>(getAdcChannel(1, 1)),
191 static_cast<uint8_t>(getAdcChannel(1, 2)));
192 ads126x.enablePGA();
193 ads126x.setGain(GAIN);
194 ads126x.setFilter(FILTER);
195 ads126x.setRate(DATA_RATE);
197 ads126x.startADC1();
198}
199
200static void on_reference_voltage_cb(void*, uint8_t reference_voltage) {
201 uint8_t ref_pos =
202 (reference_voltage == 1) ? ADS126X_REF_POS_VDD : ADS126X_REF_POS_INT;
203 ads126x.setReference(ADS126X_REF_NEG_VSS, ref_pos);
204}
205
206static void collect_chunk_cb(void*) {
208}
209
210static void send_chunks_to_cb(void*, IPAddress dest_ip, int dest_port,
211 bool also_to_abort_controller,
212 IPAddress abort_controller_ip,
213 int abort_controller_port) {
214 if (!dataChunks.empty())
215 send_chunks_to_impl(dest_ip, dest_port, also_to_abort_controller,
216 abort_controller_ip, abort_controller_port);
217}
218
219static void run_self_test_cb(void*,
221 std::vector<daq::SelfTestResult>& results_out) {
222 // 1. ADC self-test (TDAC internal)
223 auto adc_result = SensorSelfTest::run_adc_self_test(
225 results_out.push_back(daq::SelfTestResult{
226 0u, static_cast<uint8_t>(adc_result.passed ? 1 : 0)});
227
228 // 2. Sensor bias continuity test
230 for (uint8_t i = 0; i < cfg.num_sensors; i++) {
231 uint8_t id = cfg.sensor_ids[i];
232 int ch1 = getAdcChannel(id, 1);
233 int ch2 = getAdcChannel(id, 2);
234 if (ch1 < 0 || ch2 < 0)
235 continue;
236
237 auto bias = SensorSelfTest::read_sensor_bias(ads126x, Pins.ADC_DRDY_1,
238 static_cast<uint8_t>(ch1),
239 static_cast<uint8_t>(ch2));
240 uint8_t pass =
241 (bias.result == SensorSelfTest::BiasResult::CONNECTED) ? 1u : 0u;
242 results_out.push_back(daq::SelfTestResult{id, pass});
243 }
245}
246
247void setup() {
248 // Do NOT memset coreState -- it contains IPAddress (has vtable via
249 // Printable) and EthernetUDP. memset zeroes vtable pointers, causing
250 // LoadProhibited crashes on virtual calls like Serial.print(IPAddress).
251 coreState.gateway = IPAddress(0, 0, 0, 0);
252 coreState.subnet = IPAddress(255, 255, 255, 0);
253 coreState.dns = IPAddress(192, 168, 2, 1);
254
255 coreConfig.board_name = "LC";
256 coreConfig.pins = &Pins;
257 coreConfig.init_adc = init_adc_cb;
258 coreConfig.collect_chunk = collect_chunk_cb;
259 coreConfig.send_chunks_to = send_chunks_to_cb;
260 coreConfig.run_self_test = run_self_test_cb;
261 coreConfig.on_reference_voltage_config = on_reference_voltage_cb;
262 coreConfig.user_data = nullptr;
263
265}
266
#define FILTER
Definition main.h:4
#define DATA_RATE
Definition main.h:5
#define INPUT
Definition Arduino.h:21
#define HSPI
Definition Arduino.h:132
int digitalRead(int)
Definition Arduino.h:50
void pinMode(int, int)
Definition Arduino.h:46
unsigned long millis()
Definition Arduino.h:29
#define SPI_MODE1
Definition Arduino.h:131
#define LOW
Definition Arduino.h:20
void delayMicroseconds(unsigned int)
Definition Arduino.h:58
#define ADS126X_REF_POS_VDD
Definition STAR_ADS126X.h:9
#define ADS126X_REF_NEG_VSS
Definition STAR_ADS126X.h:8
#define ADS126X_REF_POS_INT
#define SENSOR_HOTFIRE_MAX_PACKET_SIZE
Shared sensor hotfire state machine core — PT / TC / LC boards.
const int IPAddress
#define ADS126X_ASSERT_FILTER_RATE(filter, rate)
constexpr uint8_t settlePulses(uint8_t filter, uint8_t data_rate, bool chop_enabled=false, bool idac_rotation_enabled=false)
static uint8_t chunk[64]
Definition board_uart9.c:12
static ADS126X ads126x
Definition main.cpp:20
SPIClass ADC_SPI(HSPI)
std::vector< daq::SensorDataChunkCollection > dataChunks
Definition main.cpp:48
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]
Definition main.cpp:15
bool g_verbose
Definition main.cpp:65
static int last_mux_p
Definition main.cpp:75
void setup()
Definition main.cpp:247
static unsigned long g_last_sensor_packet_log_ms
Definition main.cpp:62
SPIClass ADC_SPI(HSPI)
static void run_self_test_cb(void *, const SensorHotfire::StoredSensorConfig &cfg, std::vector< daq::SelfTestResult > &results_out)
Definition main.cpp:219
#define GAIN
Definition main.cpp:50
static const uint8_t LC_ADC1_CONNECTORS[]
Definition main.cpp:53
static int last_mux_n
Definition main.cpp:75
static void collect_chunk_cb(void *)
Definition main.cpp:206
static void flush_cycles(int cycles)
Definition main.cpp:67
static void send_chunks_to_impl(IPAddress dest_ip, int dest_port, bool also_to_abort_controller, IPAddress abort_controller_ip, int abort_controller_port)
Definition main.cpp:118
static SensorHotfire::Config coreConfig
Definition main.cpp:60
static void init_adc_cb(void *)
Definition main.cpp:184
static SensorHotfire::CoreState coreState
Definition main.cpp:59
static void on_reference_voltage_cb(void *, uint8_t reference_voltage)
Definition main.cpp:200
static void collect_chunk_impl()
Definition main.cpp:77
static void send_chunks_to_cb(void *, IPAddress dest_ip, int dest_port, bool also_to_abort_controller, IPAddress abort_controller_ip, int abort_controller_port)
Definition main.cpp:210
void loop()
Definition main.cpp:267
#define HF_VERBOSE(...)
#define HF_VERBOSELN(...)
#define HF_VERBOSELN_()
void setup(CoreState &s, const Config &cfg)
void loop(CoreState &s, const Config &cfg)
void sensor_bias_disable(ADS126X &adc)
Call once after the connector loop.
BiasReadResult read_sensor_bias(ADS126X &adc, uint8_t drdy_pin, uint8_t adc_channel, uint8_t aincom, uint8_t settle_cycles=5)
Read one connector with sensor bias already active.
void sensor_bias_enable(ADS126X &adc)
Call once before the connector loop.
AdcSelfTestResult run_adc_self_test(ADS126X &adc, uint8_t drdy_pin, uint8_t original_ref_neg, uint8_t original_ref_pos)
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.
constexpr int getAdcChannel(uint8_t connector, uint8_t pin)
constexpr const Layout & Pins
bool sensorBatchReady(size_t queued, size_t max_chunks, uint32_t oldest_ms, uint32_t now_ms, uint32_t max_age_ms)
Definition send_policy.h:22
#define SENSOR_MAX_CHUNKS_BEFORE_SEND
#define SENSOR_MAX_BATCH_AGE_MS
Represents a single data chunk with timestamp and sensor datapoints.