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

Asset Management System

The Asset Management System (AMS) is AutoCore’s record-keeper for the physical equipment in your machine: load cells, encoders, springs, and project-specific items like 3830’s traction surfaces. AMS owns three concerns:

  1. What’s installed — a registry of every asset, identified by a server-generated asset_id.
  2. What state it’s in — the active calibration values plus a full audit-trail of past calibrations.
  3. How much it has been used — cycle counts, hours run, and any custom counters declared per asset type.

AMS is intentionally a separate subsystem from TIS. Test records (TIS) grow per-test-run and are immutable once written. Asset records (AMS) persist across years and accumulate calibration / usage events over the equipment’s whole life. The integration point is one-way: when tis.start_test fires, the TIS servelet snapshots the active asset state into the test record, so traceability survives later recalibration.

Quick start. If you already have a project and just want to flip AMS on, skip ahead to Adding AMS to an existing project below.

The three-level data model

Every asset in AMS lives at the intersection of three keys:

LevelUser-facing nameWire/code keyWhat it is
1Asset Typeasset_typeThe shape of the equipment — built-in (load_cell, linear_encoder, spring) or custom (surface, etc.). Defined in code or in project.json::asset_types.
2Assetasset_idOne physical instance, with a serial number and install location. Server-generated identifier.
3Calibration Recordcal_idOne calibration event for an asset. Append-only.

On disk under <datastore>/assets/:

datastore/assets/
├── registry.json
├── load_cell/
│   └── LC-20251015T130022/
│       ├── asset.json
│       ├── calibrations/
│       │   ├── 20251015T130122.json
│       │   └── 20260301T091548.json
│       ├── usage.json
│       └── usage_log.jsonl    ← created on first usage reset
└── surface/
    └── SURF-20260114T093400/
        ├── asset.json         ← includes the lanes sub_locations
        ├── calibrations/
        └── usage.json

Asset IDs are server-generated

Asset IDs are always assigned by the server. The format is <prefix><YYYYMMDDTHHMMSS> — for example, LC-20260301T091548. The trailing timestamp is the asset’s creation time (UTC, second resolution). On collision (rare; bulk import inside one second) the server bumps by one second until the path is free.

The prefix comes from the asset_type’s id_prefix:

Asset typeDefault prefix
load_cellLC-
linear_encoderENC-
springSP-
Custom typesA- (override via id_prefix)

The manufacturer’s serial number is stored as a separate free-form field on asset.json. It’s recorded for traceability but not used as a unique key — vendor serials collide and reuse across product lines and we have zero control over their assignment policy.

Built-in asset types

Three asset types ship with the server. You don’t need to declare them in project.json to use them.

load_cell

Force-measuring transducer. Calibration captures scale and offset.

Calibration fieldTypeNotes
scalef32Counts-to-engineering-units multiplier.
offsetf32Zero-load counts.
unitsstringEngineering units (e.g., N, lbf).
rangef32Optional full-scale.

linear_encoder

Position-measuring transducer. Tracks counts-per-mm and accumulated travelled distance.

Calibration fieldTypeNotes
counts_per_mmf32Required.
offset_mmf32Required. Zero-position offset.
directionstring"+" or "-" — sign of increasing counts.

Extra usage counter: total_distance_mm (f64).

spring

Mechanical compression/extension spring. “Recalibration” usually means a replacement; the calibration record serves as the install record.

Calibration fieldTypeNotes
stiffness_n_per_mmf32Required.
free_length_mmf32Required.
preload_nf32Optional.

Adding AMS to an existing project

acctl add-ams
acctl push project --restart
acctl codegen

acctl add-ams writes an empty asset_types: {} block to your project.json. That single change flips three things:

  1. Project::normalize() injects the baseline AMS GM scalars (ams_asset_count, ams_alert_calibration_overdue, ams_alert_lane_unavailable).
  2. The next acctl codegen regenerates control/src/gm.rs with typed calibration structs (e.g., LoadCellCalibration with scale: f32, offset: f32) and writes www/src/autocore/ams.ts with mirrored TypeScript types and the asset_type catalog.
  3. The <AmsProvider> and HMI components below have something to render against.

Re-running acctl add-ams on a project that already has AMS enabled is a no-op.

Defining a custom asset type

Custom types extend the catalog with project-specific equipment. The 3830 traction tester uses a surface type with twelve testable lanes:

{
  "asset_types": {
    "surface": {
      "id_prefix": "SURF-",
      "label": "Traction Surface",
      "description": "Serialized test surface installed in the bay.",
      "fields": [
        { "name": "material",  "type": "string", "required": true,  "label": "Material" },
        { "name": "thickness", "type": "f32",    "units": "mm",      "label": "Thickness" }
      ],
      "calibration_fields": [],
      "sub_locations": {
        "name": "lanes",
        "count": 12,
        "per_location_state": [
          { "name": "status",      "type": "enum", "values": ["available", "in_use", "worn", "retired"] },
          { "name": "cycles_used", "type": "u64" }
        ]
      }
    }
  }
}

