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-Apple1460 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