Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Test Information System

The Test Information System (TIS) is AutoCore’s unified pipeline for collecting, storing, and exporting test data across the control program and the HMI. You declare your test schema once in project.json, and the server hands the control program a typed Rust struct, the HMI a typed TypeScript schema, and a four-tier filesystem layout that’s the same on every project.

Historical note. TIS was originally called the “Standardized Results System.” Old IPC topics (results.*), generated structs (ResultsSystem), and GM variables (results_*) were renamed wholesale during the rebrand. The original reference prose has been moved to the appendix at the bottom of this chapter for historical context — everything before the appendix is the current contract.

The four-level hierarchy

Every recorded test is identified by four canonical keys. The same names are used in the wire format, the generated code, the disk layout, and the HMI:

LevelUser-facing nameWire/code keyWhat it is
1Projectproject_idThe customer, contract, or product line. Owns many tests.
2Test Methodmethod_idThe standardised recipe: schema, chart views, validation rules. Defined in project.json under test_methods.
3Samplesample_idThe physical object in the machine right now. Operator types it on the setup form; required.
4Test Recordrun_idA single test event. Auto-generated ISO timestamp when tis.start_test fires.

On disk:

datastore/results/
└── <project_id>/
    └── <method_id>/
        └── <run_id>/
            ├── test.json          ← includes sample_id at the top level
            ├── cycles.jsonl
            ├── raw_data/<blob>.json
            └── filtered_data/<blob>.json

test.json:

{
  "project_id":  "plant_a",
  "method_id":   "translational_traction",
  "run_id":      "20260422T140231Z",
  "start_time":  "2026-04-22T14:02:31Z",
  "sample_id":   "SAMPLE-0042",
  "config":      { "specimen_notes": "…", "control_load": 1000 },
  "results":     { "avg_cof": 0.50, "max_cof": 0.55, "min_cof": 0.45 }
}

sample_id lives at the top level — peer of project_id, method_id, and run_id — because it’s part of the run’s structural identity, not a user-pickable config field.

Target integration workflow

Adding TIS to a new project takes four steps, no hand-wiring:

  1. Declare the test methods in project.json:

    {
      "test_methods": {
        "translational_traction": {
          "project_fields":  [ { "name": "customer", "type": "string", "required": true } ],
          "config_fields":   [ { "name": "control_load", "type": "f32", "units": "N" } ],
          "cycle_fields":    [
            { "name": "cycle_index", "type": "u32", "source": "gm.cycle_count" },
            { "name": "actual_load", "type": "f32", "source": "gm.zforce_load" }
          ],
          "results_fields":  [ { "name": "avg_load", "type": "f32" } ],
          "raw_data": {
            "blob_name": "trace",
            "columns": {
              "t":     { "source": "time" },
              "force": { "source": "ni.traction.channels.tsdr_fz" }
            },
            "units": { "t": "s", "force": "N" }
          },
          "views": {
            "load_per_cycle": {
              "type": "cycle_scatter",
              "x": { "field": "cycle_index", "label": "Cycle" },
              "y": [ { "field": "actual_load", "label": "Load (N)" } ]
            }
          }
        }
      }
    }
    
  2. Generate the typed code:

    acctl codegen-tags
    

    This regenerates control/src/gm.rs (with TestInformationSystem plus one *TestManager per method) and www/src/autocore/tis.ts (with one *Schema per method).

  3. In the control program:

    #![allow(unused)]
    fn main() {
    pub struct MyProgram {
        tis: TestInformationSystem,
        daq: DaqCapture,
    }
    
    impl ControlProgram for MyProgram {
        type Memory = GlobalMemory;
        fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
            if let Some(_method) = self.tis.tick_with_autostart(ctx) {
                // first tick of a new run; initialise per-run state here
            }
            // ... run the cycle ...
            if let Err(e) = self.tis.translational_traction.record_raw_trace(&self.daq, ctx) {
                log::warn!("record_raw_trace failed: {}", e);
            }
            self.tis.record_cycle(ctx);
            self.tis.end_active(ctx);
        }
    }
    }
  4. In the HMI:

    <TisProvider>
      <TabView>
        <TabPanel header="Setup">   <TestSetupForm />        </TabPanel>
        <TabPanel header="Data">    <TestDataView />         </TabPanel>
        <TabPanel header="History"> <ResultHistoryTable />   </TabPanel>
      </TabView>
    </TisProvider>
    

That’s the entire surface area. No projectId props, no GM tag indirection, no manual stage_test wiring. acctl new-tis-project <name> lays this skeleton down for you in one command.

Lifecycle in detail

