Skip to content

Commit 96078b2

Browse files
committed
nixos/linuxcnc: init module
Add a LinuxCNC module for real-time control of CNC machines from NixOS. Signed-off-by: wucke13 <wucke13+github@gmail.com>
1 parent 19fa072 commit 96078b2

6 files changed

Lines changed: 207 additions & 1 deletion

File tree

doc/release-notes/rl-2611.section.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
2020

21-
- Create the first release note entry in this section!
21+
- A LinuxCNC module has been added. This provides a notable alternative to Debian based distros for CNC machine control.
2222

2323
## Nixpkgs Library {#sec-nixpkgs-release-26.11-lib}
2424

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@
259259
./programs/less.nix
260260
./programs/liboping.nix
261261
./programs/librepods.nix
262+
./programs/linuxcnc.nix
262263
./programs/lix.nix
263264
./programs/localsend.nix
264265
./programs/mdevctl.nix
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
8+
let
9+
cfg = config.programs.${moduleName};
10+
moduleName = "linuxcnc";
11+
in
12+
{
13+
meta.maintainers = with lib.maintainers; [ wucke13 ];
14+
15+
options.programs.${moduleName} = {
16+
17+
enable = lib.options.mkEnableOption moduleName;
18+
19+
package =
20+
(lib.options.mkPackageOption pkgs "linuxcnc" {
21+
default = [ "linuxcnc" ];
22+
})
23+
// {
24+
apply = p: p.override { enableSetuidWrapperRedirection = true; };
25+
};
26+
27+
};
28+
29+
config = lib.mkIf cfg.enable {
30+
# create wrapper with setuid bit set
31+
security.wrappers = lib.mkIf cfg.enable (
32+
lib.attrsets.genAttrs' cfg.package.setuidApps (program: {
33+
name = "linuxcnc-${program}";
34+
value = {
35+
owner = "root";
36+
group = "root";
37+
inherit program;
38+
setuid = true;
39+
source = lib.meta.getExe' cfg.package "${program}-nosetuid";
40+
};
41+
})
42+
);
43+
44+
# Make all LinuxCNC programs and required tools available
45+
environment.systemPackages = [
46+
cfg.package
47+
pkgs.iptables
48+
];
49+
};
50+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,7 @@ in
904904
linkding = runTest ./web-apps/linkding.nix;
905905
linkding-postgres = runTest ./web-apps/linkding-postgres.nix;
906906
linkwarden = runTest ./web-apps/linkwarden.nix;
907+
linuxcnc = runTest ./linuxcnc.nix;
907908
listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix { };
908909
litellm = runTest ./litellm.nix;
909910
litestream = runTest ./litestream.nix;

