Title: Altair Networking Environment
Date: January 06, 2019
Tags: altair programming
========================================

After getting the WiFi modem working, the next step was to write a more useful
client for using it.  I wanted to remove the need to send commands to the modem
directly and allow for some processing of data coming back.  I ended up with a
lot more.

The basic idea of this program was to abstract the modem away from the user.  I
wanted a simple interface to connect to a host using a specific protocol and
allow for processing and formatting of output.  The scope both shrank
considerably and grew massively.

What I ended up with is a very basic modem use-case.  I only implemented a
telnet mode because the telnet client is built into the modem firmware.  The
program only has to connect, disconnect, and pass input and output from terminal
to modem like the simple modem program used previously[0].  There is no need to
parse any server output or format anything.  There are the remnants of my plans
to implement a gopher client which I want to add next.  There is also a "raw"
mode which is functionally the same as the simple program from the previous
modem article and is basically the same thing as the terminal client but without
managing the connection.

A not too difficult program turned very complex because supporting multiple
clients and parsing commands has several possible implementations and user
interactivity.  I alternated between specifically optimized implementations and
generalized implementations.  The program is also highlighting the limitations
of hand assembling and using the switches for programming.  But at the same
time, it was also very revealing about some features of assemblers, compilers,
and command interpreters or shells.

Supporting at least telnet and raw clients necessitated a framework supporting
multiple commands.  In the days of BASIC environments, using floppy disks or
cassettes, you could load and execute multiple programs.  The environment had to
write that program into memory then begin executing it and provide a way to
abort the program and return to the environment.  I simulate this feature by
creating a "command table" listing available commands and the address to jump to
in order to begin their execution.  This allows for dynamic loading of commands,
or programs, instead of hard coded compares and jumps.  I can also add gopher
later without modifying the command processing code.  For simplicity, I only
support single character commands.

If you consider more modern shell environments, you can pass parameters on the
command line when executing the command.  The executed command will parse and
check that it received the parameters it requires.  My implementation of a
parameter parser ended up somewhere in the middle of generalized and specific to
my needs here.  It's written to be called like a library function by any client
(think: a proto-getopt).  The subroutine does not know what the parameters are
expected to be or how many there are.  It is up to the client to call the
subroutine as many times as needed to get the number of parameters it expects.
Since I am only working with networking clients, I only need a hostname to
connect to and a port.  In that order.  The port can be optional, which is easy
as the last parameter, since the client will support a specific protocol and can
default to that protocol's standard port.

My original implementation of the parser used hard coded memory locations to
copy each parameter into so the client could then read that location to get the
parameter.  That, however, meant a lot of memory planning.  I needed to find
space in memory to store strings of unknown length and in the general case, an
unknown number of them.  I don't know an efficient way to do this.  Also, since
the parameters were entered at the commandline already, I was duplicating
existing data which seemed silly.  I needed a way to dynamically find the
parameter in the existing commandline string and just return that.  Since the
commandline is a single string and each parameter needs to be a null terminated
string on its own, I still needed to traverse the string and terminate it.
Since parameters are going to be space separated, that leaves a convenient spot
for the null.  The address of the start of the parameter is returned in the DE
register.  I have the client push the address to the stack for storage to allow
subsequent calls to the parser until the end of the command line is found.  The
client can then pull the addresses out of the stack and read the values as
whatever it expects.  So ignoring the value of the string, return a 0 if we
found at least one character or a 1 if not to indicate the end of the command
line.  The client can decide what to do based on the return value.  I am copying
the UNIX and C-like implementation that a 0 indicates success instead of a 1.
Why?  There is a command to jump if you have a 0 in the zero flag bit.  There is
no command to jump if you get a 1.  I could jump if not 0, but if I want to
introduce error codes later, I can jump on 0 to skip any compares that determine
what non-zero value was returned.  Just like having the stack grow downwards,
returning a 0 on success was pretty much predestined by the hardware.

