Wedding Board

This project was a request from my close friends who were getting married. Their wedding was a location wedding in Jervis Bay, New South Wales and they wanted a novel way for guests to locate where they were staying for the wedding.

The brief for the project was they wanted a board that had guest names with a button per guest, upon pressing the button for that particular guest, the guest’s accommodation location would light up on a corresponding location map.

With the brief understood, we divided the responsibilities for the project. My friends were responsible for the physical construction of the guest board and location map and I was responsible for the electronics and software.

Hardware

I reviewed the components we’d need to procure and quickly ran into a scaling problem. With 100 guests, a button at roughly $1.50 a button quickly adds significant cost to the project. This was coupled with the need to have the 100 buttons addressable on a micro controller to uniquely identify a guest. With the costs escalating, we revisited the control input for the guest board and how guests could identify themselves.

We decided to instead utilise arcade controls to reduce the number of control inputs and assign a code per guest. Guests would be required to enter this code using the arcade controls and this would look up the guest and identify their accommodation location. This reduced the costs significantly as an arcade joystick and a couple of buttons was in the order of $30. It would however add significant complexity to the software. But that was fine, it was a challenge I was interested in taking on. The control inputs would then be a Joystick with Up, Down, Left and Right, an A button, a B button, and a Coin button which would reset the system clearing any input already entered.

For the micro controller I selected an Arduino Mega as it had the storage capacity and IO required and was very cost effective. I procured the hardware and set about wiring up the electronics and developing the software. I used PlatformIO as the software development toolchain. It is a fantastic framework for cross-platform, cross-architecture software development for embedded systems. It was a very productive environment that simplified the debugging, testing and deployment of my software onto the Arduino Mega.

Input / Output

Because I was interfacing directly with GPIO and buttons, I needed to debounce the input. Thankfully AceButton provided everything I needed to simplify wiring up the control input.

I set up the pinModes for the GPIO and the event handler for the buttons using the AceButton library. I also set up an interrupt handler for the Coin Button to interrupt and reset the system if it was pressed.

// Define interrupt volatile global
volatile byte kReset = false;

// Define PIN assignments
const int kJoystickUpPin = 46;
const int kJoystickDownPin = 47;
const int kJoystickLeftPin = 48;
const int kJoystickRightPin = 49;
const int kButtonAPin = 33;
const int kButtonBPin = 34;
const int kButtonCoinPin = 2; // PIN 2 can be attached to an interrupt, resetting the board to input state

// Define button configs
ace_button::ButtonConfig btnConfig;

// Define Joystick Buttons
ace_button::AceButton buttonJoyUp(&btnConfig);
ace_button::AceButton buttonJoyDown(&btnConfig);
ace_button::AceButton buttonJoyLeft(&btnConfig);
ace_button::AceButton buttonJoyRight(&btnConfig);

// Define A, B  Buttons
ace_button::AceButton buttonA(&btnConfig);
ace_button::AceButton buttonB(&btnConfig);


void setup()
{
    // Set up Serial Communication
    Serial.begin(9600);

    // Initialize PIN modes
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(kJoystickUpPin, INPUT_PULLUP);
    pinMode(kJoystickDownPin, INPUT_PULLUP);
    pinMode(kJoystickLeftPin, INPUT_PULLUP);
    pinMode(kJoystickRightPin, INPUT_PULLUP);
    pinMode(kButtonAPin, INPUT_PULLUP);
    pinMode(kButtonBPin, INPUT_PULLUP);
    pinMode(kButtonCoinPin, INPUT_PULLUP);

    // Initialise buttons to PINs
    buttonJoyUp.init(kJoystickUpPin);
    buttonJoyDown.init(kJoystickDownPin);
    buttonJoyLeft.init(kJoystickLeftPin);
    buttonJoyRight.init(kJoystickRightPin);
    buttonA.init(kButtonAPin);
    buttonB.init(kButtonBPin);

    // Initialise button config and set handler
    btnConfig.setEventHandler(handleButtonEvent);
    btnConfig.setFeature(ace_button::ButtonConfig::kFeatureClick);
    btnConfig.setClickDelay(2000);

    // Attach interrupt to Coin Button Pin
    attachInterrupt(digitalPinToInterrupt(kButtonCoinPin), interrupt_handler, RISING);

    Serial.println("Wedding Board Setup Complete");
}

