Using an array of PROGMEM string arrays

I'm sure this topic has been covered at some point, but my searching is not turning up an answer. I'm trying to access PROGMEM data at one more level of indirection than the examples I can find.

I have multiple arrays of strings stored using PROGMEM, and I want a RAM array in which each entry is a PROGMEM array of strings. In addition, I need the size of each array stored in using PROGMEM. Though not what I'm doing, as an example of the problem imagine a Jeopardy-like game where the user first selects a category and then gets a random string from a set of strings in that category...and the number of possible strings varies between categories.

Here is the test code I've been using to try and work out the problem (based on a PROGMEM example):

#include <avr/pgmspace.h>
const char set_0_string_0[] PROGMEM = "Set 0 String 0";   
const char set_0_string_1[] PROGMEM = "Set 0 String 1";
const char set_0_string_2[] PROGMEM = "Set 0 String 2";
const char set_0_string_3[] PROGMEM = "Set 0 String 3";

const char set_1_string_0[] PROGMEM = "Set 1 String 0";   
const char set_1_string_1[] PROGMEM = "Set 1 String 1";
const char set_1_string_2[] PROGMEM = "Set 1 String 2";

const char* const set0[] PROGMEM = { set_0_string_0, set_0_string_1, set_0_string_2, set_0_string_3 };
const char* const set1[] PROGMEM = { set_1_string_0, set_1_string_1, set_1_string_2 };

PGM_VOID_P const all_sets[2] = { set0, set1 }; 

char buffer[32];    

void setup() {
  Serial.begin(9600);
  while(!Serial);
  Serial.println("OK");
}

void loop() {
  for (int i = 0; i < sizeof(set0) / sizeof(set0[0]); i++) {
    strcpy_P(buffer, (char*)pgm_read_word(&(set0[i]))); 
    Serial.println(buffer);
    delay( 500 );
  }
}

I'm able (I hope) to put each PROGMEM array in a RAM array using:

PGM_VOID_P const all_sets[2] = { set0, set1 };

But I cannot figure out the syntax to then plug an entry from that array into the example code that is directly referencing a PROGMEM-stored array:

strcpy_P(buffer, (char*)pgm_read_word(&(set0[i])));

All my attempts produce either compiler errors or I get trash output.

Nor can I figure out how to get the size of the PROGRAM stored array once I reference it. Having direct access to the array, I can get the size using:

sizeof(set0) / sizeof(set0[0])

I can think of several hackish workarounds for both problems, but I would like a clean solution.


Update. The following works, but is more by thrashing than a firm understanding. The array is declared as:

const char* const* all_sets[2] = { set0, set1 };

And is referenced:

strcpy_P(buffer, (char*)pgm_read_word(&((all_sets[1])[i])));

Beyond a parallel array, I still cannot figure out how to get the array sizes.

Hi Robertbu,

I made a box that worked like a Magic 8 Ball using a list of strings. Both the strings and the list of strings are kept in PROGMEM. Originally I also kept the string length in an array, but it turned out to be easier to calculate the string length with strlen(). Here's a short program that should be easy to follow as an example of how to keep strings, and a list of the strings in PROGMEM.

Here's my sketch*:

/*
    Magic8BallWithCounting in an mint tin printed with a Ouija board
    ATTiny85 - 1 MHz internal clock, 3 volts: 2x aaa alkalines (1 year)
    Adafruit red 14 segment I2C backpack (0x70)
    Tiny85 pins: cds on pin 3; switche on pin 6; button on  1; pin 2 unconnected for random

    History:
    date   by  flash/ram  why
    052816 clh 4082/116 - first circuit: 9 ua in snooze mode
    120316 clh 3982/127 - solder, seal & sell!
*/

// libraries
#include <TinyWireM.h> // https://github.com/adafruit/TinyWireM
#include <Narcoleptic.h> // https://github.com/brabl2/narcoleptic
#include "Adafruit_GFX.h" // https://learn.adafruit.com/adafruit-led-backpack/downloads
#include "Adafruit_LEDBackpack.h"  // as above ...

// define the pins and i2c addresses
#define cds A2 // pin --> cds --> gnd 
#define in A3 // unconnected
#define reset A0 // pin 1
#define button 1 // pin --> button --> gnd
#define SDA 0
#define SCL 2

#define alphaAddress 0x70  // 14 segment I2C device

#define startMinute 59  // Modify these defines to
#define startSecond 59  // change the timer interval

// phrases and names - first 20 are official phrases
const char yes1[] PROGMEM =  "   WITHOUT A DOUBT";
const char yes2[] PROGMEM =  "   YOU MAY RELY ON IT";
const char yes3[] PROGMEM =  "   YES DEFINITELY";
const char yes4[] PROGMEM =  "   SIGNS POINT TO YES";
const char yes5[] PROGMEM =  "   IT IS DECIDEDLY SO";
const char yes6[] PROGMEM =  "   AS I SEE IT - YES";
const char yes7[] PROGMEM =  "   YES";
const char yes8[] PROGMEM =  "   MOST LIKELY";
const char yes9[] PROGMEM =  "   IT IS CERTAIN";
const char yes10[] PROGMEM = "   OUTLOOK GOOD";
const char no1[] PROGMEM =   "   MY SOURCES SAY NO";
const char no2[] PROGMEM =   "   DONT COUNT ON IT";
const char no3[] PROGMEM =   "   VERY DOUBTFUL";
const char no4[] PROGMEM =   "   OUTLOOK NOT SO GOOD";
const char no5[] PROGMEM =   "   MY REPLY IS NO";
const char idk0[] PROGMEM =  "   BETTER NOT TELL YOU NOW";
const char idk1[] PROGMEM =  "   ASK AGAIN LATER";
const char idk2[] PROGMEM =  "   REPLY HAZY TRY AGAIN";
const char idk3[] PROGMEM =  "   CANNOT PREDICT NOW";
const char idk4[] PROGMEM =  "   CONCENTRATE AND ASK AGAIN";

