STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
PressureFeed.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <cstdint>
6#include <map>
7#include <mutex>
8#include <string>
9#include <thread>
10#include <utility>
11#include <vector>
12
13#include "config/Config.hpp"
14
15namespace sequencer {
16
17/**
18 * Live calibrated pressures, by ROLE NAME, for the sequencer.
19 *
20 * The sequencer is otherwise pressure-blind — its only recv() is the controller's FIRE ack, and its
21 * ElodinClient is publish-only. This is the one thing that reads.
22 *
23 * ── Why it is keyed by role and not by channel ────────────────────────────────────────────────
24 *
25 * ControllerService keys its measurements by channel number, and that is how its `P_copv` came to
26 * be assigned from channel 6 — which on this rig is "GN2 Regulated", the regulated downstream
27 * pressure, not the COPV at all. Its subscriber also filters the table id to board 1, which
28 * silently drops every sensor on PT board 2, "GN2 High" included. Resolving each role through
29 * fsw::config::find_pt_role makes both mistakes unrepresentable: there is no range filter to get
30 * wrong, because the table id is computed from the role.
31 *
32 * ── Why a reading can be refused ──────────────────────────────────────────────────────────────
33 *
34 * A PT whose calibration model is "cubic" with no captured points publishes a smooth, plausible
35 * 0.0 PSI with calibration_status = 0. A script that reads that for a COPV and takes 90% of it
36 * computes a target of zero — and then does nothing at all, successfully, while an operator
37 * watches the state enter and cleanly exit believing the tank came up to pressure. A stale reading
38 * is the same hazard from the other direction: a press loop cycling a valve against a number that
39 * stopped moving. Both are refused rather than returned.
40 */
42public:
43 struct Reading {
44 double psi = 0.0;
45 uint8_t cal_status = 0;
46 std::chrono::steady_clock::time_point at{};
47 };
48
49 /** Why a role could not be read. Reported so a refusal can say which sensor and why. */
50 enum class Status {
52 NotSubscribed, // no script asked for this role, or config does not declare it
53 NoReading, // subscribed, nothing has arrived yet
56 };
57
58 PressureFeed() = default;
60
61 PressureFeed(const PressureFeed&) = delete;
63
64 /**
65 * Subscribe to exactly the roles given, and nothing else.
66 *
67 * Elodin has no wildcard — "every subscriber must name what it consumes" — so the list comes
68 * from the union of every loaded script's pressure() slugs. A role that no script reads costs
69 * no traffic, and with no roles at all the thread never starts.
70 *
71 * Safe to call once, at config load. Roles that config cannot resolve to a board+channel are
72 * reported and skipped; a script naming one was already refused at load.
73 */
74 void start(const fsw::config::Config& cfg, const std::vector<std::string>& roles,
75 const std::string& host, uint16_t port);
76
77 void stop();
78
79 /** @return Ok and fills `psi` only when the reading may be acted on. */
80 Status read(const std::string& role, double& psi) const;
81
82 /** Human phrasing for a refusal, e.g. "GN2 High has no calibrated reading". */
83 static std::string explain(const std::string& role, Status s);
84
85 /** Roles this feed is subscribed to, for diagnostics. */
86 std::vector<std::string> subscribedRoles() const;
87
88 /** How old a reading may be and still be acted on. */
89 static constexpr int kMaxAgeMs = 1000;
90
91private:
92 void runLoop(std::string host, uint16_t port);
93
94 std::thread thread_;
95 std::atomic<bool> running_{false};
96
97 mutable std::mutex mtx_;
98 std::map<std::string, Reading> readings_;
99 /** Elodin table id -> canonical role name, built once from config. Keyed by the whole id: an
100 * ESP32 PT ({0x20, lo}) and a LabJack T7 PT ({0x26, lo}) can share a low byte. */
101 std::map<std::pair<uint8_t, uint8_t>, std::string> role_by_table_;
102};
103
104} // namespace sequencer
PressureFeed()=default
Status
Why a role could not be read.
void start(const fsw::config::Config &cfg, const std::vector< std::string > &roles, const std::string &host, uint16_t port)
Subscribe to exactly the roles given, and nothing else.
std::vector< std::string > subscribedRoles() const
Roles this feed is subscribed to, for diagnostics.
std::atomic< bool > running_
PressureFeed(const PressureFeed &)=delete
Status read(const std::string &role, double &psi) const
std::map< std::pair< uint8_t, uint8_t >, std::string > role_by_table_
Elodin table id -> canonical role name, built once from config.
void runLoop(std::string host, uint16_t port)
static std::string explain(const std::string &role, Status s)
Human phrasing for a refusal, e.g.
std::map< std::string, Reading > readings_
static constexpr int kMaxAgeMs
How old a reading may be and still be acted on.
PressureFeed & operator=(const PressureFeed &)=delete
static const emmc_port_t port
Definition emmc_stm32.c:210
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
std::chrono::steady_clock::time_point at