Modern cross-platform (Windows + Linux + Linux ARM64 + macOS) grblHAL gcode sender based on ioSender by Io Engineering (Terje Io).
- About
- Features
- App settings
- Supported controllers and limitations
- Plugins
- System requirements
- Origin and License
- Development
- Documentation
- Running from release assets
- Linux release build (WSL2)
- Linux CI test pass (WSL2)
- Linux arm64 (Raspberry Pi)
- Contributing
- Acknowledgements
SenderNG is a focused fork of ioSender. The main differences are cross-platform support, 2-plane machine support (hotwire CNC foam cutters) and a Demo mode that lets users test G-code without hardware. The sender supports as many axes as the controller reports — the axis count comes from the firmware, not the app; producing multi-axis G-code is CAM's scope. The base app contains only core functionality and can be extended with plugins (see Plugins).
- Cross-platform: Windows + Linux x64 + Linux ARM64 + macOS (Apple Silicon and Intel). ARM64 is tested on a Raspberry Pi 4B; x64 is tested on Ubuntu 22.04 and Mint 21.3; macOS builds are produced and smoke-tested on GitHub's macOS runners.
- 2-plane machine support (hotwire CNC foam cutters)
- Connect: three-tab connect dialog — Serial (port list, refresh, baud), Network (TCP, for grblHAL Ethernet boards) and Demo (always available, with a disclaimer). The app starts disconnected and shows an obvious "NOT CONNECTED" state until you pick one.
- Demo mode (test G-code without hardware)
- Localization (en/ru/fr/es/de). English and Russian are proofread; the other languages are automatic translations.
- Light and dark themes — follows the system, or pick System/Light/Dark in App Settings (applies live, no restart).
- Jogging with a hold-to-jog continuous step (hold to move, release to stop)
- Probing (tool-length, edge, center, rotation and height-map cycles)
- Plotter (2D/Isometric). No orbiting, but works everywhere.
- Plotter 3D. Supports orbiting and snap views, but requires OpenGL 3.3 core (GLES 3.0 on ARM64) and may be resource-hungry (tested on an Intel Compute Stick — may be slow, RPi 4B works just fine)
- Height map 3D. An interactive surface of the probed height map — orbit/pan/zoom, control-point markers, X/Y/Z axis arrows, and a live "points / max ΔZ" readout — opened from the Probing widget's 3D View action.
- Macros
- Excellon & HPGL converters
- Outline
- Coolant/mist control
- MDI
- Offsets and tool table support (grblHAL)
- G-code transforms: Rotate, ArcsToLines, Compress (File → Transform)
- Keyboard shortcuts with a fully remappable keymap (remap screen in App Settings)
- Cycle bar: the single run-control surface (start/hold/resume/stop/rewind)
- Cutting widget: live feed and speed readout, feed/rapids/speed override controls that mirror the controller, and spindle control
- Stepper calibration wizard
- Automatic controller type and features detection ($I handshake at connect)
- Instant settings update
- Machine profiles and settings presets. Settings are saved per machine profile, so switching between different machines is as easy as selecting a profile.
Note: Not affiliated with or endorsed by Io Engineering / upstream ioSender.
Status: Early stages. Cross-platform Avalonia UI + .NET 9+; demo/offline mode, serial + TCP connect, DRO/Jog/Probing, Plotter widgets, localization (en/ru/fr/es/de).
Everything is configured from App Settings, scoped to the active machine profile (one file per machine plus a global.json, with a profile manager inside the settings). Most widgets can be hidden from there — the hideable modules (Plotter, Plot 3D, Cutting, Coolant, Macro, mPos, Outline, Goto, Stepper calibration and the installed plugins) have a visibility toggle, while fixed modules (DRO, Program, Console, Grbl settings, App settings, Offsets, Work parameters) and Jog/converters always stay, so the UI shows only what your machine needs. Hotkey remapping, converter parameters and per-module options live there too.
Global → Backup and restore writes one *.ngsettings.zip archive holding the whole settings
directory (global.json, every profiles/<id>.json and each module companion) in the same internal
layout as on disk — so it can also be restored by hand: unzip it over the settings directory with
SenderNG closed. Restoring replaces those files in one step and then asks for a restart, because the
running session still holds the pre-restore values; until you restart, SenderNG refuses to save settings
so it cannot overwrite what you just restored (a backup is still allowed while a restart is pending).
The Back up / Restore buttons in the Grbl settings dialog do the same for the controller's own
$$ settings, as a JSON file carrying the controller's identity and a fingerprint of its settings.
Restoring shows a confirm strip first and writes only the settings that differ; a value the controller's
own metadata rejects is listed with its reason instead of being sent.
SenderNG speaks the grbl / grblHAL protocol:
- grblHAL — the primary target. Full capability detection via the connect-time
$Ihandshake: axis count and letters ([AXS:]), tool count, options, signals, settings catalog ($ES/$EG), the G10 L1 tool table, and extended coordinate systems (G59.2/G59.3). - Classic grbl 1.1 and forks (e.g. the mega5x 1.2i build) — verified against real boards. Detection degrades gracefully: no
[AXS:]→ 3-axis default, settings fall back to the embedded English catalog, no G10 L1 tool table, coordinate systems limited to G54..G59, and mist (M7) follows the[OPT:]Moption letter (grblHAL always supports it). - Transports — serial (COM ports,
/dev/ttyUSB*,/dev/ttyACM*), plain TCP (grblHAL Ethernet boards), or Demo mode.
Limitations:
- TCP only — no telnet option negotiation and no WebSocket transport yet.
- The app shows as many axes as the firmware reports; producing multi-axis G-code is CAM's scope.
- Machine kinematics are profile-declared: each profile declares its planes and axis roles (linear/rotary, direction, sign, optional CAM alias) and the app resolves them against the controller's reported
[AXS:]letters for the jog layout, plotter, outline and DRO. Limits: the 3D world mapping assumes Width → X, Length → Y, Height → Z, so a sideways or gantry-rotated frame cannot be expressed yet; a controller with remapped letters is unit-tested only, not bench-verified; a rotary axis's pivot is stored but not yet used by a view. - Camera support is deferred indefinitely (if ever, as a 3rd-party plugin).
- The Plot 3D tab ships on every platform but is hidden by default; on ARM64 it renders via Mesa.
Some modules ship inside the app (Jog, Cutting, Coolant, Macro, mPos, Outline, Goto, Stepper calibration); the heavier ones are plugins — a separate assembly discovered by scanning Modules/*.dll at startup. No DLL, no feature: the core bundle runs fine without them, and installing one is a straight copy of the plugin zip's contents into the bundle's Modules/ folder (see Running from release assets). This repository includes:
- Probing — tool-length, edge, center, rotation and height-map probing cycles
- Height map 3D viewer — interactive 3D surface of the probed height map (opened from the Probing widget)
- Plotter (2D/Isometric)
- Plotter 3D (OpenGL orbit view)
- Excellon converter
- HPGL converter
Lathe, DragKnife and other heavy modules are planned. To write your own plugin, see docs/plugin-development.md.
| Minimum | |
|---|---|
| Windows | Windows 10/11 x64 — the bundle is self-contained, no .NET install needed |
| Linux x64 | Distro with X11; libx11-6 libice6 libsm6 libfontconfig1 (Debian/Ubuntu package names) |
| Linux ARM64 | 64-bit Raspberry Pi OS on a Pi 4B — a 32-bit userland cannot run a linux-arm64 bundle |
| macOS | Apple Silicon (osx-arm64) or Intel (osx-x64) on macOS versions per the .NET 9 support matrix — self-contained, no .NET install; unsigned bundle, first launch needs right-click → Open |
| Serial | membership in the dialout group on Linux (see Linux) |
| Controller | grblHAL or grbl 1.1 over serial or plain TCP — see Supported controllers and limitations |
- Based on
ioSender2.0.48(terjeio/ioSender) — see original BSD 3-Clause. - License: BSD 3-Clause — LICENSE retains
Copyright (c) 2019-2023, Io Engineering (Terje Io)and addsCopyright (c) 2024-2026, Andrew Shkolik. SeeLICENSEfor full text. - Avalonia UI (
AvaloniaUI/Avalonia) is MIT — compatible with BSD.
No warranties: THE SOFTWARE IS PROVIDED "AS IS", without warranty of any kind, express or implied. See LICENSE for the full BSD 3-Clause disclaimer.
# .NET 9 SDK + Avalonia 12 required (global.json pins 9.0.100)
dotnet build
dotnet test SenderNG.sln
dotnet run --project src/SenderNG- Architecture — solution layout, engine and shell
- Localization — adding and editing locale catalogs
- arm64 cookbook — build, install and verify on a Raspberry Pi
Automated weekly builds: every Monday 06:00 UTC the Release workflow
(.github/workflows/release.yml) builds win64, linux-x64, linux-arm64,
osx-arm64 and osx-x64 with all plugins and publishes a prerelease to the
Releases page — unless nothing
changed since the previous release, in which case it skips. Formal releases use
the same workflow: bump <Version> in src/SenderNG/SenderNG.csproj, then run
the workflow manually from the Actions tab with stable checked.
Release zips are produced per platform by tools/ReleaseManager. On Windows,
build one with:
dotnet run --project tools/ReleaseManager --include-pluginstools/ReleaseManager/README.md has the per-platform runbooks and the manual
fallback. (To build the Linux zip from a Windows host, use
tools/build-linux-release.ps1 — see "Linux release build (WSL2)" below; it
writes output\linux and never reads, writes or deletes output\win64.)
A release folder (output/win64/ on Windows, output/linux/ or
output/linux-arm64/ on Linux, output/osx-arm64/ or output/osx-x64/ on
macOS) looks like this. The example is an x64 release (arm64 ships the same
plugin set — the Plot 3D tab runs on Mesa and is hidden by default):
SenderNG-<ver>-<suffix>.zip ← core bundle: the whole app
SenderNG-plugin-excellon-<ver>-<suffix>.zip
SenderNG-plugin-hpgl-<ver>-<suffix>.zip
SenderNG-plugin-plotter-<ver>-<suffix>.zip
SenderNG-plugin-plotter-gl-<ver>-<suffix>.zip
SenderNG-plugin-probing-<ver>-<suffix>.zip
SenderNG-plugin-heightmap-viewer-<ver>-<suffix>.zip
SHA256SUMS.txt ← <sha256> <relative path> per file
<suffix> is win64 on Windows, linux-x64 or linux-arm64 on Linux,
osx-arm64 or osx-x64 on macOS.
Core bundle zip — the complete self-contained app: SenderNG.exe /
SenderNG runner, all .NET + platform natives, locale catalogs, and a
Modules/ folder holding the SDK assemblies — SenderNG.Modules.Common.dll,
SenderNG.Modules.Common.Render.dll and the Silk.NET Core/Maths/OpenGL
DLLs. Extract it anywhere and run; no .NET install needed.
Plugin zips — one per optional module (excellon, hpgl,
plotter, plotter-gl, probing, heightmap-viewer). Each contains the
module's own assembly (SenderNG.Modules.<X>.dll) plus an <id>/ support
folder (its locale catalog). The render SDK and Silk.NET DLLs ship flat in the
core bundle's Modules/ (Model A), so plotter-gl no longer bundles its own
private Silk.NET copy. The app discovers modules by scanning Modules/*.dll at
startup, so installing a module is a straight copy of the zip's contents into
the extracted bundle's Modules/ folder: the DLL lands flat next to the SDK,
<id>/ becomes Modules/<id>/. Skip the ones you don't want — the core bundle
runs fine without them. probing is the in-tree first-party module
(src/SenderNG.Modules.Probing) holding the Probing flyout — tool-length,
edge, center, rotation and height-map probing cycles; heightmap-viewer
(src/SenderNG.Modules.HeightMapViewer) is the in-tree 3D height-map dialog.
Verify the downloads before installing: on Windows compare Get-FileHash
against SHA256SUMS.txt; on Linux run cd <platform dir> && sha256sum -c SHA256SUMS.txt.
- Requires Windows 10/11 x64. Nothing else — the bundle is self-contained.
- Unzip
SenderNG-<ver>-win64.zip(Explorer: right-click → Extract All). You get aSenderNGfolder containingSenderNG.exe. - Optional modules: unzip each plugin zip and copy its contents — the
SenderNG.Modules.<X>.dllplus the<id>/folder (skipREADME.txt) — intoSenderNG\Modules\. - Run
SenderNG\SenderNG.exe→ Connect → pick the machine's serial/TCP port.
-
Grant yourself access to the serial port. Linux gives
/dev/ttyUSB*and/dev/ttyACM*to thedialoutgroup, mode0660— so your account must be in that group or the kernel refuses the port (see Serial port permission errors below):sudo usermod -aG dialout $USERLog out and back in afterwards; an already-running desktop session keeps the old group set.
-
Requires Avalonia's X11 system libraries. On Debian/Ubuntu:
sudo apt install -y libx11-6 libice6 libsm6 libfontconfig1 -
Unzip
SenderNG-<ver>-linux-x64.zipand restore the runner's execute bit (zips don't always carry it):unzip SenderNG-<ver>-linux-x64.zip chmod +x SenderNG/SenderNG SenderNG/install-desktop.sh -
Optional modules: same copy step as Windows — plugin contents into
SenderNG/Modules/. -
Run
./SenderNG/SenderNG→ Connect → open the machine's port (/dev/ttyUSB*//dev/ttyACM*for serial, or TCP). -
Optional: add a desktop-menu entry. This installs
SenderNG.desktopinto~/.local/share/applications/and the icon into~/.local/share/icons/hicolor/512x512/apps/, pointing at the absolute path of this bundle. Move the bundle somewhere permanent first — moving it afterwards orphans the entry.cd SenderNG ./install-desktop.shUndo it with
./install-desktop.sh --uninstall.
A port that shows up in the dropdown but won't connect usually means the
dialout membership from step 1 is missing or was added without a fresh login.
The app reports it as:
Access to the port '/dev/ttyACM0' is denied.
That message is misleading — .NET's System.IO.Ports collapses every failed
open() into that one string, so it also appears for a wrong device path or a
port another process is holding. It is not necessarily a permissions problem.
Get the real reason without the app in the way:
ls -l /dev/ttyACM0
id -nG
python3 -c "import os
try:
fd=os.open('/dev/ttyACM0', os.O_RDWR|os.O_NOCTTY); print('OPEN OK'); os.close(fd)
except OSError as e: print('errno', e.errno, '=', os.strerror(e.errno))"
sudo fuser -v /dev/ttyACM0
- errno 13, permission denied — account not in
dialout. Fix withsudo usermod -aG dialout $USER, then log out and back in. The node itself needs nochmod; it picks up the new membership on the next open. - errno 16, device or resource busy — something else holds the port.
fusernames it; common culprits are ModemManager, brltty, or a second instance of the app. - errno 19, no such device — the device dropped off the bus. Replug it.
To try a group change without logging out, newgrp dialout opens a subshell
with it already active; launch the app from there.
- Download
SenderNG-<ver>-osx-arm64.zip(Apple Silicon) orSenderNG-<ver>-osx-x64.zip(Intel). - Unzip and run
./SenderNG/SenderNG. If "Permission denied" (the zip passed through a filesystem that drops Unix permissions, e.g. exFAT):chmod +x SenderNG/SenderNG— only the runner needs+x. - The build is unsigned, so Gatekeeper blocks the first launch ("cannot be
opened because the developer cannot be verified"). Right-click the binary
in Finder → Open → Open once, or clear the quarantine flag:
xattr -d com.apple.quarantine SenderNG/SenderNG - Optional modules: same copy step as Windows — plugin contents into
SenderNG/Modules/. - Run → Connect: serial ports appear as
/dev/cu.*(thecu.Bluetooth-*andcu.debug-consoleentries always exist but are never preselected). - The 3D PLOT view (
plotter-gl) needs OpenGL 3.3+; macOS provides 4.1 core through the deprecated OpenGL framework, which this build uses.
tools/build-linux-release.ps1 runs ReleaseManager under WSL2, mirrors the
source tree into WSL-native storage for a fast restore and publish, and drops the
result in output\linux next to output\win64.
A Linux host is still required, though not because ReleaseManager reads the
host OS — --platform overrides that. It is required because SenderNG.csproj
selects the Plot 3D plugin's native binaries by build OS, and because keeping
both Linux targets on one host means one toolchain, one mirror, and one runbook.
# Linux bundle in output\linux, tests run by default (pass -SkipTests to skip)
.\tools\build-linux-release.ps1
# include plugins
.\tools\build-linux-release.ps1 -Plugins
# exercise everything except the ReleaseManager invocation
.\tools\build-linux-release.ps1 -DryRun| Parameter | Default | Effect |
|---|---|---|
-Distro |
Ubuntu |
WSL distribution to build in |
-OutRoot |
<repo>\output |
Output lands in <OutRoot>\<Platform> |
-Platform |
linux |
linux or linux-arm64; passes --platform and selects the output folder |
-MirrorDir |
~/.cache/sendeng-linux-build in WSL |
where the source is mirrored; wiped each run |
-Plugins |
off | add --include-plugins |
-SkipTests |
off | pass --skip-tests |
-NoZip |
off | add --no-zip |
-DryRun |
off | resolve the SDK, mirror the tree, and report what would happen |
A real run replaces its own output\<Platform> folder wholesale. The other
output folders — output\win64, and whichever of output\linux /
output\linux-arm64 this run is not building — are never read, written, or
deleted.
-DryRun writes nothing to <OutRoot> at all. It stages a stub bundle inside
the script-owned mirror to prove the mirroring step worked, and leaves any
existing release in place untouched. It does still wipe and rebuild the
mirror, exactly as a real run does — so do not start a -DryRun while a real
build is using that mirror.
Tests run by default: pass -SkipTests and the script passes
--skip-tests instead.
The script does not install an SDK or touch NuGet credentials. global.json
pins 9.0.100 with rollForward: latestFeature; the preflight asks the
resolver (cd "$SRC" && dotnet --version) for what satisfies that pin rather
than looking for a literal directory.
A path containing a space is rejected, because wsl.exe cannot pass one
through as a single argument.
Nothing in the test suite performs an actual publish, so the first real run is the end-to-end check. Two things worth confirming by eye:
- The bundle is executable:
ls -l output\linux\SenderNG\SenderNGshould start with-rwx. If the exec bit was lost in the copy back, the bundle will not launch. - On a real build the WSL NuGet cache starts cold, so the first run is noticeably slower than later ones.
If you interrupt a run with Ctrl+C, check for a leftover wsl.exe before
re-running: PowerShell reports the cancellation but does not necessarily kill
the child process, which may still be writing to output\linux.
The target is 64-bit arm64 Linux (Raspberry Pi 4B); the bundle has been
built, installed, and run on a Pi 4B — see the note below.
The Plot 3D tab runs on arm64 (OpenGL via Mesa) — every plugin ships on
every platform; the tab itself is hidden by default and can be enabled in App
Settings after installing the plotter-gl plugin.
Build it from WSL2 on your Windows machine; no .NET SDK on the Pi is needed:
# arm64 bundle in output\linux-arm64
.\tools\build-linux-release.ps1 -Platform linux-arm64Output lands in output\linux-arm64\ as SenderNG-<version>-linux-arm64.zip
plus SHA256SUMS.txt. The win64 and linux folders are never touched.
For the full recipe — build, copy to the Pi (thumb drive or SSH), install, verify — see the arm64 cookbook.
The bundle is cross-published (dotnet publish -r linux-arm64) from an x64
host, so it is produced on a different machine than it runs on. It has since
been installed and launched on a Pi 4B (window renders, 2D tabs present, no
Plot 3D tab — hidden by default); nothing in either test suite performs a
publish, so every rebuild's final publish is exercised only by a real run.
On the Pi:
- Serial access needs the
dialoutgroup. See Linux above forusermod -aG dialout $USERand thenewgrpalternative. Without it the Connect tab lists no ports. - The app is a GUI and needs a desktop session. It will not present anything useful over a bare SSH shell. Use a local desktop, or forward X11.
- Use 64-bit Raspberry Pi OS. A 32-bit userland cannot run a
linux-arm64bundle.
tools/test-linux-ci.ps1 runs CI's own build and test commands inside WSL2 — the same
dotnet build … -t:Rebuild -warnaserror -m:1 and
dotnet test … --filter "Category!=Hardware&Category!=GPU&Category!=Timing" that ci.yml runs — so a
Linux-only failure can be root-caused on the development machine instead of in a red run.
# mirror, build and test: the CI pass
.\tools\test-linux-ci.ps1
# resolve the SDK and mirror the tree, then report what it would run
.\tools\test-linux-ci.ps1 -DryRun
# a targeted run
.\tools\test-linux-ci.ps1 -Filter "FullyQualifiedName~AppSettingsRestoreTests"| Parameter | Default | Effect |
|---|---|---|
-Distro |
Ubuntu |
WSL distribution to run in |
-MirrorDir |
~/.cache/senderng-linux-ci in WSL |
where the source is mirrored; wiped each run |
-Filter |
Category!=Hardware&Category!=GPU&Category!=Timing |
passes --filter to dotnet test |
-Configuration |
Release |
the configuration CI builds |
-DryRun |
off | resolve the SDK and mirror the tree, then report the commands without running them |
It keeps its own mirror (~/.cache/senderng-linux-ci, with its own ownership marker) so it can never
wipe the release build's mirror or its output\<platform> folders. The mirror is the working tree, so
uncommitted changes are what gets tested; it drops the real .git object store (large, and nothing here
needs history) but plants an empty .git marker, because the localization suites locate the checkout by
.git plus src. Exit codes: 1 precondition
(missing SDK/rsync/WSL, or a mirror this script did not create), 2 build failed, 3 tests failed,
130 cancelled. Logs stay in the mirror as ci-build.log and ci-test.log, and the failing tests are
printed at the end.
Why it exists: Windows passes tests that fail on Linux. FileShare.None, for instance, blocks a
replace on Windows but maps to an advisory lock on Unix, where rename(2) ignores it — so a test that
"locks" a file to make a write fail proves nothing there, while a directory occupying the target path
does.
Contributions are welcome. Found a bug or have an idea? Open an issue — bug reports and feature requests are just as welcome as code. Improvements are welcome as pull requests: keep changes focused, follow the existing conventions, and make sure dotnet build SenderNG.sln and dotnet test SenderNG.sln pass. See AGENT.md for the full contributor workflow.
- Terje Io / Io Engineering for ioSender and grblHAL
- AvaloniaUI community
- Silk.NET contributors for the 3D plotter's OpenGL bindings



















