Commit Graph

3 Commits

Author SHA1 Message Date
raphael 224130d99f 100%/C: Limits Monitoring (S2F45–F48, E30 §6.21)
tests / build-and-test (push) Failing after 35s
New LimitMonitorStore keyed by VID; each entry is a vector of
LimitDefinition (LIMITID + upper/lower deadband as arbitrary Items).
S2F45/F46 set, S2F47/F48 read.  VLAACK validates each VID exists.

Four new SxFy in the catalog; codegen handles the nested
list-of-(VID, list-of-LimitDefinition) shape.  LimitDefinition is
defined in store/limits.hpp and referenced as external_struct so the
data model and the message codecs share one type.

The actual "value crossed limit" detection + CEID emission is left to
the application's set_value path (E30 §6.21 leaves *how* the equipment
detects crossings up to the implementer).

COMPLIANCE.md: Limits Monitoring Additional capability flips .
Tests: 80 cases / 465 assertions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:08:19 +02:00
raphael 0721db9542 Close COMPLIANCE.md gap: spooling (E30 §6.22)
tests / build-and-test (push) Failing after 42s
Implements the largest functional gap from the compliance audit. The
equipment now queues events the host can't immediately receive (either
because there's no SELECTED session or because the demo's force-spool
flag is on) and transmits the queue on host request.

What's new

  include/secsgem/gem/store/spool.hpp
    SpoolStore: a deque queue with a configurable per-stream whitelist
    (so only streams 5+6 spool by default), a max_size cap with FIFO
    eviction on overflow, and a `force_spool` test flag.  Enqueue
    returns one of Queued / Dropped_NotSpoolable / Dropped_Full so the
    caller can fall back to live delivery when appropriate.  Drain
    pops the entire queue in FIFO order.  Two new ack enums:
    ResetSpoolAck (S2F44 RSPACK) and SpoolRequestAck (S6F24 RSDA), plus
    SpoolRequestCode (S6F23 RSDC, Transmit/Purge).

  data/messages.yaml + auto-regenerated messages.hpp
    S2F43 W   <L,n <B stream>>            Reset Spooling
    S2F44     <L,2 <B RSPACK> <L,a ...>>  Reset Spooling Ack
    S6F23 W   <B RSDC>                    Request Spooled Data
    S6F24     <B RSDA>                    Request Spooled Data Ack

  data/equipment.yaml
    `spool:` section: max_size + spoolable_streams list.  Two new host
    commands SPOOL_ON / SPOOL_OFF that flip the force-spool flag (these
    stand in for "host link down" in the demo without dropping TCP).

  include/secsgem/gem/store/host_commands.hpp
    Spec/Result gain an optional<bool> force_spool field.  S2F41
    dispatch returns the result, the server applies it after S2F42 is
    queued.

  src/config/loader.cpp
    Reads `spool:` from equipment.yaml; reads `force_spool` from each
    host_commands entry; populates SpoolStore + CommandSpec.

  apps/secs_server.cpp
    New `deliver_or_spool(msg, what)` helper.  emit_event and
    emit_alarm_set funnel through it: if force_spool is on (or there's
    no active session), msg.stream is checked against the spoolable
    list and the message is enqueued; otherwise it's sent live.
    Two new handlers:
      S2F43  parses the stream list, updates SpoolStore, replies S2F44
      S6F23  RSDC=Transmit drains and re-sends each as a fresh primary
             (posted on the executor so the S6F24 ack flushes first);
             RSDC=Purge clears the queue and acks.
    The S2F41 handler now also propagates result.force_spool into the
    SpoolStore.

  apps/secs_client.cpp
    Demo extended with 4 new steps after the FAULT branch:
      SPOOL_ON  -> S2F42 Accept
      START     -> S2F42 Accept; CEID 300 emission spooled (no live S6F11)
      SPOOL_OFF -> S2F42 Accept; queue still has the message
      S6F23(Transmit) -> S6F24 Accept; spooled S6F11 arrives next
    Then the existing S7F19/S7F5/S10F1/S1F15/Separate flow continues.

  tests/test_data_model.cpp
    Four new TEST_CASEs for SpoolStore (whitelist, FIFO eviction at
    max_size, drain ordering, force flag).

  tests/test_loader.cpp
    Confirms equipment.yaml's `spool:` section populates the store and
    `force_spool: true/false` flows through to dispatch results.

  COMPLIANCE.md
    Spooling moves from  to 🟡.  Adds S2F43/F44 + S6F23/F24 as  in
    the message coverage matrix; calls out what's still missing
    (S6F25/F26 notification, automatic activation on HSMS NOT-SELECTED,
    persistent on-disk spool).

