ATTACH: A Remote Operating Facility for NadaNet

Michael J. Mahon — November 29, 2004
Revised — February 3, 2009

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
 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  READ X: IF X < 256 THEN  POKE PC,X:PC = PC + 1: GOTO 580
 590  POKE SV + 4,NP: POKE SV + 7,NP: POKE SV + 22,NP: REM  Set NadaNet page
 600  POKE SV + 12,LC / 256: POKE SV + 11,LC - 256 *  PEEK (SV + 12): REM  LC
 610  POKE SV + 15,(LC + 1) / 256: POKE SV + 14,LC + 1 - 256 *  PEEK (SV + 12)
 620  DATA  169,1,141,77,145,32,12,145,48,8,173,00,00
 630  DATA  13,00,00,208,243,169,0,141,77,145,96,999
 640  REM  lda #1; sta servecnt; :serve jsr serve; bmi :resetcnt; lda *LC*;
 650  REM  ora *LC + 1*; bne :serve; :resetcnt lda #0; sta servecnt; rts
 660 :

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.

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

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.

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

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

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.

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

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.

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

This it the main loop of ATTACH, polling the output queue for any pending output from attached machines 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.

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

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.)

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

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.

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

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.

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

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 
 61170  IF D < 10 THEN  PRINT " ";
 61180  PRINT D":"A$;: NORMAL : PRINT "  ";
 61190  NEXT D
 61200  PRINT 
 61210  NEXT I
 61220  & TIMEOUT(): REM  Reset retrys
 61230  RETURN