To give end users feedback on button presses I wired up a basic speaker and used toneAC, a replacement to the standard tone library for Arduino, to issue tones to the speaker. This allowed me to play a success jingle that when a code was successfully entered and accepted by the system; or play a failure jingle to indicate a mistake was made and the code entered was incorrect and the system had reset, ready to accept a new input combination. The speaker also provided audible feedback for my testing, so I didn’t have to rely solely on the serial terminal output while testing.

Basic IO Test

Checking the Input Code

With the IO set up, I established the main loop to progressively check the code entered and service system resets.

void loop()
{
    // codeResult
    // -1   =   Bad/Invalid Code.
    //  0   =   No Code or Code Input Still in Progress
    // >0   =   Guest for the code that was entered

    if(codeResult > 0){
        // Guest Code already entered, wait until reset
    } else {
        // Service inputs
        buttonJoyUp.check();
        buttonJoyDown.check();
        buttonJoyLeft.check();
        buttonJoyRight.check();
        buttonA.check();
        buttonB.check();

        // Check input for code match
        codeResult = checkCode();
    }

    // Play Music based on codeResult
    resetServiced = resetServiced | playMusic(codeResult);

    // Reset code if reset flag set or invalid code was entered
    resetServiced = resetServiced | resetCode(codeResult);

    if(resetServiced == (kCodeResetFlag | kMusicResetFlag | kCodeResetFlag | kLedResetFlag)){
        // All resets have been serviced, reset reset flag.
        kReset = false;
        resetServiced = 0;
        Serial.println("Reset Serviced");
    }
}

To store the guest location and associated code with each guest, I set up constants, enums and a typedef struct to manage all this data. The guest data is then stored in a guestList[] array. The guestList[] shown below has been redacted for privacy.

The guestList[] array size exceeded the 8KB available in SRAM and required to be stored in Flash Memory. This is achieved using the PROGMEM keyword.

#ifndef WEDDING_BOARD_GUEST_DATA_H
#define WEDDING_BOARD_GUEST_DATA_H

const int kMaxCodeLength = 10;
const int kMaxNameLength = 64;


// Define enumeration for Konami Code Style
enum CodeInput {
    kNone = 0,
    kUp = 1,
    kDown = 2,
    kLeft = 3,
    kRight = 4,
    kA = 5,
    kB = 6
};

enum HouseId {
    kHomeless = 0,
    kLoveShack = 1,
    kFunHouse = 2,
    kBeachHouse1 = 3,
    kBeachHouse2 = 4,
    kBeachHouse3 = 5,
    kBoatHouse1 = 6,
    kBoatHouse2 = 7,
    kBoatHouse3 = 8,
    kBoatHouse4 = 9,
    kBoatHouse5 = 10,
    kBoatHouse6 = 11,
    kSurfHouse1 = 12,
    kSurfHouse2 = 13,
    kSurfHouse3 = 14,
    kGlampTent1 = 15,
    kGlampTent2 = 16,
    kGlampTent3 = 17,
    kGlampTent4 = 18,
    kGlampTent5 = 19,
    kGlampTent6 = 20,
    kGlampTent7 = 21,
    kGlampTent8 = 22
};

// Define Guest Struct for guest storing related data

typedef struct Guest {
    char firstName[kMaxNameLength];
    char lastName[kMaxNameLength];
    HouseId house;
    CodeInput code[kMaxCodeLength];
} Guest;


const Guest guestList[] PROGMEM = {
        {"John",
                "Smith",
                kBoatHouse2,
                {kRight, kLeft, kUp, kDown, kA, kNone, kNone, kNone, kNone, kNone}},
        {"Jane",
                "Smith",
                kBoatHouse2,
                {kRight, kRight, kUp, kDown, kA, kNone, kNone, kNone, kNone, kNone}},
};

