ATTACH: A Remote Operating Facility for NadaNet

Michael J. Mahon - November 29, 2004

Introduction

ATTACH is an Applesoft program that runs on the master machine. Its purpose is to remotely operate any serving machine on the network. It loads a small assembly language program, ATTACH.SLAVE, which redirects the CSW and KSW hooks of the attached machine to Message Server queues for the display and keyboard of the attached machine, allowing the master to display its outputs and provide it with keyboard input.

The ATTACH Program

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

 100  REM  ATTACH slave for remote operation
 110  REM         MJM - 11/14/04
 120 :

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.

 200  GOSUB 60000: REM  Set up NadaNet Defs
 210 PFX$ = "/ap/merlin/work/nadanet/":

The next lines set up some definitions for ATTACH. P$() is an array whose Ith element contains the last known prompt string for machine I. If this is null, it flags that machine I is not serving, and therefore cannot be ATTACHed.

The other definitions are for Apple II addresses, two for the keyboard and its strobe, and one for the Open-Apple key, which is used as a modifier key for commands to the ATTACH program.

Finally, a short machine language program is POKEd into $300 to provide a fast way to write out a line of characters.

 220  DIM P$(15): REM  Last prompt string
 230 KBD = 49152: REM  Kbd port
 240 KST = 49168: REM  Kbd strobe
 250 PB0 = 12 * 4096 + 6 * 16 + 1: REM  $C061
 260 :
 270  REM  POKE routine to Write Line to COUT
 280 WL = 3 * 256: REM  $300
 290  FOR I = 0 TO 13: READ X: POKE WL + I,X: NEXT 
 300  DATA  160,0,185,0,00,32,237,253,200,192,00,144,245,96
 310  POKE WL + 4,BUF / 256: REM  fix address
 320  REM  ldy #0; lda BUF,y; jsr COUT; iny; cpy #len; bcc $302; rts
 330 :

Next, we take a "census" of all machines from 2 up to MX to determine which ones are serving and, if so, what type of machine they are. If machine #2 is already running the Message Server we note that.

We then boot all unbooted AppleCrate machines with the NADA.CRATE code and re-take the census.

 340  GOSUB 61000: REM  Take census
 350 MSRV =  PEEK (ITBL + 2) = 3: REM  Save pre-boot Message Server status
 360 :
 370  REM  Boot unbooted slaves
 380  PRINT  CHR$ (4)"bload "PFX$"nada.crate,A"BUF
 390 PSRT =  PEEK (BUF + 2) * 256: REM  Prog start
 400 PLNG =  PEEK (48840) + 256 *  PEEK (48841): REM  Prog length
 410  & BOOTCODE(PSRT,PLNG,BUF): REM  Set bootcode info
 420  & SERVE#(100): REM  for BOOT
 430  PRINT "Boot of slaves completed."
 440 :
 450  GOSUB 61000: REM  Re-take census

Since machine #2 is required as the message server, we require that it now be serving. Then, if it is not already running the Message Server code, we &POKE that code and &CALL it. (Note that I have a convention that any assembly language program to be &POKEd has its run-time load page as its third byte. This is consistent with the program starting with a JMP to a routine within its first page.)

 460  IF  NOT  PEEK (ITBL + 2) THEN  PRINT "No Msg Server (2)": STOP 
 470 :
 480  REM  Load Message Server (if not running)
 490  IF MSRV GOTO 1000
 500  PRINT  CHR$ (4)"bload "PFX$"nada.mserve,A"BUF
 510 PSRT =  PEEK (BUF + 2) * 256: REM  Prog start
 520 PLNG =  PEEK (48840) + 256 *  PEEK (48841): REM  Prog length
 530  REM  Poke NADA.MSERVE code
 540  &  POKE (2,PSRT,PLNG,BUF): REM  POKE Message Server
 550  &  CALL (): REM  Run Message Server
 560  PRINT "Message Server now running on 2"
 570 MSRV = 1: POKE ITBL + 2,3: REM  Mark Msg Server
 580 :

