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”