C2: S5F9-F18 server dispatch for exception lifecycle
Wires the ExceptionStore from C1 onto the wire: State-change emitter (set on ExceptionStore at server startup): NoState -> Posted emit S5F9 (exception post notify) Recovering -> Cleared emit S5F15 with EXRESULT="OK" Recovering -> RecoverFailed emit S5F15 with EXRESULT="FAILED" Posted -> Cleared emit S5F11 (autonomous clear) RecoverFailed -> Cleared emit S5F11 Router additions: S5F13 -> ExceptionStore::on_recover(exid, exrecvra) -> S5F14 ACKC5 S5F17 -> ExceptionStore::on_recover_abort(exid) -> S5F18 ACKC5 S5F9 emission auto-spools when the link is offline (deliver_or_spool already handles that). The synthetic NoState->Posted transition fires inside ExceptionStore::post(), so any application code that calls model->exceptions.post(...) will produce wire activity through this handler without further plumbing. Verified: docker-built secsgem_tests now reports 188 cases / 1026 assertions / 0 failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,58 @@ int main(int argc, char** argv) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- E5 exception state-change emitters -------------------------------
|
||||||
|
// Translate ExceptionStore state changes into S5F9/F11/F15 on the wire,
|
||||||
|
// mirroring how PJ state changes become S16F9. The synthetic
|
||||||
|
// NoState->Posted event is the trigger for emitting S5F9 (we look up
|
||||||
|
// the metadata from the store so we can build the full body).
|
||||||
|
model->exceptions.set_state_change_handler(
|
||||||
|
[model, logfn, deliver_or_spool](uint32_t exid,
|
||||||
|
gem::ExceptionState from,
|
||||||
|
gem::ExceptionState to,
|
||||||
|
gem::ExceptionEvent trig) {
|
||||||
|
logfn(std::string("EX ") + std::to_string(exid) + ": " +
|
||||||
|
gem::exception_state_name(from) + " -> " +
|
||||||
|
gem::exception_state_name(to) + " (" +
|
||||||
|
gem::exception_event_name(trig) + ")");
|
||||||
|
|
||||||
|
if (from == gem::ExceptionState::NoState &&
|
||||||
|
to == gem::ExceptionState::Posted) {
|
||||||
|
const auto* ex = model->exceptions.get(exid);
|
||||||
|
if (!ex) return;
|
||||||
|
deliver_or_spool(
|
||||||
|
gem::s5f9_exception_post_notify(exid, ex->extype, ex->exmessage,
|
||||||
|
ex->exrecvra),
|
||||||
|
"S5F9 EXID=" + std::to_string(exid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to == gem::ExceptionState::Cleared) {
|
||||||
|
if (from == gem::ExceptionState::Recovering) {
|
||||||
|
// RecoveryComplete -> emit S5F15 with success EXRESULT.
|
||||||
|
deliver_or_spool(
|
||||||
|
gem::s5f15_exception_recover_complete_notify(exid, "OK"),
|
||||||
|
"S5F15 EXID=" + std::to_string(exid));
|
||||||
|
}
|
||||||
|
// S5F11 for any Cleared transition that didn't go through
|
||||||
|
// Recovering (autonomous clear or post-failure clear).
|
||||||
|
if (from == gem::ExceptionState::Posted ||
|
||||||
|
from == gem::ExceptionState::RecoverFailed) {
|
||||||
|
deliver_or_spool(
|
||||||
|
gem::s5f11_exception_clear_notify(exid, "", ""),
|
||||||
|
"S5F11 EXID=" + std::to_string(exid));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == gem::ExceptionState::Recovering &&
|
||||||
|
to == gem::ExceptionState::RecoverFailed) {
|
||||||
|
deliver_or_spool(
|
||||||
|
gem::s5f15_exception_recover_complete_notify(exid, "FAILED"),
|
||||||
|
"S5F15 EXID=" + std::to_string(exid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Build the SECS dispatch table once -------------------------------
|
// ---- Build the SECS dispatch table once -------------------------------
|
||||||
gem::Router router;
|
gem::Router router;
|
||||||
|
|
||||||
@@ -533,6 +585,30 @@ int main(int argc, char** argv) {
|
|||||||
alarms, [model](uint32_t id) { return model->alarms.active(id); });
|
alarms, [model](uint32_t id) { return model->alarms.active(id); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// S5F13/F14 — Exception Recover Request. Validates EXRECVRA against
|
||||||
|
// the candidates the matching S5F9 advertised; on Accept the FSM
|
||||||
|
// transitions Posted/RecoverFailed -> Recovering. Equipment-side
|
||||||
|
// recovery progress is signalled by the application calling
|
||||||
|
// model->exceptions.fire_internal(exid, RecoveryComplete/Failed).
|
||||||
|
router.on(5, 13, [model, logfn](const s2::Message& msg) {
|
||||||
|
auto req = gem::parse_s5f13(msg);
|
||||||
|
auto ack = req ? model->exceptions.on_recover(req->exid, req->exrecvra)
|
||||||
|
: gem::AlarmAck::Error;
|
||||||
|
logfn("S5F13 EXID=" + std::to_string(req ? req->exid : 0) +
|
||||||
|
" action=" + (req ? req->exrecvra : std::string{"?"}) +
|
||||||
|
" -> S5F14 ACKC5=" + std::to_string(static_cast<int>(ack)));
|
||||||
|
return gem::s5f14_exception_recover_ack(ack);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.on(5, 17, [model, logfn](const s2::Message& msg) {
|
||||||
|
auto exid = gem::parse_s5f17(msg);
|
||||||
|
auto ack = exid ? model->exceptions.on_recover_abort(*exid)
|
||||||
|
: gem::AlarmAck::Error;
|
||||||
|
logfn("S5F17 EXID=" + std::to_string(exid.value_or(0)) +
|
||||||
|
" -> S5F18 ACKC5=" + std::to_string(static_cast<int>(ack)));
|
||||||
|
return gem::s5f18_exception_recover_abort_ack(ack);
|
||||||
|
});
|
||||||
|
|
||||||
router.on(7, 3, [model, logfn](const s2::Message& msg) {
|
router.on(7, 3, [model, logfn](const s2::Message& msg) {
|
||||||
auto pp = gem::parse_s7f3(msg);
|
auto pp = gem::parse_s7f3(msg);
|
||||||
if (!pp) return gem::s7f4_process_program_ack(gem::ProcessProgramAck::LengthError);
|
if (!pp) return gem::s7f4_process_program_ack(gem::ProcessProgramAck::LengthError);
|
||||||
|
|||||||
Reference in New Issue
Block a user