From c4b9a1b1c4f8c211f765a089b34b66a2d046f3be Mon Sep 17 00:00:00 2001 From: user Date: Fri, 17 Jul 2026 01:39:29 +0200 Subject: net: chat client - async receive + OSK send over the link port The application layer of the link-port demo, and it ties the whole system together: the LCD terminal displays, the on-screen keyboard types, and the link port carries a live chat. Kernel: sys_srecv_nb (non-blocking link receive; A=byte, CF=none) and sys_pollin (poll the OSK for a typed char without blocking) - syscalls 31/32. Both are what a poll loop needs to receive and type at once. Userland: c/chat.c runs a poll loop - it feeds non-blocking bytes through a SLIP receive state machine and prints whole incoming frames as messages, while pollin() drives the on-screen keyboard; SELECT shows the keys, type a line, START sends it as a frame. libc srecv_nb()/pollin(). Host: tools/gateway.py --mode chat is a simple bot peer (echoes each GB message and injects a few async ones); --keys can drive the OSK for tests. Verified: the gateway pushes 'welcome', ' hey gameboy!', ' nice link cable' unprompted and the GB displays all three (async receive); typing 'hi' on the OSK echoes it and emits the SLIP frame \xC0hi\xC0 (send). A Game Boy in the chat, keyboard on screen, over the link cable. --- Makefile | 2 +- c/chat.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ c/gbos.h | 2 ++ c/libc.s | 19 ++++++++++++++++++- include/gbos.inc | 5 ++++- src/net.asm | 22 ++++++++++++++++++++++ src/programs.asm | 7 +++++++ src/syscall.asm | 2 ++ tools/gateway.py | 16 ++++++++++++++-- 9 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 c/chat.c diff --git a/Makefile b/Makefile index 2dc3db7..cf04419 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ $(BUILD)/%.o: src/%.asm | $(BUILD) # C programs: compiled with SDCC into ROM-bank blobs and INCBIN'd by programs.asm CBLOBS := c/chello.bin c/echo.bin c/true.bin c/false.bin c/uname.bin \ c/pid.bin c/cat.bin c/wc.bin c/head.bin c/args.bin \ - c/ls.bin c/save.bin c/rm.bin c/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin c/necho.bin c/wget.bin + c/ls.bin c/save.bin c/rm.bin c/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin c/necho.bin c/wget.bin c/chat.bin $(BUILD)/programs.o: $(CBLOBS) # C programs also depend on the shared libc sources diff --git a/c/chat.c b/c/chat.c new file mode 100644 index 0000000..987b336 --- /dev/null +++ b/c/chat.c @@ -0,0 +1,57 @@ +#include "netlib.h" +/* chat: a link-port chat client. Incoming SLIP frames from the host gateway + print as messages; SELECT brings up the on-screen keyboard, type a line and + press START to send it. A poll loop interleaves receiving and typing so + messages arrive while you're not mid-line. */ + +static char rxbuf[96]; +static unsigned char rxlen, rxin, rxesc; + +/* feed one non-blocking byte to the SLIP receiver; returns a frame length when + a whole frame has arrived (copied into out), else 0. */ +static unsigned char slip_poll(char *out) { + int b = srecv_nb(); + unsigned char c, i, n; + if (b < 0) return 0; + c = (unsigned char)b; + if (c == SLIP_END) { + if (rxin && rxlen) { + n = rxlen; + for (i = 0; i < n; i++) out[i] = rxbuf[i]; + rxlen = 0; rxin = 0; rxesc = 0; + return n; + } + rxin = 1; rxlen = 0; rxesc = 0; + return 0; + } + if (!rxin) return 0; + if (rxesc) { + if (c == SLIP_ESC_END) c = SLIP_END; + else if (c == SLIP_ESC_ESC) c = SLIP_ESC; + rxesc = 0; + } else if (c == SLIP_ESC) { rxesc = 1; return 0; } + if (rxlen < 96) rxbuf[rxlen++] = (char)c; + return 0; +} + +void main(void) { + char line[64], frame[96]; + unsigned char linelen = 0, n, i, c; + puts("chat: SELECT=keyboard START=send"); nl(); + for (;;) { + n = slip_poll(frame); /* incoming message? */ + if (n) { + for (i = 0; i < n; i++) putc(frame[i]); + nl(); + } + c = pollin(); /* typed a key? */ + if (c == '\n') { + if (linelen) { slip_send(line, linelen); linelen = 0; } + } else if (c == 8) { + if (linelen) { linelen--; putc(8); } + } else if (c) { + if (linelen < 63) { line[linelen++] = (char)c; putc((char)c); } + } + yield(); + } +} diff --git a/c/gbos.h b/c/gbos.h index d207a75..df52fbe 100644 --- a/c/gbos.h +++ b/c/gbos.h @@ -15,6 +15,8 @@ unsigned char getpid(void); unsigned char pipe(void); /* -> read fd; write end is read+1 */ 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 */ /* helpers (c/libc.c) */ void putu(unsigned int n); /* print decimal */ diff --git a/c/libc.s b/c/libc.s index 50d7860..7897a79 100644 --- a/c/libc.s +++ b/c/libc.s @@ -1,6 +1,6 @@ .module libc .globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs - .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv + .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv, _srecv_nb, _pollin ;; 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. @@ -105,6 +105,23 @@ _srecv: rst #0x30 ret + ;; int srecv_nb(void) -> byte, or -1 if none (non-blocking) +_srecv_nb: + ld c, #31 ; SYS_SRECVNB + rst #0x30 + jr c, 1$ + ld c, a + ld b, #0 + ret +1$: ld bc, #0xffff + ret + + ;; unsigned char pollin(void) -> A (typed OSK char, or 0) +_pollin: + ld c, #32 ; SYS_POLLIN + rst #0x30 + ret + ;; unsigned char open(const char *name /*DE*/, unsigned char mode /*A*/) -> A(fd) _open: ld b, a ; B = mode diff --git a/include/gbos.inc b/include/gbos.inc index 7103f48..a6467bf 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -128,7 +128,9 @@ DEF SYS_OPENDIR EQU 27 ; point ls at a directory (DE=path -> A=0/$FF) DEF SYS_PIPE EQU 28 ; make a pipe (-> A=read fd; write=A+1) 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_MAX EQU 31 +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 ; 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) @@ -215,5 +217,6 @@ DEF PROG_COUNT EQU 21 DEF PROG_PTEST EQU 22 DEF PROG_NECHO EQU 23 DEF PROG_WGET EQU 24 +DEF PROG_CHAT EQU 25 ENDC diff --git a/src/net.asm b/src/net.asm index 939b975..f2c4279 100644 --- a/src/net.asm +++ b/src/net.asm @@ -39,3 +39,25 @@ sys_srecv:: .got ld a, [rSB] ret + +; sys_srecv_nb() -> A = byte, CF set if none available (non-blocking). +sys_srecv_nb:: + 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 (got a byte) + ret + +; sys_pollin() -> A = char typed on the OSK this poll, or 0 if none. +sys_pollin:: + call pad_poll + call osk_handle ; CF set + A = char if a key was pressed + ret c + xor a + ret diff --git a/src/programs.asm b/src/programs.asm index fbe6067..180f551 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -115,6 +115,9 @@ ProgNecho: SECTION "prog_wget", ROMX[$4000], BANK[26] ProgWget: INCBIN "c/wget.bin" +SECTION "prog_chat", ROMX[$4000], BANK[27] +ProgChat: + INCBIN "c/chat.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (c/sh.c): parses >/ program id, searched by sys_lookup. @@ -263,4 +268,6 @@ NameTable:: db PROG_NECHO db "wget", 0 db PROG_WGET + db "chat", 0 + db PROG_CHAT db 0 diff --git a/src/syscall.asm b/src/syscall.asm index cb16bbe..f926d89 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -65,6 +65,8 @@ SyscallTable: dw sys_pipe ; 28 PIPE dw sys_ssend ; 29 SSEND dw sys_srecv ; 30 SRECV + dw sys_srecv_nb ; 31 SRECVNB + dw sys_pollin ; 32 POLLIN ; ----------------------------------------------------------------------------- sys_nosys: diff --git a/tools/gateway.py b/tools/gateway.py index 3129773..3b18024 100644 --- a/tools/gateway.py +++ b/tools/gateway.py @@ -46,6 +46,10 @@ def http_fetch(payload): def handle(payload, mode): if mode == "http": return http_fetch(payload) + if mode == "chat": + msg = bytes(payload).decode("latin1", "replace") + sys.stderr.write("\n[gw] GB says: %s\n" % msg) + return [(" " + msg).encode()] # a simple echo-bot peer return [bytes(payload)] # echo def reader(out, inp, mode): @@ -75,15 +79,23 @@ def reader(out, inp, mode): def main(): ap = argparse.ArgumentParser() - ap.add_argument("--mode", default="echo", choices=["echo", "http"]) + ap.add_argument("--mode", default="echo", choices=["echo", "http", "chat"]) ap.add_argument("--cmd", default="necho") ap.add_argument("--rom", default=os.path.join(os.path.dirname(__file__), "..", "gbos.gb")) + ap.add_argument("--keys", default=None) a = ap.parse_args() - p = subprocess.Popen([EMU, "--headless", "--uncapped", a.rom], + cmd = [EMU, "--headless", "--uncapped"] + if a.keys: cmd += ["--keys", a.keys] + cmd.append(a.rom) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=0) threading.Thread(target=reader, args=(p.stdout, p.stdin, a.mode), daemon=True).start() time.sleep(1.0) p.stdin.write((a.cmd + "\n").encode()); p.stdin.flush() + if a.mode == "chat": + time.sleep(1.0) + for m in [b"welcome to sl0pchat", b" hey gameboy!", b" nice link cable"]: + p.stdin.write(slip_encode(m)); p.stdin.flush(); time.sleep(0.6) time.sleep(4.0) p.stdin.write(b"exit\n"); p.stdin.flush() try: p.wait(timeout=3) -- cgit v1.3.1-sl0p