Instead of tossing the whole program up then walking through it, let's go
through the new pieces and the parts that taught a lesson.  I'll also skip over
describing the stuff we've seen in the previous echo and modem programs.

## Command Entry and Execution ##
Command entry is string based like the string echo[1] program.  Input is stored
to memory and when 'enter' is sent, we jump to process what we got.

# Process command #


; process command
; Assumes single char commands, looks for char in command table
; goes to address to execute command.  A null terminates the table
; and means the command wasn't found.
343     LDA             072 ; command buffer
344             120Q    120
345             002Q    002
346     CPI             376 ; Check if empty string
347             000Q    000
350     JZ              312 ; goto command entry
351             213Q    213
352             000Q    000
353     MVI M           066 ; Terminate string
354             000Q    000
355     LXI H           041 ; EOL string
356             010Q    010
357             002Q    002
360     CALL            315 ; write string to terminal
361             300Q    300
362             000Q    000
363     LXI H           041 ; Set pointer to start of buffer
364             120Q    120
365             002Q    002
366     MOV A,M         176 ; Get first character
367     INX H           043 ; Go past command character
370     MOV B,A         107 ; Save command char to B
371     LXI H           041 ; load command table
372             103Q    103
373             002Q    002
374     CMP M           276 ; If table char == command char
375     JZ              312 ; goto found
376             015Q    015
377             001Q    001
400     MVI A           076 ; Check for end of table
401             000Q    000
402     CMP M           276 ; If table entry == null
403     JZ              312 ; goto command not found
404             023Q    023
405             001Q    001
406     MOV A,B         170 ; Copy command back from B
407     INX H           043 ; move to next table entry
410     INX H           043
411     INX H           043
412     JMP             303 ; goto next command char
413             374Q    374
414             000Q    000
; command found
415     INX H           043 ; Goto low address byte
416     MOV E,M         136 ; Store in E
417     INX H           043 ; Get high address byte
420     MOV D,M         126 ; Store in D
421     XCHG            353 ; Swap DE into HL
422     PCHL            351 ; Execute command at HL address
; command not found
423     LXI H           041 ; Set HL to error
424             013Q    013
425             002Q    002
426     CALL            315 ; Invalid command
427             300Q    300 ; write string to terminal
430             000Q    000
431     JMP             303 ; Get a new command
432             206Q    206 ; goto command init
433             000Q    000

; command table
1103    t       164Q    164 ; telnet
1104            073Q    073
1105            001Q    001
1106    !       041Q    041 ; raw
1107            250Q    250
1110            001Q    001
1111    \0      000Q    000 ; null at end of table


Initially, command parsing was hard coded to check for each command character
and jumped accordingly but I quickly realized that adding new commands would be
difficult as it would shift all the code.  I decided to put each command into a
table with it's subroutine address.  With this, I can add any number of commands
and the same command parser would work.  Turns out, this solution is similar to
what CP/M used for calling OS sub-routines (like modern day syscalls) and I'll
explore more about that in the future.

I took some shortcuts since command parsing isn't really what I was trying to
accomplish.  I only allow single character commands.  Makes it easy to compare
and jump and be on to executing fun things.  However, I took the long cut of
allowing parameters to be passed with the command.  So, targeting a telnet
client, the user can enter 't hostname port' with port being optional and
defaulting to the telnet port 23.  Since telnet always needs a host to connect
to, might as well just enter it there.  The same will be true for gopher and any
other network client I might write and add here so it's a useful feature.

All this code does is start at the beginning of the command buffer, read the
character and compare it to the command table.  If there is no match, skip ahead
in the table past the sub-routine address to the next command character and
compare again.  The table is null terminated to make it easy to know when we've
checked all known commands and can print an error.

If the command is found, read the address into DE, so HL can still be used to
point to the table, then swap DE into HL.  Then set the program counter to the
address in HL which causes execution to continue at that addess.

If the command is not found, print an error and reset the command buffer and go
back for another one.

