Multifunctionele slimme kamerklok vervolg

,

In PC-Active 346 staat een artikel van Zeno Ottens over een multifunctionele slimme kamerklok die hij heeft gemaakt. De workshop was uitgebreid, waardoor deze niet in zijn geheel in het magazine paste. De rest van het artikel lees je hieronder.

Beschrijving hardware
Omdat de RPi Zero W geen ingebouwde A/D (analoog naar digitaal) of D/A (digitaal naar analoog) converter heeft, is gekozen voor een externe oplossing: de PCF8591, een 8-bits converter die communiceert via het I²C-protocol. De I²C-bus beslaat de lijn SDA (data, geel) en SCL (klok, blauw). De PCF8591 is via I²C-adres 00x48 aan te spreken in de software.

D/A-conversie voor status-led
Via de uitgang A0 van de PCF8591 wordt een spanning aangeboden aan de status-led, via weerstand R2. Hierdoor kan de helderheid en knipperfrequentie van de led softwarematig worden aangepast, afhankelijk van de status of meldingen.

A/D-conversie voor lichtmeting
De LDR levert een analoge spanning die via ingang A1 wordt omgezet naar een digitaal signaal. Dit signaal wordt via I²C aangeboden aan de Raspberry Pi. De software leest deze waarde uit als een maat voor het omgevingslicht. Een hoge waarde = weinig licht (donkere omgeving) en een lage waarde = veel licht (heldere omgeving).

Een PIR-sensor registreert beweging in de omgeving op basis van IR-reflectie. Het uitgangssignaal wordt hoog gemaakt wanneer beweging wordt gedetecteerd. Vervolgens blijft het signaal een in te stellen tijd actief waarna het signaal weer nul wordt. De software in de RPi leest het signaal op de GPIO pin17 (pin 11).

Omdat de pinnen van de I/O poorten van de Rpi 3,3 volt compatible zijn en die van PIR-sensor en van de PCF op 5 volt werken, is er gebruikgemaakt van een level convertor. Hierdoor worden de verschillende spanningen toch goed in- en uitgelezen. De level convertor wordt gevoed met zowel 3,3 volt als 5 volt van de Rpi.

De level-conversie is ook nodig voor de datalijn van het neopixel matrix display. Hiervoor wordt de uitgang GPIO18 (pin12 van de Rpi) gebruikt die 3,3 volt bedraagt. De neopixels willen graag 5 volt!

Als voeding is een LRS 50-5 toegepast. Deze levert 5 volt aan de neopixels en aan de Rpi. De 3,3 volt wordt geleverd door de Rpi.

