Memory-efficient design for a 78-card offline reader with SD storage


#1

I am planning a small offline uLisp project for a handheld device with a display, a few buttons and an SD card.

The device would contain a fixed set of 78 illustrated cards. The user would press a button to select one or more cards, and the display would show:

  • Card title
  • Upright or reversed orientation
  • A short interpretation
  • Position within the current spread
  • Previously selected cards in the session

This is the current reference list for the 78 card IDs, titles and meanings:

https://deckaura.com/blogs/guide/tarot-card-meanings

Disclosure: I maintain the linked website. I am sharing it only to show the real data set that I would convert into a compact offline format.

The project would not need WiFi after the data had been copied to the SD card.

I am considering a LilyGO T-Deck, PicoCalc-class device or another board with:

  • A colour display
  • An SD card slot
  • At least three input buttons
  • Enough RAM to keep a small session state
  • uLisp support

I am trying to decide how much data should remain in RAM and how much should be read from the SD card when a card is selected.

A simplified card record might contain:

("the-world"
 "The World"
 "Completion, integration and achievement."
 "Incomplete closure, delay or unfinished work.")

There would be 78 records in total.

My current idea is to store them as Lisp-readable expressions rather than JSON, so that uLisp can read the data directly without requiring a separate JSON parser.

I have several questions about the best architecture.

1. One large file or one file per card?

The simplest option would be one file containing 78 expressions:

(
  ("the-fool" "The Fool" "..." "...")
  ("the-magician" "The Magician" "..." "...")
  ...
)

However, reading the complete list may consume more RAM than necessary.

The alternatives I am considering are:

  1. One large file loaded completely into memory
  2. One large file scanned until the requested record is found
  3. One small file per card
  4. A compact index file containing byte offsets into a larger text file
  5. A generated Lisp source file containing only IDs and titles, with longer descriptions stored separately

For a data set this small, which approach would you use?

2. Lisp expressions versus a custom text format

Would Lisp-readable records be the most natural uLisp format?

For example:

(card
  :id "the-world"
  :title "The World"
  :upright "Completion and integration."
  :reversed "Unfinished work or delayed closure.")

Or would a simpler delimiter-separated format use substantially less memory while reading?

I like the idea that the file could be inspected and edited as normal Lisp data, but long strings may make the complete expression expensive to load.

3. Random selection without repeats

For a one-card draw, this is straightforward:

(random 78)

For a multi-card spread, the same card should not appear twice.

Would you recommend:

  • Building a list of the integers from 0 to 77 and shuffling it
  • Selecting random values and rejecting duplicates
  • Using a 78-element bit array to mark cards that have already been selected
  • Persisting a shuffled deck order and advancing through it

A bit array seems attractive because only the selected state needs to remain in memory.

4. Upright and reversed orientation

The orientation can be selected independently with something similar to:

(if (= (random 2) 0) 'upright 'reversed)

Would you store upright and reversed descriptions in the same record, or keep them as separate records so only the required text needs to be read?

5. Efficient access to an SD file

If all cards are stored in one large file, scanning from the beginning for every draw is simple but inefficient.

Is there an established uLisp pattern for:

  • Recording the byte position of each expression
  • Seeking directly to a stored position
  • Reading one expression without loading the rest of the file
  • Rebuilding the index when the content file changes

I have seen that SD streams support read and read-line, but I am not sure what the most portable random-access pattern is across uLisp platforms.

6. Keeping long strings outside the Lisp workspace

The titles and IDs are short, but some descriptions could be several hundred characters.

Would it be better to keep only this data in memory:

(index id title file-position)

and retrieve the description from the SD card only when the card is displayed?

The device would show only one description at a time, so retaining all text in RAM seems unnecessary.

7. Session history

I would like the user to be able to select between one and ten cards and review the current session.

A session entry might contain only:

(card-index orientation position)

Would a small array be preferable to a list for this fixed maximum size?

The session could optionally be saved to the SD card as a timestamped Lisp expression.

8. Text wrapping on a small display

The descriptions will need to wrap across multiple display lines.

Would you preprocess the text into display-width-specific lines on a computer, or implement word wrapping in uLisp so the same source data can work on different screen sizes?

Preprocessing would reduce runtime work, but it would tie the data file to one particular display width and font.

9. Updating the data

I would like the card content to be replaceable by copying a new file to the SD card without rebuilding the uLisp firmware.

Would you include a small header such as:

(version 1)
(card-count 78)
(content-hash "...")

so the program can verify that the data file is complete and compatible before using it?

10. Suitable first platform

For the first prototype, I only need:

  • A display
  • An SD card
  • A select button
  • A back button
  • Enough memory for one record and a ten-card session

Would a T-Deck, PicoCalc or another existing uLisp platform be the easiest starting point?

My first milestone would be much smaller than the final project:

  1. Load 78 IDs and titles
  2. Select one random card
  3. Select its orientation
  4. Retrieve one description from SD
  5. Display the result
  6. Avoid repeating the same card during the current session

I would appreciate recommendations on the most idiomatic uLisp data representation before I start converting the complete data set.


#2

Are you planning to show graphical images of the cards on the display? If so, what format do you plan to use to store the images? 78 such images are probably not going to fit in RAM so they will presumably need to be loaded from SD card.


#3

The most efficient way to select cards at random from the pack is to shuffle it first using a Fisher-Yates shuffle, and then remove successive cards from the top.

The best representation for the pack is an array, but a list is almost as efficient for a small pack, and it has the advantage that you can just deal cards off the top using pop.

Here are some suggested algorithms:

To make the pack:

(defun make-pack (n)
  (let (pack)
    (dotimes (i n pack) (push i pack))))

To shuffle the pack:

(defun shuffle (pack)
  (let ((len (length pack)) temp)
    (dotimes (i len pack)
      (let* ((j (- len i 1))
            (r (random (1+ j))))
        (setf temp (nth j pack))
        (setf (nth j pack) (nth r pack))
        (setf (nth r pack) temp)))))

Make a pack of 52 playing cards, shuffle it, and deal four cards:

(defvar *pack* (make-pack 52))

(shuffle *pack*)

(dotimes (n 4) (print (pop *pack*)))

#4

For hardware, I’d think your best bet might be the M5stack Tab5, which already has a ulisp port. It doesn’t have the buttons but it does have a tocuhscreen, so you can either put buttons on the screen or use the screen as the interface in other ways like swiping.

Otherwise, I’d consider the M5 ESP32 Basic Core IoT if you want a case, or the Adafruit reverse feather TFT if you want to build it yourself (you’d have to get some kind of SD card adaptor). However these don’t have ulisp ports, so you’d have to do it yourself, which might be prohibitive depending on your skill level.