ATTACH: A Remote Operating Facility for NadaNet

Michael J. Mahon — November 29, 2004
Revised — May 8, 2010

Introduction

ATTACH is an Applesoft program that runs on a master machine and a small assembly language "proxy" program, ATTACH.SLAVE, that ATTACH &BRUNs on a remote machine to intercept the CSW and KSW hooks of the ATTACHed machine. Its purpose is to enable the master machine to function as the keyboard and display of a remote machine, so that it can remotely operate any serving machine on the network. ATTACH can be found on the ProDOS boot disk.

An earlier version of ATTACH used the Message Server to pass input lines to the slave machine and output lines back to the master. The new version does not use a Message Server, but queues all input from the master in a circular buffer in the slave machine and all slave output in a circular buffer in the master machine. The queueing code makes the program a bit more complex, but the advantage of not requiring a third machine as a Message Server is well worth it.

The ATTACH Program

The Applesoft BASIC program, ATTACH, is presented here, with liberal commentary:

 100  REM  ATTACH slave for remote operation
 110  REM  Uses circular buffers instead of
 120  REM          Message Server.
 130  REM  MJM - 02/17/05, 07/13/07, 01/29/09, 05/03/10
 140 :

This program presumes that NadaNet, with the AmperNada extensions, has already been installed below the OS, HIMEM has been adjusted, and an initializing call has been made. The following GOSUB merely verifies that NadaNet is installed and sets up some pointer variables for application use.

Based on the NadaNet load page, the OS environment (ProDOS or DOS) is determined and the prefix for loading M/L code and the address containing the length of a BLOAD are defined.

 200  GOSUB 60000: REM  Set up NadaNet Defs
 210 :
 220 NP =  PEEK (975): REM  NadaNet load page
 230  IF NP <  > 145 GOTO 280: REM  Not ProDOS version
 240 PFX$ = "/ap/merlin/work/nadanet/": REM  ProDOS M/L program prefix
 250 BL = 48840: REM  ProDOS BLOAD length ($BEC8)
 260  GOTO 310
 270 :
 280  IF NP <  > 141 THEN  PRINT "NadaNet not loaded.": STOP : REM  Not DOS either!
 290 PFX$ = "":BL = 43616: REM  No prefix & DOS BLOAD length ($AA60)
 300 :

The next lines set up definitions for FN PK2(X), a function to PEEK a 16-bit value at address X. A temporary buffer is defined at page 2 and a main buffer at $4000, which is also the base address for buffer and control block allocation. Next we define the keyboard port and its strobe address, and the sense address of the Open-Apple key, which is used as a modifier key for commands to the ATTACH program.

The width of the controlling machine's screen is determined and used to set the length of the input buffer. Input to ATTACH is limited to the part of line remaining after the prompt string—in other words, input is confined to a single line.

A six byte control block for the 256-byte circular buffer for slave machine output is defined preceding BUF, and a local copy of the slave's input circular buffer control block is defined preceding that. The layout and function of the circular buffer control block is defined in the ATTACH.SLAVE program listing. The length of the circular buffer itself is constrained to 256 bytes by the simple modulo arithmetic used in ATTACH.SLAVE.

 310  DEF  FN PK2(X) =  PEEK (X) + 256 *  PEEK (X + 1)
 320 BT = 512: REM  $200 Temp buffer
 330 BUF = 4 * 4096: REM  $4000
 340 KBD = 49152: REM  Kbd port
 350 KST = 49168: REM  Kbd strobe
 360 OA = 49249: REM  Open-Apple (PB0)
 370 :
 380 ML =  PEEK (33) - 1: REM  Max input chars in buf = window width-1
 390 :
 400 LC = BUF - 6: REM  Local (output) Circular Buffer control block
 410 LB = BUF + ML:LT = LB:LH = LB:LG = 256: REM  CB Tail, Head, Buf, Leng
 420  POKE LC,0: POKE LC + 1,0: REM  CB Lock word (unlocked)
 430  POKE LC + 2,0: REM  Tail index
 440  POKE LC + 3,0: REM  Head index
 450  POKE LC + 5,LB / 256: POKE LC + 4,LB - 256 *  PEEK (LC + 5): REM  Buff
 460 :
 470 RC = LC - 6: REM  Copy of Remote (input) Circular Buffer control block
 480 :

Next, two short machine language programs are POKEd into page 3 to provide a fast way to write out a line of characters (WL) and a fast loop to call SERVE until the local output buffer is unlocked by the slave machine (SV).