#endif //WEDDING_BOARD_GUEST_DATA_H

With the guest data structure established and stored in Flash Memory, I could now look up the code entered versus the data in the guestList[]. Because the data was stored in Flash Memory, the memcpy_P() function had to be used to retrieve the data. You can read more about how PROGMEM works in the AVR LibC Documentation.

Tip

Ensure you use memcpy_P() to retrieve data stored in the Flash Memory. Failing to do so and simply using memcpy() will result in undefined behaviour.

int checkCode(){
    // readGuestEntry(Guest *guestBuffer, int index) handles reading form flash memory into a buffer
    // Set up guest entry buffer
    static Guest guestBuffer;

    int lenGuestList = sizeof(guestList)/sizeof(guestList[0]);
    int maxSuccessfulIndex = -1;

    static int ltv_code_index = 0;
    static int ltv_return = 0;

    // No code entered yet
    if (codeIndex == 0) {
        // No code has been entered yet
        ltv_return = 0;
        return 0;
    }

    // Only check code if code index has changed
    if(ltv_code_index != codeIndex) {
        // Update last time value
        ltv_code_index = codeIndex;

        // Check entered code against guest list
        for (int i = 0; i < lenGuestList; i++) {
            // read guestlist entry from flash memory into guestBuffer
            readGuestEntry(&guestBuffer, i);

            int codeCheckIndex = 0;
            while (codeBuffer[codeCheckIndex] == guestBuffer.code[codeCheckIndex]) {
                // While code matches guest entry
                maxSuccessfulIndex = max(maxSuccessfulIndex, codeCheckIndex);
                codeCheckIndex++;
                if (codeCheckIndex == kMaxCodeLength) {
                    //Code Matches! return index for guest in Guest List
                    ltv_return = i + 1;
                    return i + 1; // Return guest number in guestList for code match
                }
            }
        }
        if (maxSuccessfulIndex == codeIndex - 1) {
            // Still valid codes, continue retrieving input
            ltv_return = 0;
            return 0;
        } else {
            // No valid codes, return bad code
            ltv_return = -1;
            return -1;
        }
    } else {
        return ltv_return;
    }
}

void readGuestEntry(Guest *guestBuffer, int index) {
    // Because we're storing guestList in flash Memory (PROGMEM), we need to use special utilities to extract
    // the data to a buffer
    // memcpy_P() - like memcpy() but for flash storage into ram
    int guestEntrySize = sizeof(Guest); // Bytes

    memcpy_P(guestBuffer, &guestList[index], guestEntrySize);
}

Code IO Test

Generating Guest Codes

With the checkCode() logic validated I now needed to generate a unique code per guest. I opted to use a python script to generate the data and output a C constant static array and copied it directly into the header file. I initialised the Random Number Generator (RNG) with a static seed to ensure repeatable code assignments per guest. I also ensured that each code generated was unique.

from enum import Enum
import random
import tokenize

class KonamiCode(Enum):
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4
    A = 5
    B = 6

# Initialise seed, don't change so code assignment to individual is static
rnd_seed = 1
for c in "Wedding Board":
    rnd_seed *= ord(c)

random.seed(a=rnd_seed)

KONAMI_ARROW_LENGTH = 4
KONAMI_BUTTON_LENGTH = 1
KONAMI_CODE_LENGTH = KONAMI_ARROW_LENGTH + KONAMI_BUTTON_LENGTH

MAXCODELENGTH = 10

# Code is constructed by leading ARROW inputs, followed by button inputs
# Code length is set by KONAMI_ARROW_LENGTH and KONAMI_BUTTON_LENGTH
# e.g. KONAMI_ARROW_LENGTH =6, KONAMI_BUTTON_LENGTH = 2 would result in a generated code
# UP, UP, DOWN, RIGHT, LEFT, LEFT, B, A

