diff --git a/docs/COLLECTOR-INTEGRATION.md b/docs/COLLECTOR-INTEGRATION.md index de98cf5..633949e 100644 --- a/docs/COLLECTOR-INTEGRATION.md +++ b/docs/COLLECTOR-INTEGRATION.md @@ -201,6 +201,7 @@ 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. | | `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`. | @@ -263,6 +264,43 @@ 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 + +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'`. + +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`. +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 +`-CMM` 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. + ### PC -> printer relationship sync When a payload carries `defaultprinter` and/or `printers`, the collector syncs @@ -308,6 +346,7 @@ The fleet's classic-ASP reporter posts form fields to |---|---| | `hostname` | `hostname` | | `machineNo` | `machinenumber` | +| `measuringToolId` | `measuringtoolid` | | `pcType` | `pctype` | | `serialNumber` | `serialnumber` | | `loggedInUser` | `loggedinuser` | @@ -599,6 +638,19 @@ 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 {} + } + return '' +} + function Get-ShopdbCorpIPv4 { # Pick the corp/AESFMA NIC IP. Same allowed-range gate as # Report-AssetToShopDB.ps1 - update the ranges if the site re-VLANs. @@ -657,8 +709,9 @@ function Send-ShopdbCollectorReport { $hostname = [System.Environment]::MachineName if (-not $hostname) { $hostname = $env:COMPUTERNAME } - $machineNumber = Get-ShopdbMachineNumber - $ipAddress = Get-ShopdbCorpIPv4 + $machineNumber = Get-ShopdbMachineNumber + $measuringToolId = Get-ShopdbMeasuringToolId + $ipAddress = Get-ShopdbCorpIPv4 $serialNumber = '' try { @@ -723,6 +776,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 ($pcType) { $payload['pctype'] = $pcType } if ($pcSubType) { $payload['pcsubtype'] = $pcSubType } if ($serialNumber) { $payload['serialnumber'] = $serialNumber } diff --git a/plugins/computers/client/Report-AssetToShopDB.ps1 b/plugins/computers/client/Report-AssetToShopDB.ps1 index d5f09d8..646e5bc 100644 --- a/plugins/computers/client/Report-AssetToShopDB.ps1 +++ b/plugins/computers/client/Report-AssetToShopDB.ps1 @@ -172,6 +172,7 @@ try { # 2. C:\Enrollment\cmm\cmmid.txt - CMM bay id (e.g. CMM3). # 3. C:\Enrollment\machine-number.txt - imaging value. # keyence / genspect / part-marker have no per-bay id -> no machineNo sent. +# A metrology bay names its INSTRUMENT instead, in measuringtool-id.txt below. $machineNo = '' foreach ($regPath in @( 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General', @@ -236,6 +237,25 @@ try { Log "WARN could not read logged-in user: $($_.Exception.Message)" } +# The INSTRUMENT this PC drives, named by its own enrollment file. Deliberately +# NOT machine-number.txt: machinenumber answers "which bay is this" and is what +# GE-Enforce TargetMachineNumbers gates on, so pointing it at a tool would +# silently stop every bay-gated manifest entry from matching. A Keyence or +# Genspect bay has an instrument and no bay id, which is the case this exists +# for; a CMM already reports its bay id through cmmid.txt and the server falls +# back to that. +# +# The server ADOPTS the named instrument and refuses to mint one from a value it +# cannot resolve, so a typo here warns instead of inventing a phantom tool. +$measuringToolId = '' +$mtFile = 'C:\Enrollment\measuringtool-id.txt' +if (Test-Path -LiteralPath $mtFile) { + try { + $measuringToolId = ([string](Get-Content -LiteralPath $mtFile -First 1 -ErrorAction Stop)).Trim() + if ($measuringToolId) { Log "measuringToolId from ${mtFile}: $measuringToolId" } + } catch { Log "WARN could not read ${mtFile}: $($_.Exception.Message)" } +} + # imaging pc-type (gea-shopfloor-*), read from the enrollment file. Sent only # when present; absent -> server leaves existing pctype untouched (a bare report # never re-types a PC). Unmapped values return a warning, not an error. @@ -302,6 +322,7 @@ $body = @{ hostname = $hostname } if ($serialNumber) { $body['serialnumber'] = $serialNumber } if ($machineNo) { $body['machinenumber'] = $machineNo } if ($pcType) { $body['pctype'] = $pcType } +if ($measuringToolId) { $body['measuringtoolid'] = $measuringToolId } if ($manufacturer) { $body['vendorname'] = $manufacturer } if ($model) { $body['modelnumber'] = $model } if ($osVersion) { $body['osname'] = $osVersion } @@ -312,8 +333,8 @@ $body['lastcheckin'] = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:s $json = $body | ConvertTo-Json -Compress -Depth 4 -Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} os={6} boot={7} machineNo={8} user={9} ip={10}" -f ` - $ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, $corpIp) +Log ("POST {0} host={1} serial={2} pcType={3} make={4} model={5} os={6} boot={7} machineNo={8} user={9} ip={10} toolId={11}" -f ` + $ApiUrl, $hostname, $serialNumber, $pcType, $manufacturer, $model, $osVersion, $lastBootTime, $machineNo, $loggedInUser, $corpIp, $measuringToolId) try { $resp = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $json `