How I added FujiNet Clock support to FoRem 26M - FujiNetWIFI/fujinet-firmware GitHub Wiki
I have decided to keep a version of FoReM-26M, a classic Atari BBS that hasnโt seen the light of day in over 35 years, running on an Atari 800 that I have here in my lab.
Before I did that, though, I needed to do two things, the first was to move it from MYDOS 3.08, to some form of SpartaDOS. This ultimately meant that I could not only use much larger message files (and not have the same random access problems present in Atari DOS 2.x filesystems like MyDOS), but I could also have much larger file areas as well. This first task was as simple as copying the entire board onto two 16 megabyte SpartaDOS disks, one holding the message bases, the other holding the files, and transferring those onto my SIDE2 compact flash card on the Incognito present in the target machine, a process that took roughly an hour to build and test. The board was able to cleanly move onto SpartaDOS 3.2, without any issues and was able to answer calls. This solved the first task.
I also needed to fix the broken clock. FoReM-26M uses a custom AUTORUN.SYS that places a small machine language routine in memory that implements a running real time clock that must be set when FoReM starts. If this AUTORUN.SYS isnโt loaded, the clock doesnโt update. However, even with the provided AUTORUN.SYS, the clock wasnโt updating regularly at all and immediately got out of sync. It was useless. So I had to replace the clock routines with something that worked.
As an aside, FoReM-26M is one of the largest Atari BASIC programs ever written, totaling in at almost 32 kilobytes in size. It will not run in Atari DOS 2.0, nor newer versions of MyDOS, because they donโt leave enough memory to hold it. While it was expected that users of FoReM BBS systems would modify the board to suit their needs, this was a challenge because every modification had to be carefully planned not to exceed the remaining 2000 or so bytes of memory, and to make things even more difficult, all of the 128 available variables were in use, you couldnโt assign a new one, string or numeric. And with every single numeric value in Atari Basic taking up 6 bytes of memory, the amount of space needed to hold a floating point value, it was common to use variables, often perfoming calculations against them, to get the numeric value needed without wasting memory, at the expense of the additional computation time required to resolve the expression. But, I digress...
I sat down and proceeded to work through the FoReM-26M code, printing it out on FujiNetโs ATASCII printer, opening it up in a Chrome window, while I did text searches looking for useful information. I found the bulk of the code for the real time clock from lines 1020 to 1270 (Note the mix of variables standing in for numbers, and numeric constants. The numeric constants take up much more space than the variable stand-ins)
1020 X=PEEK(1027):F=PEEK(1026):IF NOT X AND NOT P THEN GOSUB 1560
1070 IF X THEN P=O
1180 TI$=" 0:00 AM":IF X>C10+I THEN TI$(C7)="PM"
1190 IF X>C12 THEN X=X-C12
1200 TI$(I+(X<C10),C2)=STR$(X):IF NOT X THEN TI$(I,C2)="12"
1220 TI$(C4+(F<C10),C5)=STR$(F):IF X<C10 AND X>O THEN TI$=TI$(C2)
1270 RETURN
These referenced memory locations updated by the old AUTORUN.SYS driver, and could be safely removed.
I also changed line 2890, which was placing a 19 before each two digit year, to make it Y2K compliant:
2890 OUT$(LEN(OUT$)+I)=STR$(VAL(D$(C4,C5))):OUT$(LEN(OUT$)+I)=",20":OUT$(LEN(OUT$)+I)=D$(C7):D$=OUT$:RETURN
I subsequently removed any and all code in the 30000 block which either asked for time or date, or asked for the data disk. All I needed to do now, was write my new clock routine. I knew where I was going to put it (starting at line 30), I just needed to now replace every GOSUB 1020, with GOSUB C10*C3, which took up much less space in RAM, because I knew I was going to need all the RAM I could get! There were eight of them in the code, and were easy enough to replace in preparation for the next step, all I needed to do now was just write the new routine.
Initially I tried to use the routines in SpartaDOS which return a date and time stamp. If you donโt look too closely, they look like they do the trick, right? They presented two unique problems. The first was that the method of getting this information differed between SpartaDOS versions, where disk based SpartaDOS versions exposed a date and timestamp at COMTAB+13 to +18, while SpartaDOS X versions exposed a GETTD function that you could call by setting a few parameters, calling memory location $703, and picking up the return values from $778 to $77D. Same values, same problem, explicit incompatibility. The second problem was much more implicit: the time and date stamps only updated when a file was opened for write (so the timestamps could ostensibly be updated!), this definitely wouldn't work! I wasnโt sure what to do, so I went to walk the pugs.
While I was walking the pugs, it occurred to me that I was doing the wrong approach. Since I was using this with FuijNet, and it was possible to get the time and date from SIO (which in turn gets its time and date from an NTP server, adjusted for time zone). This allowed for a solution that would work in any DOS, and would be updated as the FujiNet got new time. The firmware on the ESP32 only synchronizes with NTP every hour, so there is no network load resulting from the consistent polling that happens on the wait call screen, so this seemed to be just the ticket.
I immediately sat down and opened up an emacs window to code a little routine for MAD assembler:
opt h-
ORG $0600
PLA
LDA #$45
STA $0300
LDA #$01
STA $0301
LDA #$93
STA $0302
lda #$40
STA $0303
lda #$FA
STA $0304
lda #$04
STA $0305
lda #$06
STA $0308
LDA #$00
STA $0309
JSR $E459
RTS
This was the bare minimum I needed to be able to read the time and date from the FujiNet, corresponding to a command $93 to device $45. This command expects to send 6 bytes, so I needed to find a place to put them. DVSTAT ($02EA) is a common place to put small bits of device communication, but it is only 4 bytes long, so it was not suitable. After a bit of searching around the memory map, and verifying against the running FoReM instance, I found 6 bytes I could use starting at $04FA (the end of the page 4 user area), which is location 1274 in decimal. I coded up the above assembly routine to put the date and time stamp there, and assembled the result. The opt h- makes sure that no binary header is created, so the resulting assembly would just be the code I wrote. But how to use it?
The resulting code was assembled into an object file 45 bytes long, however, there isnโt a command in Atari BASIC to load binary code. While I could have placed it as part of an INIT segment for an AUTORUN.SYS file, this would be overly complex. I just need to use it in one place. While I could have written it as a series of DATA statements to place into an unused section of memory and called with the USR function, it would waste a lot of memory I donโt have, because Atari BASIC stores DATA statements as literal ATASCII characters, commas and all. Very wasteful especially since the values would be decimal, and would need to be POKEd into place by a FOR-NEXT loop, again, also very wasteful. As it happens, Atari BASIC has a very unique solution to this problem: the ADR function.
The ADR function will provide the address in memory of its parameter, usually a variable. This can be directly passed into the USR function to call a user function. Coincidentally, this can even be a string constant, surrounded in quotes. Atari BASIC will put it in its parsing buffer and provide the address. This is very helpful, because it means I can easily embed the machine language code, and have it take up a minimum of space.
So with a bit of BASIC code, I was able to read the assembled machine code that MAD assembler had produced, print it to the screen, as a valid BASIC statement, then close the file and end, so that I could subsequently NEW, cursor over the printed statement, and save the result as a LISTed BASIC file that I could ENTER into the existing FOREM26M.2 program:
10 OPEN #1,4,0,โD:GETTD.OBXโ
20 TRAP 50
30 PRINT โ30 X=USR(ADR(โ;CHR$(34);
40 GET #1,X:PRINT CHR$(X);:NEXT X
50 CLOSE #1
60 PRINT CHR$(34)
70 END
Once this was done, the first line of my routine was in my program, ready to go, with a bit of modification, and the rest of the routine fell right into place:
30 TD$=" / / ":TI$="00:00 ":X=USR(ADR("h๎ฉE๎๎๎๎ฉ๎๎๎๎๎ฉ๎๎๎๎๎ฉ@๎๎๎๎ฉ๎บ๎๎๎๎ฉ๎๎๎
๎๎ฉ๎๎๎๎๎ฉ๎๎๎๎ Y๎ค๎ ")):F=1274
31 X=PEEK(F+I):IF X<C10 THEN TD$(I,I)="0":TD$(C2,C2)=STR$(X):GOTO C32+I
32 TD$(I,C2)=STR$(X)
33 X=PEEK(F):IF X<C10 THEN TD$(C4,C4)="0":TD$(C5,C5)=STR$(X):GOTO C32+C3
34 TD$(C4,C5)=STR$(X)
35 X=PEEK(F+C2):TD$(C7,C8)=STR$(X)
36 X=PEEK(F+C3):IF X<C10 THEN TI$(I,I)="0":TI$(C2,C2)=STR$(X):GOTO C32+C6
37 TI$(I,C2)=STR$(X)
38 X=PEEK(F+C4):IF X<C10 THEN TI$(C4,C4)="0":TI$(C5,C5)=STR$(X):GOTO C32+C8
39 TI$(C4,C5)=STR$(X)
40 RETURN
Notice, the judicious use of computed GOTO and comparisons. Using numeric constants here would have added over 100 extra bytes of memory needed. This is important to reiterate when you understand that after this routine was added (and LISTED and re-ENTERED to avoid memory leaks!), the output of FRE(0) was: 1680. Thatโs right. 1,680 bytes free. Yeek. While some of this will be regained when the initialization routine literally eats itself after use, this is still very tight.
So with all of this, mission accomplished. FoReM now tracks time correctly. With the modifications I made, the time clock suddenly became 24 hour, but this is just fine with me. I didnโt want to waste space doing absolute value calculations to adjust for AM or PM. The resulting modifications will be folded into the version of FoReM-26M that is on atari-apps.irata.online as they will work with all FujiNet users, and it means a working clock for all.