54dcf6c532
The E84StateMachine timers landed last commit but stayed theoretical — arming was delivered via abstract callbacks the application had to glue to a real clock. This commit ships the canonical glue: - include/secsgem/gem/e84_asio_timers.hpp: header-only E84AsioTimers wraps three asio::steady_timers, wires set_timer_handlers on attach(), routes async_wait expiry back into fsm.on_timeout(). detach() cancels everything cleanly. - tests/test_e84_asio_timers.cpp: four scenarios exercised through a real asio::io_context with wall-clock timers — TA1 expiry, signal-driven cancel before TA1 fires, TA3 expiry from the Transferring state, and detach() halting further transitions. These cover the integration the synthetic unit tests in test_e84_timers.cpp can't reach. - INTEGRATION.md §4.6: the vendor-side recipe — create the port, set timeouts, make_shared<E84AsioTimers>(...)::attach(), feed signals from your I/O bridge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
2.8 KiB
C++
85 lines
2.8 KiB
C++
#pragma once
|
|
|
|
#include <asio.hpp>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#include "secsgem/gem/e84_state.hpp"
|
|
|
|
// asio-backed driver for the SEMI E84 §6 handshake timers (TA1/TA2/TA3).
|
|
//
|
|
// `E84StateMachine` is I/O-free by design: it tells the application to
|
|
// arm or cancel a timer via callbacks and expects the application to
|
|
// drive a real clock and call `on_timeout(id)` on expiry. This
|
|
// header-only adapter is the canonical asio glue — three steady_timers,
|
|
// wired so that an arm() restarts the matching timer and a cancel()
|
|
// stops it; expiry routes back into `fsm.on_timeout(id)`.
|
|
//
|
|
// Usage (one adapter per E84StateMachine, one E84StateMachine per
|
|
// load port via `E84PortStore::get(port_id)`):
|
|
//
|
|
// E84StateMachine* fsm = model->e84_ports.get(port_id);
|
|
// auto driver = std::make_shared<E84AsioTimers>(io.get_executor(), *fsm);
|
|
// driver->attach(); // wires set_timer_handlers
|
|
// fsm->set_timeouts({2s, 2s, 60s}); // SEMI defaults
|
|
//
|
|
// Keep the shared_ptr alive for as long as the FSM may transition —
|
|
// typically tied to the load-port lifetime. Detach (or just drop the
|
|
// shared_ptr) before destroying the FSM.
|
|
namespace secsgem::gem {
|
|
|
|
class E84AsioTimers : public std::enable_shared_from_this<E84AsioTimers> {
|
|
public:
|
|
E84AsioTimers(asio::any_io_executor ex, E84StateMachine& fsm)
|
|
: fsm_(fsm),
|
|
timers_{{asio::steady_timer{ex}, asio::steady_timer{ex},
|
|
asio::steady_timer{ex}}} {}
|
|
|
|
E84AsioTimers(const E84AsioTimers&) = delete;
|
|
E84AsioTimers& operator=(const E84AsioTimers&) = delete;
|
|
|
|
// Wires the FSM's arm/cancel callbacks to the internal timers. Must
|
|
// be called after construction (we need shared_from_this for the
|
|
// expiry continuation).
|
|
void attach() {
|
|
auto self = shared_from_this();
|
|
fsm_.set_timer_handlers(
|
|
[self](E84TimerId id, std::chrono::milliseconds d) {
|
|
self->arm_(id, d);
|
|
},
|
|
[self](E84TimerId id) { self->cancel_(id); });
|
|
}
|
|
|
|
// Detach so timers stop driving the FSM. Pending expiries are
|
|
// cancelled; in-flight asio handlers see ec == operation_aborted and
|
|
// return early.
|
|
void detach() {
|
|
for (auto& t : timers_) t.cancel();
|
|
fsm_.set_timer_handlers({}, {});
|
|
}
|
|
|
|
private:
|
|
void arm_(E84TimerId id, std::chrono::milliseconds d) {
|
|
auto& t = timer_for_(id);
|
|
t.expires_after(d);
|
|
auto self = shared_from_this();
|
|
t.async_wait([self, id](std::error_code ec) {
|
|
if (ec) return; // cancelled or timer destroyed
|
|
self->fsm_.on_timeout(id);
|
|
});
|
|
}
|
|
|
|
void cancel_(E84TimerId id) { timer_for_(id).cancel(); }
|
|
|
|
asio::steady_timer& timer_for_(E84TimerId id) {
|
|
return timers_[static_cast<std::size_t>(id) - 1];
|
|
}
|
|
|
|
E84StateMachine& fsm_;
|
|
std::array<asio::steady_timer, 3> timers_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|