Verified

  - Tests: 73 cases / 383 assertions pass (+4 spool cases).
  - Demo (docker compose up server client) walks the full happy path
    and the spool path, observed in the server log as:
        spool: force_spool=true (depth=0)
        spool: S6F11 CEID=300 queued (depth=1)
        spool: force_spool=false (depth=1)
        S6F23 transmit: draining 1 messages
    and on the host side as the queued S6F11 arriving in the correct
    order after S6F24.

Known limitations (logged in COMPLIANCE.md)

  - Spool activation is manual via SPOOL_ON/OFF rather than
    automatically triggered by HSMS NOT-SELECTED.
  - No S6F25/F26 spooled-data-ready notification on re-SELECT.
  - In-memory only; an equipment restart loses queued events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 22:06:55 +02:00
raphael 711ee1b40f #4 Split EquipmentDataModel into focused stores
The god-class is gone.  Each capability is now its own focused store:
StatusVariableStore, DataVariableStore, EquipmentConstantStore (with EAC
range validation), EventReportSubscriptions, AlarmRegistry, RecipeStore,
Clock, HostCommandRegistry.  Each is independently testable.

EquipmentDataModel becomes a small composite that holds one of each store
as a public member, plus three convenience methods (vid_value, vid_exists,
compose_reports_for) that span SVIDs+DVIDs and inject the right callbacks
into the EventReportSubscriptions.

New under include/secsgem/gem/store/:

  status_variables.hpp   StatusVariable, StatusVariableStore,
                         DataVariable, DataVariableStore
  equipment_constants.hpp EquipmentConstant, EquipmentConstantStore,
                          EquipmentAck. set_value() now validates
                          numeric values against min_str/max_str and
                          returns EAC=4 on out-of-range — closes the
                          COMPLIANCE.md gap about EC range validation.
  event_reports.hpp      CollectionEvent, Report, ReportData,
                         EventReportSubscriptions + DefineReportAck,
                         LinkEventAck, EnableEventAck. The store is
                         pure data; VidLookup / VidExists callbacks
                         are injected at define / emit time so the
                         service doesn't back-reference the SVID
                         store.
  alarms.hpp             Alarm, AlarmAck, AlarmRegistry.
                         Encapsulates the (enabled, active) sets and
                         ALCD byte computation.
  recipes.hpp            ProcessProgramAck, RecipeStore.
  clock.hpp              TimeAck, Clock. set_time_string applies an
                         offset so subsequent reads reflect the host
                         time without mutating system clock.
  host_commands.hpp      HostCmdAck, CommandParameter,
                         HostCommandRegistry with Spec/Result types.

include/secsgem/gem/data_model.hpp shrinks to a 50-line composite:

  struct EquipmentDataModel {
    StatusVariableStore       svids;
    DataVariableStore         dvids;
    EquipmentConstantStore    ecids;
    EventReportSubscriptions  events;
    AlarmRegistry             alarms;
    RecipeStore               recipes;
    Clock                     clock;
    HostCommandRegistry       commands;
    /* + vid_value, vid_exists, compose_reports_for sugar */
  };

src/gem/data_model.cpp is gone — every store is inline header-only.

include/secsgem/gem/messages_helpers.hpp picks up EventReportAck and
TerminalAck (S6F12 / S10F2-F4 ack enums that aren't tied to any one
store).

Call-site updates:

  apps/secs_server.cpp   model->status_variable(id) -> model->svids.get(id),
                         model->equipment_constant(id) -> model->ecids.get(id),
                         model->alarm_set(id) -> model->alarms.set_active(id),
                         model->dispatch_command(...) -> model->commands.dispatch(...),
                         and similar across every handler.  Plus
                         model->current_time_string() -> model->clock....

  src/config/loader.cpp  model.add_status_variable(sv) -> model.svids.add(sv),
                         and similar.  HostCommandRegistry::Spec replaces
                         EquipmentDataModel::CommandSpec.

  apps/secs_client.cpp   std::vector<EquipmentDataModel::CommandParam> ->
                         std::vector<CommandParameter>.

  tests/test_data_model.cpp  Rewritten around the individual stores;
                         each gets its own TEST_CASE block.  Adds three
                         new cases covering EC range validation (in
                         range / out of range / non-numeric skipped).

  tests/test_loader.cpp  m.has_event(100) -> m.events.has_event(100),
                         etc.

Verified:

  - Tests: 69 cases / 370 assertions pass (was 67 / 384; -14 stale
    composite-API assertions + 16 new store-level assertions covering
    EC range validation and the per-store add/get/list/delete paths).
  - Demo: byte-identical behaviour across the full 17-step flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:51:54 +02:00