The assembly language corresponding to the programs in the DATA statements is in the following REM statements. Note also that WL has two parameter POKE addresses defined and that SV is modified to point to the local circular buffer control block.

 490 PC = 768: REM  Program counter
 500 WL = PC: REM  Install "Write buffer to COUT"
 510 WB = WL + 3: REM  Buffer address to POKE
 520 WN = WL + 10: REM  Buffer length to POKE
 530  READ X: IF X < 256 THEN  POKE PC,X:PC = PC + 1: GOTO 530
 540  DATA  160,0,185,0,0,32,237,253,200,192,0,144,245,96,999
 550  REM  ldy #0; lda *buf*,y; jsr COUT; iny; cpy *#len*; bcc $302; rts
 560 :
 570 SV = PC: REM  Install "Serve until LC unlocked"
 580  REM  *** Update 'servecnt' address if it changes! ***
 590  READ X: IF X < 256 THEN  POKE PC,X:PC = PC + 1: GOTO 590
 600  POKE SV + 4,NP: POKE SV + 7,NP: POKE SV + 22,NP: REM  Set NadaNet page
 610  POKE SV + 12,LC / 256: POKE SV + 11,LC - 256 *  PEEK (SV + 12): REM  LC
 620  POKE SV + 15,(LC + 1) / 256: POKE SV + 14,LC + 1 - 256 *  PEEK (SV + 12)
 630  DATA  169,1,141,80,145,32,12,145,48,8,173,00,00
 640  DATA  13,00,00,208,243,169,0,141,80,145,96,999
 650  REM  lda #1; sta servecnt; :serve jsr serve; bmi :resetcnt; lda *LC*;
 660  REM  ora *LC + 1*; bne :serve; :resetcnt lda #0; sta servecnt; rts
 670 :

Having defined the data structures and installed the M/L programs, we are now ready to begin.

First, we take a "census" of all machines from 1 up to MX to determine which ones are serving and, if so, what type of machine they are. We then boot any AppleCrate machines awaiting boot with the NADA.CRATE boot image and re-take the census.

 680  GOSUB 61000: REM  Take census
 690 :
 700  REM  Boot unbooted slaves
 710  PRINT  CHR$ (4)"BLOAD "PFX$"NADA.CRATE,A"BUF
 720 PSRT =  PEEK (BUF + 2) * 256: REM  Prog start
 730 PLNG =  FN PK2(BL): REM  Prog length
 740  & BOOT(PSRT,PLNG,BUF)
 750  & SERVE#(10): REM  for GETIDs
 760  PRINT "Slaves booted."
 770 :
 780  GOSUB 61000: REM  Re-take census
 790 :

Now we load the ATTACH.SLAVE program into memory for later use.

Note that the entry point of ATTACH.SLAVE is its first byte, the third byte of the program is its load page, and that PSRT+3 is a vector to its DETACH entry point. Immediately following, at PSRT+6 (BA+6 in our local buffer) is a pointer that is initialized to our output circular buffer control block address, and PSRT+8 is a pointer to the slave's input circular buffer control block.

 800  REM  Load ATTACH.SLAVE program
 810 BA = LB + LG: REM  ATTACH.SLAVE buffer
 820  PRINT  CHR$ (4)"BLOAD "PFX$"ATTACH.SLAVE,A"BA
 830 PSRT =  PEEK (BA + 2) * 256: REM  Prog start
 840 PLNG =  FN PK2(BL): REM  Prog Length
 850 DETACH = PSRT + 3: REM  DETACH entry point
 860 X = BA + 6: REM  Address of ptr to master's CB control block
 870  POKE X + 1,LC / 256: POKE X,LC - 256 *  PEEK (X + 1)
 880 CB = PSRT + 8: REM  Addr of slave's Circular Buffer ctl block

Next we determine whether or not the Apple //e 80-column firmware is running, by printing 10 spaces and sampling the 80-column firmware's horizontal cursor position. If it is not 10, we assume that the 80-column firmware is not running and switch CH to point to the original horizontal cursor location. The sign-on message is then written.

 890 CH = 1403: REM  Horizontal cursor if 80-col firmware running
 900  PRINT 
 910  PRINT  SPC( 10);: IF  PEEK (CH) <  > 10 THEN CH = 36: REM  Not 80-col firmware
 920  PRINT "ATTACH v3.1": REM  Sign on after finding horiz cursor location
 930 :

We begin by asking for the ID of the first machine to be attached. The ID is required to in the range of 1..31, not including our own ID. If the user inputs an ID of zero, it is interpreted as a command to detach from the current slave, if any, and quit the program.

