Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Ioan Macovei
Published

Office Clock using RaspberryPi

Digital office clock, also displaying interior and exterior temperatures, made with RaspberryPi Zero W and a 16x16 Led Matrix Screen.

IntermediateFull instructions provided6 hours3,725
Office Clock using RaspberryPi

Things used in this project

Hardware components

Raspberry Pi Zero Wireless
Raspberry Pi Zero Wireless
×1
OKYSTAR OKY3525-1 16x16 Led Matrix Screen
Warning! Comes with no datasheet or instructions. Visit my Github repo to learn how to use it: https://github.com/thirdless/LedMatrix
×1
PIR Sensor, 7 m
PIR Sensor, 7 m
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
Resistor 4.75k ohm
Resistor 4.75k ohm
×1
Male/Female Jumper Wires
Male/Female Jumper Wires
×3
Female/Female Jumper Wires
Female/Female Jumper Wires
×13
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×1

Story

Read more

Schematics

Schematic

16x16 Led Matrix Reverse Engineered Schematic

Since the screen came from China without any datasheets, diagrams or instructions, and never found anything about it on the internet, I tried to reverse engineer it using the 74HC595 and 74HC138 datasheets and a multimeter.

Timing Diagram for the display

Example of signals if you want to turn on led 1, 3, 4, 5, 6, 7, 9, 10, 16 on the 3rd row of the display.

Code

Main Process - Python

Python
This is the process who manages all the modules and functions and displays on the screen
from time import sleep, time
import threading
import os
from datetime import datetime
import RPi.GPIO as GPIO
import screen
import chars
import routine

ENVIRONMENT_NAME = "WATCH_ROUTINE_DATA"
UPDATE_TRIGGER = "/var/www/UPDATE_DATA"

D = 11
C = 13
B = 15
A = 19
G = 21
DI = 23
CLK = 33
LAT = 32

PIR = 7

clock_view = True

def place_dashes(view, top):
    #show placeholders if the requested weather data is not available
    view = chars.place_character(view, "-", chars.TYPE_SMALL, {"top": top, "left": 9})
    view = chars.place_character(view, "-", chars.TYPE_SMALL, {"top": top, "left": 13})
    return view

def weather_digits(view, text, top):
    digits = abs(int(text))

    #limiting the number of characters shown
    if digits < 10:
        digits = "0" + str(digits)
    elif digits > 99:
        digits = "99"
    else:
        digits = str(digits)

    #show numbers
    view = chars.place_character(view, int(digits[0]), chars.TYPE_SMALL, {"top": top, "left": 9})
    view = chars.place_character(view, int(digits[1]), chars.TYPE_SMALL, {"top": top, "left": 13})
    return view


def render_view(env_data):
    #screen off
    view = [1] * (screen.LINES * screen.LINES)

    #if there is no available data turn the screen off
    if env_data.find(";") == -1 or len(env_data.split(';')) != 3:
        return view

    #parsing the data
    data = env_data.split(';')

    #first view, the clock
    if clock_view:
        data = data[0]        
        if data.find("=") == -1:
            data = time()
        else:
            data = data.split('=')
            data = time() - int(float(data[0])) + int(float(data[1]))
        data = datetime.fromtimestamp(data).strftime("%H%M")

        for i in range(0, 4):
            view = chars.place_character(view, int(data[i]), chars.TYPE_BIG, {"top": 9 * int(i / 2), "left": 8 * (i % 2)})
    #second view, the temperatures 
    else:
        weather = data[1]
        temperature = data[2]

        #interior temperature, given by the sensor
        view = chars.place_character(view, "i", chars.TYPE_SMALL, {"top": 1, "left": 1})
        #if cannot get sensor data, show placeholder
        if temperature != "":
            if int(temperature) < 0:
                view = chars.place_character(view, "-", chars.TYPE_SMALL, {"top": 2, "left": 5})
            view = weather_digits(view, temperature, 2)
        else:
            view = place_dashes(view, 2)

        #exterior temperature, given by the internet
        view = chars.place_character(view, "o", chars.TYPE_SMALL, {"top": 9, "left": 1})
        if weather != "":
            if int(weather) < 0:
                view = chars.place_character(view, "-", chars.TYPE_SMALL, {"top": 10, "left": 5})
            view = weather_digits(view, weather, 10)
        else:
            view = place_dashes(view, 9)

    return view

