One enrollment file names the device, for every bay without a registry

A bay with an NTLARS/eDNC MachineNo registry is identified by it, and a CMM by
cmmid.txt. Everything else - Keyence, Genspect, wax-trace, part markers, and
whatever a site declares next - had no stable identity at all, and the two
mechanisms that stood in for one both key off the PC: reuse looks for a prior
link from THIS PC asset, and minting builds `<PC number>-<SUFFIX>` where a PC's
number is its hostname.

Both survive a re-image. NEITHER survives a PC SWAP. A new hostname is a new PC
asset with no prior link and a predicted number that has never existed, so the
same physical device gets a second record while the first keeps its config and
its backup history under a dead PC's name. That is how 43 legacy MT-#### tools
ended up shadowed by minted twins, three records deep in places. The metrology
path learned this and gained an explicit id file; the part-marker path was
modelled on the metrology path as it stood BEFORE that fix, so it inherited the
defect - and its own docstring said so, describing minting "the same way it
already mints a CMM".

C:\Enrollment\asset-id.txt is now that identity for both, and for anything
declared later through subordinatedevice_<pctype>. It holds one line, the
device's assetnumber, and deliberately does NOT record what kind of device it
is: the pc-type already does, so a new device type needs no new file and no
client change. Resolution puts it first, ahead of everything PC-derived.

NOT machine-number.txt. machinenumber answers "which bay is this" and is what
GE-Enforce TargetMachineNumbers gates on, so naming a device there would
silently stop every bay-gated manifest entry from matching. A part marker still
files partof the operation from machinenumber; asset-id.txt changes which marker
the PC controls, not which operation that marker belongs to.

The wire field is `deviceid` rather than `assetid`, because assetid is already
this contract's RESPONSE field for a PC's integer primary key - the two would
have sat side by side meaning different things. measuringtoolid, shipped in
0.12.0, is accepted as an alias and the client still reads the older file, so a
bay staged in the last day keeps reporting; deviceid wins when both arrive.

Two guards, shared by both device families: a value that resolves to nothing
warns instead of minting a phantom, and a value that resolves to the WRONG KIND
of asset is refused with the asset named, so a machine number pasted into the
file cannot be filed under a device label the collector also owns.

Row creation is now one helper instead of a copy per path, because the named and
minted branches both need get-or-create on the extension row and the control
link.

1744 tests green, including a new file that pins the swap case both ways - with
the file one device, without it two. Five of its tests failed first time because
the fixtures built bare assets with no extension row and the type guard refused
them, which is the guard working.

VERIFIED ON WINDOWS 11 (build 26200), five cases: the new file alone; both files
present, new winning; the legacy file alone; neither, sending no field; and a
padded value with a trailing line.
This commit is contained in:
cproudlock
2026-08-20 10:41:00 -04:00
parent 21afa0b56e
commit 68b86d459e
4 changed files with 439 additions and 94 deletions

View File

