LED Clock – Part 1

Over the Christmas break I had some fun with my MLB LED scoreboard (see that blog for a BOM and specifications).  The LED panel doesn’t get much use during the offseason, so I decided to learn how to program it as a clock when it wasn’t functioning as a scoreboard.  The next few blogs will chronicle my journey to build an LED clock that also displays the local weather and current stock market reports.  Let’s start with the basic clock function.

LED samplebase.py

The first step was to figure out how to communicate with the LED panel. Fortunately, the rgbmatrix Python package has a starter code base, samplebase.py. I used this code as my starting point and created a short shell script to pass all of the variables my LED panel required.  The shell script (clock1.sh) looks like this:

sudo python clock1.py --led-rows 32 --led-cols 64 --led-brightness 50 --led-gpio-mapping adafruit-hat --led-slowdown-gpio 2

Yes, the LED matrix must run as root. The main routine of my clock1.py file looks like this:

def main():
    """
    from samplebase.py
    """
    sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..'))

    # parse command line arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("-r", "--led-rows", action="store", help="Display rows. 16 for 16x32, 32 for 32x32. Default: 32", default=32, type=int)
    parser.add_argument("--led-cols", action="store", help="Panel columns. Typically 32 or 64. (Default: 32)", default=32, type=int)
    parser.add_argument("-c", "--led-chain", action="store", help="Daisy-chained boards. Default: 1.", default=1, type=int)
    parser.add_argument("-P", "--led-parallel", action="store", help="For Plus-models or RPi2: parallel chains. 1..3. Default: 1", default=1, type=int)
    parser.add_argument("-p", "--led-pwm-bits", action="store", help="Bits used for PWM. Something between 1..11. Default: 11", default=11, type=int)
    parser.add_argument("-b", "--led-brightness", action="store", help="Sets brightness level. Default: 100. Range: 1..100", default=100, type=int)
    parser.add_argument("-m", "--led-gpio-mapping", help="Hardware Mapping: regular, adafruit-hat, adafruit-hat-pwm" , choices=['regular', 'adafruit-hat', 'adafruit-hat-pwm'], type=str)
    parser.add_argument("--led-scan-mode", action="store", help="Progressive or interlaced scan. 0 Progressive, 1 Interlaced (default)", default=1, choices=range(2), type=int)
    parser.add_argument("--led-pwm-lsb-nanoseconds", action="store", help="Base time-unit for the on-time in the lowest significant bit in nanoseconds. Default: 130", default=130, type=int)
    parser.add_argument("--led-show-refresh", action="store_true", help="Shows the current refresh rate of the LED panel")
    parser.add_argument("--led-slowdown-gpio", action="store", help="Slow down writing to GPIO. Range: 1..100. Default: 1", choices=range(3), type=int)
    parser.add_argument("--led-no-hardware-pulse", action="store", help="Don't use hardware pin-pulse generation")
    parser.add_argument("--led-rgb-sequence", action="store", help="Switch if your matrix has led colors swapped. Default: RGB", default="RGB", type=str)
    parser.add_argument("--led-pixel-mapper", action="store", help="Apply pixel mappers. e.g \"Rotate:90\"", default="", type=str)
    parser.add_argument("--led-row-addr-type", action="store", help="0 = default; 1=AB-addressed panels;2=row direct", default=0, type=int, choices=[0,1,2])
    parser.add_argument("--led-multiplexing", action="store", help="Multiplexing type: 0=direct; 1=strip; 2=checker; 3=spiral; 4=ZStripe; 5=ZnMirrorZStripe; 6=coreman; 7=Kaler2Scan; 8=ZStripeUneven (Default: 0)", default=0, type=int)

    args = parser.parse_args()

    # load command line arguments into options object
    options = RGBMatrixOptions()

    if args.led_gpio_mapping != None:
      options.hardware_mapping = args.led_gpio_mapping
      
    options.rows = args.led_rows
    options.cols = args.led_cols
    options.chain_length = args.led_chain
    options.parallel = args.led_parallel
    options.row_address_type = args.led_row_addr_type
    options.multiplexing = args.led_multiplexing
    options.pwm_bits = args.led_pwm_bits
    options.brightness = args.led_brightness
    options.pwm_lsb_nanoseconds = args.led_pwm_lsb_nanoseconds
    options.led_rgb_sequence = args.led_rgb_sequence
    options.pixel_mapper_config = args.led_pixel_mapper
    
    if args.led_show_refresh:
      options.show_refresh_rate = 1

    if args.led_slowdown_gpio != None:
        options.gpio_slowdown = args.led_slowdown_gpio
        
    if args.led_no_hardware_pulse:
      options.disable_hardware_pulsing = True
    # create matrix object using options
    matrix = RGBMatrix(options = options)

    try:
        print('(C) 2020-2021 MSRoth')
        print('LED clock on 64x32 LED matrix')

        # run clock
        run(matrix)
    except KeyboardInterrupt:
        print("Exiting\n")
        sys.exit(0)

if __name__ == '__main__':
    main()

After parsing the command line arguments, an RGBMatrixOptions object is created to hold the settings and used to create an RGBMatrix object.  The RGBMatrix object is passed to the run() function where all of the fun happens.

Run() Function

Here is the run() function.

def run(matrix):
    """
    Run the clock.
    """
    
    # setup canvas
    canvas = matrix.CreateFrameCanvas()
    
    # fill it with black
    canvas.Fill(0, 0, 0)
    
    # setup the fonts for the clock
    font = graphics.Font()
    font.LoadFont("../rpi-rgb-led-matrix/fonts/5x7.bdf")
    time_font = graphics.Font()
    time_font.LoadFont("../rpi-rgb-led-matrix/fonts/7x13.bdf")
    
    # text will be yellow
    textColor = graphics.Color(255, 235, 59)

    # set initial switch values
    last_switch = datetime.datetime.now()
    show_dow = False
    
    while True:
        
        # get the current time
        now = datetime.datetime.now()
        
        # switch dow and date every 5 sec
        if (now - last_switch).seconds > 5:
            last_switch = datetime.datetime.now()
            if show_dow == True:
                show_dow = False
            else:
                show_dow = True
            
        # display clock    
        if show_dow == False:
            now = datetime.datetime.now()  # so seconds tick
            date_string = now.strftime('%A')
            time_string = now.strftime('%-I:%M %p')
        else:
            date_string = now.strftime('%b %d, %Y')
            time_string = now.strftime('%H:%M:%S')
            
        # calculate element positions
        time_pos_x = int((64 - len(time_string) * 7) / 2)
        date_pos_x = int((64 - len(date_string) * 5) / 2)

        # fill canvas with black
        canvas.Fill(0, 0, 0)
        
        # put elements on canvas
        graphics.DrawText(canvas, time_font, time_pos_x, 16, textColor, time_string)
        graphics.DrawText(canvas, font, date_pos_x, 25, textColor, date_string)
        
        # display the clock
        canvas = matrix.SwapOnVSync(canvas)

The first few actions in the run() function are setup and initialization steps.  It creates a FrameCanvas object the size of the LED panel and fills it with black.  Two font sizes are defined, one for the time (larger) and one for the other text.  The text is also initialized to yellow.  You can easily change this default color using RGB values using the graphics.Color() method.

The infinite while loop keeps the clock running until Ctrl-C is pressed. The now variable is updated with the current date and time every iteration of the loop. The clock changes its display format every 5 seconds. The format switches between the time with AM/PM and the day of the week, and the time with seconds and the full date.

The first if block determines if 5 seconds has elapsed since the last time the display changed. If it has, the last_switch and show_dow variables are updated accordingly.

The next if block simply sets the values of time_string and date_string variables according to the value of the show_dow variable.

The last few lines of the function determine the horizontal spacing for the time_string and date_string, fill the canvas with black (to erase the current time display), and draws the time_string and date_string on the blank canvas.

The final command, matrix.SwapOnVSync(canvas), displays the newly drawn canvas during a vertical refresh of the LEDs.

And here’s what you get.

In part 2 of this blog series, I’ll show you how I integrated a scrolling crawl across the bottom of the screen to display the current weather.

The code for this blog series can be downloaded from GitHub.

One thought on “LED Clock – Part 1

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.