Note that a machine running the Message Server cannot be attached. The Message Server runs a very stripped-down form of NadaNet that can serve most requests, but does not contain the client side of those requests. Since ATTACH.SLAVE requires the ability to initiate requests, it is not compatible with the Message Server.

Next, we set a short timeout (in case the NXT machine is not serving) and &PEEK the first two bytes where ATTACH.SLAVE loads. If these bytes are the same as ATTACH.SLAVE in the program buffer, the NXT machine is already attached by another machine and cannot be attached again. If this is the case, we re-request a machine ID to attach to.

 940  INPUT "Attach to machine (0 to quit): ";NXT
 950  IF NXT <  > 0 GOTO 990
 960  IF AM THEN  &  CALL (AM,DETACH): PRINT "Detached from "AM"."
 970  END 
 980 :
 990  IF NXT < 1 OR NXT > 31 OR NXT = SELF GOTO 940
 1000  IF NXT = AM GOTO 1540: REM  Ignore if current machine
 1010  IF  PEEK (ITBL + NXT) = 3 THEN  PRINT "Can't attach Message Server": GOTO 940
 1020  & TIMEOUT(2): REM  Fast timeout in case not serving
 1030  &  PEEK #(NXT,PSRT,3,BT): REM  Is machine already attached?
 1040  IF  PEEK (1) GOTO 1120: REM  Not serving.
 1050  IF  FN PK2(BT) <  >  FN PK2(BA) GOTO 1100: REM  OK, not attached.
 1060  PRINT "Machine "NXT" already attached by " PEEK (BT + 2)
 1070  & TIMEOUT(): REM  Restore default
 1080  GOTO 940
 1090 :

The short timeout is still set, so we attempt to &BRUN ATTACH.SLAVE. If NXT is not serving we notify the user and re-request a machine ID to attach to.

If we have just attached to a new machine, and we were previously attached to another machine, we DETACH from it and remember that we are now attached to the new machine.

 1100  REM  Attach to machine NXT
 1110  & B RUN #(NXT,PSRT,PLNG,BA): REM  BRUN ATTACH.SLAVE
 1120 ERR =  PEEK (1)
 1130  & TIMEOUT(): REM  Restore default
 1140  IF ERR THEN  PRINT "Machine "NXT" not serving.": GOTO 940
 1150  IF AM THEN  &  CALL (AM,DETACH): REM  If currently attached, detach
 1160 AM = NXT: REM  Attached to new machine
 1170 :

This is the main loop of ATTACH, polling the output queue for any pending output from the attached machine and, if no output is waiting, polling the local keyboard for any input.

If output is waiting in our circular buffer, we write it out and continue to poll.

Each time that output is written, we sample the horizontal position of the cursor. Typically the final output is an input prompt of one or more characters, and that length will be reflected in the position of the cursor. We also limit the input length to the remaining space on the line.

 1180  REM  Remote operating loop
 1190 :
 1200  REM  Check for pending output
 1210  CALL SV: REM  Serve until unlocked
 1220 LT = LB +  PEEK (LC + 2):LH = LB +  PEEK (LC + 3): REM  Tail & Head ptrs
 1230  IF LH = LT GOTO 1380: REM  Buffer empty, check keyboard
 1240 :
 1250  REM  Process output
 1260 LW = LT - LH:L2 = 0: REM  Lengths if no wrap
 1270  IF LW < 0 THEN LW = LB + LG - LH:L2 = LT - LB: REM  If wrap
 1280  IF  PEEK (LH) = 141 THEN  POKE LH,128: REM  A leading CR --> NUL
 1290  POKE WB + 1,LH / 256: POKE WB,LH - 256 *  PEEK (WB + 1): REM  Buffer
 1300  POKE WN,LW: CALL WL: REM  Print output
 1310  POKE WB + 1,LB / 256: POKE WB,LB - 256 *  PEEK (WB + 1): REM  Buffer
 1320  IF L2 THEN  POKE WN,L2: CALL WL: REM  Print output
 1330  POKE LC + 3,LT - LB: REM  Head=Tail
 1340 HP =  PEEK (CH) + 1: REM  Save HTAB after prompt
 1350 MC = ML - HP: REM  Set max length for current line
 1360  GOTO 1210: REM  Loop...
 1370 :
 1380  IF  PEEK (KBD) < 128 GOTO 1210: REM  Check for keyboard input
 1390 :

As you would expect, once an input character is received, output is no longer processed until the input line is completed by a carriage return.