@@ -201,7 +201,8 @@ a column.
|---|---|---|
| `hostname` (required) | string | Identity. Matches `Computer.hostname` (case-insensitive), then falls back to `Asset.assetnumber`. New asset created if no match. |
| `machinenumber` | string | Identifies the MACHINE this PC drives, never the PC. A new PC always takes `assetnumber = hostname`, and an existing PC's `assetnumber` is left alone. The number resolves a machine asset and builds a PC -> machine `controls` relationship; an unknown number warns rather than creating a machine. The placeholder `9999` and empty string link nothing. See "Machine links" below. |
| `measuringtoolid` | string | Identifies the INSTRUMENT this PC drives, read from `C:\Enrollment\measuringtool-id.txt`. SEPARATE from `machinenumber` on purpose: "which bay is this" and "which instrument is this" are different facts, and `machinenumber` is what GE-Enforce `TargetMachineNumbers` gates on, so repointing it at a tool would silently stop every bay-gated manifest entry from matching. The value is an existing measuring tool's `assetnumber`. An unknown value warns rather than minting a tool, and a value that resolves to something which is NOT a measuring tool is refused with the asset named. See "Measuring tool links" below. |
| `deviceid` | string | Identifies the DEVICE hanging off this PC, read from `C:\Enrollment\asset-id.txt`. ONE file for every bay with no NTLARS/eDNC `MachineNo` registry to identify it. It names the asset and nothing else - it does NOT say what kind of device it is, because the pc-type already does - so the same file serves a part marker, a Keyence, a Genspect or anything declared later. SEPARATE from `machinenumber` on purpose: "which bay is this" and "which device is this" are different facts, and `machinenumber` is what GE-Enforce `TargetMachineNumbers` gates on, so naming a device there would silently stop every bay-gated manifest entry from matching. An unknown value warns rather than minting, and a value resolving to the wrong KIND of asset is refused with the asset named. See "PC -> device links" below. |
| `measuringtoolid` | string | The 0.12.0 name for the same thing, before the file was generalised, read from `C:\Enrollment\measuringtool-id.txt`. Accepted as an alias so a bay already staged with that file keeps reporting; `deviceid` wins when both arrive. Prefer `asset-id.txt` for anything new. |
| `pctype` | string | `gea-shopfloor-*` imaging type -> `Computer.computertypeid` via the configurable `pctypemap` settings. Unmapped value -> warning, not error. |
| `pcsubtype` | string | Accepted but not stored yet -> warning. |
| `serialnumber` | string | `Asset.serialnumber`. |
@@ -264,42 +265,53 @@ so control still follows from controlling the marker, without two markers
contesting a link only one can hold. Backups from a marker PC file against the
marker rather than the operation.
### PC -> measuring tool links
### PC -> device links (`asset-id.txt`)
A metrology pc-type (CMM, Keyence, Genspect, wax-and-trace) means the PC drives
an attached instrument. The collector links it PC -> tool as `controls`, tagged
`assetrelationships.label = 'collector:measuringtool'`.
Some pc-types mean the PC drives an attached DEVICE: a CMM, Keyence, Genspect or
wax-trace instrument, or a part marker. `SUBORDINATE_DEVICE_MAP` in
`plugins/computers/pctypemap.py` declares which, and a site can add its own
through a `subordinatedevice_<pctype>` setting without a code change.
`C:\Enrollment\asset-id.txt` names that device. One file for every bay that has
no NTLARS/eDNC `MachineNo` registry to identify it, holding one line: the
device's `assetnumber`. It does NOT record what kind of device it is - the
pc-type already does - so a bay declared later needs no new file and no client
change.
Resolution runs most-stable-identity first, and ADOPTS before it mints:
1. **`measuringtoolid`** - the instrument named in
`C:\Enrollment\measuringtool-id.txt`. Explicit, survives a PC swap, and it is
what a Keyence or Genspect bay has instead of a bay id.
2. A prior collector link on this PC, reactivated.
3. An existing tool this PC already controls that the collector did NOT create -
a legacy `MT-####` row, or one somebody made by hand. Adopting stamps the
label so it is ours from then on.
4. The reported machine number, when it resolves to a measuring tool. This is
the CMM case: `cmmid.txt` already reports `CMM4`.
1. **`deviceid`** - the asset named in `asset-id.txt`.
2. A prior collector link from this PC, reactivated.
3. An existing device this PC already controls that the collector did NOT
create - a legacy `MT-####` row, or one somebody made by hand. Adopting
stamps the label so it is ours from then on.
4. For a measuring tool only: the reported machine number, when it resolves to
one. This is the CMM case, where `cmmid.txt` already reports `CMM4`.
5. Mint, only when none of the above matched.
**Why minting is last.** It derives the asset number from the HOSTNAME, so a
permanent instrument inherited the identity of whichever PC drove it that week:
replace the PC and either the number lies or a second tool appears for the same
physical unit. It also could not see a tool it had not created itself, so at the
reference site 43 legacy `MT-####` tools ended up shadowed by minted
`<HOST>-CMM` twins, three records deep in places. Treat anything minted as a
placeholder to be reconciled.
**Why the file exists, and why it is step 1.** Every other identity here is
derived from the PC. Step 2 looks for a link from THIS PC asset; step 5 mints
`<PC number>-<SUFFIX>`, and a PC's asset number is its hostname. All of them
survive a re-image and NONE of them survives a PC SWAP: a new hostname is a new
PC asset with no prior link and a predicted number that has never existed, so
the same physical device gets a second record while the first keeps its config
and backup history under a dead PC's name. That is how 43 legacy `MT-####`
tools ended up shadowed by minted twins, three records deep in places. Treat
anything minted as a placeholder to be reconciled.
Two guards on the file's contents:
- An **unresolvable** value warns and links nothing, rather than inventing a
phantom instrument nobody can account for.
- A value that resolves to an asset which is **not a measuring tool** is refused,
naming the asset it hit. A machine number pasted into `measuringtool-id.txt`
would otherwise link the PC to that MACHINE under a measuring-tool label - a
link that reads as an instrument everywhere downstream, on a row the machine
sync also owns.
phantom device nobody can account for.
- A value resolving to the **wrong kind** of asset is refused, naming the asset
it hit. A machine number pasted into `asset-id.txt` would otherwise file that
MACHINE under a device label the collector also owns - a link that reads as an
instrument, or as a marker, everywhere downstream.
A part marker additionally stays `partof` the operation from `machinenumber`,
because several markers serve one operation. Naming the marker in `asset-id.txt`
changes which marker asset the PC controls; it does not change which operation
that marker files under.
### PC -> printer relationship sync
@@ -346,7 +358,7 @@ The fleet's classic-ASP reporter posts form fields to
|---|---|
| `hostname` | `hostname` |
| `machineNo` | `machinenumber` |
| `measuringToolId` | `measuringtoolid` |
| `deviceId` | `deviceid` |
| `pcType` | `pctype` |
| `serialNumber` | `serialnumber` |
| `loggedInUser` | `loggedinuser` |
@@ -638,15 +650,20 @@ function Get-ShopdbMachineNumber {
return $machineNumber
}
function Get-ShopdbMeasuringToolId {
# The INSTRUMENT this PC drives, for a metrology bay. Deliberately its own
# file: machinenumber is what GE-Enforce TargetMachineNumbers gates on, so
# naming a tool there would silently stop bay-gated entries from matching.
if (Test-Path 'C:\Enrollment\measuringtool-id.txt') {
try {
$v = (Get-Content 'C:\Enrollment\measuringtool-id.txt' -First 1 -ErrorAction Stop).Trim()
if ($v) { return $v }
} catch {}
function Get-ShopdbDeviceId {
# The DEVICE hanging off this PC, for any bay with no MachineNo registry.
# Names the asset and not its type - the pc-type already says that.
# Deliberately its own file: machinenumber is what GE-Enforce
# TargetMachineNumbers gates on, so naming a device there would silently
# stop bay-gated entries from matching.
foreach ($f in @('C:\Enrollment\asset-id.txt',
'C:\Enrollment\measuringtool-id.txt')) { # 2nd = pre-0.13
if (Test-Path $f) {
try {
$v = (Get-Content $f -First 1 -ErrorAction Stop).Trim()
if ($v) { return $v }
} catch {}
}
}
return ''
}
@@ -710,7 +727,7 @@ function Send-ShopdbCollectorReport {
if (-not $hostname) { $hostname = $env:COMPUTERNAME }
$machineNumber = Get-ShopdbMachineNumber
$measuringToolId = Get-ShopdbMeasuringToolId
$deviceId = Get-ShopdbDeviceId
$ipAddress = Get-ShopdbCorpIPv4
$serialNumber = ''
@@ -776,7 +793,7 @@ function Send-ShopdbCollectorReport {
if ($defaultPrinter) { $payload['defaultprinter'] = $defaultPrinter }
if ($printerIds.Count) { $payload['printers'] = $printerIds }
if ($machineNumber) { $payload['machinenumber'] = $machineNumber }
if ($measuringToolId) { $payload['measuringtoolid'] = $measuringToolId }
if ($deviceId) { $payload['deviceid'] = $deviceId }
if ($pcType) { $payload['pctype'] = $pcType }
if ($pcSubType) { $payload['pcsubtype'] = $pcSubType }
if ($serialNumber) { $payload['serialnumber'] = $serialNumber }