aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--c/gbos.h1
-rw-r--r--c/libc.s7
-rw-r--r--c/netd.c80
-rw-r--r--c/ping.c85
-rw-r--r--c/sock.h52
-rw-r--r--include/gbos.inc31
-rw-r--r--src/boot.asm1
-rw-r--r--src/socket.asm754
-rw-r--r--src/syscall.asm1
9 files changed, 872 insertions, 140 deletions
diff --git a/c/gbos.h b/c/gbos.h
index df52fbe..6ecbd4a 100644
--- a/c/gbos.h
+++ b/c/gbos.h
@@ -17,6 +17,7 @@ void ssend(unsigned char b); /* link-port: transmit one byte */
unsigned char srecv(void); /* link-port: receive one byte (blocks) */
int srecv_nb(void); /* link-port: receive, or -1 if none */
unsigned char pollin(void); /* poll OSK for a typed char, or 0 */
+unsigned char sys_net(void *req); /* socket layer trap (see sock.h) */
/* helpers (c/libc.c) */
void putu(unsigned int n); /* print decimal */
diff --git a/c/libc.s b/c/libc.s
index 7897a79..724fc3b 100644
--- a/c/libc.s
+++ b/c/libc.s
@@ -1,6 +1,7 @@
.module libc
.globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs
.globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv, _srecv_nb, _pollin
+ .globl _sys_net
;; SDCC sm83 sdcccall(1): 1st arg -> A (byte) or DE (pointer); ret A / BC.
;; rst $30 (the trap) clobbers BC/DE/HL; SDCC treats them caller-saved, so
;; wrappers need not preserve them - but we compute cleanly regardless.
@@ -116,6 +117,12 @@ _srecv_nb:
1$: ld bc, #0xffff
ret
+ ;; unsigned char sys_net(void *req /*DE*/) -> A -- socket layer trap
+_sys_net:
+ ld c, #33 ; SYS_NET (DE already = &netreq)
+ rst #0x30
+ ret
+
;; unsigned char pollin(void) -> A (typed OSK char, or 0)
_pollin:
ld c, #32 ; SYS_POLLIN
diff --git a/c/netd.c b/c/netd.c
index bce6ea7..b07abd2 100644
--- a/c/netd.c
+++ b/c/netd.c
@@ -1,78 +1,12 @@
-#include "netlib.h"
-/* netd - a minimal IP stack over SLIP. Our address is 10.0.0.2, peer 10.0.0.1.
- Milestones: A) ICMP echo, B) UDP echo. Raw byte arrays, network byte order
- (big-endian) handled explicitly. */
-
-static unsigned char pkt[256];
-
-/* one's-complement sum (RFC 1071) with a running seed, carry-folded each add */
-static unsigned int sum16(unsigned char *d, unsigned int len, unsigned int sum) {
- unsigned int w, i = 0;
- while (i + 1 < len) {
- w = ((unsigned int)d[i] << 8) | d[i + 1];
- sum += w; if (sum < w) sum++;
- i += 2;
- }
- if (len & 1) { w = (unsigned int)d[len - 1] << 8; sum += w; if (sum < w) sum++; }
- return sum;
-}
-
-/* pseudo-header sum for UDP/TCP: src+dst IP, protocol, segment length */
-static unsigned int pseudo(unsigned char proto, unsigned int seglen) {
- unsigned int sum = sum16(pkt + 12, 8, 0), w;
- w = proto; sum += w; if (sum < w) sum++;
- w = seglen; sum += w; if (sum < w) sum++;
- return sum;
-}
-
-static void swap4(unsigned char a, unsigned char b) {
- unsigned char i, t;
- for (i = 0; i < 4; i++) { t = pkt[a + i]; pkt[a + i] = pkt[b + i]; pkt[b + i] = t; }
-}
-static void swap2(unsigned char a, unsigned char b) {
- unsigned char t;
- t = pkt[a]; pkt[a] = pkt[b]; pkt[b] = t;
- t = pkt[a + 1]; pkt[a + 1] = pkt[b + 1]; pkt[b + 1] = t;
-}
-
-/* recompute the IP header checksum over ihl bytes, then send 'len' total */
-static void ip_send(unsigned char ihl, unsigned int len) {
- unsigned int c;
- pkt[10] = 0; pkt[11] = 0;
- c = ~sum16(pkt, ihl, 0);
- pkt[10] = (unsigned char)(c >> 8); pkt[11] = (unsigned char)c;
- slip_send((char *)pkt, (unsigned char)len);
-}
+#include "sock.h"
+/* netd - the network daemon. It just pumps the kernel RX path; the kernel's
+ socket layer owns SLIP/IP and auto-answers inbound ICMP echo requests, so
+ running netd makes the Game Boy respond to pings. No SLIP/IP here anymore. */
void main(void) {
- unsigned char len, ihl;
- unsigned int c, seglen;
- puts("netd: IP up (10.0.0.2)"); nl();
+ puts("netd: up (10.0.0.2)"); nl();
for (;;) {
- len = slip_recv((char *)pkt, 255);
- if (len < 20 || (pkt[0] >> 4) != 4) continue;
- ihl = (pkt[0] & 0x0f) << 2;
- pkt[8] = 64; /* TTL for any reply */
-
- if (pkt[9] == 1) { /* ICMP */
- if (len < (unsigned char)(ihl + 8) || pkt[ihl] != 8) continue; /* echo request */
- swap4(12, 16);
- pkt[ihl] = 0; /* -> echo reply */
- pkt[ihl + 2] = 0; pkt[ihl + 3] = 0;
- c = ~sum16(pkt + ihl, (unsigned int)(len - ihl), 0);
- pkt[ihl + 2] = (unsigned char)(c >> 8); pkt[ihl + 3] = (unsigned char)c;
- ip_send(ihl, len);
-
- } else if (pkt[9] == 17) { /* UDP echo */
- if (len < (unsigned char)(ihl + 8)) continue;
- seglen = (unsigned int)(len - ihl);
- swap4(12, 16); /* swap IP addresses */
- swap2(ihl, ihl + 2); /* swap UDP ports */
- pkt[ihl + 6] = 0; pkt[ihl + 7] = 0;
- c = ~sum16(pkt + ihl, seglen, pseudo(17, seglen));
- if (c == 0) c = 0xffff; /* UDP: 0 means "none" */
- pkt[ihl + 6] = (unsigned char)(c >> 8); pkt[ihl + 7] = (unsigned char)c;
- ip_send(ihl, len);
- }
+ net_poll(); /* drain RX, answer pings */
+ yield();
}
}
diff --git a/c/ping.c b/c/ping.c
index 12dabd9..c98f106 100644
--- a/c/ping.c
+++ b/c/ping.c
@@ -1,17 +1,14 @@
-#include "netlib.h"
-/* ping - originate ICMP echo requests from the Game Boy (10.0.0.2) and print
- the replies. Target defaults to 10.0.0.1 (the SLIP peer / host); with NAT on
- the bridge it can reach real internet hosts too. Usage: ping [A.B.C.D] */
+#include "sock.h"
+/* ping - originate ICMP echo requests via the kernel socket layer. No SLIP,
+ no IP, no checksums here: the kernel owns all of that. Usage: ping [A.B.C.D]
+ (default 10.0.0.1, the SLIP peer/host; reaches real hosts through NAT). */
-static unsigned char pkt[128];
+/* rbuf is declared first (and large) so the parse target dst[] lands well past
+ the argument string the shell leaves at 0xA000 - writing dst must not clobber
+ the arg while we're still reading it. */
+static unsigned char rbuf[96];
static unsigned char dst[4];
-
-static unsigned int sum16(unsigned char *d, unsigned int len, unsigned int sum) {
- unsigned int w, i = 0;
- while (i + 1 < len) { w = ((unsigned int)d[i] << 8) | d[i + 1]; sum += w; if (sum < w) sum++; i += 2; }
- if (len & 1) { w = (unsigned int)d[len - 1] << 8; sum += w; if (sum < w) sum++; }
- return sum;
-}
+static unsigned char payload[4];
static unsigned char parse_ip(char *s, unsigned char *out) {
unsigned char i, v;
@@ -24,77 +21,33 @@ static unsigned char parse_ip(char *s, unsigned char *out) {
}
return 1;
}
-
static void put_ip(unsigned char *ip) {
unsigned char i;
for (i = 0; i < 4; i++) { putu(ip[i]); if (i < 3) putc('.'); }
}
-static void send_echo(unsigned char seq) {
- unsigned int c;
- pkt[0] = 0x45; pkt[1] = 0; pkt[2] = 0; pkt[3] = 28; /* 20 IP + 8 ICMP */
- pkt[4] = 0; pkt[5] = 0; pkt[6] = 0; pkt[7] = 0;
- pkt[8] = 64; pkt[9] = 1; pkt[10] = 0; pkt[11] = 0; /* TTL, proto ICMP */
- pkt[12] = 10; pkt[13] = 0; pkt[14] = 0; pkt[15] = 2; /* src 10.0.0.2 */
- pkt[16] = dst[0]; pkt[17] = dst[1]; pkt[18] = dst[2]; pkt[19] = dst[3];
- c = ~sum16(pkt, 20, 0); pkt[10] = (unsigned char)(c >> 8); pkt[11] = (unsigned char)c;
- pkt[20] = 8; pkt[21] = 0; pkt[22] = 0; pkt[23] = 0; /* echo request */
- pkt[24] = 0x12; pkt[25] = 0x34; pkt[26] = 0; pkt[27] = seq;
- c = ~sum16(pkt + 20, 8, 0); pkt[22] = (unsigned char)(c >> 8); pkt[23] = (unsigned char)c;
- slip_send((char *)pkt, 28);
-}
-
-/* wait for the ICMP echo reply for 'seq', skipping unrelated frames (the shared
- 10.0.0.0/24 carries IGMP/mDNS/SSDP noise). Returns 1 on match, 0 on timeout.
- The emulator runs uncapped, so the spin budget is generous to span the
- wall-clock round trip through the host bridge. */
-static unsigned char ping_wait(unsigned char seq) {
- unsigned int o, i;
- unsigned char len = 0, c, ihl;
- int b;
- for (o = 0; o < 20; o++) {
- for (i = 0; i < 60000; i++) {
- b = srecv_nb();
- if (b < 0) continue;
- c = (unsigned char)b;
- if (c == SLIP_END) {
- if (len) {
- ihl = (unsigned char)((pkt[0] & 0x0f) << 2);
- if (len >= (unsigned char)(ihl + 8) && pkt[9] == 1
- && pkt[ihl] == 0 && pkt[ihl + 7] == seq) return 1;
- len = 0; /* not ours - keep waiting */
- }
- continue;
- }
- if (c == SLIP_ESC) {
- c = srecv();
- if (c == SLIP_ESC_END) c = SLIP_END;
- else if (c == SLIP_ESC_ESC) c = SLIP_ESC;
- }
- if (len < 128) pkt[len++] = c; else len = 0;
- }
- }
- return 0;
-}
-
void main(void) {
char *arg;
- unsigned char seq, got = 0;
+ unsigned char s, seq, got = 0;
arg = getargs();
if (!arg || !*arg) { dst[0] = 10; dst[1] = 0; dst[2] = 0; dst[3] = 1; }
else if (!parse_ip(arg, dst)) { puts("ping: bad address"); nl(); sexit(1); }
+ payload[0] = 'g'; payload[1] = 'b'; payload[2] = 'o'; payload[3] = 's';
+ s = net_socket(SOCK_ICMP);
+ if (s == 0xFF) { puts("ping: no socket"); nl(); sexit(1); }
+ net_connect(s, dst, 0);
+
puts("PING "); put_ip(dst); nl();
for (seq = 1; seq <= 4; seq++) {
- send_echo(seq);
- if (ping_wait(seq)) {
+ net_send(s, payload, 4);
+ if (net_recv(s, rbuf, sizeof(rbuf)) != 0xFF) {
got++;
- puts(" reply from "); put_ip(pkt + 12);
- puts(" seq="); putu(seq);
- puts(" ttl="); putu(pkt[8]); nl();
+ puts(" reply from "); put_ip(dst); puts(" seq="); putu(seq); nl();
} else {
puts(" seq="); putu(seq); puts(" timeout"); nl();
}
}
puts("-- "); putu(got); puts("/4 received"); nl();
+ net_close(s);
}
diff --git a/c/sock.h b/c/sock.h
new file mode 100644
index 0000000..9af2351
--- /dev/null
+++ b/c/sock.h
@@ -0,0 +1,52 @@
+#ifndef SOCK_H
+#define SOCK_H
+/* gbos socket API - the kernel owns SLIP/IP/ICMP/UDP/TCP; programs just do
+ socket()/connect()/send()/recv()/close(). Header-only: one copy per program.
+ The netreq layout must match include/gbos.inc (NR_* / NET_* / SOCK_*). */
+#include "gbos.h"
+
+#define SOCK_ICMP 1
+#define SOCK_UDP 2
+#define SOCK_TCP 3
+
+struct netreq {
+ unsigned char op;
+ unsigned char sock;
+ unsigned char *buf;
+ unsigned char len;
+ unsigned char ip[4];
+ unsigned char port_hi, port_lo;
+};
+static struct netreq _nr;
+
+/* -> socket id, or 0xFF if the table is full */
+static unsigned char net_socket(unsigned char type) {
+ _nr.op = 0; _nr.sock = type;
+ return sys_net(&_nr);
+}
+/* set the remote address/port for send()/recv() */
+static unsigned char net_connect(unsigned char s, const unsigned char *ip, unsigned int port) {
+ unsigned char i;
+ _nr.op = 1; _nr.sock = s;
+ for (i = 0; i < 4; i++) _nr.ip[i] = ip[i];
+ _nr.port_hi = (unsigned char)(port >> 8); _nr.port_lo = (unsigned char)port;
+ return sys_net(&_nr);
+}
+/* send 'len' payload bytes to the connected peer -> 0 ok / 0xFF error */
+static unsigned char net_send(unsigned char s, void *buf, unsigned char len) {
+ _nr.op = 2; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = len;
+ return sys_net(&_nr);
+}
+/* receive up to 'max' payload bytes -> length, or 0xFF on timeout */
+static unsigned char net_recv(unsigned char s, void *buf, unsigned char max) {
+ _nr.op = 3; _nr.sock = s; _nr.buf = (unsigned char *)buf; _nr.len = max;
+ return sys_net(&_nr);
+}
+static void net_close(unsigned char s) {
+ _nr.op = 4; _nr.sock = s; sys_net(&_nr);
+}
+/* pump the RX path once (answers inbound pings) without blocking */
+static void net_poll(void) {
+ _nr.op = 6; sys_net(&_nr);
+}
+#endif
diff --git a/include/gbos.inc b/include/gbos.inc
index a80841c..a4c9d52 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -130,7 +130,36 @@ DEF SYS_SSEND EQU 29 ; link-port: transmit one byte (E=byte)
DEF SYS_SRECV EQU 30 ; link-port: receive one byte (-> A=byte, blocks)
DEF SYS_SRECVNB EQU 31 ; link-port: receive, non-blocking (-> A=byte, CF=none)
DEF SYS_POLLIN EQU 32 ; poll OSK for a typed char (-> A=char or 0)
-DEF SYS_MAX EQU 33
+DEF SYS_NET EQU 33 ; socket layer (DE=&netreq -> A=result)
+DEF SYS_MAX EQU 34
+
+; ---- socket layer (SYS_NET) --------------------------------------------------
+; netreq struct (in caller RAM; DE points to it):
+DEF NR_OP EQU 0 ; operation (below)
+DEF NR_SOCK EQU 1 ; socket id
+DEF NR_BUF EQU 2 ; 2: pointer to payload buffer
+DEF NR_LEN EQU 4 ; 1: payload length (in) / bytes received (out)
+DEF NR_IP EQU 5 ; 4: IPv4 address
+DEF NR_PORT EQU 9 ; 2: port (hi,lo)
+DEF NR_SIZE EQU 11
+; ops
+DEF NET_SOCKET EQU 0 ; NR_SOCK(type in) -> A=sockid or $FF
+DEF NET_CONNECT EQU 1 ; NR_SOCK, NR_IP, NR_PORT -> set peer
+DEF NET_SEND EQU 2 ; NR_SOCK, NR_BUF, NR_LEN -> A=0/$FF
+DEF NET_RECV EQU 3 ; NR_SOCK, NR_BUF, NR_LEN(max) -> A=len or $FF(timeout); fills NR_IP
+DEF NET_CLOSE EQU 4 ; NR_SOCK
+DEF NET_BIND EQU 5 ; NR_SOCK, NR_PORT -> bind local port (UDP)
+DEF NET_POLL EQU 6 ; pump RX once (answer pings) -> A=0
+; socket types
+DEF SOCK_ICMP EQU 1
+DEF SOCK_UDP EQU 2
+DEF SOCK_TCP EQU 3
+DEF MAX_SOCKS EQU 4
+; SLIP framing (RFC 1055)
+DEF SLIP_END EQU $C0
+DEF SLIP_ESC EQU $DB
+DEF SLIP_ESC_END EQU $DC
+DEF SLIP_ESC_ESC EQU $DD
; SYS_KILL (9) is now implemented (B=pid -> A=0/$FF; pid 1 is immortal)
; (SYS_OPEN=4 / SYS_CLOSE=5 are now implemented by the filesystem)
diff --git a/src/boot.asm b/src/boot.asm
index 23511c4..c29267d 100644
--- a/src/boot.asm
+++ b/src/boot.asm
@@ -56,6 +56,7 @@ KernelInit:
ld a, HIGH(wKernelPCB)
ld [wCurProc+1], a
call pipe_init ; mark all pipes free
+ call net_init ; clear the socket table, set our IP (10.0.0.2)
call fs_init ; mount the disk (format on first boot; persists)
call term_init ; set up the LCD text terminal
call osk_init ; build the on-screen keyboard (window layer)
diff --git a/src/socket.asm b/src/socket.asm
new file mode 100644
index 0000000..f0d8444
--- /dev/null
+++ b/src/socket.asm
@@ -0,0 +1,754 @@
+; =============================================================================
+; socket.asm - the kernel network stack + socket layer (SYS_NET).
+;
+; The kernel owns the link port: SLIP framing, IPv4, checksums, and the per-
+; protocol logic all live here. Userland just does socket()/connect()/send()/
+; recv()/close() via one syscall (DE = &netreq). No program touches SLIP or IP.
+;
+; Our address is 10.0.0.2. Incoming ICMP echo requests are auto-answered while
+; any process pumps RX (recv/poll), so the Game Boy responds to pings for real.
+;
+; Phase 1: ICMP (echo). UDP and TCP build on the same frame/checksum core.
+; =============================================================================
+INCLUDE "include/gbos.inc"
+
+; ---- socket entry layout ----------------------------------------------------
+DEF SK_TYPE EQU 0 ; 0 = free
+DEF SK_RIP EQU 1 ; 4 remote IP
+DEF SK_RPORT EQU 5 ; 2 remote port
+DEF SK_LPORT EQU 7 ; 2 local port
+DEF SK_HASRX EQU 9 ; 1 rxbuf holds a datagram
+DEF SK_RXLEN EQU 10 ; 1 payload length
+DEF SK_SRCIP EQU 11 ; 4 sender IP
+DEF SK_SRCPORT EQU 15 ; 2 sender port
+DEF SK_RXBUF EQU 17 ; 96 payload
+DEF SK_SIZE EQU 113
+
+SECTION "net_bss", WRAM0
+wSockTab:: DS MAX_SOCKS * SK_SIZE
+wNetOurIP:: DS 4
+wNetReq:: DS NR_SIZE
+wNetSockPtr:: DS 2
+wNetIhl:: DS 1
+wNetSeq:: DS 1
+wNetTx:: DS 256 ; tx build buffer
+wNetRxBuf:: DS 256 ; rx frame assembly / parse
+wNetRxLen:: DS 1
+wNetRxEsc:: DS 1
+
+SECTION "socket", ROM0
+
+; net_init - clear the socket table and set our IP (called at boot).
+net_init::
+ ld hl, wSockTab
+ ld bc, MAX_SOCKS * SK_SIZE
+.clr
+ xor a
+ ld [hl+], a
+ dec bc
+ ld a, b
+ or c
+ jr nz, .clr
+ ld a, 10
+ ld [wNetOurIP+0], a
+ xor a
+ ld [wNetOurIP+1], a
+ ld [wNetOurIP+2], a
+ ld a, 2
+ ld [wNetOurIP+3], a
+ xor a
+ ld [wNetRxLen], a
+ ld [wNetRxEsc], a
+ ld [wNetSeq], a
+ ret
+
+; -----------------------------------------------------------------------------
+; low-level link port (byte at a time). Preserve BC/DE/HL; use A only.
+net_putbyte: ; A = byte to transmit
+ ld [rSB], a
+ ld a, $81
+ ld [rSC], a
+.w
+ ld a, [rSC]
+ bit 7, a
+ jr nz, .w
+ ret
+
+net_getbyte_nb: ; -> A = byte, CF set if none
+ ld a, $80
+ ld [rSC], a
+ ld a, [rSC]
+ bit 7, a
+ jr z, .got
+ scf
+ ret
+.got
+ ld a, [rSB]
+ and a ; CF = 0
+ ret
+
+; -----------------------------------------------------------------------------
+; net_slip_send(HL = buf, B = len) - transmit a SLIP frame.
+net_slip_send:
+ ld a, SLIP_END
+ call net_putbyte
+.loop
+ ld a, b
+ or a
+ jr z, .end
+ ld a, [hl+]
+ cp SLIP_END
+ jr z, .esc_end
+ cp SLIP_ESC
+ jr z, .esc_esc
+ call net_putbyte
+.cont
+ dec b
+ jr .loop
+.esc_end
+ ld a, SLIP_ESC
+ call net_putbyte
+ ld a, SLIP_ESC_END
+ call net_putbyte
+ jr .cont
+.esc_esc
+ ld a, SLIP_ESC
+ call net_putbyte
+ ld a, SLIP_ESC_ESC
+ call net_putbyte
+ jr .cont
+.end
+ ld a, SLIP_END
+ call net_putbyte
+ ret
+
+; -----------------------------------------------------------------------------
+; net_cksum(HL = buf, B = len, DE = seed) -> HL = inverted 16-bit checksum.
+; One's-complement sum (RFC 1071), carry-folded each add. len <= 255.
+net_cksum:
+.loop
+ ld a, b
+ cp 2
+ jr c, .tail
+ ld a, [hl+] ; high byte
+ ld c, a
+ ld a, [hl+] ; low byte
+ add e
+ ld e, a
+ ld a, c
+ adc d
+ ld d, a
+ jr nc, .nc1
+ inc de
+.nc1
+ dec b
+ dec b
+ jr .loop
+.tail
+ ld a, b
+ or a
+ jr z, .fold
+ ld a, [hl] ; last odd byte -> high half
+ add d
+ ld d, a
+ jr nc, .fold
+ inc e
+ jr nz, .fold
+ inc d
+.fold
+ ld a, d
+ cpl
+ ld h, a
+ ld a, e
+ cpl
+ ld l, a
+ ret
+
+; -----------------------------------------------------------------------------
+; net_sock_ptr(A = sockid) -> HL = &wSockTab[A] (clobbers B, DE)
+net_sock_ptr:
+ ld hl, wSockTab
+ or a
+ ret z
+ ld b, a
+ ld de, SK_SIZE
+.l
+ add hl, de
+ dec b
+ jr nz, .l
+ ret
+
+; =============================================================================
+; sys_net(DE = &netreq) -> A = result
+; =============================================================================
+sys_net::
+ ld hl, wNetReq ; copy the request out of process RAM
+ ld b, NR_SIZE
+.cp
+ ld a, [de]
+ ld [hl+], a
+ inc de
+ dec b
+ jr nz, .cp
+ ld a, [wNetReq + NR_OP]
+ cp NET_SOCKET
+ jp z, net_op_socket
+ cp NET_CONNECT
+ jp z, net_op_connect
+ cp NET_SEND
+ jp z, net_op_send
+ cp NET_RECV
+ jp z, net_op_recv
+ cp NET_CLOSE
+ jp z, net_op_close
+ cp NET_POLL
+ jp z, net_op_poll
+ ld a, $FF
+ ret
+
+; ---- socket(type) -> A = sockid or $FF ---------------------------------------
+net_op_socket:
+ ld c, 0
+.scan
+ ld a, c
+ call net_sock_ptr ; HL = &sock[c]
+ ld a, [hl]
+ or a ; SK_TYPE == 0 (free)?
+ jr z, .free
+ inc c
+ ld a, c
+ cp MAX_SOCKS
+ jr c, .scan
+ ld a, $FF ; table full
+ ret
+.free
+ ld a, [wNetReq + NR_SOCK] ; requested type
+ ld [hl], a ; SK_TYPE
+ ld a, l ; clear SK_HASRX
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ xor a
+ ld [hl], a
+ ld a, c ; return sockid
+ ret
+
+; ---- connect(sock, ip, port) - set remote --------------------------------
+net_op_connect:
+ ld a, [wNetReq + NR_SOCK]
+ call net_sock_ptr ; HL = &sock
+ ld a, l
+ add SK_RIP
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &sock.RIP
+ ld de, wNetReq + NR_IP
+ ld b, 6 ; 4 IP + 2 port (contiguous: RIP then RPORT)
+.cp
+ ld a, [de]
+ ld [hl+], a
+ inc de
+ dec b
+ jr nz, .cp
+ xor a
+ ret
+
+; ---- close(sock) -------------------------------------------------------------
+net_op_close:
+ ld a, [wNetReq + NR_SOCK]
+ call net_sock_ptr
+ xor a
+ ld [hl], a ; SK_TYPE = free
+ ret
+
+; ---- poll - pump RX once (answers pings) -------------------------------------
+net_op_poll:
+ call net_pump
+ xor a
+ ret
+
+; ---- send(sock, buf, len) ----------------------------------------------------
+net_op_send:
+ ld a, [wNetReq + NR_SOCK]
+ call net_sock_ptr
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld a, [hl] ; SK_TYPE
+ cp SOCK_ICMP
+ jp z, icmp_send
+ ld a, $FF
+ ret
+
+; ---- recv(sock, buf, maxlen) -> A = len or $FF (timeout) ---------------------
+net_op_recv:
+ ld a, [wNetReq + NR_SOCK]
+ call net_sock_ptr
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld b, 200 ; outer timeout
+.outer
+ ld de, 0
+.inner
+ call net_pump
+ call sock_has_rx ; Z clear (nz) if HASRX set
+ jr nz, .got
+ dec de
+ ld a, d
+ or e
+ jr nz, .inner
+ call SchedYield
+ dec b
+ jr nz, .outer
+ ld a, $FF ; timeout
+ ret
+.got
+ ; copy sock.RXBUF -> process NR_BUF, len = sock.RXLEN, NR_IP = sock.SRCIP
+ call sock_ptr_hl ; HL = &sock
+ push hl
+ ld a, l
+ add SK_RXLEN
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl] ; RXLEN
+ ld c, a ; C = length
+ pop hl
+ ; source IP -> wNetReq.NR_IP (returned to caller via buffer? no - copy to caller req is skipped; give src via NR_IP in kernel copy only)
+ push bc
+ push hl
+ ; dst = process buf pointer
+ ld a, [wNetReq + NR_BUF]
+ ld e, a
+ ld a, [wNetReq + NR_BUF + 1]
+ ld d, a ; DE = process buf
+ ld a, l
+ add SK_RXBUF
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &sock.RXBUF
+ ld a, c
+ or a
+ jr z, .nocopy
+.cpl
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .cpl
+.nocopy
+ pop hl ; &sock
+ pop bc ; C = length
+ ; clear HASRX
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ xor a
+ ld [hl], a
+ ld a, c ; return length
+ ret
+
+; sock_ptr_hl -> HL = current socket pointer (from wNetSockPtr)
+sock_ptr_hl:
+ ld a, [wNetSockPtr]
+ ld l, a
+ ld a, [wNetSockPtr+1]
+ ld h, a
+ ret
+
+; sock_has_rx -> Z clear (nz) if the current socket has a buffered datagram
+sock_has_rx:
+ call sock_ptr_hl
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ or a
+ ret
+
+; =============================================================================
+; icmp_send - build + transmit an ICMP echo request from the current socket.
+; =============================================================================
+icmp_send:
+ ; IP header (20 bytes)
+ ld a, $45
+ ld [wNetTx+0], a
+ xor a
+ ld [wNetTx+1], a
+ ld [wNetTx+2], a ; total len hi
+ ld a, [wNetReq+NR_LEN]
+ add 28
+ ld [wNetTx+3], a ; total len lo = 28 + payload
+ xor a
+ ld [wNetTx+4], a
+ ld [wNetTx+5], a ; id
+ ld [wNetTx+6], a
+ ld [wNetTx+7], a ; flags/frag
+ ld a, 64
+ ld [wNetTx+8], a ; ttl
+ ld a, 1
+ ld [wNetTx+9], a ; proto ICMP
+ xor a
+ ld [wNetTx+10], a
+ ld [wNetTx+11], a ; ip cksum
+ ld a, [wNetOurIP+0]
+ ld [wNetTx+12], a
+ ld a, [wNetOurIP+1]
+ ld [wNetTx+13], a
+ ld a, [wNetOurIP+2]
+ ld [wNetTx+14], a
+ ld a, [wNetOurIP+3]
+ ld [wNetTx+15], a
+ ; dst IP = sock.RIP
+ call sock_ptr_hl
+ ld a, l
+ add SK_RIP
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &sock.RIP
+ ld de, wNetTx+16
+ ld b, 4
+.dip
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .dip
+ ; ICMP header (8 bytes) at wNetTx+20
+ ld a, 8
+ ld [wNetTx+20], a ; type = echo request
+ xor a
+ ld [wNetTx+21], a ; code
+ ld [wNetTx+22], a
+ ld [wNetTx+23], a ; cksum
+ ld a, $12
+ ld [wNetTx+24], a ; id
+ ld a, $34
+ ld [wNetTx+25], a
+ xor a
+ ld [wNetTx+26], a ; seq hi
+ ld a, [wNetSeq]
+ inc a
+ ld [wNetSeq], a
+ ld [wNetTx+27], a ; seq lo
+ ; payload from process buf
+ ld a, [wNetReq+NR_BUF]
+ ld l, a
+ ld a, [wNetReq+NR_BUF+1]
+ ld h, a
+ ld de, wNetTx+28
+ ld a, [wNetReq+NR_LEN]
+ ld b, a
+ or a
+ jr z, .nopl
+.cppl
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .cppl
+.nopl
+ ; ICMP checksum over 8 + payload
+ ld a, [wNetReq+NR_LEN]
+ add 8
+ ld b, a
+ ld hl, wNetTx+20
+ ld de, 0
+ call net_cksum
+ ld a, h
+ ld [wNetTx+22], a
+ ld a, l
+ ld [wNetTx+23], a
+ ; IP checksum over 20
+ ld hl, wNetTx
+ ld b, 20
+ ld de, 0
+ call net_cksum
+ ld a, h
+ ld [wNetTx+10], a
+ ld a, l
+ ld [wNetTx+11], a
+ ; transmit: 28 + payload
+ ld a, [wNetReq+NR_LEN]
+ add 28
+ ld b, a
+ ld hl, wNetTx
+ call net_slip_send
+ xor a
+ ret
+
+; =============================================================================
+; net_pump - read available serial bytes, assemble SLIP frames, process each.
+; Non-blocking: returns when the link has no more bytes ready.
+; =============================================================================
+net_pump:
+.next
+ call net_getbyte_nb
+ ret c ; no byte ready
+ ; SLIP decode using wNetRxLen / wNetRxEsc
+ cp SLIP_END
+ jr z, .frame
+ ld c, a ; save byte
+ ld a, [wNetRxEsc]
+ or a
+ jr nz, .escaped
+ ld a, c
+ cp SLIP_ESC
+ jr z, .setesc
+ jr .store
+.escaped
+ ld a, c
+ cp SLIP_ESC_END
+ jr nz, .notend
+ ld c, SLIP_END
+.notend
+ ld a, c
+ cp SLIP_ESC_ESC
+ jr nz, .noteesc
+ ld c, SLIP_ESC
+.noteesc
+ xor a
+ ld [wNetRxEsc], a
+ ld a, c
+ jr .store2
+.setesc
+ ld a, 1
+ ld [wNetRxEsc], a
+ jr .next
+.store
+ ld a, c
+.store2
+ ; store A at wNetRxBuf[wNetRxLen] if room
+ ld c, a
+ ld a, [wNetRxLen]
+ cp 255
+ jr nc, .next ; overflow: drop
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de
+ ld a, c
+ ld [hl], a
+ ld a, [wNetRxLen]
+ inc a
+ ld [wNetRxLen], a
+ jr .next
+.frame
+ ; END: if we have a frame, process it
+ xor a
+ ld [wNetRxEsc], a
+ ld a, [wNetRxLen]
+ or a
+ jr z, .next ; empty frame
+ push bc
+ call process_frame
+ pop bc
+ xor a
+ ld [wNetRxLen], a
+ jr .next
+
+; -----------------------------------------------------------------------------
+; process_frame - IPv4 demux of the frame in wNetRxBuf (wNetRxLen bytes).
+process_frame:
+ ld a, [wNetRxBuf]
+ and $F0
+ cp $40
+ ret nz ; not IPv4
+ ld a, [wNetRxBuf]
+ and $0F
+ add a, a
+ add a, a
+ ld [wNetIhl], a ; ihl in bytes
+ ld a, [wNetRxBuf+9] ; protocol
+ cp 1
+ jr z, icmp_in
+ ret
+
+; -----------------------------------------------------------------------------
+; icmp_in - handle an inbound ICMP message.
+icmp_in:
+ ld a, [wNetIhl]
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de ; HL = &icmp
+ ld a, [hl] ; type
+ cp 8
+ jr z, icmp_request
+ or a ; type 0 = echo reply
+ jr z, icmp_reply
+ ret
+
+; incoming echo request -> answer it in place
+icmp_request:
+ ld a, [wNetIhl]
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de ; HL = &icmp
+ xor a
+ ld [hl+], a ; type = 0
+ inc hl ; skip code
+ xor a
+ ld [hl+], a
+ ld [hl], a ; cksum = 0
+ call net_swap_ip
+ ; icmp cksum over (rxlen - ihl)
+ ld a, [wNetIhl]
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de ; HL = &icmp
+ ld a, [wNetRxLen]
+ sub e
+ ld b, a
+ ld de, 0
+ push hl
+ call net_cksum ; HL = cksum
+ pop de ; DE = &icmp
+ ld a, e
+ add 2
+ ld e, a
+ ld a, d
+ adc 0
+ ld d, a ; DE = &icmp+2
+ ld a, h
+ ld [de], a
+ inc de
+ ld a, l
+ ld [de], a
+ ; IP cksum
+ xor a
+ ld [wNetRxBuf+10], a
+ ld [wNetRxBuf+11], a
+ ld a, [wNetIhl]
+ ld b, a
+ ld hl, wNetRxBuf
+ ld de, 0
+ call net_cksum
+ ld a, h
+ ld [wNetRxBuf+10], a
+ ld a, l
+ ld [wNetRxBuf+11], a
+ ; send it back
+ ld a, [wNetRxLen]
+ ld b, a
+ ld hl, wNetRxBuf
+ call net_slip_send
+ ret
+
+; incoming echo reply -> deliver to the first ICMP socket
+icmp_reply:
+ ld c, 0
+.scan
+ ld a, c
+ call net_sock_ptr ; HL = &sock
+ ld a, [hl]
+ cp SOCK_ICMP
+ jr z, .deliver
+ inc c
+ ld a, c
+ cp MAX_SOCKS
+ jr c, .scan
+ ret ; no ICMP socket
+.deliver
+ ; HL = &sock. payload = wNetRxBuf + ihl + 8, len = rxlen - ihl - 8
+ push hl
+ ; set HASRX = 1
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, 1
+ ld [hl], a
+ pop hl
+ ; SRCIP = wNetRxBuf[12..15]
+ push hl
+ ld a, l
+ add SK_SRCIP
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld de, wNetRxBuf+12
+ ld b, 4
+.sip
+ ld a, [de]
+ ld [hl+], a
+ inc de
+ dec b
+ jr nz, .sip
+ pop hl
+ ; RXLEN + RXBUF
+ ld a, [wNetIhl]
+ add 8
+ ld e, a
+ ld d, 0 ; DE = ihl+8 (payload offset)
+ ld a, [wNetRxLen]
+ sub e
+ ld c, a ; C = payload length
+ push hl
+ ld a, l
+ add SK_RXLEN
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, c
+ ld [hl], a ; RXLEN
+ pop hl
+ ; copy payload
+ ld a, l
+ add SK_RXBUF
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &sock.RXBUF (dest)
+ push hl
+ ld hl, wNetRxBuf
+ add hl, de ; HL = &payload (src)
+ pop de ; DE = dest
+ ld a, c
+ or a
+ ret z
+.cp
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .cp
+ ret
+
+; net_swap_ip - swap the src/dst IP fields of the frame in wNetRxBuf
+net_swap_ip:
+ ld hl, wNetRxBuf+12
+ ld de, wNetRxBuf+16
+ ld b, 4
+.l
+ ld a, [hl]
+ ld c, a
+ ld a, [de]
+ ld [hl+], a
+ ld a, c
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .l
+ ret
diff --git a/src/syscall.asm b/src/syscall.asm
index f926d89..5b40ef0 100644
--- a/src/syscall.asm
+++ b/src/syscall.asm
@@ -67,6 +67,7 @@ SyscallTable:
dw sys_srecv ; 30 SRECV
dw sys_srecv_nb ; 31 SRECVNB
dw sys_pollin ; 32 POLLIN
+ dw sys_net ; 33 NET
; -----------------------------------------------------------------------------
sys_nosys: