Relay Timer with LED Display

I just set up a project from a publication by Marco C. for an Arduino-based ‘Relay Timer with LED Display’

But I get into some problem:
I copied and loaded the code from Marco’s listing into my Arduino Nano. It compiled without any error and loaded the code into the Arduino.
When it runs the display is un-readable.

My debugs up to now:

  1. I unplug the D4 pin of the Arduino and the leftmost digit turn off, unplug D5 and the next leftmost digit turn off, so on for D6 and D7, so the common cathode connections are OK.

  2. I unplug D12, D13, A0 to A5 and the proper segment (a, b, c, d, e, f, g and dp) turns off, so the segment connections are OK.

  3. I turn the rotary encoder and the display changes (but unreadable): Turning clockwise and I can see the display changes. Turning counter clockwise and the display changes and stops, I guess it gets to the lowest value.

  4. Turning clockwise and click on the push button: it starts counting (unreadable of course).

I guess the multiplexing of the LED’s is not correct, but at which level? the LED spec? the cabling?
The rotary switch has 4 connections: GND, +5v, SW, DT, CLK
The 4 digits 7 segments display is a HS410561K-32, as per my research it is a Common Cathode LED display.
As a new member of the forum, I cannot make any attachment... When can I do that?

Please help

Welcome,

It looks like it's displaying 0000 but inverted

Show your code

Again, as a new member of the forum, I am limited to only one link.
I uploaded the photo so I cannot upload anything else now.
Please let me know if there any other way to upload more photos and the code?
Thanks.
Duc