Commands are a single keypress with Open-Apple depressed. Commands must be the first character on a line.

Commands for ATTACH are:

Keypress

Action

OA-M or OA-m

Attach to a new machine

OA-C or OA-c

Take census of serving machines

OA-? or OA-/

Help

OA-Q or OA-q

Quit the ATTACH program

We first mark the input buffer empty, then get the sensed input key and check whether the Open-Apple key is pressed. If it is not, the character is not a command, and we go to normal input processing.

If it is a command, it is performed immediately. If the command causes output to be written to the screen, the previous prompt string is repeated after the command. (This is complicated somewhat by the possibility that the prompt string wraps from the end of the circular buffer to the beginning, and must therefore be re-assembled by line 1540.)

 1400  REM  Accept keyboard input
 1410 L = 0:LM = 0
 1420  GET K$:K =  ASC (K$): REM  Get char
 1430  IF L > 0 OR  PEEK (OA) < 128 GOTO 1630
 1440 :
 1450  REM  Handle initial input char = Open-Apple 
 1460  IF K$ = "M" OR K$ = "m" THEN  PRINT : GOTO 940: REM  Change machines
 1470  IF K$ = "C" OR K$ = "c" THEN  PRINT : GOSUB 61000: GOTO 1540: REM  Census
 1480  IF K$ = "Q" OR K$ = "q" GOTO 960: REM  Quit
 1490  REM  Unrecognized OA-char, display help.
 1500  PRINT 
 1510  PRINT "OA-? = Help   OA-M = Switch Machines    OA-Q = Quit   OA-C = Census"
 1520 :
 1530  REM  Repeat the previous prompt
 1540 PH = LT - HP + 1: REM  Start of unwrapped prompt
 1550 PP = LB - PH: REM  Length of "wrapped" prefix
 1560  IF PP > 0 THEN  FOR I =  - 1 TO PP - 1: POKE LB - PP + I, PEEK (LB + LG - PP + I): NEXT : REM  Copy the prefix in front of buffer
 1570  IF  PEEK (PH + HP - 2) = 128 THEN PH = PH - 1: REM  Omit any trailing CR
 1580  POKE WB + 1,PH / 256: POKE WB,PH - 256 *  PEEK (WB + 1)
 1590  POKE WN,HP - 1: CALL WL: REM  Redisplay prompt
 1600  GOTO 1210: REM  Check for output
 1610 :

Here we handle non-command input characters. We simulate the the right and left arrow characters and echo all other characters and add them to the input buffer, including control characters, which are displayed in inverse.

The left and right arrow keys behave as they do in a normal INPUT statement, except that motion is confined to a single line, between the prompt and the next-to-last character position on the line.

If the input line is full, we just stop adding characters to the buffer. When a carriage return is received, we send the input line.

 1620  REM  Handle normal and control characters
 1630  IF K <  > 8 GOTO 1670: REM  Skip if not left arrow
 1640  IF L > 0 THEN L = L - 1: HTAB HP + L: REM  Left arrow
 1650  GOTO 1420
 1660 :
 1670  IF K <  > 21 GOTO 1720: REM  Skip if not right arrow
 1680  IF L = MC - 1 GOTO 1420: REM  Ignore right arrow if max line
 1690  HTAB HP + L + 1: IF L < LM THEN L = L + 1: GOTO 1420: REM  Use BUF char
 1700  IF L >  = LM THEN K = 32: GOTO 1730: REM  Add space at end
 1710 :
 1720  IF K = 13 GOTO 1780: REM  Just add CR to BUF
 1730  IF L = MC - 1 GOTO 1420: REM  Ignore non-CRs if max line
 1740 PK$ =  CHR$ (K): REM  Echo char if not CR
 1750  IF K < 32 THEN  INVERSE :PK$ =  CHR$ (K + 64): REM  Show ctl-char inverse
 1760  HTAB HP + L: PRINT PK$;: NORMAL 
 1770 :
 1780  POKE BUF + L,K + 128:L = L + 1: REM  Add char to BUF
 1790  IF L > LM THEN LM = L: REM  Keep high-water mark
 1800  IF K <  > 13 GOTO 1420: REM  Loop until CR
 1810  IF L <  = LM THEN  PRINT  SPC( LM - L + 1);: REM  Clear extra chars
 1820  GOSUB 1900: REM  Send input line
 1830  PRINT : REM  CR after input line
 1840  GOTO 1210: REM  Check for output
 1850 ::::::::::

The following subroutine is used to send the contents of the input buffer to the slave machine's 256-byte circular buffer. It is the Applesoft equivalent of the circular buffer handling in ATTACH.SLAVE. Note that the entry point is line 1880.

