December Adventure Day 04
It's Alive!
It’s been a successful fourth day of my December Adventure—there are a few too many threads but I feel like I’m making progress: I went on a tiny side-quest to fix the way tables are displayed on my website, uploaded a few more ROMs, and continued to bash my head at OPL.
Just like yesterday, I find myself incredibly grateful for the on-going interest and support others have given me—both Fabrice and Tom kept me on-track with my OPL pursuits and Alex gave me a couple of pointers about the Psion Workabout.
ROMs
You know the drill at this point: I added some new ROMs to the Psion-ROM repository—Workabout and Workabout MX this time around.
The Workabout devices are rugged, industrial handhelds, intended for use in warehouse environments. They run seemingly fully-featured EPOC16 and have absolutely tiny screens making for perhaps the cutest rendition of the operating system I’ve seen.
Psion Workabout
Device | EPOC16 Version | ROM Version | Language | Filename | MD5 Checksum |
---|---|---|---|---|---|
Psion Workabout | 3.56f | 0.24b | English | w1_v0.24b.bin | 1afac14fe87e19e7d29d494177dc58d9 |
Psion Workabout | 3.56f | 1.00f | English | w1_v1.00f.bin | 87c84a27bc71df5e19ac1208735a7a1e |
Psion Workabout | 3.96f | 2.40f | English | w1_v2.40f.bin | 10b9a0c9174aec0316571827dce42013 |
Psion Workabout MX
Device | EPOC16 Version | ROM Version | Language | Filename | MD5 Checksum |
---|---|---|---|---|---|
Psion Workabout MX | 4.31f | 7.20f | English | w2mx_v7.20f.bin | d5e5c2aa32f9888e7fec8d2214f1547e |
Unfortunately I didn’t get around to adding the new Series 7 ROM dump to the repository. Tomorrow, I hope. 🤞🏻
Thoughts-Lite
Thanks to input from both Tom and Fabrice, the EPOC version of Thoughts feels like it made massive progress today as I was finally able to put everything I’ve worked on over the last few days into an end-to-end demo.
I ended yesterday stuck trying to apply the long representation of the UTC offset returned by SIUTCOffset&:
to the date/time returned by DTNOW&:
. I had incorrectly assumed that the date/time would be conveniently represented as seconds since epoch (1970-01-01) allowing me to simply subtract the UTC offset. Instead, it turns out all Date.opx operations (functions prefixed by DT
) return an opaque pointer to an underlying date/time object which you can only interact with through dedicated functions.
Consider the following code:
timestamp&=DTNOW&:
offset&=SIUTCOffset&:
year&=DTYEAR&:(timestamp&-offset&)
This fails because, although timestamp&
is a long, it is actually a pointer, so subtracting offset&
results in a pointer to goodness knows where and the call to DTYEAR&:
ends up operating on random memory.
Finding a suitable API to work around this proved challenging. Out of the box, OPL provides the following functions:
DATETOSECS
—converts a collection of date and time components to a timestamp represented as seconds since epochSECSTODATE
—extracts the date and time components from a timestamp represented as seconds since epoch
These both look incredibly helpful and would seem to imply there’s an easy way to get the current time as seconds since epoch. Unfortunately that’s not the case: the language provides DATIM$
which, ‘returns the current date and time from the system clock as a string’, but seemingly nothing that returns seconds since epoch. In fact, it looks like the best you can do with the native OPL API is:
LOCAL timestamp&
timestamp& = DATETOSECS(YEAR, MONTH, DAY, HOUR, MINUTE, SECOND)
While it might not be immediately obvious, YEAR
, MONTH
, DAY
, HOUR
, MINUTE
, and SECOND
are all function calls, meaning this code is incredibly susceptible to race conditions and is likely to lead to some wacky results—consider what happens, for example, if the minute rolls-over before the SECONDS
call is performed.
Instead of using this mildly terrifying approach, and thanks to a suggestion from Tom, I settled on using DTSECSDIFF&:
from Date.opx which allows me to calculate the number of seconds between two dates:
PROC NOW&:
REM Return the current time as seconds since epoch.
LOCAL epoch&, now&, result&
epoch&=DTNEWDATETIME&:(1970,1,1,0,0,0,0)
now&=DTNOW&:
result&=DTSECSDIFF&:(epoch&,now&)
DTDELETEDATETIME:(epoch&)
DTDELETEDATETIME:(now&)
RETURN result&
ENDP
I have to manually define epoch&
, but this provides a robust way to get the current date/time as seconds since epoch and allows me to avoid Date.opx’s quirky opaque date/time objects wherever possible.
This approach necessitates an update to ISO8601$:
which is (fortunately) significantly simplified as I can now take advantage of SECSTODATE
:
PROC ISO8601$:(timestamp&, utcOffset&)
REM Return an ISO 8601 formatted date and time with UTC offset.
REM Result is guaranteed to be 25 characters long.
LOCAL year%, month%, day%, hour%, minute%, second%, yearday%
LOCAL offsetSign$(1), offsetHours&, offsetMinutes&
REM Extract the timestamp components.
SECSTODATE timestamp&, year%, month%, day%, hour%, minute%, second%, yearday%
REM Extract the offset components.
offsetHours& = IABS(utcOffset& / 3600)
offsetMinutes& = MOD&:(utcOffset& / 60, 60)
IF (utcOffset& < 0)
offsetSign$="-"
ELSE
offsetSign$="+"
ENDIF
RETURN NUM$(year%, 4) + "-" + ZPAD$:(month%, 2) + "-" + ZPAD$:(day%, 2) + "T" + ZPAD$:(hour%, 2) + ":" + ZPAD$:(minute%, 2) + ":" + ZPAD$:(second%, 2) + offsetSign$ + ZPAD$:(offsetHours&, 2) + ":" + ZPAD$:(offsetMinutes&, 2)
ENDP
It also allows me to generate my UTC filenames in a very similar way and just apply the UTC offset by subtracting it:
PROC BASENAME$:(timestamp&, utcOffset&)
REM Return a string representation of timestamp& and utcOffset& suitable for using in a filename.
REM Result is guaranteed to be 19 characters long.
LOCAL utcTimestamp&
LOCAL year%, month%, day%, hour%, minute%, second%, yearday%
utcTimestamp&=timestamp&-utcOffset&
SECSTODATE utcTimestamp&, year%, month%, day%, hour%, minute%, second%, yearday%
RETURN NUM$(year%, 4) + "-" + ZPAD$:(month%, 2) + "-" + ZPAD$:(day%, 2) + "-" + ZPAD$:(hour%, 2) + "-" + ZPAD$:(minute%, 2) + "-" + ZPAD$:(second%, 2)
ENDP
With all this nuanced code in place, actually assembling a Frontmatter header, writing it to a timestamped file, and launching Editor—the bulk of the functionality—seemed oddly simple (albeit a little bloated):
PROC main:
LOCAL k&
LOCAL timestamp&, offset&, date$(25)
LOCAL basename$(22), path$(255)
LOCAL content$(255)
LOCAL handle%
LOCAL r%
REM Get the current timestamp and offset.
timestamp&=NOW&:
offset&=SIUTCOffset&:
REM Format the date, path, and content.
date$ = ISO8601$:(timestamp&, offset&)
basename$ = BASENAME$:(timestamp&, offset&)
path$ = "C:\Thoughts\" + basename$ + ".md"
content$ = "---" + CRLF$: + "date: " + date$ + CRLF$: + "---" + REPT$(CRLF$:, 2)
REM Write the metadata to the file.
r% = IOOPEN(handle%, path$, 1)
IF (r% <> 0)
REM TODO: Present the error.
PRINT "Failed to open file with error " + NUM$(r%, 2) + "."
GET
RETURN
ENDIF
REM TODO: Check the number of bytes written.
IOWRITE(handle%, ADDR(content$) + 1, LEN(content$))
r% = IOCLOSE(handle%)
IF (r% <> 0)
REM TODO: Present the error.
PRINT "Failed to close file with error " + NUM$(r%,2) + "."
GET
RETURN
ENDIF
REM Open the file.
k& = RUNAPP&:("Editor", path$, "", 2)
REM GET
ENDP
Now, running my newly translated ‘Thoughts.opo’ will open a new timestamped file in Editor, ready for me to type my notes:
Tomorrow, I plan to add support for launching Thoughts from one of the Series 7’s silkscreen buttons and implementing a global hotkey for devices like the Series 5 and Revo. I’d also like to check this initial version of the project into git so others can follow along more easily.