Persistent Variables
Persistent Variables is the pair of program-variable ledgers the Controller branch grows for one
brand or the other, and no shipped preset carries both: the Fanuc-style retained common variables
and the Sinumerik R parameters. Both live on the General Setup page at /general-setup under the
Control-Tree ids equipment/controller/program-data/retained-variables and
equipment/controller/program-data/r-parameters — reached as
?tree=equipment/controller/program-data/retained-variables and
?tree=equipment/controller/program-data/r-parameters — and the tree labels them
Retained Common Variables and R Parameters. They share a page because no preset shows both:
each node is grown by a flag that one set of brand presets satisfies and the other set does not.
Important
Anatomy makes no claim that any id named here is stable across versions. The migration hop that
keeps an older ?tree= value resolving is described in
Tree Ids and Routes.
Two Nodes That Exclude Each Other
The branch builder pushes each of the two nodes inside a test of its own, one snapshot flag each, and each flag is a single type probe over the runner's proxy-resolved dependency list — RetainedCommonVariableTable behind one and SiemensRParameterTable behind the other. Which brand satisfies which gate is Brand Matrix; the short answer is that the Fanuc, Syntec and Mazak presets carry a retained-common-variable proxy, the Siemens preset carries an R-parameter proxy, and the Heidenhain preset carries neither.
The exclusion is a property of the five preset lists, not a rule in the builder. Nothing in the tree code forbids the two rows from standing together; no shipped preset simply carries both entries, and the brand select can produce nothing but a preset. A runner composed by hand or loaded from a controller file whose pipeline list named both tables would grow both rows, each with its own working panel — and the Object-Management menu on the branch root, described in Controller Branch, installs such a file without inspecting what its pipeline names, so that route is open from inside the branch.
The two panels are separate components rather than one component discriminating on the node id, so neither reads the node it was given. What they share is a shell: the same three-state body, the same dense table, the same footer, and the same commit, rollback and toast rules the whole branch follows — Editing Contract.
What Each Ledger Is
Retained Common Variables
RetainedCommonVariableTable models the Fanuc-style ISO
controller's common variables in the retained range #500–#999 — the range a real control keeps in
non-volatile memory across a power cycle, which is why the model stores it with the project rather
than with the run. It is the range, not the whole macro-variable surface: the table answers a #nnn
lookup only for 500 through 999 and returns nothing for any other key, so an out-of-range reference
falls through to the next lookup in the evaluator's chain.
The neighbouring ranges are deliberately elsewhere, and none of them has a node on this branch.
#1–#33 are call-frame locals bounded by a macro call rather than by a power cycle. #100–#499
are the non-retained commons, carried block to block in the per-block dataflow and cleared on the
block that ends the program. #1000 and above are read-only or computed from runtime state rather
than stored here: some resolve against other dependencies — the brand parameter table behind the
work-coordinate addresses, the tool offset table behind #2001–#2200 — and some come from the
block's own record, which is where the position variables are read and where a system-control write
is recorded rather than emulated.
The type name and the tree label both say common variable rather than macro variable, because the model reserves “macro” for Custom Macro B's call frame. The hint above the table is the one string in the leaf that says Retained macro variables instead.
R Parameters
SiemensRParameterTable models the Sinumerik arithmetic
variable surface R0–R999 — the R63=100.5 and C=R61 of a Siemens program. On the control these
sit in retentive memory and survive both program end and a power cycle, and the model keeps them the
same way: stored with the project, not reset with the session.
The table answers an Rn key in the canonical uppercase form the Siemens expression parser produces,
and a lowercase rn from a raw capture resolves as well. Every other key returns nothing.
Addressing and Ranges
| Retained Common Variables | R Parameters | |
|---|---|---|
| Node id | equipment/controller/program-data/retained-variables |
equipment/controller/program-data/r-parameters |
| Snapshot flag | hasRetainedVariables |
hasRParameters |
| Backing table | RetainedCommonVariableTable | SiemensRParameterTable |
| Brands | Fanuc, Syntec, Mazak | Siemens |
| Key column header | Variable | Parameter |
| Key cell reads | # then the number |
R then the number |
| Accepted ids | 500–999 | 0–999 |
| Bounds on the add field | minimum 500, maximum 999 | minimum 0, maximum 999 |
| Absent-table body | "No retained common variable table on the active runner." | "No R-parameter table on the active runner." |
Each range is a pair of constants declared on the table type, and four separate places enforce it. The panel refuses an add whose id is not a whole number inside the range and says so in a toast of its own. The numeric field carrying that id rejects a finite value outside its bounds before the panel ever sees it — the infinity and NaN literals the field also parses skip the bounds test entirely and are stopped by the panel's whole-number check instead. The write endpoint compares the id against the same two constants and answers unsuccessfully with the range spelled out in the message. And behind all three, the table's own writer ignores an out-of-range id — the guard that holds for a caller reaching the table directly rather than through the endpoint, which is how a run writes.
The endpoint's guard is not reachable from a project this app wrote. The rows a panel writes to came from the table, the add path is already filtered, and the serializer drops an out-of-range key on the way out, so no file the app saves carries one. Loading applies no such filter: a project file edited by hand can carry an id outside the range, the read lists it and the panel shows it, and editing that row's value is the one way a reader meets the endpoint's range message.
No range is narrowed by machine data. The R range in particular is the fixed pair 0–999 on the table type; the endpoint reads no value from the Siemens machine-data table before accepting an id, so the parameter count a real control would allocate does not constrain what this leaf accepts.
Neither table is dense. A row exists for an id that has been written, and for no other, so a thirty-row table and a four-row table are equally normal and the numbers in the key column need not be contiguous. A project that has never written one shows an empty table body under the header, because the proxy that materialises the table installs a bare instance carrying nothing — Program Data Plane covers the seedless proxies this plane is built from. The read returns the rows in ascending id order and the panel never re-sorts: an edit leaves a row where it is, a delete removes it in place, and only the re-read that follows an add re-orders anything.
What Persists, and What Clears
Both tables are per-case entries of the project's own dependency list, reached through a get-or-create placeholder in the runner's pipeline list. The runner file records only an empty placeholder element, so the values are written into the project file rather than into a controller asset.
Four things in the runtime could plausibly discard what these tables hold. Three of them leave both tables alone; the fourth removes a table outright rather than clearing it.
- A new session does not clear them. The session-init edge that rebuilds the syntax pipeline sweeps every dependency and syntax declaring session-scoped state — iteration counters, index allocators — and calls each one's reset. Neither table declares any, so both are skipped.
- A power reset does not clear them. The power-reset command sweeps the proxy-resolved dependency list for dependencies declaring a volatile subset and then resets the session state, dropping the per-block dataflow. None of the dependencies a brand preset carries declares a volatile subset, so what a power reset actually discards is the dataflow — which is where the non-retained commons live — while every value in these two tables stands.
- Program end does not clear them. The syntax that models a control's
M02/M30reset empties the block's volatile dictionary and cancels an active modal macro. It writes into the block's own record and reaches no dependency at all, so neither table is in its path. - A brand switch removes the whole table rather than clearing it. The sweep that follows a runner swap keeps exactly the per-case tables the new runner resolves through a proxy, so a switch to a brand that proxies neither drops the table outright, and switching back materialises a fresh empty one rather than the values that were there. Retained Common Variables is the wider of the two: all three of Fanuc, Syntec and Mazak proxy it, so it survives any switch among those three and is removed on a switch to Siemens or Heidenhain. R Parameters survives only a Siemens-to-Siemens re-flash. What a switch keeps, resets and destroys across the whole plane is Program Data Plane.
A run writes into the same table
These are not a snapshot of what a program did — they are the store the program uses. Each brand's
syntax list carries a reading syntax that consumes a literal assignment out of the parsed block and
writes it straight into the table: #500 = 1.234 on the three Fanuc-style brands, R63 = 100.5 on
Siemens. No mirror is kept in the per-block dataflow; the table is the single source of truth. A
non-literal right-hand side is resolved to a literal earlier in the same block by the expression
normaliser and then lands by the same route, so #600 = #500 + 1 reaches the table as well.
Reads run the other way through the same object: each table is a variable lookup on the effective dependency list, so an expression naming an id in range reads whatever the leaf shows.
Two consequences follow for a reader of this leaf. A played program changes what the table holds, and because no panel in the branch is pushed at, the change appears only when the panel is remounted by selecting another node and coming back. And because the table is written into the project file, those run-time writes are what the next load of that project starts from — once the project is saved. Nothing in the branch and nothing on the run path saves on its own: a panel edit and a run-time write alike stand in memory until an explicit save, and closing the project without one discards them.
Vacant is a value, and it fails loud
A stored null is a vacant entry, and so is a missing key — the table reads the two identically.
Vacant is not zero. An expression that evaluates a vacant id does not fall back to a number: the
evaluator returns a failure carrying the code Variable--Vacant and a message naming the key, which
is what the R-parameter hint means by reporting an error instead of silently using 0.
Vacant and deleted are different operations even though they read the same at run time. Clearing a cell keeps the key with a null value; the delete button drops the key. The distinction survives a save, because a vacant entry is written as an element carrying an id and no value while a deleted one is written as nothing at all — so a vacant row comes back as a row, and a deleted one does not come back.
The Editor Body
Above the table sits a one-line hint, and it is the only prose either panel shows. Both render the
literal text <vacant> inside it as inline code.
- Retained Common Variables: "Retained macro variables #500–#999 (power-off safe). Empty =
<vacant>. #100–#499 are volatile and live in the run's dataflow, not here." - R Parameters: "Sinumerik R parameters R0–R999 (retentive). Empty =
<vacant>— a program reading a vacant R parameter reports an error instead of silently using 0."
The table below it has three columns. The first is the key — headed Variable or Parameter — rendered bold as plain text and not editable, which is the branch's rule for a key column. The second is headed Value and holds the shared numeric field. The third carries the row's delete button under a blank header, so the branch's shared Actions heading does not appear on either leaf.
Editing a value
A value cell is the shared numeric widget, so it commits on blur or on Enter and never per keystroke; its parsing, its bound behaviour and the double commit an Enter can cause are Numeric Input. No value cell is bounded — neither panel passes a minimum or a maximum to it — so any finite number is accepted and stored, negatives included.
These two panels are the branch's only pair that commits a null. Every other numeric handler in the branch treats a cleared cell as no edit and returns before the request; here the null is sent, because vacating an entry is a real state of both tables and there is no other control that reaches it. Clearing the cell is therefore how a row is made vacant while keeping its key.
The handler is otherwise the branch's ordinary optimistic write — assign, send, and on failure put the
captured value back and raise the toast — and it carries no finiteness test. The widget parses
Infinity and -Infinity as values, and neither survives JSON encoding: the request body reaches the
server carrying a null, and the entry is set vacant. The cell keeps showing Infinity until the panel
is remounted, so that is the one place in the table body where the screen and the store disagree
after a write the server accepted. A typed NaN takes the same route to vacant, and the cell renders
it as empty, which is what a vacant entry looks like anyway.
Adding and removing a row
The footer under the table is the branch's fielded add form: a numeric field for the id — labelled Variable # (500–999) or Parameter # (0–999) — a second numeric field labelled Value, and a primary Add / Set button. Both fields are the same numeric widget as the value cells, so what they hold reaches the panel on blur or Enter rather than per keystroke, and neither input submits on Enter.
The button label's second word is the accurate one: the write is an upsert. Entering an id the table already lists overwrites that row's value rather than adding a second row. Leaving Value empty creates the row vacant. A successful add is followed by a full re-read of the table, which is what puts the new row in ascending order; the two fields keep what was typed in them.
Two refusals sit in front of that write, and they read differently. An id outside the field's own bounds is rejected inside the widget, which shows its message under the box, emits nothing and leaves the rejected text on screen — so the panel still holds the id the field last committed to it. On a panel that has committed none, that is nothing at all, and pressing Add / Set reports the panel's own message: "Variable number must be an integer in 500–999." or "Parameter number must be an integer in 0–999." The panel's message is also what a fractional id produces, since the panel requires a whole number and the field does not.
A field that has already committed a valid id keeps it, and nothing clears either field after an add. So an out-of-range id typed over one the field already accepted is the second place on these leaves where the screen and the store part company: the box shows the rejected number, the panel still holds the earlier id, and Add / Set upserts that earlier id with whatever the value field holds — no message of any kind, and the re-read that follows leaves the rejected number in the box.
Removal is one icon button per row, with no label, no tooltip and no confirmation dialog: the click sends the delete, and the row is filtered out of the panel's list once the request resolves. The branch guards some of its deletions with a confirmation dialog; neither of these two leaves is among them. The endpoint removes the key and reports success whether or not the key was there.
What the Surface Covers
A third table of the same shape exists in the engine and has no node.
HeidenhainQParameterTable holds the Heidenhain persistent
Q parameters — the free range Q0–Q99 and the permanent QR0–QR499, in two stores of its own —
and the Heidenhain preset carries a proxy for it beside the two documented above. It is materialised
into the project's per-case list exactly as they are, is serialized into the project file the same way,
is swept by a brand switch away from Heidenhain the same way, and is read and written during a run by
the Heidenhain reading syntax that routes Qn and QRn assignments by id range.
What it has no part in is this branch. The snapshot the tree is built from declares no flag that probes it, the branch's REST surface declares no route that reaches it, and the branch builder mints no node for it. The Controller branch's editable persistent-variable surface is the two tables named above; a Heidenhain project's Q parameters are set and read by the program that runs, and are carried by the project file between runs.
Layout
- General Setup Control Tree — the left dock of
/general-setup- Controller Node Row
- Program Data Group Row —
equipment/controller/program-data- Retained Common Variables Node Row —
equipment/controller/program-data/retained-variables; grown while the snapshot reports a retained common variable table - R Parameters Node Row —
equipment/controller/program-data/r-parameters; grown while the snapshot reports a Siemens R-parameter table
- Retained Common Variables Node Row —
- Program Data Group Row —
- Controller Node Row
- Editor Row — the panel of whichever node is selected
- Retained Common Variables Panel
- No-Runner Body — the shared "No NC runner — load a project first."; shown first
- Absent-Table Body — "No retained common variable table on the active runner."
- Editor Body
- Hint Caption — "Retained macro variables #500–#999 (power-off safe). Empty =
<vacant>. #100–#499 are volatile and live in the run's dataflow, not here." - Variable Table — dense, flat, bordered
- Header Row — Variable, Value, and a blank third heading
- Variable Row, one per stored id
- Variable Number Label — bold plain text,
#then the number, not editable - Value Numeric Field — unbounded; cleared commits vacant
- Delete Button — a bin icon, flat and dense, no label and no confirmation
- Variable Number Label — bold plain text,
- Add Row Footer
- Variable # (500–999) Numeric Field — minimum 500, maximum 999
- Value Numeric Field — unbounded; left empty adds the row vacant
- Add / Set Button — primary, unelevated
- Hint Caption — "Retained macro variables #500–#999 (power-off safe). Empty =
- R Parameters Panel — the same shell with the Siemens strings
- No-Runner Body — the shared "No NC runner — load a project first."
- Absent-Table Body — "No R-parameter table on the active runner."
- Editor Body
- Hint Caption — "Sinumerik R parameters R0–R999 (retentive). Empty =
<vacant>— a program reading a vacant R parameter reports an error instead of silently using 0." - Parameter Table — dense, flat, bordered
- Header Row — Parameter, Value, and a blank third heading
- Parameter Row, one per stored id
- Parameter Number Label — bold plain text,
Rthen the number, not editable - Value Numeric Field — unbounded; cleared commits vacant
- Delete Button — a bin icon, flat and dense, no label and no confirmation
- Parameter Number Label — bold plain text,
- Add Row Footer
- Parameter # (0–999) Numeric Field — minimum 0, maximum 999
- Value Numeric Field — unbounded; left empty adds the row vacant
- Add / Set Button — primary, unelevated
- Hint Caption — "Sinumerik R parameters R0–R999 (retentive). Empty =
- Retained Common Variables Panel
Neither panel opens a dialog, and neither carries a select, a toggle or a Show all control.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/components/controlTree/SoftNcRetainedVariablesPanel.vue— the#500–#999ledger: the hint with its vacant slot, the three-column table, the value handler that sends a null without a finiteness test, the unconfirmed delete, and the add form with its whole-number and range check.wwwroot-src/src/components/controlTree/SoftNcRParametersPanel.vue— theR0–R999ledger: the same shell with the Siemens strings, theR-prefixed key cell, and the add field bounded from 0.wwwroot-src/src/components/controlTree/softNcItemTypes.ts— the branch builder: the two node ids, their labels and label keys, the two flags each is pushed behind, and the item types binding them to the two panels.wwwroot-src/src/api/softNcRunner.ts— the two readers, their identical row shape with a nullable value, the two setters and the two removers, and the two snapshot flags the builder tests.wwwroot-src/src/api/http.ts— the helper that turns a success-flagged failure body into a thrown error, which is how an out-of-range write would reach a panel.wwwroot-src/src/components/widgets/NumericInput.vue— the value and add fields: commit on blur or Enter, the empty-text-to-null parse both panels forward, the infinity and NaN literals it also accepts, and the bound rejection that shows a message and emits nothing.wwwroot-src/src/components/controlTree/SoftNcEmptyState.vue— the shared no-runner line both panels open with.wwwroot-src/src/composables/useSoftNcRunner.ts— the shared snapshot the first empty layer reads, and the events that re-read it.wwwroot-src/src/i18n/en/softNc.ts— the two node labels, the two hints, the two absent-table bodies, the two add-field labels, the two invalid-id messages and the error contexts each panel prefixes its toast with.wwwroot-src/src/i18n/en/common.ts— the shared Value column label and the Add / Set button label.Mech/SoftNcRunnerController.cs— the REST surface: the two flag probes in the snapshot builder, the two reads that order rows by ascending id and report presence from the table alone, the two upsert writers with their range guards, the two removers, and the sweep of per-case tables the new runner references through no proxy.
HiAPI Engine
HiMech/NcParsers/Dependencys/Fanuc/RetainedCommonVariableTable.cs— the#500–#999store: the range constants, the nullable dictionary that makes vacant a value, the writer that ignores an out-of-range id, the variable lookup that answers only in range, and the serialization that keeps a vacant entry as a valueless element.HiMech/NcParsers/Dependencys/Fanuc/RetainedCommonVariableTableProxy.cs— the seedless get-or-create placeholder the three Fanuc-style presets carry, the bare table it installs into a project holding none, and the legacy element name an older project file still deserializes through.HiMech/NcParsers/Dependencys/Siemens/SiemensRParameterTable.cs— theR0–R999store: the range constants and the reason the upper one is 999, and the lookup that accepts an uppercase or lowercaseRkey.HiMech/NcParsers/Dependencys/Siemens/SiemensRParameterTableProxy.cs— the matching seedless placeholder carried by the Siemens preset alone.HiMech/NcParsers/Dependencys/Heidenhain/HeidenhainQParameterTable.cs— the third table of the same shape: the free and permanent Q stores, the ranges each accepts, and the system and volatile ranges it deliberately declines.HiMech/NcParsers/Dependencys/Heidenhain/HeidenhainQParameterTableProxy.cs— its placeholder in the Heidenhain preset, seedless like the other two.HiMech/NcParsers/SoftNcRunner.cs— the five brand presets as literal dependency lists, which is where the mutual exclusion of the two nodes is actually decided, and the proxy resolution every flag probe and every read goes through.HiMech/NcParsers/NcRunnerSuit.cs— the per-case list these tables live in, and its inline serialization beside the runner.HiMech/NcParsers/Dependencys/INcDependencyProxy.cs— the maker-and-taker contract, and why a proxy's resolved table is never written into the runner file.HiMech/NcParsers/ISessionResettable.cs— the session-scoped contract neither table implements, and its own statement of the boundary between session state and persistent state.HiMech/NcParsers/Dependencys/IPowerResettable.cs— the volatile-subset contract a power reset sweeps, which no dependency of any brand preset declares.HiMech/NcParsers/LogicSyntaxs/ProgramEndCleanSyntax.cs— the program-end clear that empties the block's volatile dictionary and names the retained range as untouched.HiMech/NcParsers/EvaluationSyntaxs/VolatileVariableReadingSyntax.cs— the#100–#499range that has no node here: the per-block dictionary it carries forward, and the session that bounds it.HiMech/NcParsers/Dependencys/Fanuc/FanucPositionVariableLookup.csandHiMech/NcParsers/EvaluationSyntaxs/Fanuc/FanucSystemControlVariableSyntax.cs— the two#1000-and-up groups no dependency answers: the position variables read from the previous block's record, and the system-control writes recorded on the block rather than emulated.HiMech/NcParsers/EvaluationSyntaxs/RetainedCommonVariableReadingSyntax.cs— the run-time writer for the retained range: the literal assignment it consumes and writes straight into the table, with no mirror kept.HiMech/NcParsers/EvaluationSyntaxs/Siemens/SiemensRParameterReadingSyntax.cs— the same shape forRnassignments.HiMech/NcParsers/EvaluationSyntaxs/Heidenhain/HeidenhainQParameterReadingSyntax.cs— the same shape forQnandQRn, routing by id range into the free store, the permanent store, a read-only warning or the volatile dataflow.HiMech/NcParsers/EvaluationSyntaxs/VariableEvaluatorSyntax.cs— the normaliser that resolves a non-literal right-hand side to a literal before the reading syntaxes run, and the lookup chain these tables join.HiMech/NcParsers/EvaluationSyntaxs/Evaluation/IVariableLookup.csandHiMech/NcParsers/EvaluationSyntaxs/Evaluation/NcExpressionEvaluator.cs— the lookup contract both tables implement, and the vacant failure an expression raises instead of reading zero.HiMech/NcParsers/Syntaxs/FanucSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/SyntecSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/MazakSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/SiemensSyntaxUtil.csandHiMech/NcParsers/Syntaxs/HeidenhainSyntaxUtil.cs— the five syntax lists, each carrying the reading syntax for its brand's persistent variable range.HiNc/MachiningProcs/LocalProjectService.cs— the facade setter every install and brand switch assigns through, and the power reset that sweeps volatile subsets and then drops the session dataflow.HiNc/MachiningProcs/SessionShell.cs— the scriptable power-reset entry point over that service call.
See Also
- Program Data Plane — the plane both leaves sit on: where their tables are stored, the seedless proxies that make them, and what a brand switch keeps or sweeps beside them