Now we load the ATTACH.SLAVE program into memory for later use. Then P$() is initialized to contain an "unknown" prompt (:?) for each machine that is not serving, since it may already be attached.

 1000  REM  Load ATTACH.SLAVE program
 1010  PRINT  CHR$ (4)"bload "PFX$"attach.slave,A"BUF + 256
 1020 PSRT =  PEEK (BUF + 258) * 256: REM  Prog start
 1030 PLNG =  PEEK (48840) + 256 *  PEEK (48841): REM  Prog Length
 1040 :
 1050  FOR D = 3 TO MX: REM  Initialize prompt if not serving
 1060  IF  NOT  PEEK (ITBL + D) THEN P$(D) =  MID$ ("123456789ABCDEF",D,1) + ":?"
 1070  NEXT 
 1080 :

Now we prime the program by asking for the ID of the first machine to be attached. The ID is required to be 3 or more, since we cannot attach the master machine (#1) or the Message Server (#2). It is also possible to quit the program from this prompt.

 1090  INPUT "Attach to which machine? ";M$
 1100  IF M$ = "q" OR M$ = "Q" THEN  END 
 1110 D =  VAL (M$): IF D < 3 GOTO 1090
 1120 OUTQ = 48: REM  Output queue ($30)
 1130 :

When we arrive here, D has been set to the machine to be attached. We compute the input queue number for machine D and check whether it is serving by checking the ID table entry and resetting the prompt string array, P$(), if it is. If a machine is not serving, we assume that it is already attached and is looping in ATTACH.SLAVE's "keyboard input" routine.

If a machine is serving, then we (re-)load the ATTACH.SLAVE code and give it control, then print out our best guess at a prompt for the machine.

 1140  REM  Attach to machine D
 1150 INQ = OUTQ + D: REM  Input queue ($3#)
 1160  IF  PEEK (ITBL + D) THEN P$(D) = "": REM  Now serving
 1170  IF P$(D) <  > "" GOTO 1270: REM  Skip load if prompt and not serving
 1180 :
 1190  REM  Poke ATTACH.SLAVE code
 1200  POKE BUF + 259,INQ: POKE BUF + 261,OUTQ: REM  ATTACH.SLAVE queues
 1210  &  POKE #(D,PSRT,PLNG,BUF + 256)
 1220  IF  PEEK (1) GOTO 1270: REM  Not serving, maybe attached..
 1230  &  CALL (): REM  Run slave program
 1240  POKE ITBL + D,0: REM  Mark not serving
 1250  PRINT "ATTACH.SLAVE loaded on "D
 1260 :
 1270  PRINT P$(D);: REM  Initial prompt
 1280 :

This it the main loop of ATTACH, polling the local keyboard for input or commands and polling the output queue for any pending output from attached machines.

 2000  REM  Remote operating loop
 2010 :
 2020  IF  PEEK (KBD) < 128 GOTO 2260: REM  No key
 2030 L = 0
 2040 :
 2050 K =  PEEK (KBD): IF K < 128 GOTO 2050: REM  Get char
 2060 I =  PEEK (KST): REM  Clear strobe
 2070  IF L > 0 OR  PEEK (PB0) < 128 GOTO 2190
 2080 :

Commands are signalled by a single keypress with Open-Apple depressed, so we skip command processing if it's not the first character on a line or if Open-Apple is not depressed.

Commands for ATTACH are:

Keypress

Action

Q or q

Quit

? or /

Take census

"0" to "F"

Attach to machine 0 to F

When an Open-Apple hex digit is used to switch to another machine (attaching it if not already attached), the most recent prompt for the current machine is updated in P$() for future use.

 2090  REM  Handle initial input char = Open-Apple keypress
 2100  IF K = 209 OR K = 241 THEN  END : REM  OA-Q or OA-q = Quit
 2110  REM  OA-? or OA-/ = Census
 2120  IF K = 191 OR K = 175 THEN  GOSUB 3000:I = D: GOSUB 61000:D = I: GOTO 1150
 2130  REM  Open-Apple  = Switch machines
 2140  IF 176 < K AND K < 186 THEN  GOSUB 3000:D = K - 176: GOTO 1150
 2150  IF 224 < K THEN K = K - 32: REM  Convert a-f to A-F
 2160  IF 192 < K AND K < 200 THEN  GOSUB 3000:D = K - 183: GOTO 1150
 2170 :

Here we handle non-command input characters. We ignore the left arrow character, since it disables 80-column firmware, and echo all other characters and add them to the input buffer.

If the buffer is full, we send the partial input line. If the character is a carriage return we send the input line.

 2180  REM  Handle normal characters
 2190  IF K = 149 GOTO 2050: REM  Skip left arrow (NAK)
 2200  IF K <  > 141 THEN  PRINT  CHR$ (K - 128);: REM  Echo if not CR
 2210  POKE BUF + L,K:L = L + 1: REM  Buffer char
 2220  IF L > 115 THEN  GOSUB 2500: REM  Limit msg length
 2230  IF K <  > 141 GOTO 2050: REM  Loop until CR
 2240  GOSUB 2500: REM  Send input line
 2250 :

After sending input, or if there is no input line started, we check the output queue (merged for all attached machines) to see if there is any pending output to be displayed. If there is, we print it, otherwise, we loop back to check for input.

 2260  REM  Check for pending output
 2270  &  GET MSG#(2,OUTQ,LL,BUF)
 2280  IF  PEEK (1) GOTO 2020: REM  No output
 2290  POKE WL + 10,LL: CALL WL: REM  Print output
 2300  GOTO 2020: REM  Loop...
 2310 :

Following are two subroutines used by the main loop. The only "tricky" code is in saving the last prompt, where we do not save it if it does not appear to be a prompt--that is, if it is longer than three characters or a lone carriage return.

 2500  REM  Send input line
 2510  & PUTMSG#(2,INQ,L,BUF)
 2520  IF  PEEK (1) GOTO 2510: REM  Keep trying...
 2530 L = 0:LL = 0
 2540  RETURN 
 2550 :
 3000  REM  Save last prompt string
 3010  IF LL > 3 GOTO 3070: REM  Keep previous
 3020 P$(D) = ""
 3030  FOR J = 0 TO LL - 1
 3040 P$(D) = P$(D) +  CHR$ ( PEEK (BUF + J) - 128)
 3050  NEXT 
 3060  IF P$(D) =  CHR$ (13) THEN P$(D) = "": REM  Lone CR means detached
 3070  PRINT 
 3080  RETURN 
 3090 :

This is the standard "suffix" for NadaNet applications, added by EXECing the text file NADADEFS. The subroutine at 60000 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 3 * 60ms. so that unresponsive machines do not stall the &PEEK for very long. The default timeout is restored when the census is completed.)

 60000  REM  NadaNet Definitions for Applesoft
 60010  REM         MJM - 11/08/04
 60020 :
 60030 NP = 3 * 256 + 12 * 16 + 15: REM  NADANET page ($3CF)
 60040  IF  PEEK (NP - 2) <  > 76 THEN  PRINT "NadaNet not loaded.": STOP 
 60050  IF  PEEK (NP) <  > 145 THEN  PRINT "Not ProDOS version of NadaNet.": STOP 
 60060 :
 60070 BUF = 4 * 4096: REM  $4000
 60080 MX = 10: REM  Max machine ID
 60090 BSLF = NP - 3: REM  Boot ID in $3CC
 60100  & IDTBL(ITBL): REM  ID table
 60120  RETURN 
 60130 :
 61000  REM  Take census of serving machines
 61010  & TIMEOUT(3)
 61020  FOR D = 2 TO MX: REM  PEEK to verify service
 61030  &  PEEK #(D,NP,4,BUF): REM  PEEK config bytes
 61040  IF  PEEK (1) THEN TY$ = "not":TY = 0: GOTO 61100
 61050 TY$ = "(unknown)":TY = 16
 61060  IF  PEEK (BUF) = 184 THEN TY$ = "(Crate)":TY = 2
 61070  IF  PEEK (BUF) = 8 THEN TY$ = "(Mserve)":TY = 3
 61080  IF  PEEK (BUF) = 145 THEN TY$ = "(ProDOS)":TY = 4
 61090  IF  PEEK (BUF) = 141 THEN TY$ = "(DOS)":TY = 3
 61100  POKE ITBL + D,TY
 61110  PRINT "Machine "D" "TY$" serving."
 61120  NEXT D
 61130  & TIMEOUT(): REM  Reset retrys
 61140  PRINT "Census completed."
 61150  RETURN