.........1.........2.........3.........4.........5.........6.........7.........8 STREAMS AND CHANNELS by Toni Baker part 4 of 5, ZX Computing March 1987 If you thought that you couldn't use the ZX Printer with your new 128 then Toni Baker can prove you wrong. [Note that the code in STREAMS.TAP "PART4"] [includes the code from "PART3". JimG] This article contains not one, but two new channels which you can use on your Spectrum. The first is a channel which enables users of the Spectrum 128 (or the 128+2) to be able to use the ZX Printer - or some other compatible printer such as an Alphacom - even in 128K mode. If you don't have a Spectrum 128 then you don't need this new channel of course, because you can use the ZX Printer anyway. The second is not really a new channel at all, but a modification to an existing channel. It enables the Spectrum to be able to communicate freely with a QL via the Local Area Network available from the ZX Interface One. Of course communication between these computers via the network is already possible, and David Nowotnik has recently been doing an excellent series in ZX Computing on that very task. It is not my intention to duplicate any of his material, but merely to remove a couple of deficiencies in the channel itself. We'll return to this later, meanwhile back to the ZX Printer. Channel Z The program as such begins at address B544. If this seems a rather arbitrary address to you then I should explain that the reason it starts at B544 is that all the code follows on from the stuff in last month's article (which ran from B000 to B543). Indeed, some of the subroutines developed last month will be used in the routines given here. The trick to using the ZX Printer on the 128 is to avoid any possibility of erasing or corrupting the OLD printer buffer (addresses 5B00 to 5BFF) - this is because the memory in this range is used by the 128 to store various paging subroutines, system variables, and an alternative machine stack for use when accessing the vast banks of paged memory. If we can arrange things so that we can avoid corrupting this memory then the good old ZX Printer can still work normally. You see, all of the ZX Printer software in the ROM was written before the advent of the 128 and is not compatible with the requirements of the new machine. All we really need to do then is to create a NEW printer buffer somewhere in memory, and use the new buffer instead of the old one. The best possible place for this buffer would be as part of the channel information block for the channel - that would mean that the area occupied by the new printer buffer would be reclaimed whenever you closed the channel. We shall call this new channel "Z", for ZX-Printer. Take a look at Figure One - it shows the structure of the channel information block for the new channel. As you can see, the first eleven bytes store completely standard information, using the standard which was developed throughout this series. (IX+05/06) contains the constant 1234h, which identifies this as being a user-defined channel. This means that it may be opened or closed using some of the software listed last month (IX+0B), (IX+0C) and (IX+0D) also store information in the same format as the "W" channel given last month - this is so that we can exploit more of last month's subroutines. As you shall see, a collection of new channels is far more advantageous than a single new channel. Finally, the new printer buffer itself runs from (IX+0E) upwards, and is 100h bytes in length (same as the old printer buffer). The variable Z_XCOORD (IX+0C) will be zero if the printer buffer is completely empty, or non-zero otherwise. If the new buffer is full it will contain 20h. Z_WIDTH (IX+0D) remains constant at 20h - the width of the buffer. This is so that the TAB and comma-control routines from last month may be exploited successfully. The program begins at the label Z_CLOSE, which is the routine to perform all of the peripheral tasks necessary to close the channel. In fact this merely consists of testing whether or not the buffer is empty, and printing a newline if it isn't. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Figure 1 IX+00 (2 bytes) Z_PRINT Address of channel "Z" output IX+02 (2 bytes) REPORT_J Address of channel "Z" input IX+04 (1 byte) Z_NAME Name of channel (ie. "Z") IX+05 (2 bytes) Z_IDEN Constant 1234h identifies new channel IX+07 (2 bytes) Z_CLOSE Address of close buffer routine IX+09 (2 bytes) Z_LEN Length of channel info (ie. 010E) IX+0B (1 byte) Z_FLAGS Various flags Bit 7: Set if leading space not reqd. for keywords, reset otherwise Bit 6: Not used Bit 5: Not used Bit 4: Not used Bit 3: Set if INVERSE status is ON, reset otherwise Bit 2: Set if OVER status is ON, reset otherwise Bit 1: Set if exactly two additional parameters reqd., reset otherwise Bit 0: Set if exactly one additional parameter reqd., reset otherwise IX+0C (1 byte) Z_XCOORD X coordinate of print position in buffer IX+0D (1 byte) Z_WIDTH Width of buffer (ie. 20h) IX+0E (100h bytes) Z_BUFFER New printer buffer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Newline The Z_NEWLINE routine may be compared to the ROM routine COPY_BUFF at address 0ECD. Its purpose is to transfer the contents of the buffer to the ZX Printer itself, before finally erasing the previous buffer contents. The routine Z_EMPTY does the actual erasing. You will notice a couple of differences between the ROM routine COPY_BUFF and my routine Z_NEWLINE. Firstly, the new printer buffer is used instead of the old one, and secondly the subroutine Z_COPY_LINE is called instead of the original COPY_LINE. In fact Z_COPY_LINE is itself very similar to the ROM's COPY_LINE routine (at 0EFA). The only difference in evidence is the effect of pressing the BREAK key - in my routine Z_EMPTY is called instead of CLEAR_PRB. This removes any possible chance of corrupting the old printer buffer area. A general purpose subroutine is included next - SEARCH_CH_ALL. It is entered with the A register containing the name of a channel. The routine will search through the channel information area looking for a channel with this name. If it finds one it will return with IX pointing to the channel information block and the carry reset, otherwise the carry will be set. We shall make use of this subroutine later. Z_PRINT Z_PRINT is the routine which "prints" a character held in the A register onto the new printer buffer. Note that the subroutine at address B12F comes from last month's article and will sort out all keywords, control parameters, and will deal with both the comma-control and the TAB function. On return from this subroutine the C register will contain any INVERSE or OVER parameters, or the x coordinate of any AT parameters. The Z_PRINT routine itself is really quite simple, bearing in mind that it has to deal with ASCII characters, block graphics, and UDGs, as well as OVER, INVERSE, AT and ENTER. Follow it through to see how all the various cases are dealt with. Finally (for this channel) we have Z_OPEN, which opens the channel and attaches it to a stream. On entry, the A register must contain the stream number to which the channel is to be attached and the subroutine will do the rest. It makes use of another subroutine from last article - the OPEN_NEW routine which will open user defined channels. Channel Q When communicating with the QL via the normal network channel you will notice a couple of problems. The first is that on the Spectrum the code for "enter" is 0D (thirteen), whereas on the QL the code for "enter" is 0A (ten). This means that, for instance, PRINT A$ on the QL cannot be matched by INPUT A$ on the Spectrum, nor vice versa. It is normally necessary to use PRINT A$;CHR$(13); on the QL in order that the Spectrum may use INPUT A$. Conversely, INPUT A$ on the QL needs to be matched by PRINT A$;CHR$ 10; on the Spectrum. What I intend to do is to modify the "N" channel attached to a particular stream so that the deficiencies are removed. In a modified "N" channel these problems disappear. PRINT A$ on the Spectrum may be matched by INPUT A$ on the QL, and vice versa. Other surprising advantages turn up; if you LIST a Spectrum program over a modified network channel then the QL will be able to LOAD the program with the single command LOAD NET1_2 (assuming that the Spectrum is station number 2) - or to transfer the program directly onto a QL microdrive ready for loading later with the single QL command COPY NET1_2 TO MDV1_PROG. The second problem is a little more complex, but not much so. The Spectrum command INPUT A$ actually expects to receive a string expression, not the contents of an actual string (although this is not the case with INPUT LINE). Fortunately for us, we seldom notice this because the Spectrum provides the surrounding quotes - this converts the string text into a string expression and everything is hunky dory - with one exception. If the string printed over the network by the QL contains a quotes character then the Spectrum will be unable to evaluate the result as a string expression and will halt with error "C Nonsense in BASIC". The cure to this problem is very simple. If, during Spectrum INPUT (as opposed to INKEY$ or INPUT LINE) a quotes character is received from a modified network channel THEN IT MUST BE CONVERTED TO TWO CONSECUTIVE QUOTES CHARACTERS in order that the expression will still evaluate. Let's take a look at the subroutines now. We begin with SHADOW_DE which will call a subroutine in the Shadow ROM whose address is DE. It achieves this by fooling the Spectrum into thinking that we are returning to the Shadow ROM from a Spectrum subroutine, and then simply paging out the Shadow ROM on completion. Q_PRINT is the routine which "prints" a character over the modified network. As you can see it is extremely simple, merely testing for an "enter" character and converting such to 0A, before using the original Shadow-PRINT subroutine to print the character. Q_INPUT is more complicated, though not much so. Firstly, the routine tests the error return address to see whether the character input is due to an INPUT command, or due to an INKEY$ function. If it turns out to be merely INKEY$ then things are just as simple as for Q_PRINT. The shadow input routine is called to read a character, and if that character is 0A then it is converted to "enter". If, however, we are dealing with INPUT (as opposed to INKEY$) then the first thing we must do is abandon altogether use of the EDITOR routine in the ROM (which we do by emptying the machine stack as far as the return address from the EDITOR call) and then supplying a new routine to replace EDITOR. We can call Q_INKEY$ as a sub- routine to fetch a single character from the network (converting 0A to 0D) and the flags will tell us if the character is valid or not. Then we can call a ROM subroutine ADD_CHAR_1 to actually insert the character into the input line. Quote characters must be inserted twice, not once (except during INPUT LINE). Finally, if an "enter" character is received then we may return, as if from the EDITOR routine, with the finished string expression in the INPUT area of RAM. MODIFY_N is the routine which will convert a "N" channel to do as it's told. On entry the A register must contain the stream number of an existing "N" channel which must have been opened normally. In such a channel (IX+00/01) and (IX+02/03) will normally contain the value 0008 which, if called, will page in the Shadow ROM and then call the actual output or input subroutine addressed by (IX+05/06) and (IX+07/08) respectively. The MODIFY_N subroutine will merely convert these 0008s to the addresses of Q_PRINT and Q_INPUT, leaving the addresses of the Shadow routines unchanged. The Shadow routines will then only be called when required by Q_PRINT or by Q_INPUT. Finally, a five byte demonstration of the "Z" channel. Simply calling Z_DEMO from BASIC will open a new channel "Z", and attach it to stream number four. Channel R In the next and final part of this series we shall examine the use of the existing channel "R", which may only ever be used in machine code. We shall also be creating a new channel, also called "R" (R for RAM-disc) which will enable you to create READ and WRITE files in the Spectrum 128's RAM-disc area, just as you can on the microdrives. Till then, keep smiling, and don't take anything seriously. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - B544 DD7E0C Z_CLOSE LD A,(Z_XCOORD) ;A= x coordinate within buffer B547 A7 AND A B548 C8 RET Z ;Return immediately if buffer empty B549 DDE5 Z_NEWLINE PUSH IX B54B E1 POP HL ;HL= address of channel info B54C 010E00 LD BC,#000E B54F 09 ADD HL,BC ;HL points to new printer buffer B550 F3 DI ;Disable interrupts B551 0608 LD B,#08 B553 C5 Z_NL_LOOP PUSH BC B554 CD73B5 CALL ZCOPYLINE ;Output next row to ZX Printer B557 C1 POP BC B558 10F9 DJNZ Z_NL_LOOP ;Output all eight rows B55A 3E04 LD A,#04 B55C D3FB OUT (#FB),A ;Switch off printer motor B55E FB EI ;Enable interrupts B55F DDE5 Z_EMPTY PUSH IX B561 E1 POP HL ;HL= address of channel info B562 010E00 LD BC,#000E B565 09 ADD HL,BC ;HL points to new printer buffer B566 AF XOR A ;A= 00 B567 77 ZEMPTLOOP LD (HL),A ;Zero next byte of buffer B568 23 INC HL ;HL points to next byte in buffer B569 10FC DJNZ ZEMPTLOOP ;Zero entire buffer B56B DDCB0BFE SET 7,(Z_FLAGS) ;Signal "Leading space not required" B56F DD770C LD (Z_XCOORD),A ;Reset x coordinate in buffer B572 C9 RET ;Return B573 78 ZCOPYLINE LD A,B ;A= row number to output B574 FE03 CP #03 B576 9F SBC A,A ;A= FF (last 2 rows); 00 (otherwise) B577 E602 AND #02 ;A= 02 (last 2 rows); 00 (otherwise) B579 D3FB OUT (#FB),A ;Switch on printer motor, but with ;slow speed for last two rows B57B 57 LD D,A ;D= "last 2 rows" flag B57C CD541F Z_CL_LOOP CALL BREAK_KEY ;Test BREAK key B57F 380A JR C,Z_CL_NEXT ;Jump forward unless BREAK pressed B581 3E04 LD A,#04 B583 D3FB OUT (#FB),A ;Switch off printer motor B585 FB EI ;Enable interrupts B586 CD5FB5 CALL Z_EMPTY ;Empty the new printer buffer B589 CF RST #08 ;Report "D, BREAK - CONT repeats" B58A 0C DEFB #0C B58B DBFE Z_CL_NEXT IN A,(#FE) ;A= status of printer B58D 87 ADD A,A B58E F8 RET M ;Return if printer not connected B58F 30EB JR NC,Z_CL_LOOP ;Wait until printer stylus is ready B591 C3120F JP COPY_L_2A Jump to print next row from buffer B594 DD2A4F5C SEARCHALL LD IX,(CHANS) ;IX points to base of chan info area B598 011400 LD BC,#0014 B59B 57 LD D,A ;D= name of channel to search for B59C DD09 SCHCHLOOP ADD IX,BC ;IX points to next chan info area B59E DD7E00 SEARCH_CH LD A,(IX+#00) B5A1 FE80 CP #80 B5A3 37 SCF B5A4 C8 RET Z ;Return with Carry set if channel ;not found B5A5 DD7E04 LD A,(IX+#04) ;A= name of channel pointed to B5A8 BA CP D B5A9 C8 RET Z ;Return with IX pointing to channel ;information area, and Carry reset, ;if search successful B5AA DD4E09 SEARCHNXT LD C,(IX+#09) B5AD DD460A LD B,(IX+#0A) ;BC= length of channel info B5B0 18EA JR SCHCHLOOP ;Loop back to continue search B5B2 DD2A515C Z_PRINT LD IX,(CURCHL) ;IX points to channel info area B5B6 CD2FB1 CALL CHR_TYPE2 ;Deal with keywords, control params, ;TAB and comma control B5B9 F8 RET M ;Return if tasks complete B5BA F5 PUSH AF B5BB 283B JR Z,ZGRAPHICS ;Jump with graphics characters B5BD 3033 JR NC,Z_ASCII ;Jump with ASCII characters B5BF FE0D CP #0D B5C1 2005 JR NZ,Z_PRCTRLS ;Jump unless char is "enter" B5C3 CD49B5 CALL Z_NEWLINE ;Print a newline B5C6 187F JR Z_EXIT ;and jump to exit B5C8 D614 Z_PRCTRLS SUB #14 B5CA 387B JR C,Z_EXIT ;Jump with controls 00 to 13 B5CC 0608 LD B,#08 ;B has bit 3 set B5CE 2805 JR Z,Z_INVOVER ;Jump with "INVERSE control" B5D0 3D DEC A B5D1 0604 LD B,#04 ;B has bit 2 set B5D3 200F JR NZ,Z_AT ;Jump unless char is "OVER control" B5D5 CB19 Z_INVOVER RR C B5D7 9F SBC A,A ;A= 00 (INVERSE 0 or OVER 0) ;or FF (INVERSE 1 or OVER 1) B5D8 DDAE0B XOR (Z_FLAGS) ;A= flags byte, but complemented if ;parameter is one B5DB A0 AND B ;A all bits reset, except that bit ;3 (INVERSE) or bit 2 (OVER) will be ;taken from flags byte, and ;complemented if parameter is one B5DC DDAE0B XOR (Z_FLAGS) B5DF DD770B LD (Z_FLAGS),A ;Bit 3 (INVERSE) or bit 2 (OVER) of ;flags byte will be assigned with ;parameter B5E2 1863 JR Z_EXIT ;Jump to exit B5E4 3D Z_AT DEC A B5E5 2060 JR NZ,Z_EXIT ;Jump unless control is "AT control" B5E7 79 LD A,C ;A= required x coordinate B5E8 FE20 CP #20 B5EA D29F1E JP NC,REPORT_B ;Error if out of range B5ED DD770C LD (Z_XCOORD),A ;Assign x coordinate as required B5F0 1855 JR Z_EXIT ;Jump to exit B5F2 ED5B365C Z_ASCII LD DE,(CHARS) ;DE= addr of character set - 100h B5F6 1811 JR Z_CHR_1 ;Jump forward B5F8 D690 ZGRAPHICS SUB #90 B5FA 3009 JR NC,Z_UDG ;Jump with user-defined graphics B5FC 47 LD B,A B5FD CD380B CALL PO_GR_1 ;Construct graphic in MEMBOT area B600 21925C LD HL,MEMBOT ;HL points to graphic form B603 180B JR Z_CHR_2 ;Jump forward B605 ED5B7B5C Z_UDG LD DE,(UDG) ;DE= address of user-defined graphics B609 6F Z_CHR_1 LD L,A B60A 2600 LD H,#00 B60C 29 ADD HL,HL B60D 29 ADD HL,HL B60E 29 ADD HL,HL B60F 19 ADD HL,DE ;HL points to required graphic form B610 DD7E0C Z_CHR_2 LD A,(Z_XCOORD) ;A= current x coordinate in buffer B613 FE20 CP #20 B615 E5 PUSH HL B616 CC49B5 CALL Z,Z_NEWLINE ;Print a newline if buffer full B619 D1 POP DE ;DE points to graphic form B61A DD7E0C LD A,(Z_XCOORD) ;A= current x coordinate in buffer B61D 3C INC A ;A= new x coordinate B61E DD770C LD (Z_XCOORD),A ;Store in system variable B621 C60D ADD A,#0D B623 4F LD C,A B624 0600 LD B,#00 ;BC= displacement to posn in buffer B626 DDE5 PUSH IX B628 E1 POP HL ;HL points to channel information B629 09 ADD HL,BC ;HL points to current posn in buffer B62A 0608 LD B,#08 B62C EB Z_CHRLOOP EX DE,HL ;DE= current buffer position ;HL points to graphic form B62D 1A LD A,(DE) ;A= byte from buffer B62E DDCB0B56 BIT 2,(Z_FLAGS) B632 2001 JR NZ,Z_CHROVER ;Jump if OVER off B634 AF XOR A B635 DDCB0B5E Z_CHROVER BIT 3,(Z_FLAGS) B639 2801 JR Z,Z_CHR_INV ;Jump if INVERSE on B63B 2F CPL B63C AE Z_CHR_INV XOR (HL) ;A= byte from graphic form, with ;OVER and INVERSE taken into account B63D 12 LD (DE),A ;Store in buffer B63E E5 PUSH HL ;Stack pointer into graphic form B63F 212000 LD HL,#0020 B642 19 ADD HL,DE ;HL points to appropriate byte in ;buffer for next row of character B643 D1 POP DE ;DE points into graphic form B644 13 INC DE ;DE points to next byte to use B645 10E5 DJNZ Z_CHRLOOP ;Print whole character into buffer B647 F1 Z_EXIT POP AF ;A= character just printed B648 A7 AND A ;Reset the Carry flag B649 C9 RET ;Return B64A 08 Z_OPEN EX AF,AF' ;A'= stream number to attach channel B64B 3E5A LD A,"Z" B64D CD94B5 CALL SEARCHALL ;Search for another "Z" channel B650 D2C415 JP NC,REPORT_J ;Error if one exists B653 DD2144B5 LD IX,Z_CLOSE ;IX= close buffer address B657 21B2B5 LD HL,Z_PRINT ;HL= output address B65A 11C415 LD DE,REPORT_J ;DE= input address B65D 010E01 LD BC,#010E ;BC= length of channel info B660 CD6DB0 CALL OPEN_NEW ;Open the channel B663 DD360B00 LD (Z_FLAGS),#00 ;Reset the flags B667 DD360D20 LD (Z_WIDTH),#20 ;Specify buffer width = 20h chars B66B C35FB5 JP Z_EMPTY ;Empty the buffer, and return B66E 210007 SHADOW_DE LD HL,UNPAGE B671 E5 PUSH HL ;Stack UNPAGE address in Shadow ROM B672 D5 PUSH DE ;Stack Shadow subroutine address B673 65 LD H,L ;HL= 0000 B674 E5 PUSH HL ;Stack 0000 signalling "Return to ;Shadow ROM address" B675 C30800 JP #0008 ;Jump to call Shadow subroutine B678 DD2A515C Q_PRINT LD IX,(CURCHL) ;IX points to channel information B67C F5 PUSH AF ;Stack character to print B67D FE0D CP "enter" B67F 2002 JR NZ,Q_PRINT_2 ;Jump unless character is "enter" B681 3E0A LD A,#0A ;A= QL's code for "enter" B683 DD5E05 Q_PRINT_2 LD E,(IX+#05) B686 DD5606 LD D,(IX+#06) ;DE= Shadow output address B689 CD6EB6 CALL SHADOW_DE ;Call network output in Shadow ROM B68C F1 POP AF ;A= character just printed B68D A7 AND A ;Reset the Carry flag B68E C9 RET ;Return B68F DD2A515C Q_INPUT LD IX,(CURCHL) ;IX points to channel information B693 2A3D5C LD HL,(ERR_SP) ;HL points to error return address B696 5E LD E,(HL) B697 23 INC HL B698 56 LD D,(HL) ;DE= error return address B699 217F10 LD HL,ED_ERROR B69C A7 AND A B69D ED52 SBC HL,DE B69F 2814 JR Z,Q_INPUT_2 ;Jump if in an INPUT channel B6A1 DD5E07 Q_INKEY$ LD E,(IX+#07) B6A4 DD5608 LD D,(IX+#08) ;DE= Shadow input address B6A7 CD6EB6 CALL SHADOW_DE ;Call network input from Shadow ROM B6AA 47 LD B,A ;B= character just input B6AB F5 PUSH AF ;Stack the flags B6AC FE0A CP #0A B6AE 2002 JR NZ,Q_INKEY$2 ;Jump unless this is a QL "enter" B6B0 060D LD B,#0D ;B= Spectrum "enter" character B6B2 F1 Q_INKEY$2 POP AF ;Restore the flags B6B3 78 LD A,B ;A= character to return B6B4 C9 RET ;Return B6B5 ED7B3D5C Q_INPUT_2 LD SP,(ERR_SP) ;Empty the machine stack down as ;far as ED_ERROR B6B9 E1 POP HL ;Drop the address ED_ERROR B6BA E1 POP HL ;HL= normal error return address ptr B6BB 223D5C LD (ERR_SP),HL ;Restore pointer to error return addr B6BE CDA1B6 Q_INPLOOP CALL Q_INKEY$ ;Read next character from network B6C1 3804 JR C,QINPSTORE ;Jump if character OK B6C3 28F9 JR Z,Q_INPLOOP ;Jump if still waiting B6C5 CF RST #08 ;Report "8, End of file" B6C6 07 DEFB #07 B6C7 FE0D QINPSTORE CP #0D B6C9 C8 RET Z ;Return (INPUT is now finished) if ;character is "enter" B6CA CD850F CALL ADD_CHAR_1 ;Store the char in the INPUT line B6CD FDCB377E BIT 7,(FLAG_X) B6D1 20EB JR NZ,QINPLOOP ;Loop back if doing INPUT LINE B6D3 FE22 CP #22 B6D5 CC850F CALL Z,ADD_CHAR_1 ;Double-up quote characters B6D8 18E4 JR Q_INPLOOP ;Go back for rest of INPUT B6DA CD2117 MODIFY_N CALL STR_DATA1 ;BC= STRMS data for stream supplied B6DD 78 LD A,B B6DE B1 OR C B6DF 280C JR Z,MOD_ERROR ;Error if stream not in use B6E1 2A4F5C LD HL,(CHANS) ;HL points to base of chan info area B6E4 09 ADD HL,BC ;HL points to 2nd byte of chan info B6E5 23 INC HL B6E6 23 INC HL B6E7 23 INC HL ;HL points to 5th byte of chan info B6E8 7E LD A,(HL) ;A= name of channel B6E9 FE4E CP "N" B6EB 2802 JR Z,MOD_QL ;Jump only if channel is "N" B6ED CF MOD_ERROR RST #08 ;Report "O, Invalid stream" B6EE 17 DEFB #17 B6EF 2B MOD_QL DEC HL B6F0 36B6 LD (HL),#B6 B6F2 2B DEC HL B6F3 368F LD (HL),#8F ;Store new input address B6F5 2B DEC HL B6F6 36B6 LD (HL),#B6 B6F8 2B DEC HL B6F9 3678 LD (HL),#78 ;Store new output address B6FB C9 RET ;[This last bit of code gets overwritten by the code in Part 5. JimG] B6FC 3E04 Z_DEMO LD A,#04 ;A= stream number to attach B6FE C34AB6 JP Z_OPEN - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -