Adds a kind-pluggable backups plugin. Configuration captured from a PC is filed against the MACHINE it controls, with a revision history and download back to the native format. NTLARS/DNC is the first kind. Settings live in the controlling PC's registry but describe the machine, so revisions attach to the machine's asset and carry no foreign key to the PC: history survives a PC being replaced or deleted, and sourcehostname records the handover. Storage splits by kind. Parseable kinds store a dialect-neutral JSON projection in ShopDB and re-render on download; opaque vendor formats (part marker and similar) keep their bytes on the SFLD share with ShopDB holding metadata and the UNC pointer. Two .reg dialects exist in the wild: NTLARS's own Save... export omits the WOW6432Node path segment, scripted exports include it. Parsing strips whichever root matched, so a stored revision commits to neither and download offers both (NTLARS Load... by default, WOW6432Node for direct reg import). Getting this backwards is silent, so the dedup hash deliberately excludes sourcedialect and both dialects of one config dedup to a single revision. Dedup is load-bearing: the collector runs every GE-Enforce cycle across the fleet, so a revision is inserted only when the content hash differs from that asset's latest for that kind. A freshly imaged PC opens NTLARS with a blank General tab. Recording that would make an empty config the newest revision exactly when someone needs the last good one, so a blank MachineNo is rejected rather than accepted as a change. Two of the 320 known-good backups on the share already have that shape. DNC Info card summarises the latest revision on the machine page: General (Cnc, NcIF, HostType), eFocas, Serial, NTSHR when populated (only 18 of 147 machines), and MARK when the machine is a marker. MARK is gated on Cnc=MARKER or the ShopDB machine type, not on the MARK key having content: MARK carries serial defaults on 145 of 147 machines and DncPatterns reads YES on 103 including ordinary lathes, so neither identifies a marker. The info card is owned by the kind (BackupKind.infopanel/buildinfo) and served by a generic endpoint, so the expected successor to DNC ships its own card by adding a class rather than changing the plugin or the panel wiring. Also: schedule and retention settings with a prune that never drops the newest or the oldest revision, and scripts/import_ntlars_backups.py to seed history from the existing per-machine .reg files (144 of 147 resolve to assets). Codec verified against all 320 real backups: round-trips clean through both dialects. Bay-side generation verified on Windows against reg.exe export.
162 lines
6.4 KiB
Python
162 lines
6.4 KiB
Python
"""DNC Info card.
|
|
|
|
Surfaces the handful of NTLARS settings a tech actually asks about on the
|
|
machine's page, so the common question ("what is this machine's controller
|
|
talking to?") is answered without downloading and reading a .reg.
|
|
|
|
Sections, drawn from the machine's LATEST ntlars revision:
|
|
|
|
eFocas Fanuc ethernet link - IP, socket, dual-path. Present on 143 of the
|
|
147 known-good backups, so it is shown whenever it has content.
|
|
Serial RS-232 link parameters. Always populated (Baud, Data Bits and
|
|
friends carry defaults even where the link is unused), so it is
|
|
always shown.
|
|
NTSHR Network share the controller pulls programs from. Populated on only
|
|
18 of 147, hence the has-content gate: showing an empty NTSHR block
|
|
on 129 machines would be noise.
|
|
MARK Part-marker settings. Gated on the ASSET being a Part Marker in
|
|
ShopDB, NOT on the key having content.
|
|
|
|
WHY MARK IS GATED ON THE ASSET, NOT THE KEY:
|
|
The obvious rule - show MARK when it has content - does not work. MARK is
|
|
populated on 145 of 147 machines because Baud/Data Bits carry serial
|
|
defaults everywhere, and the fields that would identify a marker
|
|
(CageCode, DataHost, DataPath, MarkMasterPath) are empty across the entire
|
|
corpus. The one field that is set, DncPatterns, reads YES on 103 of 147
|
|
including ordinary lathes, so it is a DNC pattern-matching option and not a
|
|
marker flag. No value in the DNC tree distinguishes a part marker, so the
|
|
machine's type in ShopDB is the only reliable signal.
|
|
"""
|
|
|
|
# Values equal to these (case-insensitively) count as "no content".
|
|
EMPTYISH = ('', '0', 'no', 'none')
|
|
|
|
DEFAULTPARTMARKERTYPES = ('Part Marker',)
|
|
|
|
|
|
def _keyvalues(projection, path):
|
|
"""Values under one subkey of the stored projection, or {}."""
|
|
for key in (projection or {}).get('keys', []):
|
|
if (key.get('path') or '').lower() == path.lower():
|
|
return key.get('values') or {}
|
|
return {}
|
|
|
|
|
|
def _hascontent(values):
|
|
"""True when at least one value carries something meaningful."""
|
|
for entry in values.values():
|
|
data = entry.get('data')
|
|
if isinstance(data, (list, tuple)):
|
|
if any(str(x).strip() for x in data):
|
|
return True
|
|
continue
|
|
if str(data).strip().lower() not in EMPTYISH:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _fields(values, mono=()):
|
|
"""Render a subkey's values as keyvalue panel fields, empties dropped."""
|
|
out = []
|
|
for name in sorted(values):
|
|
data = values[name].get('data')
|
|
if isinstance(data, (list, tuple)):
|
|
data = ', '.join(str(x) for x in data)
|
|
text = '' if data is None else str(data)
|
|
if not text.strip():
|
|
continue
|
|
out.append({
|
|
'label': name,
|
|
'value': text,
|
|
'mono': name in mono,
|
|
})
|
|
return out
|
|
|
|
|
|
def ispartmarker(assetid, typenames=None):
|
|
"""True when this asset is a Part Marker according to ShopDB.
|
|
|
|
Reads the machines plugin defensively: a lean per-site build (ADR-014) may
|
|
not install it, and the DNC Info card must degrade to "no MARK section"
|
|
rather than erroring the whole panel.
|
|
"""
|
|
typenames = tuple(t.lower() for t in (typenames or DEFAULTPARTMARKERTYPES))
|
|
try:
|
|
from shopdb.api import db
|
|
from plugins.machines.models import Machine, MachineType
|
|
except ImportError:
|
|
return False
|
|
try:
|
|
row = (db.session.query(MachineType.machinetype)
|
|
.join(Machine, Machine.machinetypeid == MachineType.machinetypeid)
|
|
.filter(Machine.assetid == assetid)
|
|
.first())
|
|
except Exception:
|
|
return False
|
|
return bool(row) and (row[0] or '').strip().lower() in typenames
|
|
|
|
|
|
def _cncismarker(projection):
|
|
"""True when NTLARS itself says the controller is a marker.
|
|
|
|
General\\Cnc reads 'MARKER' on the part markers (0600 and 0614 in the
|
|
known-good corpus) and a controller family - Fanuc 30, Fanuc 16, OKUMA,
|
|
Fidia - everywhere else. This is the one place the DNC tree does
|
|
distinguish a marker; nothing inside the MARK key does, since MARK carries
|
|
serial defaults on nearly every machine.
|
|
"""
|
|
general = _keyvalues(projection, 'General')
|
|
cnc = str((general.get('Cnc') or {}).get('data') or '').strip().upper()
|
|
return cnc == 'MARKER'
|
|
|
|
|
|
def build(projection, assetid, partmarkertypes=None):
|
|
"""Build the DNC Info card payload from a stored ntlars projection.
|
|
|
|
Returns {'fields': [...]} in the shape the generic keyvalue renderer wants,
|
|
with section headings inlined as labelless separators.
|
|
"""
|
|
sections = []
|
|
|
|
# General first: what the controller IS, before what it talks to. Cnc gives
|
|
# the controller family, NcIF the interface actually in use (EFOCAS on 127
|
|
# of 147, NTSHR on 16, SERIAL on 3, HSSB on 1), HostType the DNC host.
|
|
general = _keyvalues(projection, 'General')
|
|
wanted = ('Cnc', 'NcIF', 'HostType')
|
|
generalfields = _fields(
|
|
{n: v for n, v in general.items() if n in wanted})
|
|
if generalfields:
|
|
sections.append(('General', generalfields))
|
|
|
|
efocas = _keyvalues(projection, 'eFocas')
|
|
if _hascontent(efocas):
|
|
sections.append(('eFocas (ethernet link)', _fields(efocas, mono=('IpAddr',))))
|
|
|
|
serial = _keyvalues(projection, 'Serial')
|
|
if serial:
|
|
sections.append(('Serial (RS-232)', _fields(serial)))
|
|
|
|
ntshr = _keyvalues(projection, 'NTSHR')
|
|
if _hascontent(ntshr):
|
|
sections.append(('NTSHR (program share)',
|
|
_fields(ntshr, mono=('ShrFolder', 'ShrFolder2',
|
|
'ShrFolder3', 'ShrHost'))))
|
|
|
|
# Either signal is enough: NTLARS's own Cnc=MARKER works before anyone has
|
|
# set the machine's type in ShopDB and on lean builds with no machines
|
|
# plugin, while the ShopDB type still covers a marker whose Cnc says
|
|
# something else.
|
|
if _cncismarker(projection) or ispartmarker(assetid, partmarkertypes):
|
|
mark = _keyvalues(projection, 'MARK')
|
|
if mark:
|
|
sections.append(('MARK (part marker)',
|
|
_fields(mark, mono=('DataPath', 'MarkMasterPath'))))
|
|
|
|
fields = []
|
|
for title, entries in sections:
|
|
if not entries:
|
|
continue
|
|
fields.append({'label': title, 'value': '', 'heading': True})
|
|
fields.extend(entries)
|
|
return {'fields': fields, 'sectioncount': len(sections)}
|