Skip to content

ethernetgateway v0.8.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 23:44

Added

  • CP/M emulator — Z80 CPU + interactive console (in progress). A
    new default-off config key cpm_emu_enabled (wired into the telnet, web, and
    GUI config UIs) gates a K CP/M System main-menu item. Selecting it boots a
    real Z80 CPU (the BSD-licensed iz80 crate)
    in a 64 KB machine driven by our own CP/M 2.2 BDOS, and drops into a Rust
    CCP-lite A> prompt. The full console BDOS group (character/string output,
    console input with echo, direct console I/O, read-console-buffer, console
    status, version) is wired to the telnet/SSH session, so interactive Z80
    programs can read and write the console; built-in HELLO and ECHO demos
    exercise it. Works correctly on PETSCII (C64) terminals as well as ANSI/ASCII.
    On launch it creates the drive folders CPM/A..CPM/H under transfer_dir.
    An interactive program can be aborted with a double-ESC, and a runaway is
    bounded by an instruction budget. Completely separate from the Gateway Shell,
    which emulates no CPU; the item is hidden and the key rejected while the
    toggle is off.

    • Filesystem, part 1 — FCB + drives + sequential file I/O. The emulator
      now has a directory-backed CP/M filesystem: each drive A:–H: is a folder in
      the CPM/ container, and the BDOS file calls for opening, creating,
      closing, and sequentially reading/writing files (via the DMA buffer and a
      parsed 36-byte FCB) are implemented and jailed under transfer_dir. Drive
      select / current-disk / set-DMA are wired, 8.3 filenames are enforced
      (case-insensitive; host files that aren't valid 8.3 are invisible to CP/M),
      and the A> prompt gained A:..H: drive-change commands with a
      drive-aware prompt.
    • Filesystem, part 2 — directory search + erase. The BDOS
      search-first/search-next calls enumerate a drive's files (with ?
      wildcards, synthesizing CP/M directory entries per 16 KB extent) and the
      delete call removes matching files. The CCP-lite gained the authentic
      built-ins DIR (list the current drive) and ERA name (erase, with a
      *.* confirmation), so files uploaded into a CPM/ drive can be listed
      and removed interactively.
    • Filesystem, part 3 — random-record I/O, file size, rename. The BDOS
      random read/write calls seek to a record number (with the sequential
      position kept in sync), compute-file-size reports a file's length in
      records, and rename moves a file to a new 8.3 name (no clobber). This
      completes the CP/M 2.2 file BDOS surface, so real utilities like PIP
      and STAT become runnable once .COM loading lands.
    • Run a real .COM from a drive. A command at the A> prompt that
      isn't a built-in is now resolved as <verb>.COM on the drive (honoring a
      B: drive prefix), loaded into the TPA, and executed — so actual CP/M
      software (PIP, STAT, ASM, …) uploaded into a CPM/ drive runs over
      telnet/SSH. Page zero is set up exactly as the CCP does before launch: the
      command tail is placed at 0x0080 and the first two arguments are parsed
      into the default FCBs at 0x005C / 0x006C. The program image is loaded
      jailed under transfer_dir (canonical-prefix + symlink checks) and bounded
      by the per-file size cap.
    • Full CP/M resident command set. The A> prompt now implements all
      six authentic CP/M 2.2 resident commands — DIR, ERA, REN, TYPE,
      SAVE, USER (plus the d: drive change) — so no upload is needed for
      everyday file work. REN renames (no clobber); TYPE streams a text file
      and stops at the ^Z end-of-file marker (binary files refused); USER
      selects an area (only area 0 exists — one flat area per drive). To make
      SAVE authentic, the emulated machine now stays resident across commands:
      the transient program area survives a warm boot back to A> (as on real
      CP/M), so SAVE n file dumps the image a previous program (e.g. DDT)
      left in memory. Low-memory vectors are reinstalled on each program load so
      a program that trashes page zero can't corrupt the next one.
    • Virtual-modem UART port selection. A new config key cpm_emu_uart
      (wired into the telnet, web, and GUI config UIs, each showing a description
      beside every choice) selects which machine/port address the emulated CP/M's
      modem answers at — off (default), the RC2014/RomWBW Z80 SIO/2 channels
      (rc2014_1arc2014_2b, 0x80–0x87), or the Altair 88‑2SIO / 88‑SIO
      (altair_2sio1/altair_2sio2/altair_sio). Addresses and status-bit
      conventions are taken from the RomWBW SIO driver and David Hansel's Altair
      simulator. With a profile selected, CpmMachine's port I/O answers at those
      addresses with a valid idle UART (transmit ready, nothing received) so
      comms software can probe and initialise the port.
    • Virtual modem — outbound dialling. The CP/M modem now speaks Hayes
      AT and can place calls: ATD A / ATD B dial the gateway's own serial
      Port A / Port B (via the existing peer-dial plumbing, like one machine
      calling another), and ATDT host:port opens a TCP connection. On answer it
      reports CONNECT and becomes a transparent data pipe; +++ returns to
      command mode and ATH hangs up. A new aux profile choice puts the modem
      on the CP/M BDOS AUX: device (functions 3/4) — the hardware-independent
      path for SC126/RomWBW software (a Z180 ASCI port profile can't work: the
      Z80 core doesn't implement the Z180 IN0/OUT0 instructions the ASCI
      uses). The modem is a self-contained async layer bridged to the guest's
      synchronous UART/AUX byte rings at the CPU batch seam.
  • EGT80 — "Ethernet Gateway Terminal", a CP/M terminal of our own. Every
    period CP/M terminal is built for one machine's serial port — commonly a
    separate binary per port, or a build for one specific card, and some generic
    builds carry no serial driver at all — and pairing one with the wrong port
    produces silence rather than an error. EGT80.COM (new EGT80/ directory,
    Z80 assembly, CP/M 2.2 and CP/M 3) asks instead: a menu picks the port at run
    time, from five families — Z80 SIO/2, 6850 ACIA, RomWBW HBIOS (RST 8),
    Z180 ASCI and the CP/M AUX: device — with a free-form address entry for
    boards at unusual ports. It carries a console layer chosen once at startup
    (BIOS vectors on 2.2, BDOS 6 on 3), menus and help written for someone who
    does not yet know which port their machine uses, terminal mode with an
    escape-key menu, an ANSI/ASCII inbound filter (pass escape sequences through,
    or strip them and the high bit for a printing terminal), settings saved into
    its own .COM
    (a 128-byte-aligned patch area rewritten with a
    random-record write, signature- and range-checked at startup and falling back
    to defaults if damaged), and XMODEM transfer both ways — 128-byte blocks,
    CRC-16 with the checksum fallback an older peer may insist on, one buffer
    serving as both protocol block and CP/M record, abortable with a keypress, and
    refusing to leave a partial file behind when a download fails.

    Shipped ready to use: the .COM is compiled into the gateway binary and
    placed on CP/M drive A: when the emulator first creates its drive folders, so
    it is simply there. That copy is never overwritten afterwards, because the
    settings live inside it; deleting it restores the shipped copy. Release
    archives also carry the loose EGT80.COM for sending to real hardware.

    The port screen names the port in force and offers D — the default port
    (the one the gateway also defaults to, so the pair works together again in one
    keystroke), and the menu says (changed — press V to keep it for next time)
    while a setting is live but not yet written to the file. ^C leaves the
    wrong-port notice — which swallows everything else you type, so a half-typed
    line can no longer run menu commands by accident — aborts a transfer in
    progress, and cancels at the filename prompt.

    Built by running the real period assembler (SLR Z80ASM) under zxcc, so
    SLR80 compatibility is structural rather than hoped for, with M80+L80 and ZMAC
    as portability gates. Wrong choices are diagnosed rather than left to hang: a
    port that never accepts a byte is named and explained, HBIOS is refused on a
    machine with no RST 8 vector, and the Z180 ASCI family is refused on a
    processor that is not a Z180 (MLT tells them apart). Two paths cannot be
    exercised by any test here and are documented as reasoned-not-run: the Z180
    ASCI driver (our Z80 core has no IN0/OUT0) and the CP/M 3 console path.

    • Virtual modem — RomWBW HBIOS access (hbios_1 / hbios_2). Some CP/M
      comms software doesn't drive a UART at all: software built for RomWBW asks
      the firmware to move the byte, issuing an RST 8 with a function number in
      B and a serial unit in C. On a port profile such a program hangs before
      printing anything — its first call goes nowhere — which no port address can
      fix; RomWBW-targeted builds of period comms software are exactly this
      case. Two new cpm_emu_uart choices answer that
      API for one serial unit (the virtual modem), so those builds now run: AT
      OK, ATDT host:portCONNECT, with data flowing both ways. The RST 8
      vector is installed only for these profiles, so every other setting keeps
      the untouched page zero of a plain CP/M 2.2 machine; a call for a unit other
      than the selected one is refused, so a mismatch fails the same recognisable
      way a wrong port address does. Implemented: the serial (character device)
      group — in, out, input/output status, initialise, query, describe — plus the
      version and serial-unit-count calls. Refused with an error result: bank
      switching / memory management, disk, RTC, video, sound, DSKY (RomWBW
      hardware services with no counterpart here) — an honest failure at the
      call beats stranding a program later. A blocking call whose device isn't
      ready parks the guest on the trap and is re-reported after the driver's
      seam work, so it stays interruptible by the double-ESC break-out and
      doesn't spin the host. Written from the published HBIOS interface
      description (function numbers and register conventions); no RomWBW code is
      included. New src/cpm/hbios.rs with 11 unit tests, plus a fidelity fix the
      guests depend on: an HBIOS return now sets the flags from the result byte
      (as OR A leaves them) instead of leaving the guest's stale flags. A guest
      that hands the status straight to a JR Z was reading our stale flags as
      "transmitter not ready" and waiting forever.
    • Virtual modem — dialable as CPM@<ip> (inbound). The CP/M emulator is
      now a third dialable peer endpoint named CPM, alongside Ports A/B: from
      another modem on the gateway, ATD CPM@<ip> rings it exactly as
      A@<ip>/B@<ip> ring the serial ports. The running CP/M comms program
      sees RING and answers with ATA (or auto-answers after ATS0=n
      rings), then the machines are joined transparently. Implemented additively
      with a parallel global call slot and a CPM@host dial parser — the A/B
      peer-dial slots and routing are untouched (208 serial tests still pass).
      Gated by allow_peer_dial; the endpoint answers while a comms program is
      running (that's when the ring is polled).
    • Virtual modem — reachable over the master/slave relay. A device on a
      slave gateway can dial CPM@<master-ip>: the slave relays the address to
      the master, whose relay peer-dial handler resolves it to its own local
      CP/M endpoint (the CP/M analog of resolving A@/B@ to a local port), so
      CP/M running on the master is reachable from every attached machine. Both
      directions were verified end-to-end over a live gateway: a .COM dialing
      a TCP host via ATDT (CONNECT + data round-trip), and an external modem
      dialing CPM@<ip> (the CP/M program rang, auto-answered, and received the
      caller's data).
    • Virtual modem — slave-hosted CP/M reachable via the crossbar. CP/M
      running on a slave is now dialable as CPM@<slave-ip> from the master
      (or another slave) exactly as its A/B ports are: while a modem CP/M shell
      is active the slave registers the label CPM with the master
      (serial-register CPM) and, on a peer-dial claim, rings its own local
      endpoint and bridges. parse_remote_peer_addr accepts the CPM label; a
      master/standalone ATD CPM@<slave-ip> claims it through the crossbar; and
      an async slave-side announcer (the CP/M analog of the physical-port
      modem_slave_announce_tick, tied to the shell's lifetime) does the
      registration. So CPM@<ip> now works wherever A@<ip>/B@<ip> do.
      Additive — A/B and the existing relay are unchanged.
    • Out-of-band break-out; remote outbound dialing. A double-ESC now
      returns to A> at any time — not just at a console prompt but also from a
      compute-bound program that never reads the console (the gateway watches the
      wire out-of-band between CPU bursts), so a runaway no longer has to run out
      the instruction ceiling. The CP/M-System banner shows "Press ESC twice to
      stop a program." The CP/M modem can also dial a serial port on another
      gateway — ATD A@<remote-ip> / B@<remote-ip> routes via the master/slave
      relay (same routing and allow_peer_dial gate as the physical modem);
      previously only the gateway's own ports were reachable. The CP/M modem's
      peer-dial is now gated by allow_peer_dial like the physical modem.
    • Virtual modem — fidelity polish. Five additive enhancements to the CP/M
      modem, none of which change the working polled path: (A) the AT command
      layer parses a chained init string (ATE0Q0V1X4S0=1) and applies each
      clause — echo, quiet, verbose/numeric and X-level result codes,
      S-register set/query (S0 auto-answer, S7 peer-dial carrier wait), &C/&D,
      ATZ/AT&F reset, ATI — instead of matching a few fixed strings; (B)
      carrier is surfaced to the guest as the UART's DCD bit (SIO RR0 bit3, 6850
      bit2), active-high so the idle status byte is unchanged; (C) flow control
      both ways — the UART reports transmit-not-ready when the TX ring is full and
      the peer read is capped to the guest RX ring's free space, so a speed
      mismatch back-pressures instead of dropping bytes; (D) a fuller Z80 SIO
      register model (WR0 read-pointer + RR1/RR2), a strict superset of the
      RR0-only behaviour; (E) CPM@<ip> is now an answer pool — every
      modem-enabled CP/M session can answer the next inbound call (a hunt group),
      with one session owning the slave→master crossbar announcement.
    • ADM-3A terminal translation. The emulator presents CP/M programs with
      a Lear Siegler ADM-3A terminal and translates its screen-control stream to
      the connected client: ANSI cursor sequences for a modern terminal, native
      cursor codes for a Commodore 64 (PETSCII), best-effort for a dumb ASCII
      TTY. Client arrow keys are translated the other way into the ADM-3A cursor
      codes the program reads. This lets full-screen software (WordStar, Turbo
      Pascal, editors) installed for an ADM-3A render correctly. The decoder and
      per-terminal renderers are a self-contained, unit-tested module.
    • Configurable runaway ceiling. A new config key cpm_emu_max_minstr
      (millions of Z80 instructions per program run, default 2000 = 2 billion;
      wired into the telnet, web, and GUI config UIs) bounds a compute-bound
      .COM that never reads the console, so the A> prompt always returns.
      Interactive programs remain escapable with a double-ESC at any input
      prompt. In the GUI and web UIs the CP/M controls (enable + ceiling) moved
      into the "AI, Browser & Weather — More" panel to keep the main screen
      uncluttered; in the telnet UI they live in a CP/M submenu under Other
      Settings → E.
    • Free-TPA report. The boot banner and VER now print the size of the
      transient program area (63K TPA free (0100-FDFF)), the way a real CP/M
      system reports its memory on cold start, so a user can see how much room a
      .COM actually gets. Derived from the emulator's own TPA constants.
  • Gateway Shell: three new commands. CLS / CLEAR clears the screen;
    VER / VERSION prints the shell identity and gateway version; and
    FIND <pattern> / WHERE recursively searches all of drive A: (not just the
    current directory) for files whose name matches a wildcard, printing each
    hit's A: path. The FIND walk is bounded (scan and result caps) and never
    follows symlinks, so it stays inside the transfer-directory jail.

  • First-run setup wizard (desktop GUI). A fresh install now opens the GUI
    window into a nine-screen wizard instead of a configuration editor the
    operator has to reverse-engineer: credentials (typed twice, with a
    show-password toggle and the cleartext-storage warning), telnet, SSH, access
    control, the web server, the transfer directory, the CP/M emulator and its
    virtual modem, and the gateway role — with a master-connection screen on the
    slave path. It closes with a review listing what will be saved, the actual
    commands to connect with, and the inbound TCP ports to allow on the
    firewall
    . Beyond password-match it validates port syntax and refuses a port
    another listener already claims (including the standalone Kermit server it
    never asks about), and warns about sub-1024 ports and about an unauthenticated
    gateway with IP safety off. Choosing the Master role arms
    master_accept_relays but never silently enables the SSH server the relay
    needs: the role screen explains why a master wants it and offers a button, and
    the review screen warns if it is still off. The wizard edits a draft, so
    nothing reaches the config or the running server until its final Save and
    Restart Server
    ; exiting or skipping writes exactly one key. Re-runnable from
    Server — More… → Run setup wizard…. It is deliberately GUI-only — telnet and
    the web UI already expose every key it touches. New key
    setup_wizard_completed, asymmetric on purpose: false for a config the
    gateway creates itself (a fresh install sees the wizard), but a config file
    that lacks the key reads as true, so an upgrade is never dropped into it.

  • A loud warning when the listeners don't come up. Each listener already
    logged its own bind failure, but one line in the startup chatter is easy to
    miss and the process kept running afterwards, quietly serving nothing — the
    failure mode being a second copy of the gateway started without stopping the
    first, where the old process holds the ports and everything you connect to is
    still served by the old binary. It looks exactly like "my settings changed
    nothing". src/bindwatch.rs now collects each listener's outcome and, once
    they have all reported, says so: NONE of the N configured listener(s) could
    bind
    , which ports, that the process is serving nothing, that another copy is
    almost certainly holding them, and the commands to find and stop it
    (pgrep/ss/pkill, or netstat/Task Manager on Windows). A partial
    failure gets a shorter note; a total failure that is not address-in-use
    (e.g. ports below 1024 without root) warns without blaming a second copy; a
    serial-only setup with no listeners stays quiet.

  • The SSH server binds its own socket (run_on_socket rather than
    run_on_address, which is literally bind-then-run_on_socket). A bind failure
    is now reported as one: the old form logged "SSH server listening on port
    2222" first and then surfaced "Address already in use" as a generic
    post-hoc "SSH server error", which read as though the port had come up.

  • A slave's modem ports now register with the master at startup, so the
    master can ring them.
    The registration machinery existed but was gated
    behind allow_peer_dial, which meant a slave with the default settings could
    never be reached from its own master — the master's Serial Gateway menu
    simply never listed the port, and nothing contacted the master until the
    attached device happened to dial out. That gate was the wrong one:
    allow_peer_dial governs this gateway dialing arbitrary peers, whereas
    master/slave is already an explicit, mutual, authenticated pairing (the slave
    holds the master's credentials; the master sets master_accept_relays). A
    third party reaching the port through the master's crossbar is still gated, on
    the master. The announcer's log lines now describe registration rather than
    peer-dial.

  • The slave's CP/M endpoint is registered for the server's lifetime, the way
    Ports A and B are — not only while someone happens to have the emulator open.
    It used to announce per CP/M session, so CPM@<slave-ip> blinked in and out of
    the master's Serial Gateway list and the master could not see that the endpoint
    existed at all. A call arriving with no session running rings the answer pool,
    finds it empty and is reported unanswered — exactly like dialing a modem port
    whose device is switched off. Declines to register when the emulator is off or
    its virtual modem is off, since nothing could ever answer.

  • The slave-link summary stopped giving a stale reason. It printed
    CPM emulator not announced (needs allow_peer_dial) — true when the
    announcer was gated on that flag, and after the fix below merely misleading: it
    sent an operator hunting for a setting to change. It now names the real reason.

  • The router-block fallback is per address family. The x.x.x.1 fallback
    keyed off "no router detected at all", so a host where only an IPv6 default
    route was found would stop applying it to IPv4 — quietly weaker than the
    operator asked for, since the IPv4 router was still unknown. It now keys off
    whether that family's router is known.

  • Two config keys were in no documentation table: allow_peer_dial (a
    security-relevant one) and kermit_wait_for_receiver, though Appendix A claims
    to list every key the parser recognises. Both are documented now, along with
    the auto-managed gui_window_geometry, and allow_peer_dial's description
    states what it does not gate (registration).

  • The slave's CP/M endpoint registers with the master too. Same wrong gate
    as the serial ports: cpm_slave_announce and its spawn site both required
    allow_peer_dial, so on a default slave the CPM endpoint never announced and
    CPM@<slave-ip> was unreachable from the master. Registration is now
    automatic whenever the role is slave and a master host is set (the CP/M
    guest dialing out to a peer port stays gated, in cpm_modem.rs), and it
    re-announces on a master-settings change like the ports do.

  • A config change re-registers both slave ports. A registration is a
    standing claim — the master holds an idle channel and rings it later — so one
    made under settings that have since changed is worse than none. Both the
    modem announcer and the console register loop now watch a fingerprint of what
    a registration depends on (role, the master's host/port/username/password, and
    that port's own enabled flag, mode and device path) and re-register within
    about a second of any of it changing, whichever UI made the change. Unrelated
    edits — a weather location, a timeout — deliberately don't disturb a live
    registration, and a call in progress is never interrupted (the check only runs
    while idle).

Changed

  • Gateway Shell now surfaces the CP/M "destination first" operand order.
    COPY and MOVE take the destination before the source (COPY dst src) —
    the reverse of the order most users expect today. The shell now prints two
    reminder examples on entry, and a failing COPY/MOVE (e.g. "File not
    found." after the operands were swapped) echoes the correct form
    (e.g. COPY dst src (dest first)) so the mistake is self-correcting.

  • CP/M settings are named where they live, and the way out is spelled out.
    The GUI frame + its "More" popup and the web card + its modal that hold the
    CP/M enable toggle, runaway ceiling, and virtual-modem port are now titled
    "AI Chat, Browser, Weather & CP/M" (and "… & CP/M — More"), so the CP/M
    settings are discoverable rather than hidden behind an AI/Browser/Weather
    label. The emulator's entry banner now shows a prominent "Type EXIT to return
    to the gateway." line beside the "Press ESC twice to stop a program." hint.

  • The CP/M virtual modem is documented as polled-only. The emulated UART is
    polled (the guest reads the status register for RX/TX readiness); the core
    never raises a serial interrupt in any Z80 interrupt mode. This holds for
    every port profile — the family (Z80 SIO / 6850 ACIA / 8080 88-SIO) only
    selects the I/O port address and status-bit layout, not interrupt support —
    so polled comms software works on any profile while interrupt-driven serial
    software is unsupported. Noted in the manual and the uart module.

  • The CP/M emulator and its virtual modem are on by default.
    cpm_emu_enabled was default-off while the emulator was being built out; it is
    on now because the feature is bounded (guest jailed to transfer_dir/CPM,
    runaway stopped by cpm_emu_max_minstr, always escapable with a double-ESC,
    no path to a host command) and it ships with a terminal of its own. The
    virtual modem now defaults to rc2014_1b — the port EGT80 expects — so the
    emulator and its terminal work together untouched. Both remain settable:
    cpm_emu_enabled = false for no guest code at all, cpm_emu_uart = off for an
    emulator with no modem (guest code can dial out when a port is selected).

  • A one-click “Default port” for the CP/M virtual modem in all three UIs
    (telnet D in the CP/M panel, a button in the web CP/M section, a button
    beside the GUI combo): resets cpm_emu_uart to the default whatever it was
    showing — the answer to “I changed something and now the terminal cannot
    connect”. Guarded by a test that fails if EGT80's own default port and the
    gateway's default profile ever drift apart, since that pairing is the whole
    point and neither build would otherwise notice.

  • “Block connections from the router” (disable_gateway_connections) in all
    three UIs, on the row under the login checkmarks. Off by default, which
    changes behaviour: a connection from the local router is now allowed while
    the IP allowlist stays in force. It used to be refused outright, which left
    disable_ip_safety (dropping the allowlist entirely) as an operator's only way
    in; the narrow rule is now the opt-in. Loopback is exempt either way, public
    addresses are still refused, and the toggle applies on the next connection with
    no restart.

    • The router's address is queried from the OS, not guessed. Earlier work
      assumed the router was x.x.x.1 — a convention, not a fact, which both
      missed a router living on .254 and refused an ordinary machine that
      happened to sit on .1. src/router.rs reads the default route's next hop
      (Linux: /proc/net/route + /proc/net/ipv6_route directly, no subprocess;
      macOS/BSD: route -n get default; Windows: route print, matched on the
      addresses rather than the localised column headings) and the rule blocks
      exactly that, IPv4 and IPv6, including both routers on a multi-homed host.
      The query runs on a background thread at startup and re-runs when the cached
      answer ages past five minutes, so a DHCP change is picked up without a
      restart; it is never run on the connection path, which only reads the
      cache. The detected address is logged at startup and named in the checkbox
      label in every UI — "Block connections from the router (192.168.1.1)". Where
      it cannot be determined the rule falls back to the x.x.x.1 convention (per
      address family), so it is never silently weaker; loopback stays exempt, and
      no detected address can widen the allowlist. Every parser is a pure function
      tested from captured output on all three platforms, including a localised
      (German) Windows route table.
  • A slave's log now tells the whole story. Each attempt to reach the master
    is logged with its number (announcing to master 10.1.2.3:2223 as 'relay' (attempt 5)), so a slave that cannot connect no longer looks identical to one
    sitting idle — previously the failure reason was deduped (rightly, it repeats
    forever) and nothing showed the retries still happening. On success there is a
    plain CONNECTED to master … line followed by one consolidated block naming
    every port, the mode it is in, and what the link is doing:

    Slave link to master 10.1.2.3:2223 —
        Port A  mode=modem   registered — awaiting a pick from the master
        Port B  mode=kermit  bridging — a master user is attached
        CPM       emulator  announced — dialable as CPM@this-host
    

    Disabled ports are listed as disabled rather than omitted, because "why is
    port B missing?" is exactly what a summary should answer. The CP/M announcer
    also gained what the serial loops already had: its failure reason is now
    logged (it used to be discarded, leaving identical lines with nothing to act
    on) and its retry backs off 1s→30s instead of hammering a dead master once a
    second.

  • EGT80 v0.7 — the screen is cleared, and everything configurable is under
    Settings.
    The screen is cleared and the "Ethernet Gateway Terminal" banner
    redrawn before the main menu and on entering terminal mode, so it stays clear
    which program you are talking to once a remote system has filled the screen;
    the terminal-mode banner now also names the menu key and says that the key
    followed by E returns to the main menu. CP/M has no standard clear-screen, so
    a new Settings item picks the dialect: ANSI ESC [ 2 J (the default —
    what a terminal emulator over USB serial and the gateway's own ANSI clients
    understand), ADM-3A ^Z (a period terminal, or a PETSCII C64 through the
    gateway, whose translation re-renders it), or off for a printing terminal. ^Z
    was the first default, on the reasoning that its failure mode is silent rather
    than [2J printed as litter; it moved to ANSI because silent-failure is what a
    modern terminal on real CP/M hardware actually got. Because a clear can wipe a message before it is read, the
    places where a message is the only feedback — a damaged settings block, a save,
    a refused port family — now pause for a keypress.

    The serial-port selector moved off the main menu into Settings, which now
    shows the port, the menu key and the clear dialect alongside the filter mode.
    The menu key is any control key you press rather than a cycle through three
    fixed choices, because which key is free depends on the remote system (^Y is
    WordStar's delete-line, ^] telnet's escape, ^\ Kermit's). Five keys are
    refused with the reason: ^C backs out of every screen, CR/LF/TAB are ordinary
    typing, and ESC begins the arrow-key sequences, so a cursor key would open the
    menu. A saved key is validated at startup too, since an invalid one would trap
    that key for ever.

    • A wrong HBIOS unit froze EGT80 instead of reporting a dead port.
      Reported from an SC126: with the wrong port selected, the "nothing was sent"
      notice appeared as designed — and then ^C did nothing and the machine had
      to be reset. The published API is explicit that CIOIST/CIOOST return a
      count in A and that "negative values (bit 7 set) indicate a standard HBIOS
      result (error) code". Under EGT80's vector contract, non-zero means ready, so
      an error read as "a character is waiting"; the driver then called CIOIN,
      which the API says "will wait indefinitely". The terminal loop never got back
      to the keyboard, so the menu key and ^C were both dead. Those two status
      calls now treat a negative result as not-ready, which turns the hang into the
      diagnosis EGT80 already had: the transmitter never comes free, PSEND gives
      up, and the wrong-port notice appears with ^C live. The emulator's own
      CIOIST/CIOOST now cap their counts at 0x7F for the same reason — a
      count with bit 7 set would read as a failure on a healthy port.
    • A refused AT command is logged verbatim, and debris before AT is
      skipped.
      ATDT ethernetgateway intermittently answered ERROR from an
      SC126 in two different terminal programs, working when retyped. The new log
      line caught it on the first occurrence: refused command "CCatdt ethernetgateway" [43 43 61 74 …] — two XMODEM C handshake bytes, left in
      flight by a download that stopped early, arriving as the first characters of
      the next command line. Nothing legitimate precedes AT on a command line, so
      the command is now found rather than refused, with the skipped bytes logged.
      A real modem would answer ERROR; being stricter than the hardware here buys
      nothing and costs the user an unexplainable intermittent fault.
    • Transfers were silently corrupted on the HBIOS and AUX: drivers.
      Reported from an SC126: a downloaded file came back the correct length and
      entirely zero, with no error at either end. The cause is register
      discipline. The console entry points (CST/CIN) have always saved BC,
      DE and HL around the driver; the port entry points (PST/PIN/
      POST/POUT) were bare jumps that saved nothing. Three drivers touch only
      A and BC, so callers got away with holding a pointer in HL across a
      port call — but HBIOS is an RST 8 into RomWBW's firmware and AUX: is a
      BDOS call
      , and neither promises to preserve anything beyond its documented
      returns (real RomWBW returns values in HL for several functions).
      Both XMODEM inner loops walk the buffer with HL across exactly those
      calls, and the failure is quiet and total: on the send side the byte
      transmitted and the byte folded into the CRC both come from the wandering
      pointer, so they agree, the receiver's check passes, and a file of exactly
      the right length arrives full of whatever HL had wandered onto. On the
      receive side the bytes are stored through the wandering pointer while the CRC
      is computed on the byte in A, so again every block "passes". The four port
      entry points now save and restore BC/DE/HL exactly as the console ones
      do.
    • The emulator's HBIOS no longer preserves HL either. This bug could not
      be reproduced here because our RST 8 only set the registers the API
      documents, so HL survived — an emulator looser than the hardware turns a
      reproducible bug into a field report. The character-I/O and management calls
      now scramble HL (CIOQUERY and CIODEVICE are exempt: they return L).
      Verified afterwards with an independent XMODEM peer over TCP: a download and
      an upload on the port drivers are both byte-identical.
    • The post-transfer notice no longer costs a keystroke, and the hangup
      always finishes.
      Two faults reported from an SC126, both introduced by
      earlier fixes in this same area. The "press a key to return to the session"
      prompt collided with the far end's own prompt whenever that survived the
      line settle, so one event cost two keystrokes and the first appeared to do
      nothing but move the cursor down a line. It is now a statement rather than a
      prompt — Back in the session - press a key if the far end is waiting for one. — and the far end owns the keystroke it asked for. And the hangup's
      settle waited for silence that a talkative peer need never provide, so
      Hanging up... could be the last thing the program printed and the machine
      had to be restarted; it is now bounded to three drains and abandonable with
      ^C, because an exit path has to terminate whatever the other end does.
    • EGT80 hangs up when it exits. Leaving the program used to leave the call
      up: the gateway held the session open, and on a real phone line it would hold
      the line. There is no DTR to drop — the SIO and ACIA drivers do not touch the
      modem-control bits, HBIOS does not expose them, and CP/M AUX: has no notion
      of them — so the modem is asked, with the Hayes escape. That means honouring
      the guard time, and the guard is a duration, not an instruction count: the
      settle runs four timeout passes rather than one, because a poll loop that
      takes a second on a 4 MHz Z80 takes a fifth of that on an 18 MHz Z180, and an
      escape sent too early is not an escape — it is three plus signs typed at the
      far end. If there is no modem at all (a null-modem cable to another machine)
      the far end receives the characters as text; that is the price of not being
      able to tell, and it is smaller than walking away from a live call.
    • The virtual modem no longer forwards the +++ escape to the peer. A real
      modem swallows those characters and only sends them on if the sequence turns
      out not to be an escape — and this gateway's physical modem already did
      exactly that. The CP/M one forwarded them as they arrived, so every peer saw
      +++ whenever a guest hung up, which a guest that hangs up on exit makes
      every session. Held characters are flushed in order if the run breaks, so
      nothing is lost; a + inside a data stream is still ordinary data, protected
      by the same guard as before.
    • After an in-session transfer, EGT80 says a key is needed. Settling the
      line throws away whatever the far end said as the transfer ended — that burst
      is where a truncated escape sequence comes from, and a BBS's "press any key"
      lives in the same burst. The result was a screen showing Received. and then
      silence, with nothing to explain what to do, which reads as a hang. Both
      directions now end with Press a key to return to the session. and wait for
      one key. The key is not forwarded: the far end may not be waiting for
      anything, and a stray byte pushed into someone's menu is worse than pressing
      a key twice.
    • The line is settled after a transfer, so a lost ESC cannot print as
      litter.
      Reported from an SC126: after a download the screen showed 2J
      and did not clear — on a terminal that is definitely ANSI. The cause is the
      boundary, not the terminal. The far end starts talking the moment the last
      block is acknowledged (a BBS prints its own prompt, then redraws its menu)
      while this end is still closing the file — a BDOS close is disk I/O — and a
      polled UART holds exactly one byte, so those first bytes are overwritten and
      lost. ESC [ 2 J arriving without its first two bytes is the text 2J.
      EGT80 already had a line-purge routine, used before a receive and after a bad
      block but never at the end of a transfer; it now settles the line on every
      transfer exit, in both directions and from both the menu and terminal mode,
      and clears any half-seen escape sequence out of the filter so the session
      resumes on a boundary the far end will send whole.
    • A transfer inside a session no longer asks for a key. Reported from real
      use: after a download EGT80 said "Received.", waited for a key, and then
      something asked again. The second prompt was the far end's — the gateway's
      own File Transfer menu ends with "Press any key to continue." — so two
      programs each wanted a keystroke, back to back, which reads as a bug however
      it is explained. Every EGT80 transfer exit path was checked: none of them
      waits internally, the wait was in the caller. A transfer started from inside
      terminal mode now prints its result and returns straight to the session,
      which is what period terminals do and what a BBS expects: the BBS's own
      prompt and menu redraw follow naturally, and nothing is lost because
      returning to a session clears nothing. A transfer started from EGT80's own
      menu still waits for one key, because the menu redraw clears the screen and
      the result would otherwise vanish.
    • Coloured menus, and a terminal menu that explains itself. Headings,
      labels, values and the key letter of every menu line are now coloured.
      Colour follows the ANSI/ASCII setting already in Settings — that setting
      means exactly what colour depends on — so ASCII mode emits not one escape
      byte and a printing terminal or PETSCII console sees the plain text it always
      did. The key letters are coloured by a printer that recognises the shape
      every menu line already has (two spaces, key, two spaces, text), so the menus
      remain single blocks of text in the source and continuation lines are left
      alone; the escape sequences live in their own strings so the screen-fit test
      still measures printable width. The terminal-mode menu no longer says
      (twice=send), which told the user nothing: it now reads
      Menu: E)xit H)elp S)ettings U)pload D)ownload /
      ^Y again sends ^Y itself. Choice:, naming the actual key both times.
    • Assembly quality pass. A mechanical sweep of EGT80.Z80 found and fixed:
      two source lines over the 80-column house limit; HBCNT written but never
      called (the HBIOS unit list scanned a fixed 0–3 instead of asking how many
      units exist — it now asks, lists only those, and refuses a unit the firmware
      does not list); an orphaned label left behind when the detection hint replaced
      the older notice; and two label pairs that collide within their first six
      characters
      MASCHB/MASCHB2 (new) and XREADY/XREADY1 (latent since
      the transfer code was written). The six-character rule is in the editing rules
      because a stricter assembler may treat such a pair as one label and merge
      them silently; both are renamed. Also SHRATE fell through to the baud table
      for the 88-SIO, printing a rate for a chip with no rate register — every
      switch on the port family was audited for the new driver. Screen conventions
      are now uniform: every full screen clears first, with the one deliberate
      exception commented (the ASCI menu must not clear, or it would wipe the base
      it just reported).
    • The port menu names machines rather than chips. Choosing a port meant
      choosing a chip family first, which is a question only someone who already
      knows their board can answer — and two entries claimed an SC126 (Z180 ASCI
      and RomWBW HBIOS), with only one of them able to work. The top level now
      reads: the gateway's own emulated port (option 1, and the shipped default, so
      EGT80 and the gateway work together with nothing set), RomWBW firmware, the
      Altair 88-2SIO, the Altair 88-SIO, "other hardware", and CP/M AUX:. The old
      chip list survives intact one level down, free-form address entry included, so
      nothing became unreachable. A line above the list states which key follows
      from what the program can determine about the machine — RomWBW present, a Z180
      without it, or neither — which is as far as detection can honestly go, since
      probing an unmapped port on real hardware is not something to do blind.
    • New driver: the Altair 88-SIO (the original MITS board, 0x00/0x01 or any
      address), as its own menu item. It is a different board from the 88-2SIO, not
      just a different address: it reports ready by pulling a bit low, so a 6850
      driver pointed at it reads every test inverted and the port appears
      permanently busy and permanently empty at the same time. Verified in the
      emulator against the altair_sio profile: ATOK, ATI identifying the
      modem.
    • Z180 ASCI reaches the port the way the machine actually works. Reported
      from an SC126: atdt produced no error but a few random characters, while
      other comms software on the same wire was fine. The reason is not addressing — it is
      ownership. RomWBW's SC126 build enables its ASCI driver with interrupts in
      mode 2, so an interrupt handler is draining the receiver into the firmware's
      own buffer; a program polling the same registers races that handler, which
      reads the data register first. The transmitter still accepts bytes, hence no
      error. This is exactly why RomWBW-targeted builds of such software exist.
      Choosing a channel under option 4 therefore now asks the firmware whether it
      serves that channel, and if it does, reaches it through HBIOS the way
      RomWBW-targeted software does — saying so, and naming the unit. Direct register
      access remains for a Z180 the firmware is not driving, where it is the only
      way in. The port menu also states when RomWBW is detected, so a user need not
      know which of the two options applies to their machine.
    • Setting the ASCI I/O base no longer looks like selecting the port.
      Reported from real use: choose Z180 ASCI, set the base, back out — and the
      port menu still showed the old port, because the family is only selected
      when a channel is chosen. The base is now filled in from RomWBW on the way
      into that menu (so on a RomWBW machine it is not a step at all), the menu
      shows the base in force and says nothing is selected until a channel is
      picked, both base setters end with "now pick the channel", and Q says it
      leaves the port as it is.
    • The opening menu names the menu key, so terminal mode is visibly not a
      one-way door before you enter it: Keys: ^Y leaves terminal mode for the menu; then E returns here. The Settings item for the clear-screen dialect is
      now just Clear Screen.
    • On a RomWBW machine EGT80 asks the firmware instead of guessing. The
      HBIOS unit prompt now lists the character units that actually exist, each
      with the device type and base I/O address RomWBW reports for it
      (CIOCNT + CIODEVICE, both published calls), so "which unit?" is read off
      the screen rather than guessed; a unit that does not exist says so. The Z180
      ASCI menu gained R, which takes the register base from that same answer —
      necessary because C0 is only mostly right (RomWBW uses C0 on the Small
      Computer, RC2014-Z180, SZ180, GMZ180, DYNO and EPITX platforms, but 40 on
      N8, MK4, N8PC and RPH). The reported base belongs to a physical channel and
      channel 1 sits one address above channel 0, so the channel is subtracted to
      recover the block base.
    • The emulator's HBIOS no longer claims to be a 16C550. CIODEVICE
      reported device type 0x00 on the reasoning that zero meant "no driver" —
      but 0x00 is CIODEV_UART in the published list, so the answer was a
      definite claim to be a chip we are not. Nothing noticed until EGT80 began
      displaying that field and the virtual modem listed itself as
      UART base 00. It now reports a type outside the published range, which
      decodes as "not one of the known drivers" — the truth for a TCP connection.
    • The Z180 ASCI menu offers the RomWBW I/O base by name. The Z180's serial
      registers live inside the CPU and the register block is relocatable; RomWBW
      puts it at C0 on Small Computer Central boards, so EGT80's default of 00
      addressed nothing there and failed the way that family always fails —
      silently. C now sets C0 with the boards named on the menu, B still
      takes any base by hand, and the help says which is which. (On a RomWBW
      machine the HBIOS family sidesteps the question entirely: the firmware knows
      where its own registers are. That remains the recommended path, and the one
      RomWBW-targeted builds use.)
    • Every EGT80 screen is now checked to fit a 24×80 terminal, by a new test
      that parses the DB strings out of EGT80.Z80 — a source-level check, so it
      runs in CI even though the binary itself cannot be rebuilt there. It found
      help page 3 had been two rows over since it was written and page 2 going over
      as line settings were described in it, both of which pushed the page's own
      heading off the top; both are trimmed, and each help page now clears the
      screen instead of scrolling on below the previous one.
    • Line settings (baud rate, data bits, parity, stop bits) on a new Settings
      submenu, applied where a terminal genuinely can and refused with an
      explanation where it cannot. RomWBW HBIOS takes speed and framing in one
      CIOINIT call — the published line-characteristics word, whose baud field is
      an exponent pair (V = 75 × 2^X × 3^Y, bits YXXXX) rather than a rate, with
      RTS and DTR asserted deliberately because clearing DTR on a real modem drops
      the call. Z180 ASCI sets the rate and framing in CNTLA/CNTLB by
      read-modify-write, preserving the receiver and transmitter enables (reasoned,
      not run: our Z80 core has no IN0/OUT0). A 6850 ACIA sets framing and
      the ÷1/÷16/÷64 clock divider; the eight combinations it lacks (7 data bits
      without parity, 8 with parity and two stop bits) are refused by name rather
      than rounded to the nearest, because a framing mismatch presents as garbage
      characters. A Z80 SIO/2 has no baud generator at all — the bit rate comes
      from the board's clock or CTC and its registers are write-only, so EGT80
      declines rather than reprogram from a guess — and CP/M AUX: belongs to the
      OS. Against the gateway itself none of it applies in either direction: a TCP
      connection has no bit rate, so the emulated UART accepts line-configuration
      writes and ignores them. The default is therefore program nothing at all,
      with R to return to it: the port keeps whatever the ROM, firmware or OS set
      up. Applying makes it stick — it is re-applied whenever the port is selected,
      including at startup, guarded so a config carried to the wrong machine cannot
      RST 8 or IN0 on hardware that has neither. Verified end to end in the
      emulator: after applying 19200 7E2 through HBIOS, a CIOQUERY probe read back
      289E, exactly that framing plus RTS+DTR. The four ASCI register operands
      are patched by ASCPAT along with every other one in that family — a review
      caught them missing, which would have sent the CNTLB writes to CNTLA, whose
      receiver/transmitter enables would then have silenced the port.
  • EGT80: ^C backs out of every menu, not just the notice screens — the port
    family list, all four per-family prompts, and Settings — so one habit works
    everywhere. Q still does the same, and the menus say so.

Fixed

  • ATDT ethernetgateway now works from inside CP/M. The physical serial
    modem has always answered the gateway's own dial targets — the keywords
    ethernetgateway / ethernet-gateway / ethernet gateway and the built-in
    number 1001000 — but the CP/M emulator's virtual modem knew nothing about
    them: the keyword fell through to the TCP path, failed to parse as
    host:port, and came back NO CARRIER. Dialing it from EGT80 now spawns a
    session on this gateway's own menu over an in-memory duplex, exactly as the
    physical modem does, with raw serial semantics (no telnet IAC negotiation,
    whose bytes would reach the guest as garbage). Two more parity gaps went with
    it: a bare hostname no longer needs an explicit port (a target with no
    :port defaults to telnet's 23, so ATDT bbs.example.com dials instead of
    failing), and a phone number is looked up in the dialup phonebook as on
    the physical modem.
  • The CP/M virtual modem's AT settings now persist, as the physical ports'
    always have.
    AT&W was swallowed by the command parser's catch-all: it
    answered OK having stored nothing, and the modem was rebuilt at factory
    defaults on every entry to the emulator, so a comms program's init string had
    to be retyped each time. AT&W now writes the profile — echo, verbose, quiet,
    result-code level, DCD mode and all 28 S-registers — to new cpm_emu_* keys,
    and the modem powers up with it. ATZ restores the saved profile rather than
    the factory one, matching both a real modem and this gateway's serial ports,
    while AT&F remains the command that ignores it. Values are clamped and an
    unparsable S-register list falls back to the power-on registers, so a
    hand-edited file cannot produce a state the AT layer never would. The profile
    is editable in the web and GUI config editors beside the other CP/M settings —
    the same treatment the ports' AT&W fields get, and for the same reason.
    Verified end to end: ATE0 S0=3 S7=20 &C0 then AT&W, gateway restarted, and
    the emulator came back with echo off, S0=3, S7=20.
  • ATD <host> no longer eats the first letter of the hostname. The CP/M
    modem stripped a leading tone/pulse modifier after trimming, so any host
    beginning with T or P lost that letter when dialled without a modifier:
    ATD telnetbible.com silently dialled elnetbible.com and reported NO
    CARRIER. A T/P is now only a modifier where it sits against the D
    (ATDT host), which is how the physical modem has always behaved — it honours
    modifiers only inside a phone-like string. Found reviewing the dial path, and
    confirmed against a real BBS.
  • Backspace works at the CP/M modem's AT prompt. The byte was removed
    from the command line, but the echo was the raw BS/DEL — a bare BS only
    walks the cursor left and leaves the character on the screen, and EGT80's
    filter drops DEL outright as printing-terminal litter. Either way the line
    looked uneditable, so a typo could only be fixed by starting again. The echo
    is now a destructive erase (BS SPACE BS), and nothing is echoed when the
    line is already empty, as a real modem does.
  • The CP/M virtual modem no longer chokes on CR NUL line endings. An NVT
    telnet client writes a bare Return as CR NUL (RFC 854). The CR ended the
    command correctly, but the NUL stayed in the modem's line buffer and became
    the first character of the next command — so it no longer began with AT and
    came back ERROR. The first command of a session worked and every one after it
    failed, which made a parsing bug look like a dialling problem: a correct
    ATDT host:port was refused while the identical first attempt had been
    accepted. NUL is now ignored exactly as LF already was, which is what a
    real modem does with padding. Found from a user's screen, not from the tests —
    the test driver sent a bare CR, so nothing exercised the case; the CR-NUL
    sequence is now a regression test.
  • Kermit refuses a binary file declared as text, even with no length given.
    The length check added earlier can only fire when the peer declares a size,
    and the CP/M clients that hit this in practice do not: some send the file
    type but no length, and others send no attribute packets at all. A peer
    that declares TEXT mode and then sends bytes plain text never contains (NUL,
    and the other C0 codes that are not BEL/BS/TAB/LF/VT/FF/CR/ESC
    /^Z) is sending a binary file that stopped at the first ^Z, so the upload
    is now refused in-band on the content alone. The test is deliberately narrow —
    ANSI art, tabs, a trailing ^Z and high-bit text (WordStar, PETSCII, UTF-8)
    are all still accepted as text. A peer that declares nothing cannot be
    distinguished from a legitimate binary upload, so that case is logged with a
    warning rather than refused.
  • The CP/M emulator no longer reports a RomWBW system when none is
    configured.
    The RST 8 vector is only installed for an hbios_* profile,
    but the trap address itself is always live, so a guest that reached it another
    way (a CALL straight at it, or a stray jump) got a successful VER reply on
    a port profile — telling a program that probes before choosing how to reach
    its modem exactly the wrong thing. Every HBIOS function now fails when no
    HBIOS access mode is selected.
  • A CP/M program parked on a blocking HBIOS call now honours the session idle
    timeout.
    The timeout lived only in the console read path; the parked path
    polls the modem instead of blocking on the wire, so an abandoned session could
    sit in a 2 ms poll loop indefinitely. Any progress or keystroke resets it, so
    a program legitimately waiting for an inbound call is only closed when the
    user has actually gone away.
  • Kermit uploads that arrive incomplete are no longer saved as if whole.
    Three gaps let a corrupt upload reach disk silently — every packet's block
    check passed, the sender said "end of file", and we wrote what we had:
    • Truncated files are now refused. The receiver compares the byte count
      it collected against the length the sender declared in its attribute
      packet; a short file gets an E-packet (so the peer's user sees the failure)
      and is never committed. Arriving longer than declared stays legal — a
      text-mode sender declares its on-disk size and then expands line ends on
      the wire — and is logged, not refused.
    • A sender-abandoned file is now discarded, per spec §4.7. An EOF packet
      carrying D means the sender gave up part-way through; we previously ACKed
      it and kept the partial bytes, committing a truncated file. The record is
      now dropped and the session continues, so the rest of a batch still lands.
    • A peer uploading in text mode is now called out in the log. When the
      attribute packet declares ASCII/text file type, the receiver warns that
      binary files (.COM, game data) will be corrupted and names the one-line
      fix on the peer (SET FILE TYPE BINARY) — CP/M Kermit defaults to text
      mode, where the sender stops at the first ^Z.
  • Kermit server no longer retains every uploaded file in memory for the
    whole session.
    The server-mode dispatch loop now frees each received
    file's payload as soon as the on_file hook has committed it to disk, so a
    long-lived session on the always-on serial or standalone-TCP Kermit server
    (both reachable without authentication) can't accumulate every upload's full
    contents in memory across an unbounded number of transfers. Filenames and
    metadata are still returned for the post-session summary; no behavior change
    for callers (all committing already went through on_file).
  • CP/M emulator — correctness/stability fixes from a full review of the new
    emulator.
    None affect a released version (the emulator is new in 0.8.0):
    • Interactive programs no longer hang on console-status polling. BDOS 11
      (console status) and BDOS 6 sub-function 0xFE reported "no key ready"
      even when a keystroke was already buffered, so the standard
      LD C,11 / CALL 5 / OR A / JR Z poll idiom spun until the instruction
      ceiling — hanging full-screen / interactive .COMs. They now report a
      buffered key (both are non-blocking); BDOS 6 direct console input
      (E=0xFF) stays blocking (the common single-key / Y-N idiom), with the
      non-blocking poll served by the 0xFE status call and BDOS 11.
    • A telnet CR NUL Enter no longer skips a launched program's first
      prompt.
      A telnet client transmits a bare Enter as the NVT pair CR NUL;
      the command-line reader consumed the CR but left the NUL queued, so a
      .COM launched from that line (e.g. CLRDIR B:) had its first console
      read — often a Y/N confirmation — satisfied by the stray NUL and never
      waited for the user. The line reader now also drains the trailing NUL
      (and LF), so no terminator byte leaks to the next read.
    • A single ESC at a program's line prompt no longer drops the session.
      BDOS 10 (read-console-buffer) is now read through the same console path as
      the other calls: CR terminates, backspace edits, and a double-ESC
      aborts the program back to A> (a lone ESC was previously mistaken for a
      disconnect, and the "ESC twice to stop" promise did nothing mid-line).
    • A BDOS call made via the 0x0006 entry-address pointer is now serviced.
      Only the 0x0005 entry was trapped, so a program that called the BDOS
      address read from 0x0006 ran off into uninitialised memory.
    • The CP/M inbound-call request is cancel-safe. A request_cpm_call
      cancelled mid-wait (the slave announcer aborted on shell exit, or a dial
      racing shutdown) no longer leaves a stale call in the endpoint slot — which,
      with two or more concurrent CP/M sessions, could spuriously report BUSY to
      real callers or "answer" a dead call. Reclaimed via an RAII guard mirroring
      the A/B peer slot.
    • Existing files resolve case-insensitively. An operator-placed lowercase
      host file (foo.txt) that appeared in DIR can now actually be opened /
      TYPEd / renamed, not just listed — CP/M's uppercase 8.3 name is matched
      case-insensitively (new files are still created uppercase).
    • +++ escape guard time. The online +++ escape now requires a
      preceding idle gap (S12), so a +++ inside a binary data stream is treated
      as data instead of dropping the guest to command mode mid-transfer.
    • Altair 88-SIO honours transmit-not-ready. The altair_sio profile now
      clears its TX-ready bit when the transmit ring is full, so the no-byte-loss
      flow-control guarantee holds for it as it already did for the SIO / ACIA
      profiles.
    • STAT now reports real free space instead of 0 bytes. The disk-info
      BDOS calls STAT uses — 31 (Get Addr(DPB)) and 27 (Get Addr(Alloc)) —
      were unimplemented and returned 0, so STAT read a garbage disk-parameter
      block / allocation vector from address 0 and reported "0 bytes remaining"
      on every drive. The emulator now synthesizes a fixed 8 MB / 4 KB-block DPB
      and an allocation vector whose used bits reflect the drive's actual file
      usage, so free space is reported correctly.
    • More BDOS calls implemented (were silent no-ops returning 0). Function
      40 (write random with zero fill) now aliases function 34 instead of
      returning fake success while dropping the write — a real data-loss fix for
      any program that writes via 40. Function 24 (return login vector) reports
      all eight drives A:–H: active. Function 5 (list / LST: output) routes to
      the console so a program's printer output stays visible. Function 32
      (get/set user number) is now tracked and shared with the USER command, so
      a program's save/restore-user sequence is self-consistent (files remain a
      single flat area, not segregated by user — a documented simplification).
      Functions 7/8 (get/set I/O byte) now read and write the IOBYTE at its
      page-zero home (0x0003) so a set-then-get round-trip is consistent (device
      redirection has no effect in the single-console model, but the value is no
      longer silently dropped).
    • BIOS jump table for direct-console software. Programs that bypass BDOS
      and do console I/O straight through the BIOS jump table (MBASIC, WordStar,
      Turbo Pascal, Infocom games) now work: a real 17-entry CP/M 2.2 BIOS jump
      table is laid in high memory, the warm-boot pointer at 0x0001 points at its
      WBOOT entry, and each vector traps to the host, which services the console
      group (CONST/CONIN/CONOUT/LIST/PUNCH/READER/LISTST) against the live
      session. Also fixed: STAT B: no longer strands you at the B> prompt —
      a transient's internal drive select is now undone when it exits (the CCP
      re-selects its own default each command cycle, as real CP/M does), so only
      a bare d: command changes drives; and STAT's allocation vector no
      longer overran the new BIOS jump table.
    • All sixteen CP/M drives A:–P: now available (was A:–H:). CP/M 2.2's
      architectural maximum is 16 drives; the emulator now auto-creates a folder
      for each under CPM/ and reports all sixteen in the login vector. Each is
      a formatted, empty drive the instant its folder exists — the CP/M directory
      is synthesized from the folder's real files, so there is no format/CLRDIR
      step.
    • TYPE now paginates in the emulator. It previously streamed the whole
      file past the screen in one go; it now stops each screenful with the same
      --More-- (SPACE, RET, Q) viewer the Gateway Shell uses (SPACE = next page,
      RETURN = one line, Q/ESC = quit), expanding tabs and wrapping long lines to
      the terminal width.

Documentation

  • Documented that the Kermit client must be set to binary file mode before
    transferring programs or data.
    Vintage Kermit clients default to text
    (ASCII) mode, and a binary file sent that way silently loses bytes: a CP/M
    client reads the file in 128-byte records and in text mode treats ^Z (0x1A,
    the CP/M end-of-file pad) as padding rather than data, so a ^Z that falls on
    a record boundary is dropped and every following byte shifts down one
    position. Nothing in the protocol catches it — every packet's block check
    passes and the client reports success — so the file that lands is subtly wrong
    rather than obviously truncated. Diagnosed from a real SC126 upload where an
    8,704-byte WITNESS.COM and a 104,960-byte WITNESS.DAT arrived 1 and 8
    bytes short: nine dropped bytes out of 113,664, enough to stop the game
    running but far too few to notice by comparing file sizes casually.
    Re-uploading after SET FILE MODE BINARY produced files byte-identical to the
    originals. The gateway itself always transfers binary, byte-for-byte, in both
    directions — it never translates line endings and never trims padding — so
    this is purely a client-side setting, which is why a download can be perfect
    while an upload of the very same file is damaged. Documented in four places
    with the per-client commands (kercpm3 / CP/M Kermit SET FILE MODE BINARY,
    C-Kermit set file type binary / -i, MS-DOS Kermit): a new aside in user
    manual §8.8, a new user-manual troubleshooting entry §16.11 ("Transferred File
    Won't Run (Wrong Size by a Few Bytes)") framed around the symptom as a user
    meets it, and callouts on the web/kermit.html and web/kermitreference.html
    reference pages. Each notes that the short-file refusal added earlier in this
    release only fires when the client declares a length in its attribute packet —
    many small clients send none, leaving the receiver nothing to check the
    arriving size against — so binary mode is the habit to keep rather than
    something to rely on the guard for.
  • Documented that packet counts are not an integrity check. The same pages
    now warn against comparing the packet count of an upload against a download of
    the same file: control bytes and high-bit bytes are quoted into two or three
    wire characters each, so the expansion — and therefore the packet count —
    legitimately differs with the negotiated packet length and with which side is
    sending. A differing count is not evidence of data loss and a matching one is
    not evidence of success; compare checksums instead.
  • Regenerated usermanual.pdf from the updated HTML per versionchange.txt
    (WeasyPrint, Producer unchanged). The rebuild also picks up earlier HTML
    edits that had never been rebuilt, so the PDF grows by more than the entries
    above account for.