codelist = []

# Define functions
def check_unique(code):
    # This checks the code_list and ensures code is unique
    # construct tuple for comparison
    a = tuple(code)
    # check code list for uniqueness
    for x in codelist:
        if x == a:
            return False
    return True


def generate_konami(guests):
    # Generate konami codes for guests
    for x in range(guests):
        code = []
        not_unique = True
        while not_unique:
            # Construct ARROW inputs for code
            for i in range(KONAMI_ARROW_LENGTH):
                code.append(KonamiCode(random.randint(1, 4)))
            # Construct BUTTON inputs for code
            for i in range(KONAMI_BUTTON_LENGTH):
                code.append(KonamiCode(random.randint(5, 6)))
            # check for uniqueness
            if check_unique(code):
                # Code is unique. Store in codelist
                not_unique = False
            else:
                # Code is not unique. Regenerate code
                code = []

        # place in tuple and append to codelist
        codelist.append(tuple(code))

generate_konami(125)

print("******** Code List for Guests *********")
for c in codelist:
    print(c)


# Read Guest List, assign code, and output format required by Arduino project

# Format of Guest Entry is:
#   <Number> \t <Accomodation> \t <First Name> \t <Last Name>

guest_list = open("guestlist.txt", "r")

output = open("guestlist.h_append", "w")

# Write header start
output.write("Guest guestList[] = {\n")


# Write header contents
for line in guest_list:
    guest_entry = line.split('\t')

    # Write first and last name of guest
    output.write("               {\"" + guest_entry[2].rstrip() + "\",\n")
    output.write("                 \"" + guest_entry[3].rstrip() + "\",\n")

    # Write accommodation for guest
    if guest_entry[1] == 'LoveShack':
        output.write("                 kLoveShack,\n")
    elif guest_entry[1] == 'FunHouse':
        output.write("                 kFunHouse,\n")
    elif guest_entry[1] == 'Beach11':
        output.write("                 kBeachHouse1,\n")
    elif guest_entry[1] == 'Beach12':
        output.write("                 kBeachHouse2,\n")
    elif guest_entry[1] == 'Beach13':
        output.write("                 kBeachHouse3,\n")
    elif guest_entry[1] == 'Boat1':
        output.write("                 kBoatHouse1,\n")
    elif guest_entry[1] == 'Boat2':
        output.write("                 kBoatHouse2,\n")
    elif guest_entry[1] == 'Boat3':
        output.write("                 kBoatHouse3,\n")
    elif guest_entry[1] == 'Boat4':
        output.write("                 kBoatHouse4,\n")
    elif guest_entry[1] == 'Boat5':
        output.write("                 kBoatHouse5,\n")
    elif guest_entry[1] == 'Boat6':
        output.write("                 kBoatHouse6,\n")
    elif guest_entry[1] == 'Surf7':
        output.write("                 kSurfHouse1,\n")
    elif guest_entry[1] == 'Surf8':
        output.write("                 kSurfHouse2,\n")
    elif guest_entry[1] == 'Surf9':
        output.write("                 kSurfHouse3,\n")
    elif guest_entry[1] == 'Glamp1':
        output.write("                 kGlampTent1,\n")
    elif guest_entry[1] == 'Glamp2':
        output.write("                 kGlampTent2,\n")
    elif guest_entry[1] == 'Glamp3':
        output.write("                 kGlampTent3,\n")
    elif guest_entry[1] == 'Glamp4':
        output.write("                 kGlampTent4,\n")
    elif guest_entry[1] == 'Glamp5':
        output.write("                 kGlampTent5,\n")
    elif guest_entry[1] == 'Glamp6':
        output.write("                 kGlampTent6,\n")
    elif guest_entry[1] == 'Glamp7':
        output.write("                 kGlampTent7,\n")
    elif guest_entry[1] == 'Glamp8':
        output.write("                 kGlampTent8,\n")

    # Write code for guest
    code = codelist.pop(0)

    # Calculate padding (stop one short for padding)
    pad = MAXCODELENGTH - len(code) - 1

    output.write("                 {")
    for x in code:
        if x == KonamiCode.UP:
            output.write("kUp, ")
        elif x == KonamiCode.DOWN:
            output.write("kDown, ")
        elif x == KonamiCode.LEFT:
            output.write("kLeft, ")
        elif x == KonamiCode.RIGHT:
            output.write("kRight, ")
        elif x == KonamiCode.A:
            output.write("kA, ")
        elif x == KonamiCode.B:
            output.write("kB, ")

    for x in range(pad):
        output.write("kNone, ")

    # Write end of code
    output.write("kNone}},\n")

