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:
- What’s installed — a registry of every asset, identified by a
server-generated
asset_id. - What state it’s in — the active calibration values plus a full audit-trail of past calibrations.
- 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:
| Level | User-facing name | Wire/code key | What it is |
|---|---|---|---|
| 1 | Asset Type | asset_type | The shape of the equipment — built-in (load_cell, linear_encoder, spring) or custom (surface, etc.). Defined in code or in project.json::asset_types. |
| 2 | Asset | asset_id | One physical instance, with a serial number and install location. Server-generated identifier. |
| 3 | Calibration Record | cal_id | One 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 type | Default prefix |
|---|---|
load_cell | LC- |
linear_encoder | ENC- |
spring | SP- |
| Custom types | A- (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 field | Type | Notes |
|---|---|---|
scale | f32 | Counts-to-engineering-units multiplier. |
offset | f32 | Zero-load counts. |
units | string | Engineering units (e.g., N, lbf). |
range | f32 | Optional full-scale. |
linear_encoder
Position-measuring transducer. Tracks counts-per-mm and accumulated travelled distance.
| Calibration field | Type | Notes |
|---|---|---|
counts_per_mm | f32 | Required. |
offset_mm | f32 | Required. Zero-position offset. |
direction | string | "+" 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 field | Type | Notes |
|---|---|---|
stiffness_n_per_mm | f32 | Required. |
free_length_mm | f32 | Required. |
preload_n | f32 | Optional. |
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:
Project::normalize()injects the baseline AMS GM scalars (ams_asset_count,ams_alert_calibration_overdue,ams_alert_lane_unavailable).- The next
acctl codegenregeneratescontrol/src/gm.rswith typed calibration structs (e.g.,LoadCellCalibrationwithscale: f32, offset: f32) and writeswww/src/autocore/ams.tswith mirrored TypeScript types and the asset_type catalog. - 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 (lanes → lane)
and appending _NN. Use a singular name if your equipment doesn’t
pluralize naturally (channel → channel_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_usagezeros the counters and appends an entry tousage_log.jsonlnext tousage.jsonso 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 intotest.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 whoselocationmatcheslocationbelow.by_id_field— read the asset_id directly from a config field (dottedfrompath 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_idams_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>
);
}
| Component | Responsibility |
|---|---|
<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 topic | What it counts |
|---|---|
ams.asset_count | Number of active assets in the registry. |
ams.alert_calibration_overdue | Active assets whose current calibration’s expires_at is in the past. |
ams.alert_lane_unavailable | Sub_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_snapshotstill 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”:
- Run
acctl add-amsto enable the subsystem. - Use
<AssetRegistryTable>’s “Add Asset” dialog (or theams.create_assetRPC directly) to register each piece of equipment. Record the manufacturer serial under theserialfield for traceability. - 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. - Update each test method’s
asset_refsblock to point at the new asset locations. Runacctl codegento refresh the GM scalars.
The same add- family retrofits TIS into a project that started
without it: acctl add-tis. Both commands are idempotent.