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