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.

EPOC in miniature

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.

Timestamped files generated using Thoughts

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 epoch
  • SECSTODATE—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.