Develop .NET interpreters
When to use
This is the default implementation of an interpreter. Work on a C# interpreter: creating one, adding a word or a cycle,
making it configurable, building it for the products it must run in, debugging it. Take the census of "Inspect NC
programs" (inspect-nc-programs) first and follow "Verify interpretation" afterwards; "How the kernel reads a block"
(nc-interpreter-foundations) explains the kernel calls this skill refers to.
Words used here
- Product version — the major version of the installed CAM system or Tuner, as the name of its installation folder shows. The CAM system and Tuner of one release may carry different numbers. Each product version has its own .NET version and SDK package version: read both from the installation (see "The SDK package and the product versions" below), never from memory.
- The
.sncifile — the XML file that tells the CAM system which DLL is the interpreter. The documentation calls it the interpreter settings file. - The INI file —
InterpreterSettings.ininext to the DLL, read by the template at start. The template calls it the settings file; it holds the switches a user may change without building. - GUID — a unique identifier such as
{5DD76B4D-8ABE-43EE-AB0C-078F5D686C5E}; create a new one with[guid]::NewGuid().ToString('B').ToUpper()in PowerShell. The kernel accepts a GUID only in braces.
Workflow
- Choose the starting point and create the project (below). Say which you chose and why.
- Set the SDK package version and the target .NET version for every product version the interpreter must run in.
- Fix the known defects of the template you started from.
- Build the project unchanged; if it does not build, stop and fix that first — nothing after it can be diagnosed.
- Run the reference NC programs through it and keep the result as the baseline ("Verify interpretation"
(
verify-interpretation)). - Make one change at a time in the file of its role; make variations a setting of the INI file.
- Build, debug or trace on a real program, then follow "Verify interpretation".
Choose the starting point
- An existing interpreter for the same or a similar control — prefer it when its behavior has been checked on real
programs. Copy it into a new folder and give the copy a new identity:
- a new GUID returned by
GetIdentifierinEntry.cs, written also intoLibraryIDof the.sncifile (the two must be equal); - another new GUID in
IDof the.sncifile. Two interpreters with the same identifiers cannot be told apart by the CAM system.
- a new GUID returned by
- A template —
Supplement\NCKernel\Templatesof the installed product containsMill,TurnMilland the robot templatesABB RobotandKuka Robot; list the folder to see which templates your installation has. Create the project with the interpreter creator: Create interpreter in the list of utilities (extensions) of the CAM system, or Interpreter manager in Tuner — both start the same program. It copies the template, renames the files after the new folder, fills in the SDK package prefix, the program file name in.vscode\launch.json, and one new GUID for bothGetIdentifierandLibraryID. It refuses a folder that already exists. After it has run, check by hand what it does not do:IDin the.sncifile is still all zeros: write a new GUID there;- the SDK package version in the
.csprojis the one the template was made with — a prerelease version (-dev,-rc, sometimes with*) or a version of an older major version — not the one of the installed product: replace it (see "The SDK package and the product versions" below); - the
.sncilies inbin\Debug\, which is build output: move it to the project folder (same section); - the environment variable for the debugger is set only if you press the button that stores the program path in the creator's window, and only one of them (see "Debug" below);
- the general fields of the
.sncikeep the template's sample values —Description,CNCSystem,FileExtension(*.cncinMill),Authors,Company,Date: fill them in for the real control; NetNamespaceClassnames the class the CAM system creates (STNCInterpreter.Interpreter, the class inEntry.cs): if you rename that namespace or class, change it too;- the template comes from one control family: remove its handlers for words that mean something else on your control (fixed cycles G70–G76 or G81–G89 of one control are other functions on another) instead of leaving them active.
A child interpreter adds to a parent at run time; it is not a copy of the parent's sources. When the creator makes a
child, it writes the parent's LibraryID into ParentID and leaves ParentFileName empty. Correct both by hand:
ParentID must equal the parent's ID, and ParentFileName must name the parent's .snci — without it the kernel
loads no parent at all. A child calls the parent's methods where it adds to them (for example the parent's TransLine),
by the rules in "Inheritance and protection" of "Develop native interpreters" (develop-native-interpreter), which come
from the documentation on interpreter inheritance and hold for .NET as well.
The creator makes the child from a full copy of the template, so it starts as a complete interpreter of its own: its
registers replace the parent's registers of the same name, and nothing in it calls the parent. The base class keeps the
parent as ParentInterpreter; remove what the parent must handle and call the parent's methods as the inheritance rules
say. The kernel finds the parent by the file name in ParentFileName: in the child's own container, then in the
container's folder, in the child's folder, in the product's interpreters folder and in its containers folder. Where a
folder does not hold the file, the kernel also follows the .lnk shortcuts to folders found in it and in its subfolders
(one level of shortcuts; not in the folder of a child loaded from a container). It takes the first file with that name
and stops with an error if that file's ID differs from ParentID. During F5 the child's .snci lies in bin\Debug,
so the parent must be found from there.
The SDK package and the product versions
The interpreter references the package <Org>.NCInterpreterSDK.Net, where <Org> is the package prefix of the
product's vendor. Which version to use:
- Read the version the installed product was built with:
Bin64\STNCInterpreterSDKProject.stbuildresult.json,build_results.versions. It tells you the package name and the major version. A Tuner installation may not have this file: read it from the CAM system of the same release (it runs the same .NET version; ask the user when several installations qualify). Where Tuner has the file, the number is set by the build of Tuner, which does not publish the SDK packages, so a package with exactly that number may not exist in the feed. - Use the newest released version of the package (a version without
-dev,-rcand similar suffixes) that has the same major version and is not newer than the version from step 1. Take it from the public package feed of the product's vendor: the documentation section on AI-assisted development of that product names the feed and, when the feed is not one the .NET SDK uses by default, the command that adds it. The feed's search may show an older newest version than its package list: read the list of versions of the package itself.
A nightly or test build of the product records a development version (with a suffix such as -dev.2): a customer cannot
download it, so never write it into a project that leaves your computer. Do not use a floating version (<major>.0.*)
and do not choose one from memory. A project with an unknown version or an unreplaced $(package_namespace) does not
build; the debugger start then fails, or the product loads an older DLL left in the output folder — check the build
result before judging the behavior.
Each product version has its own .NET version. Read it from the installation: <TargetFramework> in the .csproj
files of its templates (Supplement\NCKernel\Templates), or tfm in the runtimeconfig.json of its program in
Bin64 (in Tuner, ltuner.runtimeconfig.json). Read the package name from the same stbuildresult.json as in
step 1. One project can serve several product versions: replace the template's <TargetFramework> with the list of the
targets of those versions and give each target its own package version, as below for two versions that run
.NET 10 and .NET 8. If both properties stay, only one target is built. The robot templates target the -windows
variant (net10.0-windows): there use the -windows names in the list and in the conditions.
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
<PackageReference Include="<Org>.NCInterpreterSDK.Net" Version="<version for the newer product version>"
Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageReference Include="<Org>.NCInterpreterSDK.Net" Version="<version for the older product version>"
Condition="'$(TargetFramework)' == 'net8.0'" />
When one target needs an interface that only the newer released package has, guard that code with the target's symbol
(#if NET10_0_OR_GREATER) and give the older product version a fallback that the README describes.
The template's .csproj sets OutDir and PublishDir to bin\Debug\: with two targets both builds would write into
that one folder and overwrite each other. Make both depend on the target (bin\Debug\<product version>\). If you remove
them instead, build writes to bin\Debug\net10.0 and bin\Debug\net8.0, but publish writes to
bin\Release\<target>\publish\, away from the folder the launch configuration uses. Keep the .snci and
InterpreterSettings.ini in the project folder, not in bin (build output is not kept in version control), and copy
them into every output folder:
<ItemGroup>
<None Update="<Name>.snci" CopyToOutputDirectory="PreserveNewest" />
<None Update="InterpreterSettings.ini" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
The folder name and the .csproj name must be identical. The template's .vscode\launch.json passes
Bin\Debug\${workspaceFolderBasename}.snci to the program, and its build task builds the project of the folder. With
several targets that path no longer exists: write one launch configuration per product version with the path of that
version's output folder. The template's publish task runs dotnet publish without a target; with several targets give
it -f net10.0 or -f net8.0, one task per product version. Keep the .snci, the DLL name and LibraryFileName_Net
inside the .snci consistent.
What goes where
The templates split the interpreter into files by role. Keep that split; a new feature goes into the file of its role.
Not every template has every file: Mill has no lathe cycles and no Interpreter.Types.cs; the robot templates read
their programs in Interpreter.Transline.cs and have no Interpreter.Registers.cs and no IniFile.cs; the ABB robot
template also has Program.cs and AskUserForm.cs.
| File | Contains |
|---|---|
Entry.cs |
the entry point the CAM system creates the interpreter through, and its GUID |
Interpreter.Registers.cs |
the registers: every address and G/M function the kernel reads itself, grouped as in the control's manual |
Interpreter.CommandHandlers.cs |
Cmd… methods called when a register of the block is executed (G1, X, M3) |
Interpreter.CommandTypes.cs |
the names of the commands a block puts into its queue |
Interpreter.Execution.cs |
executing the queued commands: motion, spindle, tool change |
Interpreter.Core.cs |
the state of the interpreter and the kernel calls (TransLine, DefineRegister, BeforeInterprete, AfterInterprete) |
Interpreter.Initialization.cs |
analyzer setup, machine axes, reading the INI file |
Interpreter.DrillingCycles*.cs, Interpreter.LatheCycles*.cs |
cycles and their move events |
Interpreter.Transline.cs (robots) |
reading the robot program statements |
Interpreter.Types.cs |
small types the interpreter uses |
IniFile.cs |
reading InterpreterSettings.ini |
A construction the kernel cannot read as a register — a numbered axis with =, an expression, a call with brackets, a
string — is read in TransLine (see "Words the kernel cannot read" in "How the kernel reads a block"); give such
readers their own file (Interpreter.Statements.cs, Interpreter.Cycles.cs, Interpreter.ProgramFlow.cs) when there
are many.
Known defects of the templates
Check each one in the template you started from and fix it in your copy:
- The INI file is never read.
IniFile.csdeclares the Windows functionsGetPrivateProfileStringandGetPrivateProfileSectionNameswithout their Unicode names. WriteEntryPoint = "GetPrivateProfileStringW"(and…SectionNamesW,WritePrivateProfileStringW) in eachLibraryImport. Without it reading fails, the error is caught, and the log has only an information line starting withError reading settings file, so every setting looks ignored and a log filtered by errors shows nothing. - The INI file is skipped when one interface is missing.
Interpreter.Initialization.csreturns before reading the file when the origin interface (INCT_AddOriginByShiftAndRotate) was not found, with only an information line in the log. Read the file independently of that interface. - Long values break reading.
ReadStringpasses the buffer size in bytes (buffer.Length) where Windows expects characters, so a value longer than 1023 characters (a long list of M codes) is written past the end of the buffer and reading fails.GetSectionshas the same error. Pass the size in characters and read again with a doubled size while the result fills the buffer (size - 1for a value,size - 2for the list of sections). - A one-digit tool number loads no tool.
TurnMillsplits theTword in halves (tool, offset), soT1gives an empty tool part: no tool is loaded and1becomes the offset. Check against theTformat of the control. - Lathe cycle register names disagree. In
TurnMill, the names looked up inInterpreter.Core.cs(P LCYCLE) and the names declared inInterpreter.Registers.csmust be identical; a difference silently reads the word as another register (see "Choosing a register by state" in "How the kernel reads a block"). - Lathe cycles G70 and G73. Check the feed of the finishing cycle G70 and the side the passes of G73 run on against the control's manual and a real program; both were found wrong in an interpreter built from this template.
When you fix a defect of a template in your interpreter, tell the user, so that it can be fixed in the template too: a fix in your copy does not reach the next interpreter created from the template, and a fix in the template reaches users only with a product version that ships it.
Make variations a setting, not a code change
Programs of one machine name the same thing differently: an axis is B90 in one program and B1=90 in another, the
postprocessor of one shop writes M codes another does not. Put such variations into InterpreterSettings.ini next to
the DLL, so a support engineer can adapt the interpreter without building it.
The templates read only one key ([Main] AddOriginFlipMode) and ship no InterpreterSettings.ini. Every key below is
therefore written once in code — the key, its check and its log line — and from then on the variation is a setting. A
good set of keys:
[Axes]
; numbered machine axes that occur in the programs (axis addresses of the machine schema)
MachineAxes = B1, Cs1
; axis names of the control that differ from the machine schema: name in the program : axis of the schema
ChannelAxisNames = C1:Cs1, B:B1
[MachineFunctions]
; M codes of the machine that do not move it (clamps, doors, measuring)
M = 10, 11, 50, 51, 120, 121
[Words]
; words, addresses with a value, and calls with brackets that do not change the simulated toolpath
Keywords =
Addresses =
Procedures =
[Trace]
; yes - write every block and the commands it produced to the debug console and the CAM log
Enabled = no
Every key is read once at start, every malformed value is written to the CAM log as a settings error, and the values taken are logged in one line, so the user can see what the interpreter uses. A value of the INI file is one line: INI files have no continuation lines. A word the interpreter already knows must not be added again from the INI file: a setting must not be able to switch off a supported word.
An axis name mapping must apply to every spelling of the axis the census found — B90, B=90, B1=90, B=DC(30) —
not only to the form one reader handles. Test each spelling against the machine schema: the SDK's helper for axis
indexes warns about an axis name it does not know once per name and then silently skips its values.
Machine-specific numbers — a safe position, a retract height — belong in the INI file or come from the program (its machine-coordinate blocks), not in constants in the code.
Code style
Follow the style of the templates, so the code reads as one piece:
- names from the control's own vocabulary that a machine operator recognizes (
CmdToolCall,CmdDwell,WorkpieceCoordinateSystem), not internal jargon; - one region per group of G functions, as in the control's manual;
- the register table only binds a register to its
Cmd…method — no logic in lambdas there; each queued command is executed by its ownExecute…method; keep the names and numbers of the template's command types and add new ones after them; - a register's
nameis a short identifier in the control's terms; a register that reads another word as a parameter is named<address> <TAG>(X DWELL,F THREAD); itscommentis a short English noun phrase without G numbers; - an English comment above each block saying what it does, and XML documentation on every method and field — interpreters are read by people with basic programming skills, so they carry more comments than other code. A general rule of the workspace that keeps comments short does not apply here: compare the comment density of your files with the template's files before calling the style done;
- keep the line endings and the encoding of the template's files (many are CRLF without a byte order mark). Editors and scripts convert them silently, and the diff then shows every line as changed: check new and changed files by their bytes before committing;
- keep the state of the interpreter in fields of the interpreter object, not in
staticfields: the CAM system loads the DLL and creates a new instance for every calculation of an operation, the parent and the contour of a cycle get instances of their own, each in a load context of its own, so a static value neither survives the calculation nor is shared with the parent; Tuner keeps one instance for each channel, from its first operation to its last, so modal state and any static value there carry over from one operation to the next. Reset per-operation state inNextOperation; - read numbers in programs with
CultureInfo.InvariantCulture, so that.is the decimal separator whatever the Windows language is.
Unit tests
Keep the decisions that need no kernel in classes that do not reference the SDK: reading numbers and expressions, decoding cycle parameters, the rules for axes, spindles and the plane, parsing the INI file. Cover them with a small test project that links those source files, and run it for every target framework. Test every spelling from the census, the limits and malformed input, and check that each test fails when the code it guards is broken — a test that still passes after you delete the condition it is about proves nothing. Unit tests complement the runs of the real programs ("Verify interpretation"); they never replace them, because the kernel's behavior is not in them.
Debug
You need Visual Studio Code with the C# extension of Microsoft (it contains the debugger the templates' launch
configurations use). The template's F5 configurations build the interpreter and start the CAM system or Tuner with it;
a breakpoint in a .cs file stops when a program is simulated. The template's launch.json has three configurations:
Launch CAM starts ${env:NCKernelDir}/<program file> with /Interpreter:<project folder>\Bin\Debug\<folder name>.snci, Launch Tuner starts ${env:TunerDir}/ltuner.exe with /i and the same .snci, and .NET Core
Attach attaches the debugger to a CAM system or Tuner that is already running. The program folder is taken from the
environment variables NCKernelDir (CAM system) and TunerDir (Tuner). The interpreter creator stores one of them —
the one of the product it was started from — when you press its button for the program path, and it writes the program
file name into launch.json in place of ${env:CAMName}; a template copied by hand, without the creator, keeps
${env:CAMName}, which nothing sets, so write the file name there yourself. The creator writes the variable into the
user's environment and asks you to sign out of Windows or restart the computer; at the least, close VS Code and start it
again from the Start menu or Explorer, not from a terminal that was open before. When a variable is missing, write the
program path into launch.json instead — the standard installation folder of each product, one configuration per
product version, never a folder that exists only on your computer.
Turn on Use logs and Use log interaction with the interpreter before debugging (System setup → Additional →
Logs, the same in the CAM system and in Tuner; the second can be switched on only when the first is on). Without them
the log gets none of the kernel's messages about the words it reads and none of the lines the interpreter writes with
Kernel.Log. A message the user must see even with the log off — a word that is not simulated, an interface that is
missing — goes through Kernel.Notify, which shows it in the notification panel.
To simulate a program: in the CAM system create an operation that works by an NC program, paste or load the program text, choose the interpreter in the operation's strategy, and calculate the operation.
If a breakpoint is never hit, check in this order: the build produced debug symbols (a .pdb next to the DLL; a project
or script that sets DebugType to none makes every breakpoint look dead); the operation really uses your interpreter
(not another one with the same LibraryID); the build before the start succeeded (the template's build task keeps its
terminal in the background — open the terminal panel and read the build result); the .snci passed to the program is
the one in the output folder of the product version you started.
When a colleague's or a customer's project fails on the first block, read first the interpreter file stored in the
strategy of the NC operation of that project and the project's log: an interpreter file that is not found, or a DLL that
cannot be loaded, means the interpreter never loaded, and every word then fails. The CAM system reports every failure to
load a .NET interpreter the same way — External library don't support NCT interface: and the path (in the English
interface; the text follows the interface language) — and the reason is lost: the DLL is missing beside the .snci, a
dependency or the .deps.json is missing, the DLL was built for another .NET version, or an exception was raised while
the interpreter object was being created (usually while creating the registers). When the CAM system reports this
message, check the files above, then create the interpreter class in a small console program with the same SDK package
to see the real error message.
What the interpreter did with each block is visible through a trace you add once (the [Trace] key above, in a file of
its own such as Interpreter.Trace.cs): with Enabled = yes the block text and the commands it produced appear in the
debug console and, with the interaction log on, in the CAM log (%AppData%\<vendor>\<product>\Version <n>\Log; each
process of the CAM system or Tuner writes a file of its own, named after the program and the process ID, such as
<program>-<process ID>.log). Write the trace from the same code that outputs the commands, not from a parallel copy
of its logic, or the trace and the simulation will disagree.
Rules
- Do not change the product's templates, kernel or DLLs to make an interpreter work; record what the product would have to change instead.
- Do not use SDK members that the released package does not contain: read them from the package's declarations and XML documentation.
- Keep the project building after every change; make one change at a time.
- Keep the template's build and
F5debugging: no build scripts, environment variables or extra project logic in their place. A colleague must be able to open the folder, pressF5and stop at a breakpoint. - Do not put build output, customer programs or logs into the source tree.
Done means
The project builds unchanged, then with the change, for every product version it targets, with a released SDK package;
folder, project, .snci and DLL names agree and both identifiers of the .snci are unique; the known defects of the
template are fixed or named; variations of the programs are settings with logged values; the code follows the template's
structure, style, line endings and encoding; the logic that needs no kernel has unit tests that pass for every target;
the change was debugged or traced on a real program; and "Verify interpretation" has been followed.