# Write header end
output.write("};\n")

Addressing LEDs

With the guestList[] data now generated, I moved onto mapping the addressable LEDs to identify the guest and their respective accommodation location. I used the fastLED library for controlling the addressable LEDs. I decided to identify all the guests staying in their accommodation when an end user entered their code. This way the guest could see whom they were staying with.

In the interim my friends had laid out, assembled and labelled the board for both the Guest List and Location Map. We soldered in the LEDs and had the solution largely completed. All that remained was the LED logic.

Wedding Board Control Box Assembled

Wedding Board Control Box Assembled

Guest List Board Assembled

Guest List Board Assembled

Location Map Board Assembled

Location Map Board Assembled

Upon a successful code entry, the guest index is checked against the guestList[]. Using this data a ledMap is generated comparing each guest’s location against the end user’s listed accommodation. This ledMap is then passed off to the serviceLeds() function which commands the appropriate LEDs to light up based on the ledMap.

int renderLEDs(int code) {
    // Render LEDs based on code result
    // If -1, flash red
    // If 0, do nothing
    // else render ledMap

    static int ltv_code = 0;
    static int ledMap[kMaxLeds] = {0};

    int badCode = false;

    if (kReset) {
        // Reset LEDs
        resetLedMap(ledMap);
        badCode = false;
        ltv_code = 0;
        return kLedResetFlag;
    }

    if (code == 0) {
        // No valid code yet
        ltv_code = code;
        return 0;
    } else if(ltv_code != code){
        // code has changed from 0 to either bad code or valid guest ID
        if(code == -1){
            // Bad code
            badCode = true;
        } else {
            // Valid code. Play success
            lookupLedMap(ledMap, code - 1);
        }
    }

    serviceLeds(ledMap, badCode, cheatCode);

    ltv_code = code;
    return 0;
}

int *lookupLedMap(int *ledMap, int guestIndex) {
    // Set up Guest Buffers
    static Guest guestLookup;
    static Guest guestBuffer;

    // Set up consts and local variables
    int lenGuestList = sizeof(guestList)/sizeof(guestList[0]);
    HouseId house;

    // Grab guest record for lookup
    readGuestEntry(&guestLookup, guestIndex);

    // Read where they're staying
    house = guestLookup.house;

    // Iterate over guestList and check who else are staying there, add their index to the map
    for (int i = 0; i < lenGuestList; i++) {
        // read guestlist entry from flash memory into guestBuffer
        readGuestEntry(&guestBuffer, i);
        if(house == guestBuffer.house){
            // They're also staying there
            ledMap[i] = true;
        } else {
            ledMap[i] = false;
        }

    }

    return ledMap;
}
Guest List LED Test

Guest List LED Test

With all the features developed, tested and integrated it was time to assemble the full solution. For integration testing I selected different guests at random and inputted their code on the solution. All working as expected. It was robust against malformed inputs, the reset button and interrupt handler worked reliably. We were ready for Production!

Integration Test on Full Solution

Integration Test on Full Solution

Cheat Codes

With the project finished and tested, we still had a couple weeks up our sleeve before the Wedding. I decided you can’t have an arcade control input and no cheat codes. The FastLED library provided easy control of the addressable LEDs and functions for visual effects. I ended up implementing 6 cheat codes and with different visual effects. I set up these cheat codes as appended data to the guestList[].

