165 lines
6.7 KiB
Python
165 lines
6.7 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'))))
|
|
|
|
# Emitted as SECTIONS for the tabs renderer: one visible at a time, so the
|
|
# card stays the height of its largest section. Flattened into one list it
|
|
# ran to 30 rows, which the two-column multicol layout cannot split, so it
|
|
# dragged one column far past the other.
|
|
#
|
|
# Everything DNC lives on this one card - General, the interface sections
|
|
# and MARK - rather than MARK getting a card of its own.
|
|
out = [{'label': title, 'fields': entries}
|
|
for title, entries in sections if entries]
|
|
return {'sections': out, 'sectioncount': len(out)}
|