def routine_thread():
    #creating new thread for routine checks, to check if the time or the temperatures changed meanwhile
    thread = threading.Thread(target=routine.main, args=())
    thread.start()

def main():
    global clock_view

    #led matrix class init
    led = screen.LEDMatrix(D, C, B, A, G, DI, CLK, LAT)

    #init the pir sensor pins
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(PIR, GPIO.IN)

    #init data get from the routine process and the variable which contains the message
    env_data = ""
    screen_data = [1] * (screen.LINES * screen.LINES)

    #taking the data from the routine process, blocking execution only on initialization
    if ENVIRONMENT_NAME in os.environ:
        env_data = os.environ[ENVIRONMENT_NAME]

    #initial rendering of the screen
    screen_data = render_view(env_data)

    #time variables init
    last_env_check = time() #checking environment variable with contains routine data
    last_routine = time() #checking time since last routine check
    last_view_change = time() #alternating the two views, clock and temperatures
    last_motion = time() #time passed since last motion detection
    last_motion_check = time() #time since last check of pir sensor

    active_screen = True #variable which sets the screen on if it detects motion and off if not
    screen_off_view = [1] * (screen.LINES * screen.LINES)

    try:
        while True:
            active_screen = True
            current_time = time()

            if current_time - last_motion_check > 1: #checking the pir sensor every second
                last_motion_check = current_time
                if GPIO.input(PIR) == True:
                    last_motion = current_time

            if current_time - last_motion > 10: # 10 seconds - max time to keep the screen on even if no motion is detected
                active_screen = False #turning the screen off if no motion is detected for more than 10 seconds

            if current_time - last_env_check > 2.5: # 2.5 seconds - checking the environment variable for changes
                last_env_check = current_time
                if env_data != os.environ[ENVIRONMENT_NAME]: # if something changed, change the displayed info
                    env_data = os.environ[ENVIRONMENT_NAME]
                    screen_data = render_view(env_data)

                if os.path.exists(UPDATE_TRIGGER): #if the settings changed from the web server, refresh the info
                    os.remove(UPDATE_TRIGGER)
                    last_routine = current_time
                    routine_thread()

            if active_screen and current_time - last_routine > 300: # 5 minutes - routine checkings - resync time and weather with the APIs
                last_routine = current_time
                routine_thread()

            if active_screen and current_time - last_view_change > 6: # 6 seconds - alternating the views
                last_view_change = current_time
                clock_view = not clock_view # changing the view
                screen_data = render_view(env_data) # refresh the screen

            if active_screen:
                led.Draw(screen_data) # if it's on, draw the data
            else:
                led.Draw(screen_off_view) # ... else, draw an empty screen

            sleep(0.001)

    except KeyboardInterrupt:
        GPIO.cleanup()
        exit(0)

main()

PHP Apache Webpage