A copy of the slave's circular buffer control block is &PEEKed to minimize the number of network interactions required.

 1860  REM  Send input line
 1870  &  POKE (AM,CB,2,RC): REM  Release lock
 1880  & SERVE(2): REM  Serve for 40ms.
 1890  REM  Send input line entry
 1900  &  PEEK  POKE (AM,CB,1,LV): REM  Acquire Lock on slave Circular Buffer
 1910  IF LV GOTO 1880: REM  Lock busy, retry.
 1920  POKE RC,0: POKE RC + 1,0: REM  Fill in unlocked value
 1930  &  PEEK (AM,CB + 2,4,RC + 2): REM  Get slave's CB control block
 1940 RT =  PEEK (RC + 2): REM  Remote Tail index
 1950 RH =  PEEK (RC + 3): REM  Remote Head index
 1960 RB =  FN PK2(RC + 4): REM  Remote Buff start
 1970 RG = 256: REM  Remote Leng
 1980 SP = RH - RT - 1 + (RH <  = RT) * RG: REM  Free space in buffer
 1990  IF SP < L GOTO 1870: REM  Not enough, release and retry.
 2000 L1 = RG - RT: IF L1 > L THEN L1 = L: REM  Part before wrap
 2010  IF L1 <  > L THEN  &  POKE (AM,RB,L - L1,BUF + L1): REM  Part wrapped
 2020  &  POKE (AM,RB + RT,L1,BUF): REM  Part that didn't wrap
 2030 RT = RT + L: IF RT >  = RG THEN RT = RT - RG: REM  Advance RT mod RG
 2040  POKE RC + 2,RT
 2050  &  POKE (AM,CB,3,RC): REM  Release lock and update Tail index
 2060 L = 0:LL = 0
 2070  RETURN 
 2080 ::::::::::

This is the standard "suffix" for NadaNet applications, added by EXECing the text file NADADEFS.

The subroutine at 60000 verifies NadaNet installation and sets up a few named pointers. The subroutine at 61000 performs a "census" of serving machines on the net and saves the results in a table at ITBL. (Note that because the census loop is probing machines that may not be serving (or even exist), it temporarily sets the &TIMEOUT value down to 2 * 60ms. so that unresponsive machines do not stall the &PEEK for very long. The default timeout is restored when the census is completed.)

The census report adapts to the current screen width and lists machines in a multiple column format. In ATTACH, the census routine has been slightly modified to show the currently attached machine (AM) in inverse (line 61165 and NORMAL added in line 61180).

 60000  REM  NadaNet Definitions for Applesoft
 60010  REM         MJM - 01/13/09
 60020 :
 60030 MX = 20: REM  Max machine ID
 60040  IF  PEEK (973) <  > 76 THEN  PRINT "NadaNet not loaded.": STOP 
 60050 SELF =  PEEK (972)
 60060  & IDTBL(ITBL): REM  ID table
 60070  RETURN 
 60080 :
 61000  REM  Take census of serving machines
 61010  & TIMEOUT(2): REM  Set short timeout
 61020 QC =  INT ( PEEK (33) / 13): REM  Number of columns
 61030 QL =  INT ((MX + QC - 1) / QC): REM  Number of lines
 61040  FOR I = 1 TO QL
 61050  FOR D = I TO MX STEP QL
 61060  IF D = SELF THEN J =  PEEK (975): GOTO 61110
 61070 A$ = "        "
 61080  &  PEEK #(D,975,1,512): REM  Machine type
 61090  IF  PEEK (1) THEN K = 0: GOTO 61150
 61100 J =  PEEK (512)
 61110  IF J = 184 THEN A$ = "CRATE   ":K = 2
 61120  IF J = 008 THEN A$ = "MSERVER ":K = 3
 61130  IF J = 145 THEN A$ = "PRODOS  ":K = 4
 61140  IF J = 141 THEN A$ = "DOS     ":K = 5
 61150  POKE ITBL + D,K: REM  Save type in IDTBL
 61160  IF D = SELF THEN A$ = "==SELF=="
 61165  IF D = AM THEN  INVERSE : REM  Highlight ATTACHed machine
 61170  IF D < 10 THEN  PRINT " ";
 61180  PRINT D":"A$;: NORMAL : PRINT "  ";: REM  Highlight off
 61190  NEXT D
 61200  PRINT 
 61210  NEXT I
 61220  & TIMEOUT(): REM  Reset retrys
 61230  RETURN