# Parse parameters #


; parse parameters
; expects pointer to args in HL
; returns pointer to terminated param in DE
; and return value in B
434     MVI B           006 ; Preload return with 1
435             001Q    001
436     MOV A,M         176 ; Read until first arg
437     INX H           043
440     CPI             376 ; If space
441             040Q    040
442     JZ              312 ; read next letter
443             036Q    036
444             001Q    001
445     DCX H           053 ; Go back to first non-space character
446     MOV D,H         124 ; Copy param pointer
447     MOV E,L         135 ; to DE
450     MOV A,M         176 ; Get character
451     CPI             376 ; If null
452             000Q    000
453     RZ              310 ; return
454     CPI             376 ; If space
455             040Q    040
456     JZ              312 ; goto done
457             067Q    067
460             001Q    001
461     MVI B           006 ; Found a char
462             000Q    000 ; Set return to 0
463     INX H           043 ; Increment args
464     JMP             303 ; Jump to get character
465             050Q    050
466             001Q    001
; done
467     MVI M           066 ; Terminate parameter string
470             000Q    000
471     INX H           043 ; Move past \0
472     RET             311

; telnet
473     LXI H           041 ; Load command buffer
474             120Q    120
475             002Q    002
476     INX H           043 ; move past command char
477     CALL            315 ; Call parse parameters
500             034Q    034
501             001Q    001
502     MOV A,B         170 ; Get return value
503     CPI             376 ; Check if return is 1
504             001Q    001
505     JZ              312 ; Yes, no hostname
506             207Q    207 ; goto telnet error
507             001Q    001
510     PUSH D          325 ; Save hostname address to stack
511     CALL            315 ; call parse params again
512             034Q    034
513             001Q    001
514     MOV A,B         170 ; Get return value
515     CPI             376 ; Check if return is 1
516             001Q    001
517     JNZ             302 ; No, goto connect
520             134Q    134
521             001Q    001
522     INX H           043 ; Skip over null
523     MVI M           066 ; Set port to 23
524             062Q    062
525     INX H           043
526     MVI M           066
527             063Q    063
530     INX H           043
531     MVI M           066 ; and terminate it
532             000Q    000
533     INX D           023 ; Increment param pointer in DE
; connect
534     PUSH D          325 ; Push port address to stack
..


Here is a snippet of the telnet program which uses parse parameters.  More
shortcuts.  Telnet knows it needs a hostname and that the port can be optional.
Think of it as telnet working with something like the ARGV variable we have in C
today.  It starts with the command itself and the parameters follow.  So telnet
starts at the beginning of the command buffer and skips ahead a character past
the command itself.  The parse parameters sub-routine expects HL to be the
pointer to the current location in the command buffer so subsequent calls will
continue on to the next parameter until the end of the buffer.  It returns the
start of the parameter value in DE and a 0 if a value was found or a 1 if not.

Parse parameters starts by setting the return value to 1 in case nothing is
found.  Then we grab characters and loop over spaces until a non-space
character.  The side effect here is that we don't need a space after the
command, but we need at least one space between parameters.  Once we find a
non-space character, move the pointer back to set up for the next read loop.
That means we will read this character twice.  I should try to optimize that.
That also puts the pointer in HL at the beginning of the parameter string so we
can save it to DE.  Read the character, if we have a null, we're done.  If not,
check again for a space, if not we have a parameter value so set the return
value to 0.  Check the next characters until space or null.  If space, write a
null over the space to terminate the parameter value string and increment the HL
pointer past it.  Then return to the caller which might call back for another
parameter.

This process allows a program to call parse parameters until there is a return
value of 0.  The program keeps track of the number of parameters it needs and if
it got enough of them.  Telnet, in this example, pushes each parameter pointer
to the stack which allows for an arbitrary number of parameters to be found.  We
know telnet only needs one parameter and can fill in a default for the second so
we only use a return of 0 to indicate the optional port parameter wasn't passed
and we need to supply a default.  That is done by writing it to the command
buffer as if it had been entered by the user and since parse parameters saved
the address it thought might be a parameter value in DE we can use it by
incrementing and pushing it to the stack.  If there are more parameters after a
port, they just get ignored.