When you create a surface asset, AMS auto-materializes twelve lane records (lane_01 through lane_12) with default state — available status and cycles_used: 0. Operators can update individual lanes via ams.update_sub_location (or via the <SubLocationPicker> UI).

The name field on sub_locations is the user-facing label; the per- location IDs are formed by trimming the trailing s (laneslane) and appending _NN. Use a singular name if your equipment doesn’t pluralize naturally (channelchannel_01).

Reading current calibration from the control program

acctl codegen emits one <TypeName>Calibration Rust struct per asset type, plus a from_ams_response helper that parses the values out of an ams.read_calibration response. The control program reads the current calibration for an asset by fetching it once at startup and stashing it on the program struct:

#![allow(unused)]
fn main() {
use autocore_std::CommandClient;

pub struct MyProgram {
    load_cell: Option<LoadCellCalibration>,
    // …
}

impl MyProgram {
    pub fn new() -> Self {
        MyProgram { load_cell: None }
    }

    pub fn load_calibrations(&mut self, client: &mut CommandClient, asset_id: &str) {
        if let Some(asset) = client.invoke("ams.read_asset", json!({ "asset_id": asset_id })) {
            if let Some(cal_id) = asset.data.get("current_calibration_id").and_then(|v| v.as_str()) {
                if let Some(resp) = client.invoke("ams.read_calibration",
                    json!({ "asset_id": asset_id, "cal_id": cal_id }))
                {
                    self.load_cell = Some(LoadCellCalibration::from_ams_response(&resp.data));
                }
            }
        }
    }

    pub fn process_tick(&mut self, ctx: &mut TickContext<GM>) {
        if let Some(cal) = &self.load_cell {
            let raw_counts = ctx.gm.load_cell_z_raw as f32;
            let force_n = cal.scale * raw_counts + cal.offset;
            ctx.gm.zforce_load = force_n;
        }
    }
}
}

For testing methods that declare an asset_refs list, the control program can read the resolved asset_id from the auto-injected GM scalar — ctx.gm.ams_active_load_cell_z_asset_id — instead of hard-coding the ID.

Wiring usage counters

Phase 4 of AMS ships RPC-driven usage counters. Call ams.tick_usage from your control program’s record_cycle flow:

#![allow(unused)]
fn main() {
// Inside your program's tick:
self.tis.record_cycle(ctx);

if let Some(asset_id) = ctx.gm.ams_active_load_cell_z_asset_id.as_str_opt() {
    client.invoke("ams.tick_usage", json!({
        "asset_id":     asset_id,
        "delta_cycles": 1,
        "delta_hours":  ctx.dt.as_secs_f64() / 3600.0,
    }));
}
}

For surface lanes specifically, where each lane has its own cycles_used counter under sub_locations.items[*], update the lane directly:

#![allow(unused)]
fn main() {
client.invoke("ams.update_sub_location", json!({
    "asset_id":    surface_id,
    "location_id": active_lane_id,
    "partial":     { "cycles_used": new_count },
}));
}

Audit trail for resets. ams.reset_usage zeros the counters and appends an entry to usage_log.jsonl next to usage.json so the history of “we rebuilt this asset on date X” survives the reset.

TIS ↔ AMS integration: asset_refs and asset_snapshot

A test method declares which assets it depends on under asset_refs:

"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" },
      { "field": "encoder_x",
        "asset_type": "linear_encoder",
        "select": "by_location", "location": "x_axis",
        "calibration_required": "require" },
      { "field": "surface",
        "asset_type": "surface",
        "select": "by_id_field", "from": "config.surface_asset_id",
        "calibration_required": "ignore" }
    ]
  }
}

Each asset_ref has:

  • field: the key written into test.json::asset_snapshot.
  • asset_type: which catalog entry to look up.
  • select: how to pick the right asset.
    • by_location — pick the unique active asset whose location matches location below.
    • by_id_field — read the asset_id directly from a config field (dotted from path into the staged payload).
  • calibration_required: stage-time policy.
    • ignore — never blocks, never warns.
    • warn (default) — surfaces a warning on stage but lets the test start.
    • require — blocks staging with a clear error.

When tis.start_test fires, AMS resolves each ref and writes the snapshot into test.json:

