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