char phrase[36]; //      -->|"                                    "|<--

const char* const theAnswers[] PROGMEM = { yes1, yes2, yes3, yes4, yes5, yes6, yes7, yes8, yes9, yes10,
                                           no1, no2, no3, no4, no5, idk0, idk1, idk2, idk3, idk4,
                                         };

int numerics[] = {    // my numerals (rather than use the ugly builtins)
  0b0000000000111111, // 0
  0b0000010000000110, // 1        A
  0b0000100010001011, // 2      FhjkB
  0b0000010010001101, // 3      g1 g2
  0b0000000011100110, // 4      ElmnC
  0b0000000110001101, // 5        D  dp
  0b0000100010111100, // 6
  0b0000110011000001, // 7  binary: 0, dp, n, m, l, k, j, h, g2, g1, F, E, D, C, B, A
  0b0000000011111111, // 8  1 = lit up, 0 = dark
  0b0000000011100111, // 9
};

unsigned long previousSecondMillis = 0UL;
const long oneSecond = 1000L;

byte minutes = startMinute;
byte seconds = startSecond;

byte answer;
byte scrollPosition;
byte answerLength;

boolean isBlank;
boolean colon;

// output device
Adafruit_AlphaNum4 alpha = Adafruit_AlphaNum4();

void setup()
{
  pinMode(cds, INPUT_PULLUP);
  pinMode(button, INPUT_PULLUP);
  pinMode(reset, INPUT_PULLUP);

  alpha.begin(alphaAddress);  // i2c address of the alphanumeric display
  randomSeed(analogRead(in));

  previousSecondMillis = millis() + Narcoleptic.millis();
}


void loop() {
  snooze(125); // nap while timing lighting interface

  // if the box is closed, turn it off
  if (analogRead(cds) < 960) {
    isBlank = false;
    scrollPosition = 0;

    // select a random answer number
    answer = random(21);
    
    // set start time for hourglass
    minutes = startMinute;
    seconds = startSecond;

    while (analogRead(cds) < 900) {
      snooze(187); // nap while timing the scroll rate

      // ambience dimming
      byte ambience = constrain(map(analogRead(cds), 500, 0, 0, 30), 0, 11);
      alpha.setBrightness(ambience);  // values from trial-and-error

      // read the switch
      if (digitalRead(button)) {  // do hourglass ...

        // do this part once a second
        if ((millis() + Narcoleptic.millis()) - previousSecondMillis >= oneSecond) {
          previousSecondMillis += oneSecond;

          // show the time on the LED
          colon = !colon;
          alpha.writeDigitRaw(0, numerics[minutes / 10]);
          alpha.writeDigitRaw(1, (numerics[minutes % 10] | (colon ? 0b0100000000000000 : 0))); // flash decimal point
          alpha.writeDigitRaw(2, (numerics[seconds / 10]));
          alpha.writeDigitRaw(3, (numerics[seconds % 10]));
          alpha.writeDisplay();

          // decriment seconds
          if (seconds-- == 0) {
            if (minutes == 0) {  // time is up
              minutes = startMinute;
              seconds = startSecond;

              // show the ararm
              for ( byte n = 0; n < 5; n++) {
                alpha.writeDigitAscii(0, 'D');
                alpha.writeDigitAscii(1, 'O');
                alpha.writeDigitAscii(2, 'N');
                alpha.writeDigitAscii(3, 'E');
                alpha.writeDisplay();
                snooze(250);
                blankDisplay();
              }
            } else {  // decriment minutes
              minutes--;
              seconds = 59;
            }
          }
        }  // end 'once a second' part
        
      } else {  // ... do 8 ball

        // a family of tomatos is walking, but the baby lags behind. The father stomps and shouts Catch Up!
        previousSecondMillis = millis() + Narcoleptic.millis();

        // copy the phrase into RAM: https://www.arduino.cc/en/Reference/PROGMEM
        strcpy_P(phrase, (char*)pgm_read_word(&(theAnswers[answer])));

        // add 4 spaces to provide a short 'all off', so it can go to sleep if the box is closed
        strcat(phrase, "    ");
        answerLength = (byte)strlen(phrase) - 4;

        // scroll phrase across 14 segment display
        if (scrollPosition > answerLength) scrollPosition = 0;
        alpha.writeDigitAscii(0, phrase[scrollPosition]);
        alpha.writeDigitAscii(1, phrase[scrollPosition + 1]);
        alpha.writeDigitAscii(2, phrase[scrollPosition + 2]);
        alpha.writeDigitAscii(3, phrase[scrollPosition + 3]);
        alpha.writeDisplay();
        scrollPosition++;

      }
    }
  } else {  // turn all 14 segments off
    if (!isBlank) {
      isBlank = true;
      blankDisplay();
    }
  }
}
void blankDisplay() {
  alpha.writeDigitRaw(0, 0);
  alpha.writeDigitRaw(1, 0);
  alpha.writeDigitRaw(2, 0);
  alpha.writeDigitRaw(3, 0);
  alpha.writeDisplay();
}

// sleep with ADC off - http://forum.arduino.cc/index.php?topic=248690.msg1778100
void snooze(unsigned long int ms) {
  byte adcsra_save = ADCSRA;
  ADCSRA = 0; // turn adc off
  Narcoleptic.delay(ms); // Zzzzzzz.
  ADCSRA = adcsra_save; // adc back on
}

EDIT: * Additional replies have been removed for the 9000 character limit.