nx_moonraker_io.nx source
↩ module page · 211 lines · 8834 B
1// nx_moonraker_io.nx -- network roundtrip layer for nx_moonraker_client.
2//
3// v2 of the Moonraker client arc: composes the v1 request builders
4// (URL / JSON / POST/GET byte assembly) with real socket I/O so we
5// can actually talk to a Klipper printer over the LAN.
6//
7// Loopback-only for this primitive (composes tcp_connect_loopback
8// from nx_tls13_io for testability via fork-based smoke). An
9// operator-facing variant accepting arbitrary sockaddr_in is queued
10// as v3 -- trivial extension once loopback is proven.
11//
12// Composition map:
13// nx_moonraker_client -- request builders (v1)
14// nx_tls13_io -- tcp_connect_loopback (loopback helper)
15// nx_syscalls -- sys_write / sys_read / sys_close
16//
17// Per cardinal feedback-engineering-sciences-bits-up-3d-print-first:
18// this primitive demonstrates that the substrate already has every
19// piece needed to talk to networked engineering tools (printers,
20// CNCs, robotics controllers) -- the operator-side workflow is just
21// composition of shipped primitives.
22//
23// Per cardinal feedback-real-playtest-loop-not-just-logs: the smoke
24// uses fork() with a real fake Moonraker server in the parent and
25// our real client in the child. Bytes flow over a real loopback
26// socket through the real Linux kernel TCP stack in qemu.
27//
28// license_tier: ORIGINAL
29
30import "nx_syscalls.nx"
31import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
32import "nx_tls13_io.nx"
33import "nx_moonraker_client.nx"
34
35// ===== verdicts ===================================================
36
37const NX_MR_IO_OK: i64 = 0
38const NX_MR_IO_ERR_BUILD_FAILED: i64 = 1
39const NX_MR_IO_ERR_CONNECT_FAILED: i64 = 2
40const NX_MR_IO_ERR_WRITE_FAILED: i64 = 3
41const NX_MR_IO_ERR_READ_FAILED: i64 = 4
42
43func nx_mr_io_verdict_name(v: i64) -> *u8 {
44 if v == NX_MR_IO_OK { return "OK" }
45 if v == NX_MR_IO_ERR_BUILD_FAILED { return "BUILD_FAILED" }
46 if v == NX_MR_IO_ERR_CONNECT_FAILED { return "CONNECT_FAILED" }
47 if v == NX_MR_IO_ERR_WRITE_FAILED { return "WRITE_FAILED" }
48 if v == NX_MR_IO_ERR_READ_FAILED { return "READ_FAILED" }
49 return "UNKNOWN"
50}
51
52// ===== generic IPv4 TCP connect ====================================
53//
54// Builds sockaddr_in inline for an arbitrary IPv4 destination.
55// Caller passes 4 octet bytes (e.g. 192, 168, 1, 42) + port.
56// Returns connected socket fd on success, negative on failure.
57//
58// AF_INET sockaddr_in layout (16 bytes):
59// [0..1] sa_family = AF_INET (2)
60// [2..3] sin_port (network byte order, big-endian)
61// [4..7] sin_addr (IPv4 octets in network byte order)
62// [8..15] zero pad
63
64// IP packed: low 32 bits hold (a << 24) | (b << 16) | (c << 8) | d.
65// Caller uses nx_mr_pack_ipv4(a, b, c, d) to construct.
66func nx_mr_pack_ipv4(a: i64, b: i64, c: i64, d: i64) -> i64 {
67 return ((a & 0xff) << 24) | ((b & 0xff) << 16) | ((c & 0xff) << 8) | (d & 0xff)
68}
69
70func nx_mr_tcp_connect_ipv4(ipv4: i64, port: i64) -> i64 {
71 let fd: i64 = sys_socket(AF_INET, 1, 0)
72 if fd < 0 { return fd }
73 let addr: *u8 = sys_mmap(16)
74 addr[0] = 2; addr[1] = 0
75 addr[2] = (port >> 8) & 0xff
76 addr[3] = port & 0xff
77 addr[4] = (ipv4 >> 24) & 0xff
78 addr[5] = (ipv4 >> 16) & 0xff
79 addr[6] = (ipv4 >> 8) & 0xff
80 addr[7] = ipv4 & 0xff
81 addr[8] = 0; addr[9] = 0; addr[10] = 0; addr[11] = 0
82 addr[12] = 0; addr[13] = 0; addr[14] = 0; addr[15] = 0
83 let rc: i64 = nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS)
84 if rc < 0 { return rc }
85 return fd
86}
87
88// ===== POST roundtrip over loopback ================================
89//
90// Builds the POST request, connects to 127.0.0.1:port, sends the
91// request bytes, reads response into out_buf (up to out_cap), and
92// returns the number of response bytes read (or negative verdict).
93//
94// scratch_buf is caller-provided storage for the constructed request.
95// Size it generously (4KB+ should cover any reasonable Moonraker POST).
96
97func nx_mr_post_loopback(port: i64,
98 host: *u8, host_len: i64,
99 path: *u8, path_len: i64,
100 body: *u8, body_len: i64,
101 api_key: *u8, api_key_len: i64,
102 scratch_buf: *u8, scratch_cap: i64,
103 out_buf: *u8, out_cap: i64) -> i64 {
104 let req_len: i64 = nx_mr_build_post_request(host, host_len,
105 path, path_len,
106 body, body_len,
107 api_key, api_key_len,
108 scratch_buf, scratch_cap)
109 if req_len <= 0 { return 0 - NX_MR_IO_ERR_BUILD_FAILED }
110
111 let fd: i64 = tcp_connect_loopback(port)
112 if fd < 0 { return 0 - NX_MR_IO_ERR_CONNECT_FAILED }
113
114 let n_w: i64 = sys_write(fd, scratch_buf, req_len)
115 if n_w != req_len {
116 sys_close(fd)
117 return 0 - NX_MR_IO_ERR_WRITE_FAILED
118 }
119
120 let n_r: i64 = sys_read(fd, out_buf, out_cap)
121 sys_close(fd)
122 if n_r < 0 { return 0 - NX_MR_IO_ERR_READ_FAILED }
123 return n_r
124}
125
126// ===== POST/GET to arbitrary IPv4 (operator-facing) ===============
127//
128// Same as the loopback variants but takes 4 IPv4 octets + port for
129// the actual destination. Operator calls these with their real
130// printer LAN IP (e.g., 192, 168, 1, 42, 7125) when running against
131// hardware. The loopback wrappers above are special cases of these
132// for testability.
133
134func nx_mr_post_ipv4(ipv4: i64, port: i64,
135 host: *u8, host_len: i64,
136 path: *u8, path_len: i64,
137 body: *u8, body_len: i64,
138 api_key: *u8, api_key_len: i64,
139 scratch_buf: *u8, scratch_cap: i64,
140 out_buf: *u8, out_cap: i64) -> i64 {
141 let req_len: i64 = nx_mr_build_post_request(host, host_len,
142 path, path_len,
143 body, body_len,
144 api_key, api_key_len,
145 scratch_buf, scratch_cap)
146 if req_len <= 0 { return 0 - NX_MR_IO_ERR_BUILD_FAILED }
147 let fd: i64 = nx_mr_tcp_connect_ipv4(ipv4, port)
148 if fd < 0 { return 0 - NX_MR_IO_ERR_CONNECT_FAILED }
149 let n_w: i64 = sys_write(fd, scratch_buf, req_len)
150 if n_w != req_len {
151 sys_close(fd)
152 return 0 - NX_MR_IO_ERR_WRITE_FAILED
153 }
154 let n_r: i64 = sys_read(fd, out_buf, out_cap)
155 sys_close(fd)
156 if n_r < 0 { return 0 - NX_MR_IO_ERR_READ_FAILED }
157 return n_r
158}
159
160func nx_mr_get_ipv4(ipv4: i64, port: i64,
161 host: *u8, host_len: i64,
162 path: *u8, path_len: i64,
163 api_key: *u8, api_key_len: i64,
164 scratch_buf: *u8, scratch_cap: i64,
165 out_buf: *u8, out_cap: i64) -> i64 {
166 let req_len: i64 = nx_mr_build_get_request(host, host_len,
167 path, path_len,
168 api_key, api_key_len,
169 scratch_buf, scratch_cap)
170 if req_len <= 0 { return 0 - NX_MR_IO_ERR_BUILD_FAILED }
171 let fd: i64 = nx_mr_tcp_connect_ipv4(ipv4, port)
172 if fd < 0 { return 0 - NX_MR_IO_ERR_CONNECT_FAILED }
173 let n_w: i64 = sys_write(fd, scratch_buf, req_len)
174 if n_w != req_len {
175 sys_close(fd)
176 return 0 - NX_MR_IO_ERR_WRITE_FAILED
177 }
178 let n_r: i64 = sys_read(fd, out_buf, out_cap)
179 sys_close(fd)
180 if n_r < 0 { return 0 - NX_MR_IO_ERR_READ_FAILED }
181 return n_r
182}
183
184// ===== GET roundtrip over loopback =================================
185
186func nx_mr_get_loopback(port: i64,
187 host: *u8, host_len: i64,
188 path: *u8, path_len: i64,
189 api_key: *u8, api_key_len: i64,
190 scratch_buf: *u8, scratch_cap: i64,
191 out_buf: *u8, out_cap: i64) -> i64 {
192 let req_len: i64 = nx_mr_build_get_request(host, host_len,
193 path, path_len,
194 api_key, api_key_len,
195 scratch_buf, scratch_cap)
196 if req_len <= 0 { return 0 - NX_MR_IO_ERR_BUILD_FAILED }
197
198 let fd: i64 = tcp_connect_loopback(port)
199 if fd < 0 { return 0 - NX_MR_IO_ERR_CONNECT_FAILED }
200
201 let n_w: i64 = sys_write(fd, scratch_buf, req_len)
202 if n_w != req_len {
203 sys_close(fd)
204 return 0 - NX_MR_IO_ERR_WRITE_FAILED
205 }
206
207 let n_r: i64 = sys_read(fd, out_buf, out_cap)
208 sys_close(fd)
209 if n_r < 0 { return 0 - NX_MR_IO_ERR_READ_FAILED }
210 return n_r
211}