Beschrijving software
Het aansturen van neopixels is in veel bibliotheken beschreven en voor meerdere programmeeromgevingen geschikt gemaakt. De basis van de software in deze klok is een specifieke Python-library, gebaseerd op de bekende NeoPixel-bibliotheken van Adafruit (https://github.com/adafruit/Adafruit_CircuitPython_framebuf/tree/main/examples). Ieder Python-programma begint met het inlezen van de toegepaste bibliotheken. Voor de kamerklok zijn dat:

import os
import time
import RPi.GPIO as GPIO
import smbus
import requests
import json
import adafruit_framebuf
from neopixel_matrix import NeoPixelMatrix, Color

Voor algemene taken worden os en time geïmporteerd. De wat oudere bibliotheek Rpi. GPIO en smbus worden gebruikt om eenvoudig en overzichtelijk de I2C-bus te kunnen gebruiken voor de PCF8591. De requests- en json-bibliotheek worden voor de communicatie met de database op de website gebruikt. Dan blijft de adafruit_framebuf en de neopixel_matrix-bibliotheek over die samen worden toegepast. Deze zijn te vinden in de githup repository (https://github.com/leosok/micropython_neopixel_matrix en https://github.com/adafruit/micropython-adafruit-bitmap-font?tab=readme-ov-file).

De bibliotheken zijn geschreven voor circuitpython. Voor het gebruik van python3 zijn enkele kleine aanpassingen in de bibliotheek noodzakelijk die ik eenvoudig kon oplossen met hulp van AI (Microsoft Copilot).

Het fontbestand (zie hiervoor) is geplaatst in de directory waar alle programma’s en bibliotheken zijn geplaatst:

font_path = "/home/pi/Progs/font5x8.bin"

Het matrix display is als volgt gedefinieerd:

np_matrix = NeoPixelMatrix(pin=18, width=64, height=8, brightness=0.01, bg_color=Color.BLACK)

Pin=18 is de GPIO pin op de RPi zero (fysieke pin 12), width en height is het aantal pixels in de matrix display voor de breedte en hoogte, de brightness (dus energieverbruik) kan tussen 0 en 1 worden ingesteld en is met 0.01 wel tot een minimum ingesteld. Door black te kiezen als pixelkleur, geeft dit aan dat de pixel gedoofd is.

Om iets op het matrix display te zetten wordt de np_print()’ procedure gebruikt:

#--------------------------------------- melding printen
def np_mprint (s):
 np_matrix.brightness = 0.01 # 0.05 is minimale brithness voor groen!
 np_matrix.text(s, 0, 0, color=Color.WHITE, center = False)
 return

Voordat de leds worden aangestuurd wordt eerst de brightness ingesteld op een (minimaal) niveau:

np_matrix.brightness = 0.01 # 0.05 is minimale brithness voor groen!
np_matrix.text(s, 0, 0, color=Color.WHITE, center = False)

Vervolgens kan de tekst s in kleur white geprint worden. Met center= False begint het eerste karakter op positie (0,0). Nu kan dus al snel de tijd en tekst worden geprint. Voor de definitie van de tijd is een volgende procedure gemaakt. Afhankelijk van de keuze van f wordt de tijd weergegeven in een bepaald format:

# Haal de afzonderlijke tijdcomponenten op uit localtime()
def real_time(f):
 year = time.localtime()[0]
 month = time.localtime()[1]
 day = time.localtime()[2]
 dw = weekdag[time.localtime()[6]]
 hours = time.localtime()[3]
 minutes = time.localtime()[4]
 seconds = time.localtime()[5]

# Formatteer de tijd als een string en maak een keuze in het format wat je wilt zien
if f==0:
 return "{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(year, month, day, hours, minutes, seconds)
elif (f==1):
 return "{:s} {:02d}:{:02d}".format(dw, hours, minutes)
elif (f==2):
 return "{:02d}:{:02d}".format(hours, minutes)
elif (f==3):
 return "{:02}:{:02d}:{:02d}".format(hours, minutes, seconds)
else:
 return ""

Deze procedure levert dus de tekst ‘2025-08-01 12:01:20’ indien f=0 en indien f=3 levert dan de tekst bijvoorbeeld ‘16:32:35’ op. In dit voorbeeld is dit op de klok zichtbaar:

fig4

Prototype in aanbouw, met een vel papier als lichtdiffusor

Structuur van het programma
Het python programma doorloopt continu de volgende loop:

while True:
 # 1. snelle loop
 time.sleep(0.5) # Matig de belasting van de CPU

# 2. display aan of uit
if presence_detected and ((time.time() - last_detected_time ) >= min_on_time):
 last_detected_time=time.time()
 if not GPIO.input(pir_pin): # Geen bewegingmeer
 presence_detected = False
 # Display uitschakelen
 display_on = False
 VerzamelData(data_lijst)
 np_matrix.clear()

# 3. Klokfunctie
If time.time() - t0 >= 10: # Elke 10 seconde uitvoeren
 t0 = time.time()
 np_dprint(data_lijst[0]["tijd"])

 # 4. Database melding
If time.time() - t1 >= 60: # Elke 60 seconde uitvoeren
 t1 = time.time()
 meldingen = filter(GetLastDataUitDatabase())
 np_print (data_lijst[0]["melding"])

Voor deze structuur kan worden gekozen wanneer niet tijd kritische processen plaats vinden (op mille seconden basis). In de snelle loop van de lus kunnen alsnog processen die bijvoorbeeld op mS basis moeten plaatsvinden aangeroepen worden. Vervolgens wordt eens in de min_on_tijd (minimum aan tijd van de display) gekeken of er nog iemand aanwezig is en of dus het display moet aanblijven of mag doven (display_on = False).

Elke 10 seconde wordt de tijd op het display ververst. De tijd wordt ook bijgehouden in de datalijst van het programma (VerzamelData(data_lijst)). In deze lijst worden ook nog andere zaken bijgehouden zoals meldingen en lichtmetingen.

Dan wordt elke 60 seconde gecontroleerd of er een melding van een item uit de database moet worden geprint op het display.

De procedure GetLastDataUitDatabase() haalt de laatst ingevoerde data van de sensoren uit huis uit de database op en kijkt of een grenswaarde wordt overschreden die moet worden gemeld (bv. wordt er elektriciteit geproduceerd door de zonnepanelen, of is het CO2 gehalte in de woonkamer te hoog of is het buiten erg koud).

Het ophalen van data uit een (mysql) database gaat relatief eenvoudig via een request:

# haal data van de website , bijvoorbeeld url=https://www.mysite.nl/lastin.php
response = requests.get(url)
if response.status_code == 200:
 data = response.json() # JSON omzetten naar een Python-dictionary

Op mijn website met de domotica database draait een php script lastin.php met een sql-query dat laatst ingevoerde data uit de verschillende tabellen haalt. Het resultaat wordt geleverd aan de response van het python script. Indien de status van de data ‘oke’ is dan wordt van de response een goed leesbaar json formaat gemaakt. De melding wordt vervolgens met witte karakters op het display geprint. Er zijn enkele figuren weergegeven met verschillende meldingen:

fig5a

Prototype kamerklok op de werkbank, met PIR sensor

fig5b

De kamerklok in de woonkamer met de tijd

fig5c

Tb is de temperatuur buiten

fig5d

Hk is de relatieve vochtigheid in de kamer

np_matrix.text(melding 0, 0, color=Color.WHITE, center = False)

Resultaten
Het hier getoonde ontwerp van een kamerklok is makkelijk na te bouwen. Het volledige programma is verkrijgbaar bij de auteur en is goed gedocumenteerd. Het ontwerp is makkelijk uit te breiden met extra meldingen of alarmeringen met geluid dankzij het gebruik van de Rpi, die dit volledig ondersteunt.

 

'Meld je aan voor de nieuwsbrief'

'Abonneer je nu op een of meerdere van onze nieuwsbrieven en blijf op de hoogte van onze activiteiten!'

Aanmelden