Three IPC verbs drive the lifecycle from the HMI side:

  • tis.stage_test — Operator filled in the form, the form is valid, the rig is ready. Payload: { project_id, method_id, sample_id, config }. Stage is persistent: a successful start_test does NOT consume it. The operator can run sample after sample by changing only sample_id and clicking Start. Cancel via tis.clear_staged.

  • tis.start_test — Operator (or the control program) opens a record on disk. Server creates test.json + the empty cycles.jsonl + raw_data/ + filtered_data/ directories. Sets tis.active = true and broadcasts the four tis.active_* scalars. The control program’s tick_with_autostart does this for you.

  • tis.finish_test — Closes the record. Flips tis.active = false. The control program’s end_active(ctx) calls this.

Auto-injected GM scalars (added by Project::normalize() at server load when test_methods is non-empty):

GM variableLinked toType
tis_stagedtis.stagedbool
tis_staged_project_idtis.staged_project_idstring
tis_staged_method_idtis.staged_method_idstring
tis_staged_sample_idtis.staged_sample_idstring
tis_activetis.activebool
tis_active_project_idtis.active_project_idstring
tis_active_method_idtis.active_method_idstring
tis_active_sample_idtis.active_sample_idstring
tis_active_run_idtis.active_run_idstring

These nine scalars are the contract between the control program and the server’s lifecycle state. Don’t add them to project.json yourself — normalize() will inject them, and they’re typed Internal (not bound to any hardware).

Filtered data mirrors raw data

Every test method that declares a raw_data block automatically gets a parallel filtered_data blob with the same columns and the same blob name. The server creates both directories on start_test, and exposes symmetric IPC verbs:

RawFiltered
tis.add_raw_datatis.add_filtered_data
tis.read_rawtis.read_filtered
tis.list_rawtis.list_filtered

Codegen emits record_raw_trace(daq, ctx) and set_filtered_trace(col_a, col_b, ..., ctx) per TestManager. The control program normally ships raw_data; a post-processing step (inline, Python, or offline) writes filtered_data. Absent filtered files are non-errors — consumers degrade gracefully.

raw_data.columns — typed channel sources

raw_data.columns is a map of column_name → { source }. The codegen reads source and emits the right channel pull for each:

  • "time" — synthesizes a linear time axis from data.actual_samples and data.sample_rate (no DAQ channel needed).
  • "ni.<daq>.channels.<chan>" — looks up the channel index in project.json’s modules.ni.config.daq[<daq>].channels array and emits a typed pull from DaqCapture.

A single record_raw_trace(daq: &DaqCapture, ctx) per TestManager ships the whole columnar blob:

#![allow(unused)]
fn main() {
match self.tis.active_test {
    Some(TestType::TranslationalTraction) =>
        self.tis.translational_traction.record_raw_trace(&self.daq, ctx),
    Some(TestType::RotationalTraction) =>
        self.tis.rotational_traction.record_raw_trace(&self.daq, ctx),
    None => Ok(()),
}
}

Failures (DAQ not ready, channel missing, schema error) return RawTraceError; the control program decides whether to log, retry, or surface the failure on the HMI.

The legacy string-array form of raw_data.columns (e.g., ["t", "force"]) is rejected at parse time. Migrate every column to the explicit { source: "..." } form.

RPC reference

Verbs (caller → server, ack/response)

TopicCallerPurpose
tis.stage_testHMI{ project_id, method_id, sample_id, config } — declare a run intent. Persistent.
tis.clear_stagedHMIDrop the staged entry.
tis.start_testControlOpen a new run on disk. sample_id taken from the staged record (or supplied directly).
tis.add_cycleControlAppend one cycle to cycles.jsonl.
tis.add_raw_dataControlWrite raw_data/<name>.json.
tis.add_filtered_dataPost-processWrite filtered_data/<name>.json.
tis.update_resultsControlPatch the results block of test.json.
tis.finish_testControlClose the active run.
tis.list_schemasHMI{ test_methods, default_method_id } from project.json. Called by <TisProvider> on mount.
tis.list_projectsHMIList directories under base_directory.
tis.list_methodsHMIList method dirs under one project.
tis.list_testsHMI{ project_id, method_id? }method_id optional; omit it to aggregate runs across methods.
tis.read_testHMIFull test.json for one run.
tis.read_cyclesHMIPaginated cycles.jsonl.
tis.read_raw / tis.list_rawHMIRead or list raw_data/.
tis.read_filtered / tis.list_filteredHMIRead or list filtered_data/.
tis.statusEitherRead the staged record (diagnostic; prefer tis.staged* scalars for gating).

Broadcasts (server → clients, no subscription needed)

TopicFires whenPayload
tis.cycle_addedadd_cycle succeeds{ project_id, method_id, run_id, cycle }
tis.results_updatedupdate_results succeeds{ project_id, method_id, run_id, results }
tis.stagedstage_test, clear_stagedbool (scalar)
tis.staged_project_idstage_teststring
tis.staged_method_idstage_teststring
tis.staged_sample_idstage_teststring
tis.activestart_test, finish_testbool (scalar)
tis.active_project_idstart_test, finish_teststring
tis.active_method_idstart_test, finish_teststring
tis.active_sample_idstart_test, finish_teststring
tis.active_run_idstart_test, finish_teststring