/*
Relay timer - User settable timer controlling a relay output
Implementation 2 - Arduino outputs directly runs LED segments

Hardware requirements

Function

  • Arduino manages the display as a multiplexed POV refresh using direct I/O
  • Rotary encoder allows setting the required timer value
  • Rotary encoder switch used to start the timer
  • Timer can be paused with a press of switch. Second press resumes and long press ends the timer.
  • While timer is active (running or paused) the relay output is switched on.

Circuit Connections

+---------+--------------------+
| Arduino | Connected to |
+---------+--------------------+
| D0 | N/C (Arduino Rx) |
| D1 | N/C (Arduino Tx) |
| D2 | Rotary Encoder B |
| D3 | Rotary Encoder A |
| D4 | LED pin 6 (dig 4) |
| D5 | LED pin 8 (dig 3) |
| D6 | LED pin 9 (dig 2) |
| D7 | LED pin 12 (dig 1) |
| D8 | Selection Switch |
| D9 | N/C |
| D10 | Relay Output |
| D11 | N/C |
| D12 | LED pin 11 (seg A) |
| D13 | LED pin 7 (seg B) |
| A0 | LED pin 4 (seg C) |
| A1 | LED pin 2 (seg D) |
| A2 | LED pin 1 (seg E) |
| A3 | LED pin 10 (seg F) |
| A4 | LED pin 5 (seg G) |
| A5 | LED pin 3 (seg DP) |
+---------+--------------------+

Library Dependencies

MD_KeySwitch and MD_REncoder can be found at GitHub - MajicDesigns/MD_KeySwitch: KeySwitch Digital Input Library - SUPERCEDED - See README

Revision History

Feb 2016 - version 1.0

  • First release

Copyright

Copyright (C) 2015 Marco Colli. All rights reserved.

This is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include <MD_REncoder.h>
#include <MD_KeySwitch.h>

// Debugging stuff -------------------------
#define DEBUG 0

#if DEBUG
#define PRINTS(s) Serial.print(F(s));
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); }
#define PRINTX(s, v) { Serial.print(F(s)); Serial.print(F("0x")); Serial.print(v, HEX); }
#else
#define PRINTS(s)
#define PRINT(s, v)
#define PRINTX(s, v)
#endif

// Hardware pin definitions ---------------
// Rotary Encoder
const uint8_t RE_A_PIN = 3;
const uint8_t RE_B_PIN = 2;

// Selection key
const uint8_t SEL_PIN = 8;

// Control output
const uint8_t OUTPUT_PIN = 10;

// Digit multiplex selection - [0] is least sig dig (LSD), [n] is MSD
const uint8_t digitPin = {4, 5, 6, 7 };

// Segment multiplex selection - in order segments A B C D E F G DP
const uint8_t segmentPin = { 12, 13, A0, A1, A2, A3, A4, A5 };

// Static Data tables ----------------------
// LED segments are defined as uint8_t values with bits mapped as follows:
// MSB LSB
// +----+-----------------+
// |Bit | 7 6 5 4 3 2 1 0 |
// +----+-----------------+
// |Seg |DP G F E D C B A |
// +----+-----------------+

// LED segments for digits
const PROGMEM uint8_t digits =
{
0x3f, 0x06, 0x5b, 0x4f, // 0123
0x66, 0x6d, 0x7d, 0x07, // 4567
0x7f, 0x6f //89
};

// LED segments for alpha characters
const PROGMEM uint8_t alpha =
{
0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71, // ABCDEF
0x3d, 0x76, 0x06, 0x0e, 0x00, 0x38, // GHIJKL
0x00, 0x54, 0x3f, 0x73, 0x67, 0x60, // MNOPQR
0x6d, 0x78, 0x3e, 0x3e, 0x00, 0x00, // STUVWX
0x6e, 0x5b // YZ
};

const uint8_t DECIMAL = 0x80;

// Miscellaneous ---------------------------
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

// Digital state to turn each digit on or off during multiplexing
const uint8_t DIGIT_ON = LOW;
const uint8_t DIGIT_OFF = HIGH;
const uint8_t SEGMENT_ON = (DIGIT_ON == HIGH ? LOW : HIGH);
const uint8_t SEGMENT_OFF = (DIGIT_OFF == HIGH ? LOW : HIGH);

// Finite state machine states
typedef enum state_t { S_INIT, S_IDLE, S_START, S_RUNNING, S_PAUSE, S_END };

// All time duration b=values for setup and storage are held in units of
// the smallest time interval, in seconds. This is translated into minutes
// and seconds for the display and countdown.
const uint8_t TIME_MAX_MINUTES = 99;
const uint8_t TIME_INTERVAL = 5; // smallest time interval in seconds
const uint16_t TIME_MAX_SP = ((TIME_MAX_MINUTES * 60) + 59) / TIME_INTERVAL; // max setting value

// Global Objects --------------------------
MD_KeySwitch SW(SEL_PIN);
MD_REncoder RE(RE_A_PIN, RE_B_PIN);

// Code ------------------------------------
void updateSegments(uint8_t val)
// update the segments with the value specified
{
for (uint8_t i=0; i<ARRAY_SIZE(segmentPin); i++)
{
digitalWrite(segmentPin[i], (val & 1 ? SEGMENT_ON : SEGMENT_OFF));
val >>= 1;
}
}

void displayMessage(char *pMesg, uint16_t duration = 2000)
// Display the message specified for the specified duration in milliseconds
{
uint32_t startTime = millis();
char *p; // temporary string pointer
uint8_t v; // LED pattern for display

while ((millis() - startTime) < duration)
{
p = pMesg;
for (int8_t i = ARRAY_SIZE(digitPin) - 1; i >= 0; i--)
{
if (*p != '\0')
{
v = pgm_read_byte(alpha + toupper(*p) - 'A');
p++;
}
else
v = 0;

  // turn all the digits off
  for (uint8_t j = 0; j < ARRAY_SIZE(digitPin); j++)
    digitalWrite(digitPin[j], DIGIT_OFF);

  // SPI transfer of new data
  updateSegments(v); // send value

  // show new data
  digitalWrite(digitPin[i], DIGIT_ON);
}

}
}

void displayTime(uint16_t val, uint8_t decDigit = 0)
{
uint8_t dig = 0;

for (uint8_t i=0; i<ARRAY_SIZE(digitPin); i++)
{
dig = val % 10;
val /= 10;

// turn all the digits off
for (uint8_t j=0; j<ARRAY_SIZE(digitPin); j++)
  digitalWrite(digitPin[j], DIGIT_OFF);

// SPI transfer of new data
updateSegments(pgm_read_byte(digits + dig) + (decDigit == i ? DECIMAL : 0)); // send value

// show new data
digitalWrite(digitPin[i], DIGIT_ON);

}
}

void setup()
{
#if DEBUG
Serial.begin(57600);
PRINTS("[Timer]\n");
#endif // DEBUG

RE.begin();
SW.begin();

// initialise hardware
pinMode(OUTPUT_PIN, OUTPUT);

for (uint8_t i = 0; i<ARRAY_SIZE(digitPin); i++)
{
pinMode(digitPin[i], OUTPUT);
digitalWrite(digitPin[i], DIGIT_OFF);
}

for (uint8_t i = 0; i<ARRAY_SIZE(segmentPin); i++)
{
pinMode(segmentPin[i], OUTPUT);
digitalWrite(segmentPin[i], SEGMENT_OFF);
}

displayMessage("helo");
}

void loop()
{
static state_t state = S_INIT;
static uint16_t setPoint = 0;
static int8_t minutes = 0, seconds = 0;
static uint32_t timeStart = 0;
static boolean inMessage = false;

if (!inMessage)
displayTime((minutes*100)+seconds, 2);

switch (state)
{
case S_INIT:
PRINT("\nS_INIT set:", setPoint);
{
uint16_t totalSeconds = setPoint * TIME_INTERVAL;
seconds = totalSeconds % 60;
minutes = totalSeconds / 60;

  if (minutes > TIME_MAX_MINUTES) minutes = TIME_MAX_MINUTES;

  state = S_IDLE;
}
break;

case S_IDLE: // handle user input or just wait
//PRINTS("\nS_IDLE ");
// rotary encoder block
{
uint8_t x = RE.read();

  switch (x)
  {
  case DIR_CW: PRINTS(" CW");  if (setPoint < TIME_MAX_SP) setPoint++; break;
  case DIR_CCW: PRINTS(" CCW"); if (setPoint > 0) setPoint--; break;
  }
  if (x != DIR_NONE) state = S_INIT;
}
// switch block
if (SW.read() == MD_KeySwitch::KS_PRESS && setPoint != 0)
{
  PRINTS(" Press");
  state = S_START;
}
break;

case S_START:
PRINTS("\nS_START");
timeStart = millis();
digitalWrite(OUTPUT_PIN, HIGH);
state = S_RUNNING;
break;

case S_RUNNING:
PRINTS("\nS_RUNNING");
// Time counter
if(millis() - timeStart >= 1000)
{
timeStart += 1000;
if (seconds != 0)
seconds--;
else
{
seconds = 59;
if (minutes != 0)
minutes--;
else
state = S_END;
}
}
// Check if pausing
if (SW.read() == MD_KeySwitch::KS_PRESS)
{
PRINTS(" Pause");
state = S_PAUSE;
}
break;

case S_PAUSE:
PRINTS("\nS_PAUSE");
inMessage = true;
displayMessage("pause", 50);
{
uint8_t x = SW.read();

  switch (x)
  {
  case MD_KeySwitch::KS_PRESS:
    PRINTS(" Restart");
    timeStart = millis();   // approximate, but we have paused so accurate time is out the window
    inMessage = false;
    state = S_RUNNING;
    break;

  case MD_KeySwitch::KS_LONGPRESS:
    PRINTS(" Stop");
    inMessage = false;
    state = S_END;
    break;
  }
}
break;

case S_END:
PRINTS("\nS_END");
digitalWrite(OUTPUT_PIN, LOW);
displayMessage("end");
state = S_INIT;
break;

default:
PRINTS("\nDEFAULT");
state = S_IDLE; // in case we get into trouble!
break;
}
}

If you could edit your post and put your code inside code tags (the "</>" icon)

Try change these values

const uint8_t SEGMENT_ON = (DIGIT_ON == HIGH ? LOW : HIGH);
const uint8_t SEGMENT_OFF = (DIGIT_OFF == HIGH ? LOW : HIGH);

to

const uint8_t SEGMENT_ON = DIGIT_ON;
const uint8_t SEGMENT_OFF = DIGIT_OFF;
/*
Relay timer - User settable timer controlling a relay output
Implementation 2 - Arduino outputs directly runs LED segments

Hardware requirements
---------------------
- Arduino Uno/Nano/Mini/etc.
- 7 segment 4 Digit LED display (similar to http://www.taydaelectronics.com/led-displays/7-segment-4-digit/led-display-7-segment-4-digit-0-56-inch-common-cathode-super-yellow.html)
- Rotary encoder with built in selection switch
- Relay output or relay module (commonly available on eBay and other suppliers)

Function
--------
- Arduino manages the display as a multiplexed POV refresh using direct I/O
- Rotary encoder allows setting the required timer value
- Rotary encoder switch used to start the timer
- Timer can be paused with a press of switch. Second press resumes and long press ends the timer.
- While timer is active (running or paused) the relay output is switched on.

Circuit Connections
-------------------
+---------+--------------------+
| Arduino | Connected to       |
+---------+--------------------+
| D0      | N/C (Arduino Rx)   |
| D1      | N/C (Arduino Tx)   |
| D2      | Rotary Encoder B   |
| D3      | Rotary Encoder A   |
| D4      | LED pin 6 (dig 4)  |
| D5      | LED pin 8 (dig 3)  |
| D6      | LED pin 9 (dig 2)  |
| D7      | LED pin 12 (dig 1) |
| D8      | Selection Switch   |
| D9      | N/C                |
| D10     | Relay Output       |
| D11     | N/C                |
| D12     | LED pin 11 (seg A) |
| D13     | LED pin 7 (seg B)  |
| A0      | LED pin 4 (seg C)  |
| A1      | LED pin 2 (seg D)  |
| A2      | LED pin 1 (seg E)  |
| A3      | LED pin 10 (seg F) |
| A4      | LED pin 5 (seg G)  |
| A5      | LED pin 3 (seg DP) |
+---------+--------------------+

Library Dependencies
--------------------
MD_KeySwitch and MD_REncoder can be found at https://github.com/MajicDesigns/MD_KeySwitch

Revision History 
----------------
Feb 2016 - version 1.0
- First release

Copyright
---------
Copyright (C) 2015 Marco Colli. All rights reserved.

This is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include <MD_REncoder.h>
#include <MD_KeySwitch.h>

// Debugging stuff -------------------------
#define DEBUG 0

#if DEBUG
#define PRINTS(s)   Serial.print(F(s));
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); }
#define PRINTX(s, v) { Serial.print(F(s)); Serial.print(F("0x")); Serial.print(v, HEX); }
#else
#define PRINTS(s)
#define PRINT(s, v)
#define PRINTX(s, v)
#endif

// Hardware pin definitions  ---------------
// Rotary Encoder
const uint8_t RE_A_PIN = 3;
const uint8_t RE_B_PIN = 2;

// Selection key
const uint8_t SEL_PIN = 8;

// Control output
const uint8_t OUTPUT_PIN = 10;

// Digit multiplex selection - [0] is least sig dig (LSD), [n] is MSD
const uint8_t digitPin[] = {4, 5, 6, 7 };

// Segment multiplex selection - in order segments A B C D E F G DP
const uint8_t segmentPin[] = { 12, 13, A0, A1, A2, A3, A4, A5 };

// Static Data tables ----------------------
// LED segments are defined as uint8_t values with bits mapped as follows:
//     MSB               LSB
// +----+-----------------+
// |Bit | 7 6 5 4 3 2 1 0 |
// +----+-----------------+
// |Seg |DP G F E D C B A |
// +----+-----------------+

// LED segments for digits
const PROGMEM uint8_t digits[] = 
{ 
  0x3f, 0x06, 0x5b, 0x4f, // 0123
  0x66, 0x6d, 0x7d, 0x07, // 4567
  0x7f, 0x6f  //89
};

// LED segments for alpha characters
const PROGMEM uint8_t alpha[] =
{
  0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71, // ABCDEF
  0x3d, 0x76, 0x06, 0x0e, 0x00, 0x38, // GHIJKL
  0x00, 0x54, 0x3f, 0x73, 0x67, 0x60, // MNOPQR
  0x6d, 0x78, 0x3e, 0x3e, 0x00, 0x00, // STUVWX
  0x6e, 0x5b  // YZ
};

const uint8_t DECIMAL = 0x80;

// Miscellaneous ---------------------------
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

// Digital state to turn each digit on or off during multiplexing
const uint8_t DIGIT_ON = LOW;
const uint8_t DIGIT_OFF = HIGH;
const uint8_t SEGMENT_ON = (DIGIT_ON == HIGH ? LOW : HIGH);
const uint8_t SEGMENT_OFF = (DIGIT_OFF == HIGH ? LOW : HIGH);

// Finite state machine states
typedef enum state_t { S_INIT, S_IDLE, S_START, S_RUNNING, S_PAUSE, S_END };

// All time duration b=values for setup and storage are held in units of 
// the smallest time interval, in seconds. This is translated into minutes 
// and seconds for the display and countdown.
const uint8_t TIME_MAX_MINUTES = 99;
const uint8_t TIME_INTERVAL = 5; // smallest time interval in seconds
const uint16_t TIME_MAX_SP = ((TIME_MAX_MINUTES * 60) + 59) / TIME_INTERVAL;  // max setting value

// Global Objects --------------------------
MD_KeySwitch SW(SEL_PIN);
MD_REncoder  RE(RE_A_PIN, RE_B_PIN);

// Code ------------------------------------
void updateSegments(uint8_t val)
// update the segments with the value specified
{
	for (uint8_t i=0; i<ARRAY_SIZE(segmentPin); i++)
	{
		digitalWrite(segmentPin[i], (val & 1 ? SEGMENT_ON : SEGMENT_OFF));
		val >>= 1;
	}
}

void displayMessage(char *pMesg, uint16_t duration = 2000)
// Display the message specified for the specified duration in milliseconds
{
  uint32_t startTime = millis();
  char *p;      // temporary string pointer
  uint8_t v;    // LED pattern for display

  while ((millis() - startTime) < duration)
  {
    p = pMesg;
    for (int8_t i = ARRAY_SIZE(digitPin) - 1; i >= 0; i--)
    {
      if (*p != '\0')
      {
        v = pgm_read_byte(alpha + toupper(*p) - 'A');
        p++;
      }
      else
        v = 0;

      // turn all the digits off
      for (uint8_t j = 0; j < ARRAY_SIZE(digitPin); j++)
        digitalWrite(digitPin[j], DIGIT_OFF);

      // SPI transfer of new data
      updateSegments(v); // send value

      // show new data
      digitalWrite(digitPin[i], DIGIT_ON);
    }
  } 
}

void displayTime(uint16_t val, uint8_t decDigit = 0)
{
  uint8_t dig = 0;

  for (uint8_t i=0; i<ARRAY_SIZE(digitPin); i++)
  {
    dig = val % 10;
    val /= 10;

    // turn all the digits off
    for (uint8_t j=0; j<ARRAY_SIZE(digitPin); j++)
      digitalWrite(digitPin[j], DIGIT_OFF);

    // SPI transfer of new data
    updateSegments(pgm_read_byte(digits + dig) + (decDigit == i ? DECIMAL : 0)); // send value

    // show new data
    digitalWrite(digitPin[i], DIGIT_ON);
  }
}

void setup()
{
#if DEBUG
  Serial.begin(57600);
  PRINTS("[Timer]\n");
#endif // DEBUG

  RE.begin();
  SW.begin();

  // initialise hardware
  pinMode(OUTPUT_PIN, OUTPUT);

  for (uint8_t i = 0; i<ARRAY_SIZE(digitPin); i++)
  {
    pinMode(digitPin[i], OUTPUT);
    digitalWrite(digitPin[i], DIGIT_OFF);
  }

  for (uint8_t i = 0; i<ARRAY_SIZE(segmentPin); i++)
  {
    pinMode(segmentPin[i], OUTPUT);
    digitalWrite(segmentPin[i], SEGMENT_OFF);
  }

  displayMessage("helo");
}

void loop()
{
  static state_t state = S_INIT;
  static uint16_t setPoint = 0;
  static int8_t  minutes = 0, seconds = 0;
  static uint32_t timeStart = 0;
  static boolean  inMessage = false;

  if (!inMessage)
    displayTime((minutes*100)+seconds, 2);

  switch (state)
  {
  case S_INIT:
    PRINT("\nS_INIT set:", setPoint);
    {
      uint16_t totalSeconds = setPoint * TIME_INTERVAL;
      seconds = totalSeconds % 60;
      minutes = totalSeconds / 60;

      if (minutes > TIME_MAX_MINUTES) minutes = TIME_MAX_MINUTES;

      state = S_IDLE;
    }
    break;

  case S_IDLE:  // handle user input or just wait
    //PRINTS("\nS_IDLE ");
    // rotary encoder block
    {
      uint8_t x = RE.read();

      switch (x)
      {
      case DIR_CW: PRINTS(" CW");  if (setPoint < TIME_MAX_SP) setPoint++; break;
      case DIR_CCW: PRINTS(" CCW"); if (setPoint > 0) setPoint--; break;
      }
      if (x != DIR_NONE) state = S_INIT;
    }
    // switch block
    if (SW.read() == MD_KeySwitch::KS_PRESS && setPoint != 0)
    {
      PRINTS(" Press");
      state = S_START;
    }
    break;

  case S_START:
    PRINTS("\nS_START");
    timeStart = millis();
    digitalWrite(OUTPUT_PIN, HIGH);
    state = S_RUNNING;
    break;

  case S_RUNNING:
    PRINTS("\nS_RUNNING");
    // Time counter
    if(millis() - timeStart >= 1000)
    {
      timeStart += 1000;
      if (seconds != 0)
        seconds--;
      else
      {
        seconds = 59;
        if (minutes != 0)
          minutes--;
        else
          state = S_END;
      }
    }
    // Check if pausing
    if (SW.read() == MD_KeySwitch::KS_PRESS)
    {
      PRINTS(" Pause");
      state = S_PAUSE;
    }
    break;

  case S_PAUSE:
    PRINTS("\nS_PAUSE");
    inMessage = true;
    displayMessage("pause", 50);
    {
      uint8_t x = SW.read();

      switch (x)
      {
      case MD_KeySwitch::KS_PRESS:
        PRINTS(" Restart");
        timeStart = millis();   // approximate, but we have paused so accurate time is out the window
        inMessage = false;
        state = S_RUNNING;
        break;

      case MD_KeySwitch::KS_LONGPRESS:
        PRINTS(" Stop");
        inMessage = false;
        state = S_END;
        break;
      }
    }
    break;

  case S_END:
    PRINTS("\nS_END");
    digitalWrite(OUTPUT_PIN, LOW);
    displayMessage("end");
    state = S_INIT;
    break;

  default:
    PRINTS("\nDEFAULT");
    state = S_IDLE; // in case we get into trouble!
    break;
  }
}


Hi Guix,
After I made the change you suggest:

  1. While in the initialization it displays all 8's for 2 seconds (supposed to display 'HELO').
  2. Then it goes to ready state, displays all 0's (looks good).
  3. When I turn the rotary switch clockwise one click, it displays all 8's.
  4. When I click the switch, I see the count down but the display shows all 8's with some segments brighter than others.
    Note that all 4 decimal points are always on.

Disconnect rotary encoder for now.. Get the display to work first

I suggest that you try this library : GitHub - DeanIsMe/SevSeg: Seven segment display controller library for Arduino

Also you (and the guy who wrote that tutorial) forgot to use resistors

As someone pointed out, the display seems to be turning on the complementary LEDs from those that should be turned on.

Try changing this code and see if it makes the right difference:

void updateSegments(uint8_t val)
// update the segments with the value specified
{
  val = ~val;    // invert the bits

  for (uint8_t i=0; i<ARRAY_SIZE(segmentPin); i++)
  {
    digitalWrite(segmentPin[i], (val & 1 ? SEGMENT_ON : SEGMENT_OFF)); 
    val >>= 1;
  }
}

The only change is adding the line to invert the bits.

Actually, no. This is a considered design decision in this hobby project (vs a production environment). The reason that resistors are used is to limit the current through the LED. Continuous excess current destroys the LED, mostly likely to heat build up (thermal stress), as I understand it. In this project each LEDs is multiplexed, so it is actually only 'on' less than 25% of the time. This allows any excess heat to dissipate and, in practice works ok. A problem could arise if the LED was left 'stuck' on with the excess current (eg, if the multiplexing software fails with LEDs stuck on).

Having said that, if someone wants to add in resistors there will be no difference to how the circuit performs, so go for it.