Identify a marker by its device, without moving what NTLARS reports

A part marker has no machine number of its own - the device is what a backup
belongs to, and C:\Enrollment\asset-id.txt is what names it. Reading that file
inside the shared machine-number lookup looked like the tidy place for it and
was wrong: marker bays run eDNC too, so Backup-NtlarsSettings.ps1 reaches the
same lookup, and it would have started posting a machine's DNC settings under
the marker's asset number where today it exits quietly. The marker script asks
for the fallback explicitly instead, which is what -MachineNumberFallback is
for.

Also from review:

Returning a byte array bare lets the pipeline enumerate it, so an EMPTY file
came back as $null, ComputeHash threw, and with ErrorActionPreference=Continue
the manifest entry silently kept the previous file's hash - a corrupted
manifest stored as a good revision. Empty parameter files are plausible.

Reading registry values directly under StrictMode throws when one is absent, so
the "no Port Id" guard was unreachable and a partially configured marker bay
reported itself as having no marker at all, permanently and quietly. Values are
now read through a presence check with the fleet's own defaults.

The outside-window quiet state embedded the hour, so the say-once module said
it hourly. Command replies now have their checksum verified like data blocks
do, since a corrupted listing entry would otherwise be trusted and a file
requested under a name the controller does not have. A controller that
genuinely holds nothing is no longer reported as a failed capture. A snapshot
that could not be written to the share now counts as a missed backup like the
rest, and the header no longer claims orphaned snapshots get tidied - they are
kept on purpose, because a timed-out post may still have been recorded.
This commit is contained in:
cproudlock
2026-08-25 13:21:45 -04:00
parent abbdc9873c
commit b228a69364
3 changed files with 131 additions and 15 deletions

View File

@@ -46,6 +46,16 @@ $script:LOGDIR = 'C:\Logs\Shopfloor'
$script:SHOPDBREG = 'HKLM:\SOFTWARE\GE\ShopDB'
$script:KEYFILE = 'C:\Enrollment\shopdb-key.txt'
$script:PCCONFIG = 'C:\Enrollment\pc-config.txt'
# The DEVICE this PC drives, by asset number - a part marker, a CMM, a Keyence.
# Written at staging and read by the computers collector, which resolves it with
# _asset_by_number and refuses a name it cannot find rather than minting a
# phantom. Marker and measuring-tool bays have no machine number of their own
# ("keyence / genspect / part-marker have no per-bay id" in
# Report-AssetToShopDB.ps1), so for those this file IS the identity.
$script:ASSETIDFILE = 'C:\Enrollment\asset-id.txt'
# 0.12.0 shipped this name before the file was generalised; a bay staged then
# still has it.
$script:LEGACYASSETIDFILE = 'C:\Enrollment\measuringtool-id.txt'
# The action ShopDB reported for the most recent post, per kind: 'created',
# 'noop', or '' when nothing has been posted. Send-ShopdbBackup returns only
@@ -144,6 +154,26 @@ function Get-ShopdbCollectorKey {
}
function Get-ShopdbAssetId {
<#
The asset number of the device this PC drives, or ''.
Same file and same precedence the computers collector uses, so a bay
cannot report its marker under one identity and its backup under
another.
#>
foreach ($file in @($script:ASSETIDFILE, $script:LEGACYASSETIDFILE)) {
if (Test-Path $file) {
try {
$value = ([string](Get-Content -LiteralPath $file -First 1 -EA Stop)).Trim()
if ($value) { return $value }
} catch { }
}
}
return ''
}
function Get-ShopdbMachineNumber {
<#
The machine number this PC reports, from pc-config.txt.
@@ -168,6 +198,19 @@ function Get-ShopdbMachineNumber {
if ($val) { return ([string]$val).Trim() }
} catch { }
}
# NO asset-id.txt fallback here, deliberately. It belongs to the CALLER.
#
# A part-marker bay has no machine number of its own but does have the DNC
# registry tree, so Backup-NtlarsSettings.ps1 passes its key check and
# reaches this function. A shared fallback would make it post the machine's
# DNC settings under the MARKER's asset number - a kind that resolves by
# machine number, handed a device asset number - where today it exits
# quietly. That is a behaviour change to a script running in production.
#
# A kind that legitimately identifies itself by device passes
# -MachineNumberFallback { Get-ShopdbAssetId } instead. The per-kind
# mechanism already exists; this is what it is for.
return ''
}
@@ -479,4 +522,5 @@ Export-ModuleMember -Function Initialize-ShopdbBackup, Send-ShopdbBackup,
Get-ShopdbRegValue, Get-ShopdbCollectorKey, Get-ShopdbMachineNumber,
Get-ShopdbIntervalHours, Get-ShopdbLogPath,
Get-ShopdbLastBackupAction, Test-ShopdbBackupNoop,
Add-ShopdbBackupSkip, Get-ShopdbBackupSkips, Clear-ShopdbBackupSkips
Add-ShopdbBackupSkip, Get-ShopdbBackupSkips, Clear-ShopdbBackupSkips,
Get-ShopdbAssetId

