16 static const std::string kSuffix =
".script";
17 if (name.size() <= kSuffix.size())
19 if (name.compare(name.size() - kSuffix.size(), kSuffix.size(), kSuffix) != 0)
21 const std::string stem = name.substr(0, name.size() - kSuffix.size());
25 const bool ok = (c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z') ||
26 (c >=
'0' && c <=
'9') || c ==
'_' || c ==
'-';
36 std::ifstream f(
path, std::ios::binary);
39 std::ostringstream ss;
45std::string
joinPath(
const std::string& dir,
const std::string& leaf) {
48 if (dir.back() ==
'/')
50 return dir +
"/" + leaf;
55 return file +
" line " + std::to_string(d.
line) +
": " + d.
message;
72 std::string config_level_problem;
73 for (
const auto&
b : build.bad)
74 config_level_problem += (config_level_problem.empty() ?
"" :
"; ") +
b.where +
" \"" +
75 b.canonical +
"\" has no usable slug (got \"" +
b.slug +
"\")";
76 for (
const auto& c : build.collisions)
77 config_level_problem += (config_level_problem.empty() ?
"" :
"; ") + c.where +
"s \"" +
78 c.first +
"\" and \"" + c.second +
"\" both slug to " + c.slug;
80 for (
const auto& sd : cfg.states) {
81 if (sd.script_file.empty())
83 if (sd.id < 0 || sd.id > 255 || sd.name.empty())
85 const State self =
static_cast<State>(
static_cast<uint8_t
>(sd.id));
87 auto refuse = [&](
const std::string& why) {
88 out.refused[self] = sd.name +
": " + why;
91 if (!config_level_problem.empty()) {
92 refuse(
"config cannot be resolved to script names — " + config_level_problem);
99 "carries both is_flow and a script — the characterization hold and a script "
100 "would both own this state's timer");
104 refuse(
"is the [fire] state and cannot also run a script");
109 "is an abort state and cannot run a script — an abort must reach the valves "
110 "immediately, never behind an interpreter");
116 refuse(
"script_file \"" + sd.script_file +
117 "\" is not a bare <name>.script filename (letters, digits, _ and - only)");
122 if (sd.script_timeout_ms == 0) {
124 "has a script but no script_timeout_ms — an unbounded script has no safe "
129 refuse(
"script_timeout_ms is " + std::to_string(sd.script_timeout_ms) +
131 " ms ceiling — a typo must not arm a long valve-open window");
136 if (sd.script_return_target.empty()) {
138 "has a script but no script_return_target — where it lands when the script "
139 "runs off its end must be stated, not defaulted");
142 if (sd.script_timeout_target.empty()) {
144 "has a script but no script_timeout_target — where it lands when the timeout "
145 "expires must be stated, not defaulted");
150 const std::string& name;
154 {
"script_return_target", sd.script_return_target,
156 {
"script_timeout_target", sd.script_timeout_target,
159 bool target_bad =
false;
160 for (
const Target& t : targets) {
162 refuse(std::string(t.key) +
" \"" + t.name +
"\" is not a known state");
166 if (t.resolved == self) {
170 refuse(std::string(t.key) +
" is this state itself — it would re-arm forever");
175 refuse(std::string(t.key) +
" \"" + t.name +
176 "\" is not an allowed transition from here — the state would have no way "
188 "has no column in state_machine_actuators.csv — a dynamic state still needs "
189 "one, because entry applies it first to put every valve in a defined position "
190 "before the script runs");
195 const std::string
path = joinPath(joinPath(ctx.
config_dir,
"scripts"), sd.script_file);
198 refuse(
"cannot read " +
path +
199 " — the script did not deploy with the profile, or the name is wrong");
206 refuse(renderDiag(sd.script_file, parsed.
diagnostics.front()));
210 refuse(sd.script_file +
" is empty — a dynamic state must actually do something");
217 for (
const auto& other : cfg.states) {
218 if (other.name.empty())
226 if (!issues.empty()) {
227 refuse(renderDiag(sd.script_file, issues.front()));
240 std::set<std::string> roles;
247 auto it =
tables.actuators.find(ref.slug);
248 if (it !=
tables.actuators.end())
253 auto it =
tables.sensors.find(ref.slug);
254 if (it !=
tables.sensors.end()) {
256 roles.insert(it->second);
262 tables.states.count(ref.slug) ?
tables.states.at(ref.slug) : std::string());
268 out.dynamic[self] = std::move(ds);
static State fromName(const std::string &name)
Translate a CSV name (e.g.
bool isAllowed(State from, State to) const
Returns true if transitioning from→to is allowed.
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
std::string slugify(const std::string &name)
Canonical name -> slug: trim, uppercase, collapse internal whitespace runs to a single '_'.
ParseResult parse(const std::string &source)
Lex and parse, applying every check that does not need config.
TableBuild build_slug_tables(const fsw::config::Config &cfg)
Build the actuator / sensor / state slug tables from a loaded config.
std::vector< Diagnostic > validate(const ScriptProgram &program, const SlugTables &tables)
Config-dependent checks.
std::string renderDiag(const std::string &file, const fsw::script::Diagnostic &d)
One diagnostic rendered the way an operator should see it on the panel.
std::string joinPath(const std::string &dir, const std::string &leaf)
bool is_valid_script_filename(const std::string &name)
True when name is a safe bare script filename: ^[A-Za-z0-9_-]+.script$, no separators.
static std::string readFile(const std::string &path)
constexpr uint32_t kMaxScriptTimeoutMs
Hard ceiling on script_timeout_ms, independent of per-state config.
ScriptLoadResult load_dynamic_states(const fsw::config::Config &cfg, const std::string &config_text, const ScriptLoadContext &ctx)
Resolve, load and validate every [[states]] entry carrying a script_file.
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
The whole parsed config.toml, restricted to what C++ consumes.
std::string message
Human wording, including the offending text.
std::vector< Diagnostic > diagnostics
std::vector< SlugRef > slugs
Every slug reference in SOURCE ORDER, with spans.
What a script's slugs are allowed to name, built from the loaded config.
One dynamic state, resolved and proven usable.
State return_target
Where the state lands when the script runs off its end.
std::vector< std::string > pressure_roles
Canonical PT role names this script reads, deduplicated.
std::vector< std::string > slug_names
Parallel to program.slugs — what each slug reference resolves to, decided at load.
State timeout_target
Where it lands when timeout_ms expires.
fsw::script::ScriptProgram program
std::vector< State > slug_states
Everything load_dynamic_states needs that it cannot get from Config alone.
const StateMachine * state_machine
Transition matrix, already loaded.
State fire_state
The [fire] state, so a script cannot be attached to the burn.
std::function< bool(const std::string &state_name)> has_actuator_column
True when the named state has a column in state_machine_actuators.csv.
std::string config_dir
Directory holding config.toml; scripts resolve to <config_dir>/scripts/<script_file>.
What a config load made of its dynamic states.
static SlugTables tables()
The tables every case validates against.