const Guest guestList[] PROGMEM = {
        {"John",
                "Smith",
                kBoatHouse2,
                {kRight, kLeft, kUp, kDown, kA, kNone, kNone, kNone, kNone, kNone}},
        {"Jane",
                "Smith",
                kBoatHouse2,
                {kRight, kLeft, kUp, kDown, kA, kNone, kNone, kNone, kNone, kNone}},
        {"Konami",
                    "Cheat Code",
                    kHomeless,
                    {kUp, kUp, kDown, kDown, kLeft, kRight, kLeft, kRight, kB, kA}},
        {"Hadouken",
                 "Street Fighter",
                 kHomeless,
                 {kDown, kDown, kRight, kRight, kA, kNone, kNone, kNone, kNone, kNone}},
        {"Special Cup",
                 "Super Mario Kart",
                 kHomeless,
                 {kLeft, kRight, kLeft, kRight, kLeft, kLeft, kRight, kRight, kA, kNone}},
        {"Snake",
                 "Cheat Code",
                 kHomeless,
                 {kUp, kLeft, kDown, kRight, kA, kNone, kNone, kNone, kNone, kNone}},
        {"Sonic",
                    "The Hedgehog",
                    kHomeless,
                    {kUp, kB, kDown, kB, kLeft, kB, kRight, kB, kA, kNone}},
        {"Mortal",
                    "Kombat",
                    kHomeless,
                    {kA, kB, kA, kB, kA, kB, kB, kNone, kNone, kNone}}
};
int *serviceLeds(int *ledMap, int badCode, int cheatCode) {
    // ledMap is the index for each LED
    // Setup such that index 0 maps to guest index 0
    // entries >= 100 will map to the houses

    static unsigned long last_service = 0;
    unsigned long render_interval = 30; //ms

    int badCodeFlashInterval = 200;
    int badCodeFlashes = 4;

    if(badCode){
        // Render bad code LED Show
        for(int i=0; i< badCodeFlashes; i++){
            for(int j=0; j < kMaxLeds; j++){
                leds[j] = CRGB::Red;
            }
            FastLED.show();
            delay(badCodeFlashInterval);
            // clear down leds to turn off.
            memset(leds, 0, sizeof(leds[0])*kMaxLeds);
            FastLED.show();
            delay(badCodeFlashInterval);

        }
    } else {
        // Service LED code  until reset
        if(millis() - last_service >= render_interval){
            // DEBUG
            if (cheatCode != 0) {
                // Render cheat code LED Show
                if (cheatCode == kKonamiCode) {
                    renderNoiseLed();
                } else if (cheatCode == kHadouken) {
                    renderFireLed();
                } else if (cheatCode == kSpecialCupCode) {
                    renderSpecialLed();
                } else if (cheatCode == kSnakeCode) {
                    renderSnakeLed();
                } else if (cheatCode == kSonicCode) {
                    renderSonicLed();
                } else if (cheatCode == kKombatCode) {
                    renderKombatLed();
                }
            } else {
                // Service LEDS using guest look up
                for (int i = 0; i < kMaxLeds; i++) {
                    if (ledMap[i] == true) {
                        leds[i] = CRGB::Green;
                    }
                }
            }
            // Service Leds and update last_service
            FastLED.show();
            last_service = millis();
        }
    }

    return ledMap;
}

Konami Cheat Code Visual Effect

With the project finished, complete with Easter Eggs, it was ready for the Wedding. The board was packed up and set up at the main reception area for the wedding venue. Guest’s codes were printed, laminated and placed on the desk with the board ready for wedding patrons to look up their accommodation location in a fun and novel way.

Wedding Board Ready to Greet Guests

Wedding Board Ready to Greet Guests

The solution performed flawlessly with no reported faults or failures. It even got set up near the dance floor towards the end of the night to provide some additional disco ambience using the Konami Cheat Code visual effect. This was a very enjoyable project completed with my friends, motivated by a hard deadline and culminated in a very memorable and cherished weekend.