PHP
PHP source code for the Apache Webserver
<?php

    $file_path = "/var/www/settings.json";

    //error message for API pages
    function send_error($message){
        http_response_code(403);
        echo $message;
        die();
    }

    //function for settings changes
    function change_settings($param, $value){
        global $file_path;
        
        //object which stores the desired settings
        $content = (object)[];

        //loading the already existent settings to be overwritten by the new changes
        if(file_exists($file_path))
            $content = json_decode(file_get_contents($file_path));

        //changing if the value exists, and if it doesn't, the property gets deleted
        if($value == "")
            unset($content->$param);
        else
            $content->$param = $value;

        //writing the values back into the file
        file_put_contents($file_path, json_encode($content));

        //alternative and inefficient - instant execution of the routine function to modify the shown values on the screen
        // exec("sudo python3 /home/pi/watch/routine.py 2>&1", $output);
        // print_r($output);

        //creating a new empty file to let the main process know that there were changes to the configuration file
        file_put_contents("/var/www/UPDATE_DATA", "");

        //exiting php script
        die();
    }

    $type = null;

    //checking if the typed location exists and return the API page status
    function check_weather($loc){
        @file_get_contents("http://wttr.in/" . urlencode($loc) . "?format=%t", false, stream_context_create(["http" => ["ignore_errors" => true]]));

        //checking http header and returning the page status
        if(!empty($http_response_header) && count($http_response_header) > 1){
            $parts = explode(" ", $http_response_header[0]);
            return (int)$parts[1];
        }
        else{
            //returning 404 error if the API cannot be reached
            return 404;
        }
    }

    //choosing the setting type
    //syncronization setting, just storing the boolean option
    if(isset($_POST["sincronizare"]))
        $type = "sincronizare";
    //location setting, checking if the requested location exists
    else if(isset($_POST["locatie"])){
        $check = check_weather($_POST["locatie"]);

        if($check == 200)
            $type = "locatie";
        else
            send_error("Eroare: locatia selectata nu exista");
    }
    //custom time setting, checking if the requested time is correct and also storing a system timestamp - chosen timestamp pair, for difference calculus
    else if(isset($_POST["ora"]) && strpos($_POST["ora"], ";") !== false){
        $parts = explode(";", $_POST["ora"]);
        $parts[0] = $parts[0] == "" ? 0 : $parts[0];
        $parts[1] = $parts[1] == "" ? 0 : $parts[1];
        $custom = strtotime(date("j F Y") . " " . $parts[0] . ":" . $parts[1]);

        if($custom === false || (int)$parts[0] < 0 || (int)$parts[1] < 0)
            send_error("Eroare: timpul selectat este unul incorect");

        change_settings("ora", time() . ";" . $custom);
    }

    //changing the settings by type, one at a time
    if($type != null)
        change_settings($type, $_POST[$type]);

    //getting the existing settings to be shown on the webpage
    if(file_exists($file_path)){
        //reading the file
        $settings = json_decode(file_get_contents($file_path));

        //getting the simple settings
        if(isset($settings->sincronizare))
            $sync = $settings->sincronizare;
        if(isset($settings->locatie))
            $location = $settings->locatie;
        
        //calculating the custom time, the sum of the current system timestamp and the difference between the two values stored as a pair
        if(isset($settings->ora) && strpos($settings->ora, ";") !== false){
            $diff = time() - (int)(explode(";", $settings->ora)[0]);
            $custom = (int)(explode(";", $settings->ora)[1]) + $diff;
            $custom = explode(";", date("G;i", $custom));
            $hours = $custom[0];
            $minutes = $custom[1];
        }
    }

    //checking the time sync checkbox
    $sincronizare = isset($sync) && $sync == "true";

?>
<!DOCTYPE html>
<html>
    <head>
        <title>Office clock - SM Project</title>
        <meta charset="utf-8"/>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Montserrat"/>
        <link rel="stylesheet" href="index.css"/>
    </head>
    <body>
        <div class="main">
            <h1>Office clock - SM Project</h1>
            <div>
                <span>Sync clock with the internet:</span>
                <label class="ceas_internet">
                    <input type="checkbox" name="ceas_internet" <?= $sincronizare ? "checked" : "" ?>>
                    <div class="check"></div>
                </label>
            </div>
            <div class="set <?= $sincronizare ? "disabled" : "" ?>">
                <span>Set the time:</span>
                <input type="number" name="ceas_ora" placeholder="H" value="<?= isset($hours) ? htmlspecialchars($hours) : "" ?>">
                <input type="number" name="ceas_minut" placeholder="M" value="<?= isset($minutes) ? htmlspecialchars($minutes) : "" ?>">
            </div>
            <div>
                <span>Weather location:</span>
                <input type="text" name="ceas_locatie" placeholder="Current location by IP" value="<?= isset($location) ? htmlspecialchars($location) : "" ?>">
            </div>
        </div>
        <script src="index.js"></script>
    </body>
</html>

Routine module

Python
This is the module which gets the informations from the sensors and the internet regularly
import os
import json
import requests
import urllib
import time
import Adafruit_DHT
import RPi.GPIO as GPIO

#CONFIG_PATH = os.path.dirname(os.path.realpath(__file__)) + "/settings.json"
CONFIG_PATH = "/var/www/settings.json"
DHT11BCM = 18 #DHT11 DATA GPIO TYPE PIN
DHT11BOARD = 12 #DHT11 DATA BOARD TYPE PIN