{
  "project_id": "plant_a",
  "method_id":  "translational_traction",
  "run_id":     "20260422T140231Z",
  "sample_id":  "S-0042",
  "asset_snapshot": {
    "load_cell_z": {
      "asset_id":        "LC-20251015T130022",
      "asset_type":      "load_cell",
      "calibration_id":  "20260301T091548",
      "values":          { "scale": 9.81234, "offset": -0.0042, "units": "N" }
    },
    "encoder_x": { /* … */ },
    "surface":   { /* … */ }
  }
}

This is the audit trail. When someone re-calibrates LC-20251015T130022 in 2027, the 2026 test results still show the calibration that was active when they were taken.

For each asset_ref, AMS auto-injects two GM scalars so the control program and HMI can react:

  • ams_active_<field>_asset_id
  • ams_active_<field>_calibration_id

(For example: ams_active_load_cell_z_asset_id.)

Building an HMI for asset management

Drop <AmsProvider> once at the top of your HMI. The four AMS components below are zero-prop and read from context.

import {
    AmsProvider,
    AssetRegistryTable,
    AssetDetailView,
    SubLocationPicker,
} from '@adcops/autocore-react';

function AssetsTab() {
    return (
        <AmsProvider>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
                <AssetRegistryTable />
                <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
                    <AssetDetailView />
                    <SubLocationPicker />
                </div>
            </div>
        </AmsProvider>
    );
}
ComponentResponsibility
<AssetRegistryTable>List every asset with type/status filters and an “Add Asset” button. Click a row to pin selection.
<AssetDetailView>Show the selected asset’s header (id, type, serial, location, status), full calibration history table, and a “+ Calibration” button.
<CalibrationEntryDialog>Form generated from the asset_type’s calibration_fields schema. Triggered by <AssetDetailView>’s button.
<SubLocationPicker>Grid view for asset types with sub_locations (typically lanes). Click a cell to mark a lane available / in_use / worn / retired.

The provider also exposes hooks for custom UI:

import { useAms, useAmsAlerts, useAmsAssets } from '@adcops/autocore-react';

function CalibrationStatusBadge() {
    const { calibrationOverdue } = useAmsAlerts();
    if (calibrationOverdue === 0) return null;
    return (
        <Tag value={`${calibrationOverdue} cal overdue`} severity="warning" />
    );
}

Calibration alerts

AMS publishes three derived counters on every mutation:

Broadcast topicWhat it counts
ams.asset_countNumber of active assets in the registry.
ams.alert_calibration_overdueActive assets whose current calibration’s expires_at is in the past.
ams.alert_lane_unavailableSub_location entries marked worn or retired across all active assets.

The <AmsProvider> keeps these in useAmsAlerts() automatically. Control programs can link to them as GM scalars (ams_alert_*) and gate features accordingly — for example, refusing to start a test when overdue calibrations exist on require-policy refs.

Backup and restore

The full AMS dataset round-trips through a single JSON document.

Export

acctl ams export --output ams_backup_2026-04-29.json

Pulls the registry, every asset’s full record, every calibration, and usage counters from the running server. The output is a single JSON file with this shape:

{
  "version":     1,
  "exported_at": "2026-04-29T16:00:00Z",
  "registry":    { "assets": [/* registry entries */] },
  "assets": [
    {
      "asset":        { /* full asset.json */ },
      "calibrations": [/* every cal record */],
      "usage":        { /* usage.json */ }
    }
  ]
}

Import

# Dry-run first to see what would change.
acctl ams import --input ams_backup_2026-04-29.json --dry-run

# Apply it.
acctl ams import --input ams_backup_2026-04-29.json

Default behaviour is merge: assets that already exist on the target server are left in place; assets that don’t are created with their original IDs preserved; calibrations are appended (the server honours the explicit cal_id in the document); usage counters take the max of existing-and-imported, so a stale backup never decreases counts.

--dry-run walks the document and prints what would happen without writing anything.

Round-trip fidelity. Asset_id and cal_id are preserved on import, which means the audit trail in test.json::asset_snapshot still resolves correctly after a restore.

Migration: how this differs from a hand-rolled equipment registry

If you’re migrating from an ad-hoc spreadsheet or Linear-tracked list of “what’s installed”:

  1. Run acctl add-ams to enable the subsystem.
  2. Use <AssetRegistryTable>’s “Add Asset” dialog (or the ams.create_asset RPC directly) to register each piece of equipment. Record the manufacturer serial under the serial field for traceability.
  3. For each asset, add the most recent known calibration via <CalibrationEntryDialog>. Backfill historical calibrations only if you need the audit trail; otherwise just enter the current one.
  4. Update each test method’s asset_refs block to point at the new asset locations. Run acctl codegen to refresh the GM scalars.

The same add- family retrofits TIS into a project that started without it: acctl add-tis. Both commands are idempotent.