37 int bn =
static_cast<int>(
board_id % 10);
40 return static_cast<uint8_t
>((bn - 1) * 0x20 + local_channel);
46static std::string
trimVal(
const std::string& s) {
47 size_t a = s.find_first_not_of(
" \t\r\n\"");
48 size_t b = s.find_last_not_of(
" \t\r\n\"");
49 return (
a == std::string::npos) ?
"" : s.substr(
a,
b -
a + 1);
52static std::string
toLower(std::string s) {
53 std::transform(s.begin(), s.end(), s.begin(), ::tolower);
63 bind_addr_ = address.empty() ?
"0.0.0.0" : address;
67 const std::string& delay_csv_path) {
76 bind_addr_ = cfg.actuator_service.bind_address;
83 std::map<int, std::string> board_id_to_ip;
84 for (
const auto&
b : cfg.boards)
85 if (!
b.ip.empty() &&
b.board_id > 0)
86 board_id_to_ip[
b.board_id] =
b.ip;
87 if (board_id_to_ip.empty())
88 for (
int i = 11; i <= 14; ++i)
89 board_id_to_ip[i] =
"192.168.2." + std::to_string(i);
92 for (
const auto& [name,
r] : cfg.actuator_roles) {
93 if (
r.channel < 1 ||
r.channel > 10)
102 role.
is_pwm = !
r.controller_role.empty();
109 auto it = board_id_to_ip.find(
r.board_id);
111 (it != board_id_to_ip.end()) ? it->second :
"192.168.2." + std::to_string(
r.board_id);
114 std::cout <<
"[ActuatorCommander] Loaded " <<
roles_.size() <<
" actuator roles from config"
122 const char* fallbacks[] = {
123 "config/state_machine_actuators.csv",
124 "../config/state_machine_actuators.csv",
125 "../../config/state_machine_actuators.csv",
126 "config/profiles/default/state_machine_actuators.csv",
127 "../config/profiles/default/state_machine_actuators.csv",
128 "../../config/profiles/default/state_machine_actuators.csv",
131 std::ifstream f(csv_path);
132 std::string used_csv = csv_path;
134 for (
const char* fb : fallbacks) {
135 if (std::string(fb) == csv_path)
145 std::cerr <<
"[ActuatorCommander] Cannot open state_machine_actuators.csv" << std::endl;
150 if (!std::getline(f, line))
154 std::vector<std::string> headers;
156 std::istringstream iss(line);
158 while (std::getline(iss, cell,
','))
159 headers.push_back(
trimVal(cell));
162 while (std::getline(f, line)) {
163 std::vector<std::string> cells;
164 std::istringstream iss(line);
166 while (std::getline(iss, cell,
','))
167 cells.push_back(
trimVal(cell));
168 if (cells.empty() || cells[0].empty())
171 const std::string& act_name = cells[0];
172 for (
size_t col = 1; col < headers.size() && col < cells.size(); ++col) {
173 if (headers[col].empty())
175 std::string val = cells[col];
176 std::transform(val.begin(), val.end(), val.begin(), ::toupper);
180 else if (val ==
"CLOSE" || val ==
"CLOSED")
194 std::string delay_csv = delay_csv_path;
195 if (delay_csv.empty()) {
196 delay_csv = used_csv;
197 const std::string suffix =
"state_machine_actuators.csv";
198 if (delay_csv.size() >= suffix.size() &&
199 delay_csv.compare(delay_csv.size() - suffix.size(), suffix.size(), suffix) == 0) {
200 delay_csv.replace(delay_csv.size() - suffix.size(), suffix.size(),
201 "state_machine_actuator_delays.csv");
206 std::cerr <<
"[ActuatorCommander] No delay CSV given and \"" << used_csv
207 <<
"\" is not named state_machine_actuators.csv — running with NO "
208 "actuator delays. Set [state_machine].actuator_delay_csv."
213 std::ifstream df(delay_csv);
216 std::vector<std::string> dheaders;
217 if (std::getline(df, dline)) {
218 std::istringstream iss(dline);
220 while (std::getline(iss, cell,
','))
221 dheaders.push_back(
trimVal(cell));
224 while (std::getline(df, dline)) {
225 std::vector<std::string> cells;
226 std::istringstream iss(dline);
228 while (std::getline(iss, cell,
','))
229 cells.push_back(
trimVal(cell));
230 if (cells.empty() || cells[0].empty())
232 for (
size_t col = 1; col < dheaders.size() && col < cells.size(); ++col) {
233 if (dheaders[col].empty() || cells[col].empty())
236 double d = std::stod(cells[col]);
248 std::cout <<
"[ActuatorCommander] Loaded " << nonzero
249 <<
" non-zero actuator delay(s) from " << delay_csv << std::endl;
254 std::cout <<
"[ActuatorCommander] Loaded " <<
state_actuators_.size() <<
" states from "
255 << used_csv << std::endl;
268 const std::string lower =
toLower(state_name);
270 if (
toLower(jt->first) == lower)
280 const std::vector<std::pair<uint8_t, uint8_t>>& id_state_pairs) {
281 return sendBatch({{board_ip, id_state_pairs}});
297 const std::map<std::string, std::vector<std::pair<uint8_t, uint8_t>>>& by_board) {
298 if (by_board.empty())
303 struct sockaddr_in dest;
304 std::vector<uint8_t> buf;
306 std::vector<Outgoing> outgoing;
307 outgoing.reserve(by_board.size());
309 const uint32_t ts_ms =
310 static_cast<uint32_t
>(std::chrono::duration_cast<std::chrono::milliseconds>(
311 std::chrono::steady_clock::now().time_since_epoch())
315 for (
const auto& [board_ip, id_state_pairs] : by_board) {
316 if (id_state_pairs.empty())
318 std::vector<daq::ActuatorCommand> cmds;
319 cmds.reserve(id_state_pairs.size());
320 for (
const auto& [
id, st] : id_state_pairs)
321 cmds.push_back({id, st});
326 std::cerr <<
"[ActuatorCommander] create_actuator_command_packet returned 0 for "
327 << board_ip << std::endl;
331 o.dest.sin_family = AF_INET;
333 if (inet_pton(AF_INET, board_ip.c_str(), &
o.dest.sin_addr) != 1) {
334 std::cerr <<
"[ActuatorCommander] bad board IP " << board_ip << std::endl;
337 o.buf.assign(buf, buf + len);
338 outgoing.push_back(std::move(
o));
340 if (outgoing.empty())
343 int sock = socket(AF_INET, SOCK_DGRAM, 0);
345 std::cerr <<
"[ActuatorCommander] socket() failed: " << strerror(errno) << std::endl;
353 struct timeval tv{.tv_sec = 0, .tv_usec = 100000};
354 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv,
sizeof(tv));
357 struct sockaddr_in local{};
358 local.sin_family = AF_INET;
359 inet_pton(AF_INET,
bind_addr_.c_str(), &local.sin_addr);
360 if (bind(sock,
reinterpret_cast<struct sockaddr*
>(&local),
sizeof(local)) < 0) {
361 std::cerr <<
"[ActuatorCommander] bind(" <<
bind_addr_ <<
") failed: " << strerror(errno)
368 for (
int round = 0; round < 3; ++round) {
369 for (
const auto&
o : outgoing) {
371 sendto(sock,
o.buf.data(),
o.buf.size(), 0,
372 reinterpret_cast<const struct sockaddr*
>(&
o.dest),
sizeof(
o.dest));
373 if (sent !=
static_cast<ssize_t
>(
o.buf.size())) {
377 char ip[INET_ADDRSTRLEN] = {0};
378 inet_ntop(AF_INET, &
o.dest.sin_addr, ip,
sizeof(ip));
379 std::cerr <<
"[ActuatorCommander] sendto(" << ip <<
":" <<
actuator_port_
380 <<
") round " << (round + 1) <<
" failed: " << strerror(errno)
399 std::cerr <<
"[ActuatorCommander] No CSV entry for state: " << state_name << std::endl;
408 std::string role_name;
409 std::string board_ip;
414 uint8_t cmd_table_hi;
416 std::vector<PendingCmd> pending;
422 std::map<std::string, int> script_positions;
429 for (
const auto& [act_name, logical_pos] : it->second) {
430 auto role_it =
roles_.find(act_name);
431 if (role_it ==
roles_.end())
434 if (is_fire && role.
is_pwm)
441 int pos = logical_pos;
442 auto sp = script_positions.find(act_name);
443 if (sp != script_positions.end())
447 if (sp != script_positions.end() && ov->second != sp->second)
452 uint8_t hw_state =
static_cast<uint8_t
>(role.
is_no ? (1 - pos) : pos);
455 pending.push_back({act_name, role.
board_ip,
static_cast<uint8_t
>(role.
channel), hw_state,
456 global_ch,
static_cast<uint8_t
>(pos), role.
cmd_table_hi});
475 std::map<double, std::vector<PendingCmd>> staged;
476 for (
size_t i = 0; i < pending.size(); ++i) {
477 const auto& pc = pending[i];
481 const bool script_owned = script_positions.count(pc.role_name) > 0;
484 if (is_transition && !script_owned) {
487 auto it = ds->second.find(pc.role_name);
488 if (it != ds->second.end() && it->second > 0.0)
492 if (!is_transition && !script_owned) {
497 staged[d].push_back(pc);
504 auto emit = [
this, state_name](
const std::vector<PendingCmd>& cmds,
double delay_s) {
513 std::map<std::string, int> script_positions;
518 std::map<std::string, int> overrides;
524 std::map<std::string, std::vector<std::pair<uint8_t, uint8_t>>> by_ip;
525 std::vector<std::tuple<uint8_t, uint8_t, uint8_t>> logical;
526 for (
const auto& pc : cmds) {
527 uint8_t hw = pc.hw_state;
528 uint8_t pos = pc.logical_pos;
529 auto sp = script_positions.find(pc.role_name);
532 if (overrides.count(pc.role_name))
533 sp = script_positions.end();
534 if (sp != script_positions.end() && sp->second != pc.logical_pos) {
535 auto role_it =
roles_.find(pc.role_name);
536 if (role_it !=
roles_.end()) {
537 pos =
static_cast<uint8_t
>(sp->second);
539 static_cast<uint8_t
>(role_it->second.is_no ? (1 - sp->second) : sp->second);
540 std::cout <<
"[ActuatorCommander] Staged command for \"" << pc.role_name
541 <<
"\" superseded by the running script's position" << std::endl;
544 by_ip[pc.board_ip].emplace_back(pc.channel, hw);
545 logical.emplace_back(pc.cmd_table_hi, pc.global_ch, pos);
552 for (
const auto& [ip, c] : by_ip)
554 std::cout <<
"[ActuatorCommander] Sent " <<
n <<
" commands for state " << state_name;
556 std::cout <<
" (+" << delay_s <<
"s)";
557 std::cout << std::endl;
559 std::cerr <<
"[ActuatorCommander] UDP send failed for state " << state_name
562 for (
const auto& [hi,
ch, pos] : logical)
567 auto zero = staged.find(0.0);
568 if (zero != staged.end())
569 emit(zero->second, 0.0);
571 if (staged.size() <= (zero != staged.end() ? 1u : 0u))
577 std::vector<std::pair<double, size_t>> order;
578 for (
const auto& [d, _] : staged)
580 order.emplace_back(d, 0);
582 std::map<double, std::vector<std::string>> roles_by_delay;
583 for (
const auto& pc : pending) {
587 auto it = ds->second.find(pc.role_name);
588 if (it != ds->second.end() && it->second > 0.0)
589 roles_by_delay[it->second].push_back(pc.role_name);
592 std::thread([
this, gen, staged, order,
emit, roles_by_delay]()
mutable {
593 auto t0 = std::chrono::steady_clock::now();
594 for (
const auto& [delay_s, _] : order) {
595 const auto due = t0 + std::chrono::microseconds(
static_cast<long long>(delay_s * 1e6));
596 while (std::chrono::steady_clock::now() < due) {
599 std::this_thread::sleep_for(std::chrono::milliseconds(1));
603 emit(staged[delay_s], delay_s);
606 for (
const auto&
r : roles_by_delay[delay_s])
622 std::cout <<
"[ActuatorCommander] Continuous loop started for state "
627 bool entry = allow_delays;
628 bool skip = defer_first_pass;
656 std::this_thread::sleep_for(std::chrono::milliseconds(10));
658 std::cout <<
"[ActuatorCommander] Continuous loop stopped" << std::endl;
672 const std::string& role)
const {
676 auto it = ds->second.find(role);
677 return (it != ds->second.end() && it->second > 0.0) ? it->second : 0.0;
682 const std::string& state_name)
const {
684 return it ==
state_actuators_.end() ? std::map<std::string, int>{} : it->second;
690 auto it =
roles_.find(name);
692 const std::string lower =
toLower(name);
693 for (
auto jt =
roles_.begin(); jt !=
roles_.end(); ++jt) {
694 if (
toLower(jt->first) == lower) {
701 std::cerr <<
"[ActuatorCommander] Unknown actuator role: " << name << std::endl;
706 uint8_t hw_state =
static_cast<uint8_t
>(role.
is_no ? (1 - pos) : pos);
707 bool ok =
sendUDP(role.
board_ip, {{static_cast<uint8_t>(role.channel), hw_state}});
709 std::cout <<
"[ActuatorCommander] Manual: " << name <<
" -> "
710 << (pos == 1 ?
"OPEN" :
"CLOSED") << std::endl;
713 publishCommandedState(role.cmd_table_hi, global_ch,
static_cast<uint8_t
>(pos));
723 std::string key = name;
724 auto it =
roles_.find(name);
726 const std::string lower =
toLower(name);
727 for (
auto jt =
roles_.begin(); jt !=
roles_.end(); ++jt) {
728 if (
toLower(jt->first) == lower) {
742 static std::mutex mtx;
743 static std::set<std::string> said;
744 std::lock_guard<std::mutex>
lk(mtx);
745 if (said.insert(role).second)
746 std::cerr <<
"[ActuatorCommander] Manual override on \"" << role
747 <<
"\" is overriding a running script's position for it" << std::endl;
754 std::string canonical = name;
755 auto it =
roles_.find(name);
757 for (
const auto& [rname,
r] :
roles_) {
758 if (rname.size() == name.size() &&
759 std::equal(rname.begin(), rname.end(), name.begin(), [](
char a,
char b) {
760 return std::tolower(static_cast<unsigned char>(a)) ==
761 std::tolower(static_cast<unsigned char>(b));
798 uint8_t logical_pos) {
802 auto ts_ns =
static_cast<uint64_t
>(std::chrono::duration_cast<std::chrono::nanoseconds>(
803 std::chrono::steady_clock::now().time_since_epoch())
807 std::array<uint8_t, 2> table_id = {table_hi, channel_id};
808 elodin_->publish(table_id, msg);
818 for (
const auto& [name, role] :
roles_) {
820 uint8_t logical_pos = role.is_no ? 1 : 0;
825 std::cout <<
"[ActuatorCommander] Published initial state for " <<
roles_.size()
826 <<
" actuators to Elodin DB" << std::endl;
void clearAllManualOverrides()
Clear all manual overrides (e.g.
static void logOverrideShadowingScript(const std::string &role)
Warn once per role that a manual override is fighting a running script for a valve.
void publishCommandedState(uint8_t table_hi, uint8_t global_channel, uint8_t logical_pos)
void setManualOverride(const std::string &name, int pos)
Debug: hold this logical position for the role until cleared or state transition.
void setScriptPosition(const std::string &name, int pos)
A running script's position for one valve — the script layering on top of its state's CSV column.
void clearScriptPositions()
Drop every script position.
void publishInitialState()
Publish initial de-energized state for all actuators to Elodin DB [0x32].
std::map< std::string, int > manual_overrides_
void stopContinuousLoop()
Stop the continuous re-send loop (blocks until the thread exits).
std::mutex script_positions_mutex_
static void logOverrideShadowingScript(const std::string &role)
Warn once per role that a manual override is fighting a running script for a valve.
State fire_state_
Bumped on every transition so an in-flight delayed stage from the previous state aborts instead of la...
std::map< std::string, std::map< std::string, int > >::const_iterator findStateActuators(const std::string &state_name) const
void applyForState(State state, bool is_transition=false)
Send actuator commands for the given state (one shot).
void publishCommandedState(uint8_t table_hi, uint8_t global_channel, uint8_t logical_pos)
fsw::elodin::ElodinClient * elodin_
bool sendBatch(const std::map< std::string, std::vector< std::pair< uint8_t, uint8_t > > > &by_board)
Send to every board with the retransmits interleaved, so boards stay in step.
void setDefaultBindAddress(const std::string &address)
Pin outgoing actuator commands to a NIC, unless [actuator_service].bind_address already named one exp...
bool sendUDP(const std::string &board_ip, const std::vector< std::pair< uint8_t, uint8_t > > &id_state_pairs)
std::map< std::string, int > positionsForState(const std::string &state_name) const
The logical positions the CSV declares for state_name (role -> 0 CLOSED / 1 OPEN).
std::set< std::string > pending_roles_
Roles whose delayed stage has not fired yet.
void startContinuousLoop(State state, bool allow_delays=true, bool defer_first_pass=false)
Start the 1 Hz continuous re-send loop for the given state.
double delayForRole(const std::string &state_name, const std::string &role) const
The staged delay, in seconds, that role waits before moving on entry to state_name.
bool load(const std::string &config_content, const std::string &csv_path, const std::string &delay_csv_path="")
Load actuator roles from config content and state→actuator map from CSV.
std::atomic< bool > loop_running_
std::map< std::string, int > script_positions_
Valves a running script has taken over, role -> logical position.
std::mutex overrides_mutex_
std::map< std::string, std::map< std::string, int > > state_actuators_
std::atomic< uint64_t > schedule_gen_
bool sendSingleActuator(const std::string &name, int pos)
Send one UDP packet for a single role (debug manual command).
std::map< std::string, ActuatorRole > roles_
std::mutex pending_roles_mutex_
std::map< std::string, std::map< std::string, double > > state_actuator_delays_
state -> actuator -> delay in seconds, from state_machine_actuator_delays.csv.
static std::string name(State s)
Human-readable name for a State (e.g.
static ActuatorControllerState state
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
typename Container::const_iterator const_iterator
*brief create JSON pointer *sa reference_tokens const string_t & a
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.
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
constexpr uint8_t kBallValveCmdHi
*brief user defined string literal for JSON values *sa std::size_t n
static std::string toLower(std::string s)
static uint8_t actuator_elodin_low_byte(uint32_t board_id, uint8_t local_channel)
Elodin low byte for [0x32, lo]: (board_slot - 1) * 0x20 + local_channel; slot = board_id % 10,...
comms::CommsMessage< uint64_t, uint8_t, uint8_t > ActuatorCommandedMsg
static std::string trimVal(const std::string &s)
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
static void emit(sensors_core_t *c, uint8_t type, const void *data, uint8_t len)
CommsMessage - Template-based message serialization.
The whole parsed config.toml, restricted to what C++ consumes.
Config entry for one named actuator role loaded from config.toml [actuator_roles].
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")