I initially had parse parameters read from the command buffer and copy values
into new locations but it occurred to me that if the values were already in
memory, as entered by the user, why not just use that?  Also managing memory
manually is hard.  Where would I put those values?  What if there are a lot of
them?  We'll dig into more dynamic memory management some day, I hope.

## Memory management ##
Speaking of managing memory, enough iterations happened while writing this
program that I had to map out where things were so when they changed, I would
know what to update.  When manually assembling, you also need to manually keep
tack of where everything will be in memory.


000 start up
025 delete
070 interrupt handler
200 reset
206 command init
213 command entry
265 write char to terminal
300 write string to terminal
314 write char to modem
327 write string to modem
343 process command
434 (001 034) parse parameters
 467 (001 067) done
473 (001 073) telnet
 534 (001 134) connect
 607 (001 207) telnet error
650 (001 250) raw mode
675 (001 275) raw terminal handler
720 (001 320) raw modem handler
736 (001 336) abort connection
(754 end of program)

static data
1000 (002 000) hostname error
1010 EOL
1013 invalid command
1035 telnet command prefix
1043 telnet command suffix
1046 gopher command prefix
1053 gopher command suffix
1056 modem echo off
1064 modem echo on
1072 modem disconnect
1077 delete string
(1102 end of static data)

command table
1103 (002 103)
(1111 end of table)

dynamic data
1120 (002 120) command buffer


Each sub-routine's starting address is listed as well as any other jump points
into it.  Also all the static data:  strings and the command table, and dynamic
storage:  in this case, just the command buffer.  An assembler will allow you to
create data statements to store numbers and strings without having to worry
about their location.

I also got to learn first hand why the world switched from octal to hexadecimal.
I already knew this, but now I've lived it.  As you can see, I have
parentheticals to show the separate high and low bytes in octal.  This
illustrates the problem with octal.  When you separate the bytes, the values
change.  So an address of 434Q, when stored as 2 8 bit bytes becomes 001Q 034Q.
The same address in hexadecimal would be 0x011C which separates out to 0x01 and
0x1C.  Much easier to deal with and why hexadecimal as adopted over octal.

This also highlights how bad life can be without an assembler.  Every tweak and
change to code changes the memory values after it and all references need to be
updated.  An assembler takes care of that for you and allows you to use labels
to reference specific addresses without needing to know the specific address.

## Conclusions ##
The lesson learned from all of this was not how to write an interface for using
the WiFi modem but rather how the development workflow needed to evolve very
quickly to allow more complex programming.  This is not a particularly long
program (though the longest I have written for the Altair) but I think I have
already reached the limit of hand assembly as well as manual entry.

The solution to the second problem is the next step in the history of home
computing.  As teletypes and terminals became more available, more efficient
program entry was possible.  Monitor programs, which were stored in non-volatile
PROM chips allowed interaction with the system using a teletype or terminal and
also storage of boot loaders for other systems like BASIC.

I want to write a custom monitor that will allow me to leverage my modern
"terminal" (a computer with a terminal emulator) to write programs to the Altair
and read them back to store on the computer.

From there we can add assembly-like functionality to make programming easier and
more easily improve our command environment that's been started here.

--------------------------------------------------------------------------------

I'm not going to get into the telnet program here, this article is long enough
and there isn't much more of interest to it.  It has some missing features and
it's just been too difficult to work on to continue as is.  I already cheated
and wrote it in a text editor on my computer.  I knew there would be too much
iteration to do on paper.  I'll revisit and share it when we have a better
development process.


[0] https://blog.kagu-tsuchi.com/articles/altair_on_wifi.html
[1] https://blog.kagu-tsuchi.com/articles/8080_IO_string_echo.html