View File

@@ -84,19 +84,43 @@ function Get-TelesisPortConfig {
}
$props = Get-ItemProperty -Path $script:MARKKEY -ErrorAction Stop
$portid = [string]$props.'Port Id'
# Every value is read through a presence check because this module runs
# under StrictMode Latest, where touching a registry value that is not
# there throws PropertyNotFoundException. Reading them directly made the
# "no Port Id" guard below unreachable, and turned a PARTIALLY configured
# marker bay into "no Telesis marker configured on this PC" - permanent,
# quiet, and the exact failure this feature exists to prevent.
$names = $props.PSObject.Properties.Name
function Read-MarkValue {
param([string]$Name, [string]$Default = '')
if ($names -contains $Name) {
$value = $props.$Name
if ($null -ne $value) { return ([string]$value).Trim() }
}
return $Default
}
$portid = Read-MarkValue 'Port Id'
if (-not $portid) { throw "No 'Port Id' under $script:MARKKEY - marker serial port is not configured." }
# Defaults match the fleet's own configuration (9600-8-N-1 in WJPRT.reg), so
# a key missing one value still produces a usable port rather than a zero
# baud rate that fails at Open with an unhelpful message.
$baud = [int]((Read-MarkValue 'Baud' '9600') -replace '\D', '')
if (-not $baud) { $baud = 9600 }
$databits = [int]((Read-MarkValue 'Data Bits' '8') -replace '\D', '')
if (-not $databits) { $databits = 8 }
[pscustomobject]@{
PortName = $portid.Trim()
BaudRate = [int]("$($props.Baud)".Trim() -replace '\D', '')
Parity = switch -Regex ("$($props.Parity)".Trim()) {
PortName = $portid
BaudRate = $baud
Parity = switch -Regex (Read-MarkValue 'Parity' 'None') {
'^(?i)odd' { 'Odd' }
'^(?i)even' { 'Even' }
default { 'None' }
}
DataBits = [int]("$($props.'Data Bits')".Trim() -replace '\D', '')
StopBits = if ("$($props.'Stop Bits')".Trim() -eq '2') { 'Two' } else { 'One' }
DataBits = $databits
StopBits = if ((Read-MarkValue 'Stop Bits' '1') -eq '2') { 'Two' } else { 'One' }
}
}
@@ -232,7 +256,20 @@ function Invoke-TelesisCommand {
if ($start -lt 0 -or $end -lt $start) {
throw "Malformed reply to command '$Type' '$Data'."
}
return $text.Substring($start + 1, $end - $start - 1)
$reply = $text.Substring($start + 1, $end - $start - 1)
# Check the reply's own BCC, not just the data blocks'. A corrupted byte in
# a LISTING entry would otherwise be trusted, and the wrong filename would
# be requested and stored under a name nothing on the controller has.
$tail = $text.Substring($end + 1)
if ($tail.Length -ge 3) {
$expected = New-TelesisBcc -Type $Type -Data ([Text.Encoding]::ASCII.GetBytes($reply))
if ($tail.Substring(0, 3) -ne $expected) {
throw ("Bad checksum on the reply to command '{0}' '{1}' (got {2}, expected {3})." -f
$Type, $Data, $tail.Substring(0, 3), $expected)
}
}
return $reply
}
@@ -394,7 +431,14 @@ function Read-TelesisFile {
$final = [Text.Encoding]::ASCII.GetBytes($finaltext)
$Port.Write($final, 0, $final.Length)
return $data
# The comma is load-bearing. A bare `return $data` is ENUMERATED by the
# pipeline: the caller gets Object[] of boxed bytes for a normal file, a
# scalar for a one-byte file, and $null for an EMPTY one. A null then
# reaches ComputeHash in the manifest builder, which throws, and under
# ErrorActionPreference=Continue the assignment is skipped so the entry
# silently keeps the PREVIOUS file's hash - a corrupted manifest stored as
# a good revision. Empty parameter files are entirely plausible.
return ,$data
}
@@ -418,7 +462,7 @@ function Write-TelesisFile {
$header = '{0};{1:D6};{2}' -f $FileName, $Data.Length, $script:BLOCKSIZE
[void](Invoke-TelesisCommand -Port $Port -Type 'D' -Data $header)
$total = [Math]::Ceiling($Data.Length / [double]$script:BLOCKSIZE)
$total = [int][Math]::Ceiling($Data.Length / [double]$script:BLOCKSIZE)
while ($true) {
# ACK + 5-digit block number + CR, from the controller.
$request = New-Object Text.StringBuilder