FujiNet Programming Guide for the Intellivision - FujiNetWIFI/fujinet-firmware GitHub Wiki
Programming the FujiNet for the Mattel Intellivision
A programmer's guide and command reference for driving FujiNet from IntyBASIC through the cartridge's shared-memory mailbox. Everything is PEEK and POKE.
This is the wiki edition of the FujiNet Programmer's Guide for Intellivision (the print-styled PDF lives in the fujinet-manuals repository under intv/fujinet-programmers-guide/). It covers the mailbox protocol, the Network and Fuji device command sets, AppKeys, the reusable fujinet.bas library, and three complete networked games β 5 Card Stud, Battleship and Fujitzee β together with their Go servers. By the last section you will have a working netcat on an Intellivision.
Source-verified. Every command byte, cell address, byte offset and wire format here was read out of the sources, not remembered: the cartridge firmware (
fujinet-firmware/pico/intellivision/βfuji_mailbox.h,fujinet.c,fujibus.h), the ESP32-S3 command handlers (lib/device/rs232/,lib/bus/rs232/,lib/device/fujiDevice/,include/fujiCommandID.h,include/fujiDeviceID.h), the CONFIG client (fujinet-config/intv/), the three game clients, and the game servers (fujinet-game-system). The example programs compile with IntyBASIC v1.4.2 and assemble with as1600 without errors.
See also: Fujinet Intellivision Mailbox Protocol for the firmware-side view of the same mailbox, and the AppKey Registry Page.
Contents
- Introduction
- The Cartridge Mailbox
- Your First Transaction
- Survival Rules for IntyBASIC Network Code
- The Network Device
- The Fuji Device
- The Clock Device
- The fujinet.bas Library
- Game One: 5 Card Stud
- Game Two: Battleship
- Game Three: Fujitzee
- The Lobby
- Error Codes
- The Library: fujinet.bas
- netcat.bas
- kbd.bas
Introduction
The Intellivision has no peripheral bus a FujiNet could sit on, so the Intellivision FujiNet takes a different road: the cartridge itself is the peripheral. Inside the cartridge are two computers:
Intellivision --CP-1610 bus--> RP2040/RP2350 (Minty fork) --USB CDC--> ESP32-S3 fujiversal-rs232 --WiFi--> Internet
your game PEEK/POKE $9C00-$9F3F mailbox FujiBus/SLIP
The RP2040 (or RP2350) serves your program's ROM and maps a small window of cartridge RAM at $9C00β$9F3F that both sides can read and write: the mailbox. Your program fills in a request β device, command, parameters, maybe a payload β and rings a bell. The RP2040 re-encodes it as a FujiBus packet and relays it over an internal USB serial link to an ESP32-S3 running the standard fujiversal-rs232 build of fujinet-firmware. The reply lands back in the mailbox.
Because the RP2040βESP32 wire format is byte-for-byte the FujiBus framing used elsewhere, every device and command ID below is the same one used by every other FujiNet platform.
What you can do from IntyBASIC:
- Open network connections by URL β
N:HTTPS://β¦,N:TCP://host:port/, TELNET, UDP, TNFS and more β then read, write and poll them. - Scan WiFi, manage host/device slots, browse server directories, mount disk images β and boot another ROM over the network.
- Store small records (player name, saved game, server URL) on the FujiNet with AppKeys.
- Read the real-world clock in seven formats.
- Use the FujiNet as a coprocessor: Base64, MD5/SHA hashes, QR codes.
The Cartridge Mailbox
The mailbox is 832 locations at $9C00β$9F3F. Each holds one byte in the low 8 bits of a 16-bit word β always mask reads with AND 255. It sits at the top of the $8000β$9FFF window so $8000β$9BFF (7168 words) stays free for JLP RAM (or your scratch buffers).
Memory map
| Address | Name | Who writes | Meaning |
|---|---|---|---|
$9C00 |
MAGIC0 | RP2040 | ASCII F (70) β mailbox present |
$9C01 |
MAGIC1 | RP2040 | ASCII N (78) |
$9C02 |
PROTO_VER | RP2040 | protocol version, currently 1 |
$9C03 |
SEQ | Inty | bump to start a transaction (wraps, skip 0) |
$9C04 |
ACKSEQ | RP2040 | set equal to SEQ when the reply is ready |
$9C05 |
DEVICE | Inty | device id ($70 Fuji, $71 N1:, β¦) |
$9C06 |
CMD | Inty | command id |
$9C07 |
NPARAM | Inty | parameter count, 0β8 |
$9C08/09 |
TXLEN | Inty | payload length, little-endian |
$9C0A |
STATUS | RP2040 | diagnostic only: 0 idle, 1 busy, 2 ok, 3 err |
$9C0B |
ERR | Inty | link result of last transaction (errors) |
$9C0C/0D |
RXLEN | RP2040 | reply payload length, little-endian |
$9C0E |
REPLY_CMD | RP2040 | $06 ACK or $15 NAK |
$9C0F |
LINK | RP2040 | 1 if the ESP32-S3 is up on the USB link |
$9C10-17 |
PARAM_SIZE | Inty | size of each parameter: 1, 2 or 4 bytes |
$9C18 |
BOOT_STATE | RP2040 | network-boot progress: 0 idle, 1 opening, 2 transfer, 3 mapping, $80 failed |
$9C19 |
BOOT_PCT | RP2040 | network-boot progress, 0β100 |
$9C1A |
BOOT_ERR | RP2040 | failure reason (errors) |
$9C1B |
DOORBELL | Inty | write $B5 β RP2040 reboots to BOOTSEL for reflashing |
$9C20-3F |
PARAM_VAL | Inty | 8 slots Γ 4 bytes, little-endian |
$9C40-$9D3F |
TX | Inty | request payload, up to 256 bytes |
$9D40-$9F3F |
RX | RP2040 | reply payload, up to 512 bytes |
The handshake
- Fill in DEVICE, CMD, NPARAM, the parameter table, TXLEN and the TX payload β everything except SEQ.
- Read ACKSEQ, add 1 (wrap past 255, skip 0), write it to SEQ. Last. Writing SEQ is what submits the request.
- Poll until ACKSEQ equals the SEQ you wrote (the library allows 900 frames β 15 s).
- Check REPLY_CMD:
$06= ACK, reply at RX with its length in RXLEN. Anything else β ERR holds the link-level reason.
Always derive the new SEQ from the ACKSEQ the RP2040 itself publishes, never from your own counter. Console Reset restarts your program and zeroes IntyBASIC variables β but not the RP2040. A locally-counted SEQ recomputes the value it already used, and your first transaction after every reset silently hangs.
The interlock is a sequence number, not a busy flag: re-poking the same SEQ mid-flight is a no-op, so a retry can never make the RP2040 run a command twice. STATUS ($9C0A) is diagnostic β poll ACKSEQ.
Parameters and payloads
Commands take up to eight numeric parameters (the "aux" bytes of other platforms). Parameter i is one word at PARAM_SIZE + i (its size: 1, 2 or 4) plus up to four words at PARAM_VAL + i*4 (value, little-endian, one byte per word). Bulk data β URLs, filenames, passwords β goes in TX with the count in TXLEN.
Replies stay in RX until the next transaction. Read replies in place β IntyBASIC has 228 eight-bit variables and a game-server reply is 400+ bytes. PEEK straight out of RX; copy only what must survive the next transaction into scratch RAM below $9C00.
Timeouts, the link byte, and network boot
The RP2040 gives an ordinary command 5 seconds on the USB link, MOUNT_IMAGE 60, and waits up to 3 seconds for the ESP32 to enumerate before the very first command. LINK ($9C0F) shows the live link state.
MOUNT_IMAGE on this platform boots the selected ROM: the ESP32 pushes the file (+ optional .cfg sibling) back through the cartridge, the RP2040 decodes it straight into cartridge ROM space, remaps, and resets the console. Progress is published out-of-band in BOOT_STATE / BOOT_PCT / BOOT_ERR so your wait loop can draw a progress bar. Flat .bin, Intellicart .rom, and .bin+.cfg (including JLP variables) all load; a mapping that would collide with the mailbox is refused.
Declaring the window
ASM MEMATTR $8000, $9BFF, "+RWN"
Stop at $9BFF. On real hardware the RP2040 maps the mailbox regardless, but jzIntv's --fujinet emulation registers its own handler for $9C00β$9FFF β a cartridge that claims those addresses as plain RAM shadows the emulated FujiNet and the mailbox never comes up under emulation.
Your First Transaction
' hello.bas -- ask the Fuji device for its WiFi status
GOTO main
INCLUDE "fujinet.bas"
CONST COL_WHITE = 7
main:
MODE 0, 0, 0, 0, 0 : WAIT
CLS
PRINT AT 0 COLOR COL_WHITE, "FUJINET HELLO"
PRINT AT 20, "WAITING FOR MAILBOX"
GOSUB fn_wait_mailbox
IF fn_ok = 0 THEN
PRINT AT 40, "NO MAILBOX - IS THIS"
PRINT AT 60, "A FUJINET CARTRIDGE?"
GOTO halt
END IF
' One transaction: device $70 (Fuji), command $FA (GET WIFI STATUS),
' no parameters, no payload. The one-byte reply lands in FN_RX.
mb_dev = $70
mb_cmd = $FA
mb_nparam = 0
#fn_txlen = 0
GOSUB fn_transact
IF fn_ok = 0 THEN
PRINT AT 40, "TRANSACTION FAILED "
PRINT AT 60, "FN ERR = "
PRINT AT 69, <2>#mb_err
GOTO halt
END IF
IF (PEEK(FN_RX) AND 255) = 3 THEN
PRINT AT 40, "WIFI IS CONNECTED "
ELSE
PRINT AT 40, "WIFI NOT CONNECTED "
END IF
halt:
WAIT
GOTO halt
That is genuinely all there is.
Survival Rules for IntyBASIC Network Code
Every rule below was paid for in debugging time on the three games.
- Jump over your includes. INCLUDE pastes text in place, and falling into a
PROCEDUREorDATAblock corrupts the return stack. Line one of your program:GOTO main. - Respect the memory map. IntyBASIC compiles into
$5000β$6FFFand keeps going past the end if you outgrow it β into$7000, which is not safe on modern cartridge PCBs. AddASM ORG $D000so cold code lands in$C100β$FFFF. (Fujitzee's boot genuinely hung on real hardware from a 20-word spill jzIntv forgave.) Safe ranges:$2000-$2FFF,$5000-$6FFF,$A000-$BFFF,$C100-$FFFF. - Parenthesize
(PEEK(x) AND 255). IntyBASIC's=binds tighter thanANDβPEEK(x) AND 255 = 0compiles asPEEK(x) AND (255 = 0), which is always zero. - Mask into 16-bit variables. IntyBASIC v1.4.2 silently drops the
AND 255when the destination ofvar = PEEK(...) AND 255is an 8-bit variable (no ANDI is emitted).#-prefixed 16-bit destinations compile correctly. - Nest IFs instead of chaining AND. Two equality comparisons joined by
AND, where a side comes from aDEF FNcontaining its ownAND 255, can miscompile to always-false. - Use
<.n>not<n>in PRINT. Zero-padded<n>can print garbage in unused leading digits; space-padded<.n>initializes its pad register. - Wait for key release.
CONT1.KEY= 12 means "nothing pressed" β wait for it before reading a new key. And checkCONT1.B1/B2beforeCONT1.BUTTON(BUTTON masks all three action buttons).
The Network Device
Devices $71β$78 are eight independent connections, N1: through N8:. A connection is named by a devicespec staged in TX at OPEN:
N:PROTOCOL://HOST[:PORT]/PATH[?QUERY]
Protocols: HTTP, HTTPS, TCP, UDP, TELNET, TNFS, FTP, SMB, NFS, SSH. TLS, redirects and DNS are the ESP32's job.
Life cycle: OPEN β STATUS (until data) β READ/WRITE β CLOSE. Two subtleties: for HTTP, the actual request happens at the first STATUS, and STATUS reports bytes available so far β poll until two consecutive reads agree (the library's api_call does). An HTTP error page is still a readable body β check the status error byte (1 = success), not just the byte count.
Command reference
| Cmd | Name | Parameters / payload | Reply |
|---|---|---|---|
$4F 'O' |
OPEN | p0 mode, p1 translation (0 none, 1 CR, 2 LF, 3 CRLF, 4 PETSCII); payload devicespec | ACK/NAK |
$43 'C' |
CLOSE | β | |
$52 'R' |
READ | p0 length (2 bytes) | data in RX |
$57 'W' |
WRITE | p0 length (2); payload data | |
$53 'S' |
STATUS | p0 0, p1 type | 4 bytes: avail lo, avail hi, connected, error |
$FC |
CHANNEL MODE | p0 0, p1: 0 raw, 1 JSON, 2 SGML | |
$50 'P' |
PARSE | β (JSON/SGML mode) | |
$51 'Q' |
QUERY | payload: query path e.g. /0/name |
then STATUS len + READ |
$4D 'M' |
HTTP CHANNEL | p0 0, p1: 0 body, 1 collect hdrs, 2 get hdrs, 3 set hdrs, 4 POST data | |
$54 'T' |
TRANSLATION | p0 0, p1 mode | |
$4C 'L' |
SET EOL | p0/p1 EOL bytes (p0=0 restores default) | |
$25 '%' |
SEEK | p0 offset (4) β HTTP does a Range request | |
$26 '&' |
TELL | β | 4-byte LE offset |
$30 '0' |
GETCWD | β | 256-byte prefix |
$2C ',' |
CHDIR | payload path | |
$FD |
USERNAME | payload | |
$FE |
PASSWORD | payload | |
$41 'A' |
TCP ACCEPT | β (listening socket: N:TCP://:6502/) |
|
$63 'c' |
TCP CLOSE CLIENT | β | |
$44 'D' |
UDP SET DESTINATION | payload host:port |
|
$20 |
RENAME | payload old,new |
|
$21 '!' |
DELETE | payload spec | |
$23/$24 |
LOCK / UNLOCK | payload spec | |
$2A/$2B |
MKDIR / RMDIR | payload spec |
STATUS with no channel open and p1 = 1/2/3/4 returns the adapter's IP / netmask / gateway / DNS, 4 bytes each.
OPEN access modes:
| Mode | Meaning | Protocols |
|---|---|---|
| 4 | READ / HTTP GET | all |
| 5 | HTTP DELETE | HTTP(S) |
| 6 | DIRECTORY | filesystem protocols |
| 8 | WRITE / HTTP PUT | all |
| 9 | APPEND / HTTP DELETE+headers | file / HTTP(S) |
| 12 | READ-WRITE / HTTP GET+header access | sockets, HTTP(S) β what the games use |
| 13 | HTTP POST | HTTP(S) |
| 14 | HTTP PUT+header access | HTTP(S) |
The Fuji Device
Device $70 is the FujiNet's control panel. Everything CONFIG does, your program can do.
WiFi
| Cmd | Name | Parameters / payload | Reply |
|---|---|---|---|
$FA |
GET WIFI STATUS | β | 1 byte: 3 = connected (other values are ESP32 WL codes) |
$FD |
SCAN NETWORKS | β | 1 byte: count |
$FC |
GET SCAN RESULT | p0 index | 34 bytes: SSID[33] + RSSI (signed) |
$FB |
SET SSID | p0 any (required!); payload 97 bytes: ssid[33]+password[64] | joins & saves |
$FE |
GET SSID | β | 97 bytes |
$EA |
GET WIFI ENABLED | β | 1 byte |
The exact-length payload rule. The ESP32 handler for a fixed-size payload fails if fewer bytes arrive than it expects β SET SSID wants exactly 97, OPEN DIRECTORY and SET DEVICE FULLPATH exactly 256. Pad with NULs to the documented size.
Hosts, directories, mounts
| Cmd | Name | Parameters / payload | Reply |
|---|---|---|---|
$F4 |
READ HOST SLOTS | β | 256 bytes: 8 Γ 32 |
$F3 |
WRITE HOST SLOTS | payload 256 bytes (all 8 at once) | |
$F9 |
MOUNT HOST | p0 slot 0β7 | |
$E1/$E0 |
SET / GET HOST PREFIX | p0 slot; payload/reply prefix | |
$F7 |
OPEN DIRECTORY | p0 slot; payload 256: path NUL filter NUL padding | |
$F6 |
READ DIR ENTRY | p0 maxlen, p1 flags (0 plain, $80 details) |
name (+/ on dirs); EOF = two $7F |
$F5 |
CLOSE DIRECTORY | β | |
$E5/$E4 |
GET / SET DIR POSITION | p0 pos (2) | 2 bytes LE |
$F2/$F1 |
READ / WRITE DEVICE SLOTS | 8 Γ {host, mode, name[36]} | |
$E2 |
SET DEVICE FULLPATH | p0 dev slot, p1 host slot, p2 mode; payload 256 | |
$DA |
GET DEVICE FULLPATH | p0 slot | path |
$F8 |
MOUNT IMAGE | p0 slot, p1 flags (1 read) | boots the ROM on Intellivision (60 s budget) |
$E9 |
UNMOUNT IMAGE | p0 slot | |
$D7 |
MOUNT ALL | β | |
$E7 |
NEW DISK | payload {sectors u16, size u16, host, dev, name[256]} | |
$D8 |
COPY FILE | p0 src, p1 dst; payload spec | |
$D9/$D6 |
CONFIG BOOT / SET BOOT MODE | p0 | (no effect on Inty β CONFIG boots from cartridge flash) |
With $80 in READ DIR ENTRY's flags, a 12-byte prefix precedes the name: modified date (6), size (4 LE), flags (1: bit 0 directory, bit 1 truncated), media type (1).
AppKeys
Up to 64 bytes (256 in mode 2) stored on the FujiNet, addressed by creator id (16-bit, registered on the AppKey Registry Page), app id and key id.
| Cmd | Name | Parameters / payload | Reply |
|---|---|---|---|
$DC |
OPEN APPKEY | payload 6 bytes: creator lo, creator hi, app, key, mode (0 read, 1 write, 2 read-256), reserved 0 | |
$DD |
READ APPKEY | β | 2-byte LE length prefix, then the data |
$DE |
WRITE APPKEY | payload = value | |
$DB |
CLOSE APPKEY | β |
All six OPEN bytes are required β a 5-byte payload reads back as a timeout. The length prefix on READ is specific to this bus (the reply describes itself). Game-system slots (creator 1, app 1): key 0 shared player name, key 1 five card stud, key 3 fujitzee, key 5 battleship.
Adapter information
$C4 GET ADAPTERCONFIG EXTENDED returns 240 bytes ($E8 returns the first 140, without strings):
| Offset | Size | Field | Offset | Size | Field | |
|---|---|---|---|---|---|---|
| 0 | 33 | ssid | 125 | 15 | fn_version | |
| 33 | 64 | hostname | 140 | 16 | sLocalIP | |
| 97 | 4 | localIP | 156 | 16 | sGateway | |
| 101 | 4 | gateway | 172 | 16 | sNetmask | |
| 105 | 4 | netmask | 188 | 16 | sDnsIP | |
| 109 | 4 | dnsIP | 204 | 18 | sMacAddress | |
| 113 | 6 | macAddress | 222 | 18 | sBssid | |
| 119 | 6 | bssid |
Utility and coprocessor commands
| Cmd | Name | Notes |
|---|---|---|
$FF |
RESET | reboots the ESP32 (link drops and returns) |
$00 |
DEVICE READY | replies 512 Γ 'A' β link self-test |
$BB |
GENERATE GUID | fresh unique id |
$53 |
STATUS | p0 = 1: per-slot mount timestamps |
$D0/$CF/$CE/$CD |
BASE64 ENCODE input/compute/length/output | input: p0 len + payload; length: 4-byte reply; output: p0 len |
$CC/$CB/$CA/$C9 |
BASE64 DECODE | mirror set |
$C8/$C7/$C6/$C5 |
HASH input/compute/length/output | compute p0: 0 MD5, 1 SHA-1, 2 SHA-256, 3 SHA-512; length/output p0: 1 = hex text |
$C3/$C2 |
HASH compute-no-clear / clear | |
$BC/$BD/$BE/$BF |
QR input/encode/length/output | encode p0 version, p1 ecc, p2 shorten |
The Clock Device
Device $45, the APETime command set. GET commands accept optional p0 = 1 to use the alternate time zone set with $74.
| Cmd | Name | Reply |
|---|---|---|
$93 |
GET TIME | 6 bytes: day, month, yearβ2000, hour, minute, second |
$9A |
GET TZ TIME | same 6 bytes, always alternate zone |
$47 'G' |
GET SIMPLE | 7 bytes: century, year, month, day, hour, minute, second |
$4D 'M' |
GET SIMPLE + HUNDREDTHS | 8 bytes |
$50 'P' |
GET PRODOS | 4 bytes packed |
$53 'S' |
GET SOS | NUL-terminated string |
$49 'I' |
GET ISO LOCAL | NUL-terminated ISO-8601 |
$5A 'Z' |
GET ISO UTC | NUL-terminated ISO-8601 |
$99 |
SET TZ | payload POSIX TZ, saved |
$74 't' / $54 'T' |
SET ALT TZ | payload POSIX TZ, session only |
$4C 'L' |
GET TZ LENGTH | 1 byte |
The fujinet.bas Library
The full listing is at the bottom of this page. Include it after your opening GOTO:
GOTO main
INCLUDE "fujinet.bas"
main:
| Procedure | In | Out | Purpose |
|---|---|---|---|
fn_wait_mailbox |
β | fn_ok |
wait β€3 s for the mailbox magic at boot |
fn_transact |
mb_dev, mb_cmd, mb_nparam, #fn_txlen, staged params/payload |
fn_ok, #mb_err |
run one transaction (SEQ from ACKSEQ, 15 s guard, ACK check) |
fn_param |
pm_i, pm_size, #pm_val |
β | stage parameter i |
fn_putstr |
#fn_src, fn_len |
advances #fn_txlen |
append bytes (ROM DATA via VARPTR, or RAM) to TX |
fn_strlen |
#fn_src, ls_max |
fn_len |
measure a NUL-padded field (so padding never leaks into a URL) |
net_open |
devicespec staged in TX | fn_ok |
OPEN N1: mode 12, no translation |
net_status |
β | #net_avail, #net_err, fn_ok |
STATUS; folds "error β 1" into fn_ok |
net_read |
#net_readlen |
#net_gotlen, data at FN_RX |
READ (captures RXLEN before CLOSE clobbers it) |
net_write |
fn_len bytes at FN_TX |
fn_ok |
WRITE |
net_close |
β | fn_ok |
CLOSE |
api_call |
URL in TX, #net_readlen |
fn_ok, reply in FN_RX |
open β settle-poll STATUS β clamp β read β close |
appkey_open |
ak_creator_lo/hi, ak_app, ak_key, ak_mode |
fn_ok |
select a key |
appkey_read |
#fn_src (dest), ls_max |
fn_len, NUL-terminated dest |
read (handles the 2-byte length prefix) |
appkey_write |
#fn_src, fn_len |
fn_ok |
write |
appkey_close |
β | fn_ok |
close |
fn_putnum |
pn_val (0β999) |
appends to TX | decimal ASCII for URLs like /attack/47 |
The library owns scratch RAM $9100β$917F (SC_NAME player name, SC_ENDPT endpoint, SC_QUERY); the games start their own buffers at $9180.
Game One: 5 Card Stud
Sources: fujinet-5cardstud intv/ β server: fujinet-game-system.
A thin client that draws whatever the server says the table looks like β it never knows the rules of poker. Flow: boot β name from AppKey 1/1/0 (validated to AβZ 0β9, with one retry for a cold USB link) β Lobby room from AppKey 1/1/1 (split_room_url) β table select β poll loop.
URLs are composed byte-by-byte from DATA literals (IntyBASIC has no strings):
N:https://5card.carr-designs.com/state?table=r1&player=THOM&bin=1
Paths: tables?bin=1, state, move/XX (2-char code), leave β all with ?table=β¦&player=β¦&bin=1. bin=1 selects the server's binary serialization: fixed offsets, little-endian u16s, every string lowercased and NUL-terminated (hence "size + 1" fields). (&be=1 big-endian exists but was retired on Inty after byte-order bugs; little-endian is the well-trodden road.)
Tables reply: count (1), then 36-byte records: table[9] name[21] players[6] (literal "2 / 8").
Game reply (β€ 418 bytes):
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 81 | lastResult | one-shot message |
| 81 | 1 | round | 0 wait, 1β4 streets, 5 showdown |
| 82 | 2 | pot | u16 LE |
| 84 | 1 | activePlayer | signed, $FF none, 0 = you |
| 85 | 1 | moveTime | server-computed, minus 4 s network grace |
| 86 | 1 | viewing | 1 = spectator |
| 87 | 1 | validMoveCount | |
| 88 | 65 | validMoves[5] | 13 each: code[3] + name[10] |
| 153 | 1 | playerCount | |
| 154 | 33ΓN | players | name[9] status(1) bet(2 LE) move[8] purse(2 LE) hand[11] |
Wire index 0 is always you (the server rotates per client). Hands are ASCII pairs (2β9,t,j,q,k,a Γ d,h,c,s; ?? = hidden). Validation is layered: length β₯ 154, round β€ 5, playerCount β€ 8, then length β₯ 154 + 33 Γ playerCount β a short read leaves stale bytes from a previous larger reply in RX, and an HTTP error page fails the sanity gates instead of rendering as garbage.
Rendering is differential (the STIC has no page-flip): full CLS only when the layout changes; otherwise only changed cells are poked, one WAIT per seat so no burst outruns vblank. Your turn: the server names the moves (FO fold, CH check, CA call, BL/BH bet, RA raise, BB bring-in); the client draws them, counts down moveTime, and submits the highlighted one on timeout.
Server: Go/gin; per-table mutex; bots move in 3 s, humans get 39 s; a hidden test table is reserved for client developers.
Game Two: Battleship
Sources: fujinet-battleship intv/.
Four 10Γ10 boards at once β via the STIC's colored-squares mode: a BACKTAB word with bit 12 set and bit 11 clear is four independently colored 4Γ4-pixel quadrants. board.bas wraps it (cs_fill, read-modify-write cs_plot, board_cell); mind that the bottom-right quadrant's high color bit lives at bit 13, and the update mask must clear-and-re-add the enable bit (preserving it doubles bit 12 into bit 13 and the card snaps back to text mode).
The game's rule: one shot lands on the same coordinate of every opposing board. Requests add &bin=1&v=2 (v2 reveals the winner's ships at game over).
State reply: header β playerCount (1), prompt[33], status (1: 0 lobby, 1 place, 10 start, 11 miss, 12 hit, 13 sunk, 99 over), playerStatus (1: 0 playing, 1 defeated, 2 viewing, 3 ready, 10 placing), activePlayer (1, signed), moveTime (1). Then:
- Lobby: serverName[21] + 10-byte records {name[9], ready}.
- In play: lastAttackPos (1), myShips[10] (yours [0..4], winner's [5..9] at game over; each byte = cell + 100Γdirection, 0 across / 1 down), then 115-byte records {name[9], status, gamefield[100], shipsLeft[5]}. Gamefield: 0 unknown, 1 hit, 2 miss.
Endpoints: /state, /ready, /place/25,113,4,167,89 (five encoded ships), /attack/47, /leave, /tables. Placement is validated client-side against a local occupancy map; the server's myShips echo is authoritative (covers reconnects). Miss/hit/sunk sounds pair status with lastAttackPos so they fire once per event, not per poll. Timing: bots 3 s, humans 45 s (250 s alone), +5 s after a status change, 4 s grace.
Game Three: Fujitzee
Sources: fujinet-fujitzee intv/.
A 15-category scorecard per player doesn't fit a 20Γ12 screen β so the client shows one player's card at a time (disc pages it; the view auto-snaps to whoever's turn begins), beside a permanent seat strip, dice row and prompt.
State reply (same shape every round):
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 1 | playerCount | seats + spectators, up to 12 |
| 1 | 21 | serverName | |
| 22 | 41 | prompt | |
| 63 | 1 | round | 0 lobby, 1β13, 99 over |
| 64 | 1 | rollsLeft | |
| 65 | 1 | activePlayer | signed |
| 66 | 1 | moveTime | |
| 67 | 1 | viewing | |
| 68 | 6 | dice | 5 ASCII 1β6 + NUL; empty at turn start |
| 74 | 6 | keepRoll | 5 ASCII 0/1 + NUL β 1 rerolls |
| 80 | 15 | validScores | signed; $FF not selectable |
| 95 | 42ΓN | players | name[9] alias(1) scores[16] (u16 LE) |
Scores: 0β5 upper, 6β7 computed upper total/bonus, 8β14 lower (14 = Fujitzee), 15 running total; $FFFF unset. In the lobby scores[0] doubles as the ready flag (1 ready, $FFFE spectator). Note the ceiling: 95 + 12Γ42 = 599 bytes exceeds the 512-byte RX window β at most 9 player records fit; seats cap at 6, so play is unaffected, but a spectator-heavy table can make polls fail the length gate.
Your turn is a two-mode machine: DICE (toggle keep flags, submit /roll/01100) and SCORE (pick a category, submit /score/11 β wire index, skipping computed rows 6β7). Out of rolls, the client auto-enters SCORE mode parked on the best open category. When someone else scores, the client diffs their scores against a per-poll shadow and flashes the picked cell for ΒΎ s β so a bot's instant move is still a visible event. Rolls animate on every card via rollsLeft/turn-change detection.
Endpoints: /state, /ready, /roll/:keep, /score/:index, /leave, /tables. Timing: bots 3 s, humans 45 s (15 s penalized, 250 s alone), start countdowns 31/6/3 s. All scoring is server-side.
The Lobby
The FujiNet Lobby is the directory of live game rooms:
- Game servers upsert themselves β POSTing a GameServer JSON record (game, region, server URL with
?table=per room, cur/max players, per-platform client download URLs) whenever a room's population changes; game results at match end. Development servers talk toqalobby.fujinet.online. - The Lobby client hands off through AppKeys β writing
https://server/?table=idinto the game's registered slot (creator 1 / app 1 / key 1, 3 or 5) and booting the game client. - The game rejoins on its own β reading its slot at boot, splitting at the
?(validatingtable=+ alphanumerics; the value goes into URLs unescaped). Picking a table by hand writes the slot back (console reset rejoins); deliberately quitting clears it.
Treat both the shared username slot (key 0) and the room slots as hostile input β validate byte-by-byte.
Error Codes
Mailbox link (ERR, $9C0B): 0 OK Β· 1 ENOLINK (no USB link β check LINK, wait, retry) Β· 2 ETIMEOUT Β· 3 EBADFRAME Β· 4 ETOOBIG.
Network status error byte: 1 SUCCESS Β· 131 write-only/read-only family (131/135) Β· 132 invalid command Β· 136 EOF Β· 138 timeout Β· 144 general Β· 146 not implemented Β· 151 file exists Β· 162 no space Β· 165 invalid devicespec Β· 166 invalid POINT Β· 167 access denied Β· 170 file not found Β· 200 connection refused Β· 201 network unreachable Β· 202 socket timeout Β· 203 network down Β· 204 connection reset Β· 205 already in progress Β· 206 address in use Β· 207 not connected Β· 208 server not running Β· 209 no connection waiting Β· 210 service unavailable Β· 211 connection aborted Β· 212 bad credentials Β· 213 could not parse JSON Β· 214 client error (HTTP 4xx) Β· 215 server error (HTTP 5xx) Β· 255 could not allocate buffers.
Network boot (BOOT_ERR, $9C1A): 1 REJECTED (bad ROM header) Β· 2 TRUNCATED Β· 3 NOMAP (no .cfg, no header, unknown size) Β· 4 MAILBOX (mapping collides with the mailbox) Β· 5 CFGBAD.
The Library: fujinet.bas
' fujinet.bas -- FujiNet mailbox transport + network/appkey primitives.
'
' Mailbox layout is the hand-synchronized copy of
' fujinet-firmware/pico/intellivision/firmware/fuji_mailbox.h, exactly as
' proven working by intv/fujitest.bas in that repo. Do not change these
' addresses without re-checking that file.
'
' MEMATTR intentionally stops at $9BFF, short of the $9C00-$9FFF mailbox
' itself: on real PiRTO II hardware the RP2040 maps that whole window as
' RAM unconditionally (inty_cart.c hardcodes it, independent of what this
' .cfg says), so declaring less here doesn't affect real hardware or what
' POKE can reach at runtime. But jzIntv's --fujinet peripheral emulation
' registers its own handler for $9C00-$9FFF *after* the cart's generic
' MEMATTR RAM, and its layered bus dispatch lets whichever peripheral
' registered first answer a given address -- so declaring the full
' $8000-$9FFF range here (as fujitest.bas does) silently shadows the
' emulator's FujiNet peripheral with inert RAM, and the mailbox never
' comes up under --fujinet even though it works on real hardware.
ASM MEMATTR $8000, $9BFF, "+RWN"
CONST FN_MAGIC0 = $9C00
CONST FN_MAGIC1 = $9C01
CONST FN_SEQ = $9C03
CONST FN_ACKSEQ = $9C04
CONST FN_DEVICE = $9C05
CONST FN_CMD = $9C06
CONST FN_NPARAM = $9C07
CONST FN_TXLEN_LO = $9C08
CONST FN_TXLEN_HI = $9C09
CONST FN_ERR = $9C0B
CONST FN_RXLEN_LO = $9C0C
CONST FN_RXLEN_HI = $9C0D
CONST FN_REPLY_CMD = $9C0E
CONST FN_PARAM_SIZE = $9C10
CONST FN_PARAM_VAL = $9C20
CONST FN_TX = $9C40
CONST FN_RX = $9D40
CONST FUJICMD_ACK = $06
CONST FUJICMD_NAK = $15
' Fuji device (config/appkey) commands.
CONST FUJI_DEVICEID = $70
CONST FUJICMD_OPEN_APPKEY = $DC
CONST FUJICMD_CLOSE_APPKEY = $DB
CONST FUJICMD_WRITE_APPKEY = $DE
CONST FUJICMD_READ_APPKEY = $DD
' Network device (N1:) commands.
CONST NET_DEVICEID = $71
CONST NETCMD_OPEN = $4F
CONST NETCMD_CLOSE = $43
CONST NETCMD_READ = $52
CONST NETCMD_WRITE = $57
CONST NETCMD_STATUS = $53
' $0C is HTTP "GET with header access" and, on file/socket protocols,
' plain READ-WRITE -- the same value serves both names.
CONST OPEN_MODE_HTTP_GET_H = $0C
CONST OPEN_MODE_RW = $0C
CONST OPEN_TRANS_NONE = $00
' Scratch RAM ($9000-$97FF) -- ours, outside the mailbox proper. Holds
' state that must survive across multiple mailbox transactions (the
' mailbox's own TX/RX buffers get overwritten by every call).
CONST SC_NAME = $9100 ' playerName, 9 bytes (8 + NUL)
CONST SC_ENDPT = $9110 ' serverEndpoint, 64 bytes
CONST SC_QUERY = $9150 ' query (?table=...&player=...), 48 bytes
' fn_ok: 1 = last transaction produced ACK, 0 = timeout or NAK.
' mb_err: FN_ERR value on failure (0 on timeout, since the RP2040 never answered).
DIM fn_ok, #mb_err
DIM mb_dev, mb_cmd, mb_nparam, mb_seq
DIM #fn_txlen
DIM #fn_t ' generic frame-count timeout counter
DIM #fn_src ' VARPTR source for putstr/getstr
DIM fn_len, fn_i ' generic length/index for putstr/getstr
' ---------------------------------------------------------------------------
' fn_wait_mailbox: bounded wait for the RP2040 magic bytes at boot.
' Sets fn_ok = 1 if the mailbox came up within 180 frames (3s), else 0.
' ---------------------------------------------------------------------------
fn_wait_mailbox: PROCEDURE
#fn_t = 0
WHILE ((PEEK(FN_MAGIC0) AND 255) <> 70) AND ((PEEK(FN_MAGIC1) AND 255) <> 78) AND (#fn_t < 180)
#fn_t = #fn_t + 1
WAIT
WEND
IF #fn_t >= 180 THEN
fn_ok = 0
ELSE
fn_ok = 1
END IF
END
' ---------------------------------------------------------------------------
' fn_transact: issue the transaction described by mb_dev/mb_cmd/mb_nparam/
' #fn_txlen (payload already staged at FN_TX) and block for the reply.
' seq is ALWAYS derived from the RP2040's own FN_ACKSEQ, never from a local
' counter -- a console reset zeroes IntyBASIC vars but not the RP2040, so a
' locally incrementing seq would recompute the same value forever and never
' trigger a second transaction after the first boot.
' Sets fn_ok = 1 on ACK, 0 on timeout/NAK (mb_err holds the reason).
'
' mb_err/net_err are deliberately 16-bit (#-prefixed), not the 8-bit plain
' variables their DIM comments once declared them as: IntyBASIC v1.4.2
' silently drops the AND 255 mask on any assignment of the literal shape
' "plain_var = PEEK(...) AND 255" (with or without wrapping parens) when
' the destination is an 8-bit variable -- confirmed by reading the
' generated .lst, no ANDI instruction is emitted at all, so whatever
' garbage sits in the peeked word's upper byte survives into the
' variable. #-prefixed (16-bit) destinations don't hit this; the ANDI
' shows up correctly. net_err in particular is compared with "<> 1"
' immediately after, so a stray upper byte there would misreport a good
' status as a failure.
' ---------------------------------------------------------------------------
fn_transact: PROCEDURE
POKE (FN_DEVICE), mb_dev
POKE (FN_CMD), mb_cmd
POKE (FN_NPARAM), mb_nparam
POKE (FN_TXLEN_LO), #fn_txlen AND 255
POKE (FN_TXLEN_HI), #fn_txlen / 256
mb_seq = (PEEK(FN_ACKSEQ) AND 255) + 1
IF mb_seq = 0 THEN mb_seq = 1
POKE (FN_SEQ), mb_seq
#fn_t = 0
WHILE ((PEEK(FN_ACKSEQ) AND 255) <> mb_seq) AND (#fn_t < 900)
#fn_t = #fn_t + 1
WAIT
WEND
IF #fn_t >= 900 THEN
fn_ok = 0
#mb_err = 0
RETURN
END IF
IF (PEEK(FN_REPLY_CMD) AND 255) <> FUJICMD_ACK THEN
fn_ok = 0
#mb_err = (PEEK(FN_ERR) AND 255)
RETURN
END IF
fn_ok = 1
END
' ---------------------------------------------------------------------------
' fn_param: stage transaction parameter #pm_i (0-based), #pm_size bytes
' (1 or 2), value #pm_val, little-endian, into the mailbox's param table.
' ---------------------------------------------------------------------------
DIM pm_i, pm_size
DIM #pm_val
fn_param: PROCEDURE
POKE (FN_PARAM_SIZE + pm_i), pm_size
POKE (FN_PARAM_VAL + pm_i * 4), #pm_val AND 255
IF pm_size > 1 THEN POKE (FN_PARAM_VAL + pm_i * 4 + 1), #pm_val / 256
END
' ---------------------------------------------------------------------------
' fn_putstr: append fn_len ASCII bytes into FN_TX, starting at the current
' #fn_txlen, from the address in #fn_src -- either VARPTR of a ROM DATA
' label (a literal like lit_tables(0)) or a RAM address (a scratch buffer
' like SC_NAME, or even FN_RX). PEEK is uniform across ROM/RAM on the
' CP-1610's unified address space, so one procedure covers both. Advances
' #fn_txlen.
' ---------------------------------------------------------------------------
fn_putstr: PROCEDURE
FOR fn_i = 0 TO fn_len - 1
POKE (FN_TX + #fn_txlen + fn_i), PEEK(#fn_src + fn_i) AND 255
NEXT fn_i
#fn_txlen = #fn_txlen + fn_len
END
' ---------------------------------------------------------------------------
' fn_strlen: scan the NUL-padded field at #fn_src (max ls_max bytes) and set
' fn_len to the length up to (but not including) the first NUL. Use this
' before fn_putstr whenever the source is a fixed-width padded field (table
' id, player name) -- putstr has no idea where the real string ends, and
' blindly copying the full padded width embeds a literal $00 byte plus
' whatever garbage follows it into the URL, which is sent as-is (the URL's
' length is a byte count, not NUL-terminated, so nothing downstream stops
' at that NUL either -- it corrupts the HTTP request on the wire).
' ---------------------------------------------------------------------------
DIM ls_max
fn_strlen: PROCEDURE
fn_len = 0
WHILE (fn_len < ls_max) AND ((PEEK(#fn_src + fn_len) AND 255) <> 0)
fn_len = fn_len + 1
WEND
END
' ---------------------------------------------------------------------------
' net_open: open devicespec (ASCII bytes already staged at FN_TX, length in
' #fn_txlen) for HTTP GET. Leaves fn_ok set.
' ---------------------------------------------------------------------------
net_open: PROCEDURE
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_OPEN
mb_nparam = 2
pm_i = 0 : pm_size = 1 : #pm_val = OPEN_MODE_HTTP_GET_H : GOSUB fn_param
pm_i = 1 : pm_size = 1 : #pm_val = OPEN_TRANS_NONE : GOSUB fn_param
GOSUB fn_transact
END
' ---------------------------------------------------------------------------
' net_status: query byte count available. Result in #net_avail; fn_ok as usual.
' ---------------------------------------------------------------------------
DIM #net_avail
' net_err: the 4th byte of the NDeviceStatus reply (nDevStatus_t). 1 =
' SUCCESS. For an HTTP-backed devicespec, the *actual* GET is deferred
' until this first STATUS call (see NetworkProtocolHTTP::status_file()/
' http_transaction() in fujinet-firmware) and its result code lands here
' -- an HTTP error response (e.g. a 400) still has a real, readable body
' (the error page), so `avail` alone can't tell it apart from a good
' response. Checking net_err is what actually can.
DIM #net_err
net_status: PROCEDURE
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_STATUS
mb_nparam = 2
pm_i = 0 : pm_size = 1 : #pm_val = 0 : GOSUB fn_param
pm_i = 1 : pm_size = 1 : #pm_val = 0 : GOSUB fn_param
#fn_txlen = 0
GOSUB fn_transact
IF fn_ok THEN
#net_avail = (PEEK(FN_RX) AND 255) + (PEEK(FN_RX + 1) AND 255) * 256
#net_err = (PEEK(FN_RX + 3) AND 255)
IF #net_err <> 1 THEN fn_ok = 0
ELSE
#net_avail = 0
END IF
END
' ---------------------------------------------------------------------------
' net_read: read #net_readlen bytes into FN_RX (reply payload lands there
' directly -- callers PEEK it in place, per the "never buffer in IntyBASIC
' variables" rule). fn_ok as usual.
' ---------------------------------------------------------------------------
DIM #net_readlen
' #net_gotlen: the actual byte count the RP2040/FujiNet peripheral reported
' receiving (RXLEN), captured here because the CLOSE transaction that
' follows in api_call overwrites FN_RXLEN_LO/HI with its own (always 0)
' reply length -- callers need this checked before that happens.
DIM #net_gotlen
net_read: PROCEDURE
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_READ
mb_nparam = 1
pm_i = 0 : pm_size = 2 : #pm_val = #net_readlen : GOSUB fn_param
#fn_txlen = 0
GOSUB fn_transact
IF fn_ok THEN
#net_gotlen = (PEEK(FN_RXLEN_LO) AND 255) + (PEEK(FN_RXLEN_HI) AND 255) * 256
ELSE
#net_gotlen = 0
END IF
END
' ---------------------------------------------------------------------------
' net_write: send fn_len bytes already staged at FN_TX out the open
' channel. The byte count rides twice -- as parameter 0 (the command's
' length argument) and as the transaction payload length -- and both come
' from fn_len here. fn_ok as usual. The three games never write to the
' network (their moves are GET query strings), but a TCP/TELNET client
' needs this, so the library carries it.
' ---------------------------------------------------------------------------
net_write: PROCEDURE
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_WRITE
mb_nparam = 1
pm_i = 0 : pm_size = 2 : #pm_val = fn_len : GOSUB fn_param
#fn_txlen = fn_len
GOSUB fn_transact
END
' ---------------------------------------------------------------------------
' net_close
' ---------------------------------------------------------------------------
net_close: PROCEDURE
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_CLOSE
mb_nparam = 0
#fn_txlen = 0
GOSUB fn_transact
END
' ---------------------------------------------------------------------------
' api_call: full round trip -- open the URL staged at FN_TX/#fn_txlen,
' check status, read up to #net_readlen bytes (caller sets this to the max
' expected reply size, e.g. 418 for /state, 361 for /tables), close.
' Leaves fn_ok = 1 and the reply in FN_RX on success.
' ---------------------------------------------------------------------------
' ac_i/#ac_prev: loop counter and previous STATUS reading for api_call's
' stabilization poll (see below).
DIM ac_i
DIM #ac_prev
api_call: PROCEDURE
GOSUB net_open
IF fn_ok = 0 THEN RETURN
' The RP2040/ESP32 can report STATUS as soon as *some* bytes of the
' real internet response have arrived, well before the full response
' does -- open+status+read all happening within the same poll can
' easily race ahead of that. Rather than guess a fixed delay, poll
' STATUS repeatedly and treat two consecutive equal (nonzero)
' readings as "the download has settled" before committing to a
' read length. Bounded to ~20 frames (~0.33s) of polling so a
' genuinely stuck connection still falls through to the normal
' fn_ok=0/timeout handling instead of hanging here.
#ac_prev = 0
FOR ac_i = 0 TO 19
WAIT
GOSUB net_status
IF fn_ok = 0 THEN RETURN
IF #net_avail > 0 AND #net_avail = #ac_prev THEN EXIT FOR
#ac_prev = #net_avail
NEXT ac_i
IF #net_avail < #net_readlen THEN #net_readlen = #net_avail
GOSUB net_read
GOSUB net_close ' close regardless of read result
END
' ---------------------------------------------------------------------------
' AppKey. The wire struct (fujiDevice.h's `struct appkey`, packed) is 6
' bytes: creator_lo, creator_hi, app, key, mode, reserved. mode: 0=read,
' 1=write. Sending only 5 bytes (omitting reserved) leaves the firmware's
' transaction_get() blocked waiting for a byte that never arrives, which
' reads back as a timeout, not a protocol error -- easy to misdiagnose.
' appkey_open must be followed by appkey_read or appkey_write, then
' appkey_close, mirroring the C client's read_appkey()/write_appkey().
' ---------------------------------------------------------------------------
DIM ak_creator_lo, ak_creator_hi, ak_app, ak_key, ak_mode
appkey_open: PROCEDURE
mb_dev = FUJI_DEVICEID
mb_cmd = FUJICMD_OPEN_APPKEY
mb_nparam = 0
POKE (FN_TX + 0), ak_creator_lo
POKE (FN_TX + 1), ak_creator_hi
POKE (FN_TX + 2), ak_app
POKE (FN_TX + 3), ak_key
POKE (FN_TX + 4), ak_mode
POKE (FN_TX + 5), 0 ' reserved
#fn_txlen = 6
GOSUB fn_transact
END
' Reads into SC_ names buffer at address #fn_src (caller sets), up to
' fn_len bytes. Actual byte count returned in fn_len after the call (from
' RXLEN); callers should NUL-terminate at that offset.
' Caller sets #fn_src (destination) and ls_max (destination buffer size,
' including room for the NUL this always writes at fn_len). Clamps to
' ls_max-1 bytes copied -- appkeys can be up to 64 bytes but destinations
' like SC_NAME are much smaller, and always NUL-terminates at fn_len so a
' short stored value doesn't leave trailing bytes undefined for callers
' (like compose_url via fn_strlen) that scan for the terminator.
appkey_read: PROCEDURE
mb_dev = FUJI_DEVICEID
mb_cmd = FUJICMD_READ_APPKEY
mb_nparam = 0
#fn_txlen = 0
GOSUB fn_transact
IF fn_ok THEN
' The rs232 transport (rs232Fuji::fujicore_read_app_key, verified
' against fujinet-firmware source and a live byte dump) prepends
' a 2-byte little-endian length ahead of the actual appkey bytes
' -- unlike a network READ, an appkey READ takes no length
' parameter, so the firmware makes the reply self-describing
' instead. This is specific to appkey reads on this transport;
' network reads carry no such prefix. RXLEN (FN_RXLEN_LO/HI)
' covers the prefix + data together, so use the embedded prefix
' itself as the real data length and skip past it.
fn_len = (PEEK(FN_RX) AND 255) + (PEEK(FN_RX + 1) AND 255) * 256
IF fn_len > ls_max - 1 THEN fn_len = ls_max - 1
FOR fn_i = 0 TO fn_len - 1
POKE (#fn_src + fn_i), PEEK(FN_RX + 2 + fn_i) AND 255
NEXT fn_i
POKE (#fn_src + fn_len), 0
ELSE
fn_len = 0
END IF
END
' Writes fn_len bytes from #fn_src (an SC_ buffer) as the appkey payload.
appkey_write: PROCEDURE
mb_dev = FUJI_DEVICEID
mb_cmd = FUJICMD_WRITE_APPKEY
mb_nparam = 0
FOR fn_i = 0 TO fn_len - 1
POKE (FN_TX + fn_i), PEEK(#fn_src + fn_i) AND 255
NEXT fn_i
#fn_txlen = fn_len
GOSUB fn_transact
END
appkey_close: PROCEDURE
mb_dev = FUJI_DEVICEID
mb_cmd = FUJICMD_CLOSE_APPKEY
mb_nparam = 0
#fn_txlen = 0
GOSUB fn_transact
END
' ---------------------------------------------------------------------------
' fn_putnum: append the decimal (no leading zeros) representation of pn_val
' (0-999) into FN_TX at the current #fn_txlen. IntyBASIC has no built-in
' itoa; three digits covers every value the game servers take in a URL
' (board positions, score indices, ship placements).
' ---------------------------------------------------------------------------
DIM pn_val, pn_h, pn_t, pn_o, pn_started
fn_putnum: PROCEDURE
pn_h = pn_val / 100
pn_t = (pn_val / 10) % 10
pn_o = pn_val % 10
pn_started = 0
IF pn_h > 0 THEN
POKE (FN_TX + #fn_txlen), pn_h + 48 : #fn_txlen = #fn_txlen + 1
pn_started = 1
END IF
IF pn_t > 0 OR pn_started THEN
POKE (FN_TX + #fn_txlen), pn_t + 48 : #fn_txlen = #fn_txlen + 1
END IF
POKE (FN_TX + #fn_txlen), pn_o + 48 : #fn_txlen = #fn_txlen + 1
END
netcat.bas
The traditional closing program: a line-mode terminal. Type any N: devicespec on the on-screen keyboard β the same character grid FujiNet CONFIG uses for WiFi SSIDs, ported as kbd.bas (below) with the value display widened to a three-row, 60-character tail-anchored window over a 256-byte buffer, so long URLs wrap and then scroll while you type. The disc moves the highlight, the action button types the highlighted character, and the bottom row offers SPC DEL OK ESC (keypad: 0 space, CLEAR backspace, ENTER = OK).
OK on the URL screen dials; a failed connection keeps your text for editing, and ESC restores the default (tcpbin.com's echo service, so an untouched OK demonstrates the whole loop with no server of your own). In the terminal, the action button opens the keyboard to compose a line (OK sends it plus CR LF), and keypad CLEAR hangs up and returns to the URL screen. The terminal keeps a 200-byte shadow of its cells so it repaints intact after the keyboard has been over it; while the keyboard is open the connection is simply not polled β incoming bytes wait on the FujiNet and drain when the terminal returns.
A ready-to-build copy of this program (with fujinet.bas and kbd.bas, Makefile and jzIntv run script) lives in the netcat repository under intv/.
' netcat.bas -- a line-mode network terminal in IntyBASIC. Type any N:
' devicespec on the on-screen keyboard (the same character grid FujiNet
' CONFIG uses for WiFi SSIDs), connect, and everything the far end sends
' scrolls up rows 0-9 of the screen. The action button opens the keyboard
' again to compose a line; OK sends it (plus CR LF). The default URL is
' tcpbin.com's echo service, so an untouched OK at the URL screen gives a
' self-test needing no server of your own.
'
' URL screen: disc + action button pick characters on the grid
' OK (or keypad ENTER) connect
' ESC restore the default URL
' keypad 0 / CLEAR space / backspace
' The URL buffer holds 256 bytes; rows 0-2 are a 60-character
' window that scrolls once a long URL outgrows them.
'
' Terminal: action button open the keyboard to compose a line
' (OK sends + CR LF, ESC cancels)
' keypad CLEAR hang up, back to the URL screen
'
' While the keyboard is open the connection is not polled; incoming bytes
' simply wait on the FujiNet and are drained when the terminal returns.
'
' Build: intybasic netcat.bas netcat.asm && as1600 -o netcat netcat.asm
GOTO main
INCLUDE "fujinet.bas"
INCLUDE "kbd.bas"
CONST TERM_CELLS = 200 ' rows 0-9 are the terminal
CONST STATUS_ROW = 220 ' row 11: status + key hints
' Scratch RAM, ours, above fujinet.bas's buffers (which end at $917F).
CONST SC_URL = $9200 ' devicespec, 256 bytes (255 chars + NUL)
CONST SC_LINE = $9300 ' composed line, 253 bytes (252 + NUL)
CONST SC_TERM = $9400 ' 200-byte shadow of the terminal cells
' The default devicespec (24 bytes): "N:TCP://TCPBIN.COM:4242/"
lit_spec:
DATA 78,58,84,67,80,58,47,47,84,67,80,66,73,78
DATA 46,67,79,77,58,52,50,52,50,47
CONST LEN_SPEC = 24
DIM term_pos, nc_i, nc_c, nc_cr, nc_row
' ---------------------------------------------------------------------------
' term_clear_row: blank the terminal row term_pos sits in, screen and
' shadow both -- called on entering a fresh row so wrapped-around output
' never interleaves with a stale line.
' ---------------------------------------------------------------------------
term_clear_row: PROCEDURE
nc_row = (term_pos / 20) * 20
FOR nc_i = 0 TO 19
#BACKTAB(nc_row + nc_i) = CS_BLACK
POKE (SC_TERM + nc_row + nc_i), 32
NEXT nc_i
END
' ---------------------------------------------------------------------------
' term_putc: draw ASCII nc_c at the terminal cursor, handling CR/LF and
' wrap-around, mirroring every cell into SC_TERM so the display can be
' repainted after the keyboard has been over it. GROM cards 0-94 cover
' ASCII 32-126 directly.
' ---------------------------------------------------------------------------
term_putc: PROCEDURE
IF nc_c = 13 THEN nc_cr = 1 : GOSUB term_newline : RETURN
IF nc_c = 10 THEN
' collapse the LF of a CR LF pair; a bare LF is a newline
IF nc_cr = 0 THEN GOSUB term_newline
nc_cr = 0
RETURN
END IF
nc_cr = 0
IF nc_c < 32 OR nc_c > 126 THEN RETURN
#BACKTAB(term_pos) = (nc_c - 32) * 8 + COL_NORMAL
POKE (SC_TERM + term_pos), nc_c
term_pos = term_pos + 1
IF term_pos >= TERM_CELLS THEN term_pos = 0
IF (term_pos % 20) = 0 THEN GOSUB term_clear_row
END
term_newline: PROCEDURE
term_pos = (term_pos / 20) * 20 + 20
IF term_pos >= TERM_CELLS THEN term_pos = 0
GOSUB term_clear_row
END
' ---------------------------------------------------------------------------
' term_init / term_repaint: reset the pane, or redraw all 200 cells from
' the shadow after the keyboard borrowed the screen.
' ---------------------------------------------------------------------------
term_init: PROCEDURE
term_pos = 0
nc_cr = 0
FOR nc_i = 0 TO TERM_CELLS - 1
POKE (SC_TERM + nc_i), 32
NEXT nc_i
END
term_repaint: PROCEDURE
FOR nc_i = 0 TO TERM_CELLS - 1
nc_c = PEEK(SC_TERM + nc_i) AND 255
IF nc_c < 32 OR nc_c > 126 THEN nc_c = 32
#BACKTAB(nc_i) = (nc_c - 32) * 8 + COL_NORMAL
NEXT nc_i
END
' ---------------------------------------------------------------------------
' seed_url: (re)load the default devicespec into SC_URL.
' ---------------------------------------------------------------------------
seed_url: PROCEDURE
FOR nc_i = 0 TO LEN_SPEC - 1
POKE (SC_URL + nc_i), PEEK(VARPTR lit_spec(0) + nc_i) AND 255
NEXT nc_i
POKE (SC_URL + LEN_SPEC), 0
END
' ---------------------------------------------------------------------------
' url_screen: full-screen URL editor on the character grid. Returns with
' fn_ok = 1 and the accepted devicespec NUL-terminated in SC_URL. ESC
' restores the default and keeps editing (there is nothing to cancel to).
' ---------------------------------------------------------------------------
url_screen: PROCEDURE
us_again:
CLS
PRINT AT STATUS_ROW COLOR COL_DIM, "TYPE URL - OK DIALS "
#ge_dst = SC_URL
#g_max = 256
GOSUB grid_entry
IF fn_ok = 0 THEN
GOSUB seed_url
GOTO us_again
END IF
IF g_len = 0 THEN
GOSUB seed_url
GOTO us_again
END IF
END
' ---------------------------------------------------------------------------
' compose_line: the same grid over the terminal screen. On OK, sends the
' line plus CR LF out the open channel. Restores the terminal afterward.
' ---------------------------------------------------------------------------
compose_line: PROCEDURE
CLS
PRINT AT STATUS_ROW COLOR COL_DIM, "OK SENDS - ESC BACK "
POKE SC_LINE, 0 ' fresh line every time
#ge_dst = SC_LINE
#g_max = 253
GOSUB grid_entry
IF fn_ok THEN
#fn_txlen = 0
#fn_src = SC_LINE : ls_max = 253 : GOSUB fn_strlen : GOSUB fn_putstr
POKE (FN_TX + #fn_txlen), 13
POKE (FN_TX + #fn_txlen + 1), 10
fn_len = #fn_txlen + 2
GOSUB net_write
END IF
CLS
GOSUB term_repaint
PRINT AT STATUS_ROW COLOR COL_DIM, "BTN TYPE - CLR URL "
END
main:
MODE 0, 0, 0, 0, 0 : WAIT
CLS
PRINT AT 0 COLOR COL_NORMAL, "FUJINET NETCAT"
PRINT AT 40, "CONNECTING TO FUJINET"
GOSUB fn_wait_mailbox
IF fn_ok = 0 THEN
PRINT AT 40, "NO CARTRIDGE MAILBOX "
GOTO halt
END IF
GOSUB seed_url
dial:
GOSUB url_screen
' Open the accepted devicespec: read-write, no translation (we handle
' CR LF ourselves).
CLS
PRINT AT 0 COLOR COL_NORMAL, "DIALING..."
#fn_txlen = 0
#fn_src = SC_URL : ls_max = 255 : GOSUB fn_strlen : GOSUB fn_putstr
mb_dev = NET_DEVICEID
mb_cmd = NETCMD_OPEN
mb_nparam = 2
pm_i = 0 : pm_size = 1 : #pm_val = OPEN_MODE_RW : GOSUB fn_param
pm_i = 1 : pm_size = 1 : #pm_val = OPEN_TRANS_NONE : GOSUB fn_param
GOSUB fn_transact
IF fn_ok = 0 THEN
PRINT AT 40 COLOR COL_ERROR, "CONNECT FAILED"
PRINT AT 60 COLOR COL_DIM, "PRESS BUTTON TO EDIT"
con_wait:
WAIT
GOSUB in_poll
IF in_btn = 0 THEN GOTO con_wait
GOTO dial ' SC_URL still holds the typo -- fix it
END IF
CLS
GOSUB term_init
GOSUB term_clear_row
PRINT AT STATUS_ROW COLOR COL_DIM, "BTN TYPE - CLR URL "
term_loop:
WAIT
' --- receive: anything waiting? read up to 64 bytes and print it ---
GOSUB net_status
IF fn_ok = 0 THEN
PRINT AT STATUS_ROW COLOR COL_ERROR, "CONNECTION LOST "
lost_wait:
WAIT
GOSUB in_poll
IF in_btn = 0 THEN GOTO lost_wait
GOSUB net_close ' free the unit regardless
GOTO dial
END IF
IF #net_avail > 0 THEN
#net_readlen = #net_avail
IF #net_readlen > 64 THEN #net_readlen = 64
GOSUB net_read
IF fn_ok THEN
FOR nc_i = 0 TO #net_gotlen - 1
nc_c = PEEK(FN_RX + nc_i) AND 255
GOSUB term_putc
NEXT nc_i
END IF
END IF
' --- input ---
GOSUB in_poll
IF in_btn THEN GOSUB compose_line
IF in_key = KEYPAD_CLEAR THEN
GOSUB net_close
GOTO dial
END IF
GOTO term_loop
halt:
WAIT
GOTO halt
kbd.bas
The on-screen keyboard netcat uses, ported from fujinet-config/intv's input.bas:
' kbd.bas -- edge-detected input + on-screen character-grid keyboard,
' ported from fujinet-config/intv (input.bas + the scr_recolor helper from
' screen.bas), with one change: the value display spans THREE rows (0-2)
' instead of one, a 60-character tail-anchored window onto a buffer of up
' to 256 bytes -- long N: URLs wrap across the rows and scroll once they
' outgrow them.
'
' All 95 printable characters (32-126) fit in 6 rows x 16 columns, so the
' cursor position IS the character (ch = 32 + gy*16 + gx) -- no SHIFT/SYM
' paging. A row of action buttons (SPC/DEL/OK/ESC) sits below the grid.
' The keypad works too: 0 = space, CLEAR = backspace, ENTER = accept.
'
' grid_entry contract (same as fujinet-config's): caller sets #ge_dst
' (destination buffer) and #g_max (its size, including the NUL) BEFORE
' calling, and primes the buffer -- NUL at offset 0 for a fresh field, or
' existing NUL-terminated text to edit (it is preserved and editable).
' #ge_dst is mutated live REGARDLESS of accept or cancel. Returns fn_ok =
' 1 (OK button or keypad ENTER) or 0 (ESC button only); g_len holds the
' final length and #ge_dst is NUL-terminated there. g_len is an 8-bit
' variable, so the ceiling is #g_max = 256 (255 characters + NUL) -- one
' character shy of what the mailbox TX window could carry to an OPEN, and
' #g_max must be the 16-bit variable it is: as an 8-bit variable, 256
' would silently wrap to 0 and disable grid_append's overflow guard.
'
' Screen layout used by grid_entry (rows 9 and 11 are never touched, so
' the caller can keep hints there):
' rows 0-2 value window (60 cells, tail-anchored, green cursor block)
' rows 3-8 the character grid
' row 10 SPC / DEL / OK / ESC
' Color-stack mode foreground colors.
CONST CS_BLACK = 0
CONST CS_BLUE = 1
CONST CS_RED = 2
CONST CS_TAN = 3
CONST CS_DARKGREEN = 4
CONST CS_GREEN = 5
CONST CS_YELLOW = 6
CONST CS_WHITE = 7
CONST COL_NORMAL = CS_WHITE
CONST COL_HILIGHT = CS_YELLOW
CONST COL_DIM = CS_BLUE
CONST COL_ERROR = CS_RED
CONST COL_VALUE = CS_TAN
CONST COL_CURSOR = CS_GREEN
CONST SCREEN_COLS = 20
DEF FN screenpos(aColumn, aRow) = (((aRow)*SCREEN_COLS)+(aColumn))
' Disc directions (as reported by in_poll).
CONST DISC_UP = $0004
CONST DISC_RIGHT = $0002
CONST DISC_DOWN = $0001
CONST DISC_LEFT = $0008
' Keypad, as decoded by CONT.KEY.
CONST KEYPAD_0 = 0
CONST KEYPAD_CLEAR = 10
CONST KEYPAD_ENTER = 11
CONST KEYPAD_NONE = 12
' Grid geometry. Value rows 0-2; charset rows 3-8; actions row 10.
CONST VAL_ROW0 = 0
CONST VAL_ROWS = 3
CONST VAL_CELLS = 60 ' VAL_ROWS * SCREEN_COLS
CONST GRID_ROW0 = 3
CONST GRID_COL0 = 2
CONST GRID_ROWS = 6
CONST GRID_COLS = 16
CONST GRID_ACTION_ROW = 10
CONST GRID_ACT_COL0 = 2 ' SPC
CONST GRID_ACT_COL1 = 7 ' DEL
CONST GRID_ACT_COL2 = 12 ' OK
CONST GRID_ACT_COL3 = 17 ' ESC
CONST IN_REPEAT_DELAY = 18 ' frames held before auto-repeat (~0.3s)
CONST IN_REPEAT_RATE = 6 ' frames between repeats (~0.1s)
DIM in_disc, in_pdisc, in_rdelay
DIM in_braw, in_btn, in_pbtn
DIM in_key, in_pkey
' ---------------------------------------------------------------------------
' in_poll: call once per frame (after WAIT). Sets, per call:
' in_disc - DISC_UP/DOWN/LEFT/RIGHT on a fresh press or an auto-repeat
' tick while held; 0 otherwise.
' in_btn - 1 on a fresh action-button press (any of B0/B1/B2).
' in_key - the decoded keypad value on a fresh press, else KEYPAD_NONE.
' Uses the unqualified CONT.* pseudo-variables (both controllers OR'd).
' ---------------------------------------------------------------------------
in_poll: PROCEDURE
in_disc = 0
IF CONT.UP THEN in_disc = DISC_UP
IF CONT.DOWN THEN in_disc = DISC_DOWN
IF CONT.LEFT THEN in_disc = DISC_LEFT
IF CONT.RIGHT THEN in_disc = DISC_RIGHT
IF in_disc <> 0 THEN
IF in_disc <> in_pdisc THEN
in_rdelay = IN_REPEAT_DELAY
ELSE
IF in_rdelay > 0 THEN
in_rdelay = in_rdelay - 1
in_disc = 0
ELSE
in_rdelay = IN_REPEAT_RATE
END IF
END IF
END IF
in_pdisc = 0
IF CONT.UP THEN in_pdisc = DISC_UP
IF CONT.DOWN THEN in_pdisc = DISC_DOWN
IF CONT.LEFT THEN in_pdisc = DISC_LEFT
IF CONT.RIGHT THEN in_pdisc = DISC_RIGHT
in_braw = 0
IF CONT.B0 OR CONT.B1 OR CONT.B2 THEN in_braw = 1
IF in_braw <> 0 AND in_pbtn = 0 THEN
in_btn = 1
ELSE
in_btn = 0
END IF
in_pbtn = in_braw
in_key = KEYPAD_NONE
IF CONT.KEY <> KEYPAD_NONE AND CONT.KEY <> in_pkey THEN
in_key = CONT.KEY
END IF
in_pkey = CONT.KEY
END
' ---------------------------------------------------------------------------
' grid_entry -- see the header comment for the contract.
' ---------------------------------------------------------------------------
DIM g_x, g_y, g_px, g_py, g_len, g_ch, ga_idx
DIM #ge_dst, #g_max
DIM k_row, k_col, k_i, k_c, k_max, k_color
DIM #k_word
grid_entry: PROCEDURE
GOSUB grid_draw_charset
GOSUB grid_draw_actions
g_len = 0
WHILE (g_len < #g_max - 1) AND ((PEEK(#ge_dst + g_len) AND 255) <> 0)
g_len = g_len + 1
WEND
GOSUB grid_draw_value
g_x = 0 : g_y = 0
g_px = 255 : g_py = 255
DO WHILE 1
WAIT
GOSUB grid_draw_cursor
GOSUB in_poll
IF in_disc = DISC_UP AND g_y > 0 THEN g_y = g_y - 1
IF in_disc = DISC_DOWN AND g_y < GRID_ROWS THEN g_y = g_y + 1
IF in_disc = DISC_LEFT AND g_x > 0 THEN g_x = g_x - 1
IF in_disc = DISC_RIGHT THEN
IF g_y = GRID_ROWS THEN
IF g_x < 3 THEN g_x = g_x + 1
ELSE
IF g_x < GRID_COLS - 1 THEN g_x = g_x + 1
END IF
END IF
IF g_y = GRID_ROWS AND g_x > 3 THEN g_x = 3
IF in_key = KEYPAD_0 THEN g_ch = 32 : GOSUB grid_append
IF in_key = KEYPAD_CLEAR THEN GOSUB grid_backspace
IF in_key = KEYPAD_ENTER THEN
fn_ok = 1
EXIT DO
END IF
IF in_btn <> 0 THEN
IF g_y < GRID_ROWS THEN
g_ch = 32 + g_y * GRID_COLS + g_x
IF g_ch <= 126 THEN GOSUB grid_append
ELSE
IF g_x = 0 THEN g_ch = 32 : GOSUB grid_append
IF g_x = 1 THEN GOSUB grid_backspace
IF g_x = 2 THEN
fn_ok = 1
EXIT DO
END IF
IF g_x = 3 THEN
fn_ok = 0
EXIT DO
END IF
END IF
END IF
LOOP
END
' grid_draw_charset: paints all 96 cells (95 real chars + one always-blank).
grid_draw_charset: PROCEDURE
FOR g_y = 0 TO GRID_ROWS - 1
FOR g_x = 0 TO GRID_COLS - 1
g_ch = 32 + g_y * GRID_COLS + g_x
IF g_ch > 126 THEN g_ch = 32
#BACKTAB((GRID_ROW0 + g_y) * SCREEN_COLS + GRID_COL0 + g_x) = (g_ch - 32) * 8 + COL_VALUE
NEXT g_x
NEXT g_y
END
grid_draw_actions: PROCEDURE
PRINT AT screenpos(GRID_ACT_COL0, GRID_ACTION_ROW) COLOR COL_DIM,"SPC"
PRINT AT screenpos(GRID_ACT_COL1, GRID_ACTION_ROW) COLOR COL_DIM,"DEL"
PRINT AT screenpos(GRID_ACT_COL2, GRID_ACTION_ROW) COLOR COL_DIM," OK"
PRINT AT screenpos(GRID_ACT_COL3, GRID_ACTION_ROW) COLOR COL_DIM,"ESC"
END
' grid_recolor: change only the COLOR of k_max+1 already-drawn cells on row
' k_row starting at column k_col -- the glyphs underneath stay. (The
' scr_recolor helper from fujinet-config's screen.bas, renamed so a program
' including this file can keep its own s_* variables.)
grid_recolor: PROCEDURE
FOR k_i = 0 TO k_max
#k_word = (#BACKTAB(k_row * SCREEN_COLS + k_col + k_i) AND $FFF8) + k_color
#BACKTAB(k_row * SCREEN_COLS + k_col + k_i) = #k_word
NEXT k_i
END
' grid_draw_cursor: un-highlight the previous cell (g_px/g_py), highlight
' the current one (g_x/g_y). g_px=255 on the very first call skips the
' un-highlight.
grid_draw_cursor: PROCEDURE
IF g_px <> 255 THEN
IF g_py = GRID_ROWS THEN
ga_idx = g_px : GOSUB grid_action_col
k_row = GRID_ACTION_ROW : k_max = 2 : k_color = COL_DIM
GOSUB grid_recolor
ELSE
g_ch = 32 + g_py * GRID_COLS + g_px
IF g_ch > 126 THEN g_ch = 32
#BACKTAB((GRID_ROW0 + g_py) * SCREEN_COLS + GRID_COL0 + g_px) = (g_ch - 32) * 8 + COL_VALUE
END IF
END IF
IF g_y = GRID_ROWS THEN
ga_idx = g_x : GOSUB grid_action_col
k_row = GRID_ACTION_ROW : k_max = 2 : k_color = COL_HILIGHT
GOSUB grid_recolor
ELSE
g_ch = 32 + g_y * GRID_COLS + g_x
IF g_ch > 126 THEN g_ch = 32
#BACKTAB((GRID_ROW0 + g_y) * SCREEN_COLS + GRID_COL0 + g_x) = (g_ch - 32) * 8 + COL_HILIGHT
END IF
g_px = g_x : g_py = g_y
END
' grid_action_col: given ga_idx (0-3), sets k_col to that action button's
' starting column.
grid_action_col: PROCEDURE
IF ga_idx = 0 THEN k_col = GRID_ACT_COL0
IF ga_idx = 1 THEN k_col = GRID_ACT_COL1
IF ga_idx = 2 THEN k_col = GRID_ACT_COL2
IF ga_idx = 3 THEN k_col = GRID_ACT_COL3
END
' grid_draw_value: tail-anchored 60-cell window (rows 0-2) onto #ge_dst,
' with a trailing cursor block. A value longer than 59 characters scrolls:
' the window always shows the tail, where typing is happening.
grid_draw_value: PROCEDURE
k_i = 0
IF g_len > VAL_CELLS - 1 THEN k_i = g_len - (VAL_CELLS - 1)
FOR k_col = 0 TO VAL_CELLS - 1
k_c = 32
IF k_i + k_col < g_len THEN k_c = PEEK(#ge_dst + k_i + k_col) AND 255
IF k_c < 32 OR k_c > 126 THEN k_c = 32
#BACKTAB(VAL_ROW0 * SCREEN_COLS + k_col) = (k_c - 32) * 8 + COL_VALUE
NEXT k_col
IF g_len - k_i < VAL_CELLS THEN
#BACKTAB(VAL_ROW0 * SCREEN_COLS + (g_len - k_i)) = (95 - 32) * 8 + COL_CURSOR
END IF
END
grid_append: PROCEDURE
IF g_len >= #g_max - 1 THEN RETURN
POKE (#ge_dst + g_len), g_ch
g_len = g_len + 1
POKE (#ge_dst + g_len), 0
GOSUB grid_draw_value
END
grid_backspace: PROCEDURE
IF g_len = 0 THEN RETURN
g_len = g_len - 1
POKE (#ge_dst + g_len), 0
GOSUB grid_draw_value
END