nixos/tests/linuxcnc.nix

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
{ hostPkgs, ... }:
2+
{
3+
name = "linuxcnc";
4+
5+
nodes.machine =
6+
{ nodes, pkgs, ... }:
7+
let
8+
user = nodes.machine.users.users.alice;
9+
in
10+
{
11+
imports = [
12+
./common/user-account.nix
13+
./common/x11.nix
14+
];
15+
test-support.displayManager.auto.user = user.name;
16+
virtualisation.memorySize = 4096;
17+
18+
programs.linuxcnc.enable = true;
19+
environment = {
20+
variables.LINUXCNC_FORCE_REALTIME = "1";
21+
systemPackages = [ pkgs.xdotool ];
22+
};
23+
};
24+
25+
enableOCR = true;
26+
27+
testScript =
28+
{ nodes, ... }:
29+
let
30+
user = nodes.machine.users.users.alice;
31+
in
32+
''
33+
import shlex
34+
35+
machine.wait_for_x()
36+
37+
# required so that wait_for_window can access alice's X session
38+
machine.succeed("xauth merge ~${user.name}/.Xauthority")
39+
40+
def spawn_as_user(command: str):
41+
command += " >&2 &"
42+
escaped_command = shlex.quote(command)
43+
machine.succeed(f"su - ${user.name} -c -- {escaped_command}")
44+
45+
def window_close():
46+
machine.succeed("xdotool getactivewindow windowquit")
47+
48+
def window_maximize():
49+
machine.succeed("xdotool getactivewindow windowsize --sync 100% 100%")
50+
51+
screenshot_counter: int = 0
52+
def screenshot(name: str):
53+
global screenshot_counter
54+
machine.screenshot(f"{screenshot_counter:02d}-{name}")
55+
screenshot_counter += 1
56+
57+
58+
with subtest("Test latency-test"):
59+
spawn_as_user("latency-test")
60+
machine.wait_for_window("LinuxCNC / HAL Latency Test")
61+
machine.wait_for_text("Let this test run for a few minutes")
62+
machine.wait_for_text("Reset Statistics")
63+
screenshot("latency-test")
64+
window_close()
65+
machine.wait_until_fails("pgrep rtapi_app")
66+
67+
with subtest("Test latency-histogram"):
68+
spawn_as_user("latency-histogram")
69+
machine.wait_for_window("latency-histogram")
70+
machine.wait_for_text("ylogscale")
71+
machine.wait_for_text("binsize")
72+
screenshot("latency-histogram")
73+
window_close()
74+
machine.wait_until_fails("pgrep rtapi_app")
75+
76+
with subtest("Test latency-plot"):
77+
spawn_as_user("latency-plot")
78+
machine.wait_for_window("latency-plot")
79+
window_maximize()
80+
machine.wait_for_text("Latency \\S+ vs Time \\S+")
81+
machine.wait_for_text("Pts")
82+
screenshot("latency-plot")
83+
window_close()
84+
machine.wait_until_fails("pgrep rtapi_app")
85+
86+
87+
with subtest("Test LinuxCNC configuration selector"):
88+
spawn_as_user("linuxcnc")
89+
machine.wait_for_window("LinuxCNC Configuration Selector")
90+
window_maximize()
91+
machine.wait_for_text("Welcome to LinuxCNC.")
92+
screenshot("linuxcnc-configuration-selector")
93+
94+
with subtest("Test copy to new AXIS config"):
95+
machine.send_key("ret")
96+
machine.wait_for_text("Would you like to copy the")
97+
machine.send_key("ret")
98+
machine.wait_for_text("The configuration file has been copied to")
99+
machine.send_key("ret")
100+
101+
with subtest("Test AXIS GUI"):
102+
# The "AXIS Manual Toolchanger" window will appear briefly
103+
machine.wait_for_window("^axis.ngc")
104+
window_maximize()
105+
machine.wait_for_text("ESTOP")
106+
screenshot("linuxcnc-axis")
107+
108+
# Basic test of AXIS
109+
machine.send_key("f1") # toggle e-stop
110+
machine.wait_for_text("OFF")
111+
machine.send_key("f2") # toggle machine power to on
112+
machine.wait_for_text("ON")
113+
machine.send_key("f4") # switch from Preview to DRO
114+
machine.send_key("ctrl-home") # home all axes
115+
machine.sleep(5) # wait until homing is at least started
116+
machine.wait_for_text("X:\\s+0.0000") # wait until homing is done
117+
machine.wait_for_text("Y:\\s+0.0000")
118+
machine.wait_for_text("Z:\\s+0.0000")
119+
machine.send_key("f5") # switch from Manual Control to MDI
120+
machine.send_chars("G0 X.1337 Y.42 Z-.13\n") # go to specific coordinates
121+
machine.wait_for_text("0.1337") # wait until machine reached desired X coordinate
122+
screenshot("linuxcnc-axis-moved-mdi-dro")
123+
machine.send_key("f4") # switch from DRO to Preview
124+
machine.sleep(5) # avoid the keys beeing eaten
125+
machine.send_key("f3") # switch from MDI to Manual Control
126+
machine.wait_for_text("Touch off") # wait for Manual Control to be rendered
127+
screenshot("linuxcnc-axis-moved-manual-control-preview")
128+
129+
with subtest("Test image-to-gcode"):
130+
machine.copy_from_host("${hostPkgs.nixos-icons}/share/icons/hicolor/128x128/apps/nix-snowflake.png",
131+
"/home/${user.name}/linuxcnc/nc_files/test-image.png")
132+
machine.send_key("o")
133+
machine.wait_for_window("Open")
134+
machine.send_chars("test-image.png\n")
135+
machine.wait_for_window("Image to G-code")
136+
screenshot("linuxcnc-axis-image-to-gcode")
137+
machine.send_key("ret")
138+
machine.wait_for_text("M3")
139+
machine.wait_for_text("P3")
140+
screenshot("linuxcnc-axis-image-to-gcode-preview")
141+
142+
with subtest("Test shutdown AXIS"):
143+
window_close()
144+
machine.wait_for_text("Do you really want to close LinuxCNC?")
145+
machine.send_key("tab")
146+
machine.send_key("ret")
147+
machine.wait_until_fails("pgrep rtapi_app")
148+
149+
150+
machine.shutdown()
151+
'';
152+
}

pkgs/by-name/li/linuxcnc/package.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
lib,
33
stdenv,
4+
nixosTests,
45

56
fetchurl,
67
fetchFromGitHub,
@@ -382,6 +383,7 @@ stdenv.mkDerivation (finalAttrs: {
382383
passthru = {
383384
inherit pythonEnv;
384385
inherit (finalAttrs) setuidApps;
386+
tests.nixos-module = nixosTests.linuxcnc;
385387
};
386388

387389
meta = {

0 commit comments

Comments
 (0)