#internet request function
def request(url):
    try:
        response = requests.get(url, timeout=2)
    except:
        return False

    #logging
    print("Cerere la " + url + " cu raspunsul " + str(response.status_code))

    if response.status_code >= 200 and response.status_code < 300:
        #return the response if the request is successful
        return response.text
    else:
        #returning False if something wrong happened
        return False

def find_weather(location):
    #request to wttr.in to get informations about chosen location
    res = request("http://wttr.in/" + urllib.parse.quote(location, safe='') + "?format=%t")

    if res is False or len(res) > 10:
        #if the request was unsuccessful
        return ""
    else:
        #parsing the number before the special character
        return res.split('°')[0]

def get_temperature():
    #reading temperature data from the DHT11, 5 retries, 0.5 delay between retries
    h, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, DHT11BCM, 5, 0.5)

    #logging
    print("Temperatura citita de la senzor " + str(temperature))

    #returning temperature if the reading was correct
    if temperature is not None:
        return str(int(temperature))
    else:
        return ""

def get_time():
    #reading the time from the ntp server
    res = request("http://worldtimeapi.org/api/ip")
    #storing the system timestamp
    unix = time.time()

    if res is not False:
        #server response - json parsing
        content = json.loads(res)
        
        if "unixtime" in content:
            unix = int(float(content["unixtime"]))

    #sending the system timestamp - ntp server response pair, to get the offset between the two
    return str(int(time.time())) + "=" + str(unix)


def main():
    clock = ""
    weather = ""
    temperature = ""

    #init pins for DHT11
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(DHT11BOARD, GPIO.IN)

    #getting the interior temperature
    temperature = get_temperature()

    #if configuration exists, displaying with the current settings
    if os.path.exists(CONFIG_PATH):
        #reading config
        config = open(CONFIG_PATH, "r", encoding="utf-8")
        config = json.loads(config.read())
        
        #if the weather location is specified
        if "locatie" in config:
            weather = find_weather(config["locatie"])
        else:
            weather = find_weather("")

        #if the internet sync is on or a custom time is specified
        if "sincronizare" not in config or ("sincronizare" in config and config["sincronizare"] == "true"):
            clock = get_time()
        elif "ora" in config and config["ora"].find(";") != -1:
            parts = config["ora"].split(';')
            #sending the system - custom time pair
            clock = parts[0] + "=" + parts[1]
    #if configuration doesn't exist, display with default settings
    else:
        weather = find_weather("")
        clock = get_time()

    #sending data to environment variable for main thread processing
    os.environ["WATCH_ROUTINE_DATA"] = clock + ";" + weather + ";" + temperature

    #logging
    print(os.environ["WATCH_ROUTINE_DATA"])


main()

Screen module

Python
This is the modules which manages the signals sent to the screen and facilitates the display process.
import RPi.GPIO as GPIO
from time import sleep

LINES = 16