The scalar broadcasts get auto-linked into GM by normalize(). The two JSON-payload broadcasts (cycle_added, results_updated) are consumed directly by <TestDataView> and don’t need to be added to project.json.

HMI components

The four TIS components self-drive when wrapped in a <TisProvider>. All props are optional overrides — pass them only when you want to lock a particular axis (e.g., a per-method history page).

ComponentDefault behaviour
<TestSetupForm>Renders the schema for selection.methodId. Required Sample ID input alongside Project ID. Stages tests itself via tis.stage_test whenever the form validates.
<TestDataView>Reads the run pinned in selection, falls back to the active run, renders cycle scatter + cycle table + results. Header shows Sample ID prominently.
<TestRawDataView>Same selection rules; renders any type: "raw_trace" views from the schema.
<ResultHistoryTable>Project-scoped (across every method) by default — switching the loaded method on Setup tab doesn’t hide already-recorded runs. Sample ID is the primary column.

Hooks exposed by <TisProvider>

useTis()           // direct access to the whole context value
useTisSchemas()    // SchemaRegistry from tis.list_schemas
useTisState()      // live readiness scalars (staged*, active*)
useTisSelection()  // [{ projectId, methodId, sampleId, runId }, setSelection]
useTisRuns(projectId?, methodId?)  // run list + refresh()
useTisRun(runId?)  // { meta, cycles, results, rawData, loading }

Selection has two layers: explicit pins (set via setSelection({ runId: "..." })) and an active follower (each field auto-tracks its tis.active_* scalar until pinned). Pass null to a field to clear its pin.

Asset traceability — asset_refs

A test method may declare a list of asset_refs so each test record captures the active calibration values of the equipment that produced it. At tis.start_test, AMS resolves the refs and writes the snapshot into test.json::asset_snapshot:

"test_methods": {
  "translational_traction": {
    "config_fields": [/* … */],
    "asset_refs": [
      { "field": "load_cell_z", "asset_type": "load_cell",
        "select": "by_location", "location": "tsdr_z",
        "calibration_required": "warn" }
    ]
  }
}

calibration_required is one of ignore, warn (default), or require. See Chapter 16 for the full AMS surface.

See also

  • Chapter 12acctl new-tis-project, acctl add-tis, and acctl codegen-tags.
  • Chapter 16 — Asset Management System (ams.* RPCs, calibration history, surface lanes, the asset_refs/asset_snapshot integration).

Appendix: original reference (Standardized Results System)

The pre-rename reference prose follows. It documents the same filesystem layout and most of the same RPC verbs but uses the legacy identifiers (results.*, definition_id, ResultsSystem, results_staged_*). The plumbing has been replaced — code samples here will not compile against the current control-program API or match what the server now broadcasts. Treat this section as historical.


The system is designed for high-performance industrial environments:

  1. Schema Definition: All test structures are defined in project.json.
  2. Code Generation: Auto-generates typed Rust structs and TypeScript interfaces.
  3. Real-Time Collection: The control program pushes cycle data via IPC (non-blocking).
  4. Asynchronous Storage: A dedicated servelet handles disk I/O, UTC timestamping, and checksumming.
  5. Filesystem-Based: Data is stored as standard JSON and JSONL files for maximum portability.

Auto-provided fields in the legacy contract:

FieldDescription
test_idISO-8601 timestamp string assigned on results.start_test. Becomes the directory name under datastore/results/<project_id>/<definition_id>/.
created_atUTC timestamp set when the test record is first created.
completed_atUTC timestamp set when the test is closed.
checksumSHA-256 of the final test.json payload.
schemaSnapshot of the definition used.
project_idSupplied by the UI in results.start_test / stage_test.

Legacy schema example (now test_methods / method_id):

{
  "test_definitions": {
    "impact_test": {
      "config_fields": [
        { "name": "drop_height", "type": "f32", "units": "mm", "source": "gm.drop_height_mm" }
      ],
      "cycle_fields": [
        { "name": "drop_index", "type": "u32", "source": "gm.cycle_count" },
        { "name": "peak_g", "type": "f32", "source": "gm.total_peak_load" },
        { "name": "judgement", "type": "string" }
      ]
    }
  }
}

The legacy results.* IPC verbs (results.stage_test, results.start_test, results.add_cycle, results.add_raw_data, results.update_results, results.finish_test, etc.) and broadcast topics (results.staged, results.active, results.cycle_added, results.results_updated) have been replaced 1:1 by their tis.* counterparts, with definition_idmethod_id in every payload and the addition of sample_id as a top-level structural field.