Continuing my Psion-themed December Adventure, I returned to Sunday’s sorting of ROMs, and chipped away at the OPL take on Thoughts I started yesterday. I’m also rapidly learning that one of the advantages of slowing down a little and writing up my daily adventures is getting to share them with others and, quite wonderfully, receiving realtime feedback, suggestions and contributions.

ROMs

I spent some time adding more Psion ROMs to explit7/Psion-ROM—Sienna, Series 3c, and Series 3mx this time around. Thanks to feedback from Alex, I’ve also added MD5 checksums and EPOC16 versions (the latter really helps differentiate between device variants and language variants). There are still a few gaps in the table as I’ve yet to find for myself where the EPOC16 version comes from but hopefully I can flesh that out over the next couple of days.

The additions are as follows:

Psion Siena

Device EPOC16 Version ROM Version Language Filename MD5 Checksum
Psion Siena 3.70f 4.20f English vine_v4.20f.bin 242e80fdbf9b353a05f6ff4d1db1c769

Psion Series 3c

Device EPOC16 Version ROM Version Language Filename MD5 Checksum
Psion Series 3c 3.91f 5.20f English oak_v5.20f_eng.bin 3c1a079f53c00916e8d0dc11b35a0390

Psion Series 3mx

Device EPOC16 Version ROM Version Language Filename MD5 Checksum
Psion Series 3mx 4.08f 6.16f English maple_v6.16f_uk.bin 64572cc3522447179d1e6f3b8fb45360
Psion Series 3mx 6.20f French maple_v6.20f_fre.bin 1b367e2fb862545cf420ff74a7f85ea7

Looking over the listings of ROMs have been dumped for these devices, it’s abundantly clear that we’re missing many localizations—I understand we know of at least English, French, German, Italian, Flemish, and Dutch variants of the 3c, so we’ve a long way to go. If you have a non-English version of these devices, please get in touch by dropping me a mail or joining us in Discord. We’d absolutely love to talk you through the process and would be eternally grateful!

As an aside, I love how the ROM files use the device codenames and am tempted to surface those as an explicit column. I’ve also just learned that we have a newly minted Series 7 ROM dump, so that’s on the list for tomorrow.

Thoughts-Lite

Fabrice Cappaert spotted a bug in my ZPAD implementation (see yesterday’s notes)—I was using an IF instead of a WHILE-loop when checking to see if my target string was long enough, meaning it would never have worked for pads longer than 1. (Just goes to show that even for small programs, it’s worth unit testing things.) Thankfully, he also offered up a significant simplification which we both suspect will work more efficiently given the costs of certain operations in OPL:

PROC ZPAD$:(value&,length%)
    REM Left-pad strings with '0' up to a length, length%.
    RETURN RIGHT$(REPT$("0",length%)+NUM$(value&,length%),length%)
ENDP

This approach also allows me to avoid using a local fixed-size string which makes it far more flexible than the previous implementation which implicitly limited length% to 10.

Perhaps most-crucially however, was Fabrice’s help with OPL time zone wrangling: the macOS version of Thoughts stores the UTC offset in the ISO 8601 timestamp and this is functionality I really want to preserve in the OPL version1. Yesterday, I was starting to get a little nervous about my ability to implement this as I couldn’t find built-in functions to get the user’s UTC offset, even though it’s clear from the World app that the Psion knows where I am and my time relative to UTC.

My Series 7 clearly knows I live in Hawaii

Thankfully, Fabrice pointed me at one of the first-party after-market native libraries—SystInfo.opx—which provides SIUTCOffset&:. This returns the UTC offset in seconds and, a few calls to MOD and IABS later, I was able add the UTC offset to my ISO 8601 formatted date:

PROC ISO8601$:(datetime&,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&
    LOCAL offsetSign$(1), offsetHours&, offsetMinutes&
    LOCAL result$(25)

    REM Extract the datetime components.
    year&=DTYEAR&:(datetime&)
    month&=DTMONTH&:(datetime&)
    day&=DTDAY&:(datetime&)
    hour&=DTHOUR&:(datetime&)
    minute&=DTMINUTE&:(datetime&)
    second&=DTSECOND&:(datetime&)

    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’s not pretty, but it works

It’s not all been plain sailing though. In addition to storing an ISO 8601 date and time with a UTC offset in the note metadata, I wish to name the file using a UTC date and time to ensure they sort correctly on the file system. For example, a note published at 2024-12-02T16:05:06-10:00 should be named 2024-12-03-02-05-06.md. While I had expected this to be simple (subtract utcOffset% from datetime& and generate a string similarly to the code above), it seems OPL date/times are a little bit magical.

Consider the following code:

LOCAL timestamp&, offset&, utcTimestamp&
LOCAL year&

REM Get the current timestamp and offset.
timestamp&=DTNOW&:
offset&=SIUTCOffset&:

REM Calculate the UTC timestamp.
utcTimestamp&=timestamp&-offset&

REM Extract the datetime components.
year&=DTYEAR&:(utcDatetime&)

While you might expect this to work, it fails with ‘Invalid arguments’ as utcTimestamp& is apparently no longer a valid date/time, even though both timestamp& and utcTimestamp& both behave like longs when passed to many functions. I can only assume they’re backed by a magical object under the hood. Hopefully I can find a way to get the UTC timestamp directly, or initialize a new date/time with a long, but that’s one for another day.


  1. The UTC offset allows notes to be displayed in note-local time, UTC time, or the reader-local time.