class LEDMatrix:

    #matrix rotation, changing the screen orientation from vertical to horizontal
    def rotate_matrix(self, m):
        return [m[j * LINES + i] for i in range(LINES - 1, -1, -1) for j in range(LINES)]

    def Delay(self, timp):
        #sleep(timp / 100000000.0) # 1 timp = ~ 10 ns - alternative option with longer delay, requested from the OS

        #NOP for n number of cycles
        n = 4
        i = 0
        while i < timp * n:
            i += 1

    def __LineSelect(self, line):
        #selecting the current line for the LED matrix, transforming from decimal to binary
        if line >= LINES:
            return

        b = [0] * 4

        for i in range(0, 4):
            b[i] = line >> i
            b[i] = b[i] & 0x1

        GPIO.output(self.__D, b[3])
        GPIO.output(self.__C, b[2])
        GPIO.output(self.__B, b[1])
        GPIO.output(self.__A, b[0])


    def Draw(self, array):
        #from vertical to horizontal
        array = self.rotate_matrix(array)

        #G signal turns the whole screen off, we don't want this
        GPIO.output(self.__G, 0)

        for line in range(0, LINES):
            #setting the signals on low
            GPIO.output(self.__LAT, 0) #latch
            GPIO.output(self.__CLK, 0) #clock
            
            self.__LineSelect(line)

            for column in range(LINES - 1, -1, -1):
                self.Delay(1)

                #sending the current bit on the led
                GPIO.output(self.__DI, array[line * LINES + column])

                #clock signal
                self.Delay(1)
                GPIO.output(self.__CLK, 1)

                self.Delay(1)
                GPIO.output(self.__CLK, 0)

            #latch signal
            GPIO.output(self.__LAT, 1)
            self.Delay(1)


    def __init__(self, D, C, B, A, G, DI, CLK, LAT):
        self.__D = D
        self.__C = C
        self.__B = B
        self.__A = A
        self.__G = G
        self.__DI = DI
        self.__CLK = CLK
        self.__LAT = LAT

        #pins configuration
        GPIO.setmode(GPIO.BOARD)

        GPIO.setup(D, GPIO.OUT)
        GPIO.setup(C, GPIO.OUT)
        GPIO.setup(B, GPIO.OUT)
        GPIO.setup(A, GPIO.OUT)
        GPIO.setup(G, GPIO.OUT)
        GPIO.setup(DI, GPIO.OUT)
        GPIO.setup(CLK, GPIO.OUT)
        GPIO.setup(LAT, GPIO.OUT)

Characters module

Python
This is the module which manages the loading and placing the character on a given matrix
import os
import screen

BIG_DIMENSIONS = {"width": 8, "height": 7}
SMALL_DIMENSIONS = {"width": 3, "height": 5}

big_characters = {}
small_characters = {}

TYPE_BIG = 1
TYPE_SMALL = 2

#reading the character from file on init
def get_matrix(path, dimensions):
    filename = os.path.dirname(os.path.abspath(__file__)) + path
    array = [1] * (dimensions["width"] * dimensions["height"])
    arrayIndex = 0
    
    if os.path.exists(filename):
        file = open(filename, "r", encoding="utf-8")
        lines = file.read().split('\n')

        #reading each line
        for i in range(0, len(lines)):
            columns = lines[i].split(' ')

            #reading every value on the line
            for j in range(0, len(columns)):
                if columns[j] != "":
                    #int to string casting, and if it is impossible, turn the led off
                    try:
                        array[arrayIndex] = int(columns[j])
                    except:
                        array[arrayIndex] = 1

                    #moving to the next stored bit
                    arrayIndex += 1

                #returning from the function if the values count is bigger than the char dimension
                if arrayIndex >= (dimensions["width"] * dimensions["height"]):
                    break

            if arrayIndex >= (dimensions["width"] * dimensions["height"]):
                break

    return array

def place_character_intern(parent, character, dimensions, position):
    index = 0
    #replacing the lines with the character values
    for i in range(position["top"], position["top"] + dimensions["height"]):
        for j in range(position["left"], position["left"] + dimensions["width"]):
            parent[i * screen.LINES + j] = character[index]
            index += 1

    return parent

#function to place the char on the given position by type - BIG / SMALL
def place_character(parent, character, type, position):
    dimensions = BIG_DIMENSIONS
    array = big_characters

    if type == TYPE_SMALL:
        dimensions = SMALL_DIMENSIONS
        array = small_characters
    elif type == TYPE_BIG:
        pass
    else:
        return

    return place_character_intern(parent, array[character], dimensions, position)

#module initialization
def init():
    #storing the numbers
    for i in range(0, 10):
        big_characters[i] = get_matrix("/big_numbers/" + str(i), BIG_DIMENSIONS)
        small_characters[i] = get_matrix("/small_numbers/" + str(i), SMALL_DIMENSIONS)

    # and the special characters
    small_characters["o"] = get_matrix("/small_numbers/o", SMALL_DIMENSIONS)
    small_characters["i"] = get_matrix("/small_numbers/i", SMALL_DIMENSIONS)
    small_characters["-"] = get_matrix("/small_numbers/dash", SMALL_DIMENSIONS)

init()

Github Repository for full code sources

Credits

Ioan Macovei

Ioan Macovei

1 project • 1 follower

Comments