Contrôleur OLED via Python

Bonjour, j’ai un module OLED 0.91" que je contrôle via Python. Je l’utilise dans quelques projets et je désire améliorer certaines choses. Le script du module passe les demandes en créant une image qui envoie au module.

Selon la documentation, garder une image fixe trop longtemps va endommager les pixels du OLED, alors faudrait je me crée un genre d’écran de veille. Je veux garder mon matériel en bon état et en même temps, créer une animation permet de vérifier l’état du RPi (actif, ralenti, fermé ou planté).

Mais c’est la que j’ai besoin d’un coup de main, je visualise un peu mal comment j’arriverais à le faire correctement. C’est que je me sert aussi de l’écran pour afficher des états d’activité. Alors faudra je me crée un service en background qui attend des appels pour afficher le rendu demandé.

Dans mes projets, l’écran « stand by » est toujours une identification projet avec l’adresse réseau pour le joindre. Après quand je lance une action, il va changer avec des détails de l’action. Idéalement, il ne change pas pour le « stand by » sans qu’une commande lui soit faite, bref, il n’y a pas de « timer ».

Alors en premier temps, j’aimerais savoir comment me créer un services qui;

  1. Permet de garder un exécution en arrière plan et recevoir par ligne de commande les requêtes d’affichage
  2. Que la commande soit le plus rapide possible
  3. Que l’animation du mode « stand by » ne ralentisse pas trop le système et permet de laisser le service rapide en réponse (que le « timer » entre les transitions ne soit pas de quoi qui occupe le temps processeur)

Je dois déjà faire des tests pour savoir si le script du module gère mieux le chargement de bitmap ou la création en mémoire pour la rapidité d’affichage, car j’ai un « lag » qui impact la vitesse d’exécution. Mais note, je roule sur des Raspberry Pi Zero WH v1.

Dans mes projets, noter que j’utilise déjà un serveur Web par Python avec un API pour une interface Web. Et je ne sais pas si je passe par des cURL je pourrais faire mes appels, tout en répondant aux critères, car actuellement je n’ai pas de multithread et ça fait que les action « gèle » l’activité le temps qui se complète. (besoin d’optimisation dans mes gestion processus)

Alors bien sur, je ne demande pas de coder pour moi. Mais m’aligner sur quelques bout de code (j’apprend beaucoup par analyse d’exemple) ou des tutoriels sur des projets similaire de services en Python. (Je suis un peu fatigué des recherches sur Google qui aboutisse sur du code sur StackOverflow pour des réponses sur Python 2 ou que des réponses partiels)

Module OLED : https://www.waveshare.com/wiki/0.91inch_OLED_Module
Ma méthode d’installation : RPi OLED 0.91 i2c — Wiki levelKro
Exemple de mon code actuel pour créer un affichage basé sur leur démo, modifié pour usage dynamique par ligne de commande:

#!/usr/bin/python
# -*- coding:utf-8 -*-

import sys
import os
import logging    
import time
import traceback
import getopt
from waveshare_OLED import OLED_0in91
from PIL import Image,ImageDraw,ImageFont
#logging.basicConfig(level=logging.DEBUG)

import socket   
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
IPAddr=s.getsockname()[0]
path=os.path.dirname(os.path.realpath(__file__))

argv = sys.argv[1:]

opts, args = getopt.getopt(argv,"h:t:i:n:x:")

out_title="RPi-QL"
out_info=""
out_xtra=""
out_noimg=False

for opt, arg in opts:
    if opt == '-h':
        print("*** Help of OLED 0.91 Display")
        print('Syntax: oled.py -t "title text" -i "info text" -n "extra info text"')
        
        sys.exit()
    elif opt in ("-t"):
        out_title = arg
    elif opt in ("-i"):
        out_info = arg.replace(":ipaddr:",IPAddr)
    elif opt in ("-n"):
        out_xtra = arg.replace(":ipaddr:",IPAddr)
    elif opt in ("-x"):
        out_noimg = True
    else:
        print("Found: "+str(opt))


disp = OLED_0in91.OLED_0in91()
# Initialize library.
disp.Init()

# Clear display.
disp.clear()

# Create blank image for drawing.
image1 = Image.new('1', (disp.width, disp.height), "WHITE")
draw = ImageDraw.Draw(image1)
font1 = ImageFont.truetype("DejaVuSansMono.ttf", 12)
font2 = ImageFont.truetype("DejaVuSansMono.ttf", 11)
font3 = ImageFont.truetype("DejaVuSansMono.ttf", 9)

if(out_noimg):
    draw.text((0,0), out_title, font = font1, fill = 0)
    draw.text((2,12), out_info, font = font2, fill = 0)
    draw.text((2,23), out_xtra, font = font3, fill = 0)
else:
    img = Image.open(path+"/logo.jpg")
    image1.paste(img, (0,0))
    draw.text((32,0), out_title, font = font1, fill = 0)
    draw.text((33,12), out_info, font = font2, fill = 0)
    draw.text((33,23), out_xtra, font = font3, fill = 0)
    image1=image1.rotate(0) 

disp.ShowImage(disp.getbuffer(image1))

Bordel que je n’aime pas ce qu’internet devient… que des tutos et exemples en vidéo… plus personne ne sait lire ?

JUste pour trouver comment avoir le temps en format Unix, c’est fou comme Google ne cherche plus aussi bien, j’avais que des résultats pour convertir le Unix en format humain.

Bon, j’ai pondu une solution, ce n’est pas totalement ce que je voulais mais c’est mieux que rien.

J’ai été avec la solution du « While » et des fichier « ini ». Il scan un dossier, si un fichier « .ini » est présent, il le charge, le traite et l’efface. Dans ce fichier je peux donner des directives.

exemple.ini

[info]
lines = 3
icon = False
icon_posx = 0
icon_posy = 0
mintime = 5

[line1]
type = 1
text = Main Title text
posx = 0
posy = 0

[line2]
type = 2
text = Sub Title text
posx = 0
posy = 12

[line3]
type = 3
text = Simple text demo
posx = 0
posy = 23

Le fichier oled.py a été refait

#!/usr/bin/python
# -*- coding:utf-8 -*-
from waveshare_OLED import OLED_0in91
from PIL import Image, ImageDraw, ImageFont
import sys, os, time, datetime
import configparser
import socket
from datetime import datetime
#
# par Mathieu Légaré <levelkro@yahoo.ca>
#
# v1.2.1218
#

config=False    
def loadConfig():
    global config
    config = configparser.ConfigParser()
    config.read('config.ini', encoding='utf-8')
    
def getConfig(value):
    global config
    try:
        return config['oled'][value]
    except:
        return ""
    
def getIP():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    return s.getsockname()[0]

def getTimeNow():
    return datetime.datetime.timestamp(presentDate)*1000

def getFontFromType(id):
    global font1, font2, font3
    if int(id) == 1: return font1
    elif int(id) == 2: return font2
    elif int(id) == 3: return font3
    else: return False
    
def pasteText(txt):
    txt=txt.replace(":ipaddr:",getIP())
    return str(txt)
    
path=os.path.dirname(os.path.realpath(__file__))

loadConfig()

# Initialize library.
disp = OLED_0in91.OLED_0in91()
disp.Init()

font1 = ImageFont.truetype(getConfig("font1_font"), int(getConfig("font1_size"))) # Main title
font2 = ImageFont.truetype(getConfig("font2_font"), int(getConfig("font2_size"))) # Sub title
font3 = ImageFont.truetype(getConfig("font3_font"), int(getConfig("font3_size"))) # Text

last=False
rotate=0
rotate_last="1"
while True:
    display=False
    for x in sorted(os.listdir("oled"), reverse=True):
        if x.endswith(".oled"):
            oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
            oled_draw = ImageDraw.Draw(oled_image)   
            try:
                display = configparser.ConfigParser()
                display.read("oled/"+x, encoding='utf-8')
                i=1         
                while i<=int(display['info']['lines']):
                    oled_draw.text((int(display['line'+str(i)]['posx']),int(display['line'+str(i)]['posy'])), pasteText(display['line'+str(i)]['text']), font = getFontFromType(display['line'+str(i)]['type']), fill = 0)
                    i=int(i + 1)
                #oled_image=oled_image.rotate(0)       
                disp.clear()  
                disp.ShowImage(disp.getbuffer(oled_image))
                if(display['info']['mintime'] != "False" or display['info']['mintime'] != "0"):
                    time.sleep(int(display['info']['mintime']))
            except:
                print("Error with file "+str(x))
            print("End, delete")
            os.remove("oled/"+x)
            rotate=0
    if(display is False and last is not True and rotate_last=="2"):
        oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
        oled_draw = ImageDraw.Draw(oled_image)  
        img = Image.open("oled/oled_standby.jpg")
        oled_image.paste(img, (0,0))
        #oled_image=oled_image.rotate(0)      
        disp.clear()     
        disp.ShowImage(disp.getbuffer(oled_image))
        rotate=datetime.now().timestamp()
        last=True
    if(display is False and last is not True and rotate_last=="1"):
        oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
        oled_draw = ImageDraw.Draw(oled_image)  
        if(config['oled']['icon']!=""):
            img = Image.open(config['oled']['icon'])
            oled_image.paste(img, (int(config['oled']['icon_posx']),int(config['oled']['icon_posy'])))
        if(config['oled']['name']!=""):
            oled_draw.text((int(config['oled']['name_posx']),int(config['oled']['name_posy'])),pasteText(config['oled']['name']), font=getFontFromType(int(config['oled']['name_type'])), fille=0)
        if(config['oled']['info1']!=""):
            oled_draw.text((int(config['oled']['info1_posx']),int(config['oled']['info1_posy'])),pasteText(config['oled']['info1']), font=getFontFromType(int(config['oled']['info1_type'])), fille=0)
        if(config['oled']['info2']!=""):
            oled_draw.text((int(config['oled']['info2_posx']),int(config['oled']['info2_posy'])),pasteText(config['oled']['info2']), font=getFontFromType(int(config['oled']['info2_type'])), fille=0)
        #oled_image=oled_image.rotate(0)      
        disp.clear()     
        disp.ShowImage(disp.getbuffer(oled_image))
        rotate=datetime.now().timestamp()
        last=True
    if(int(datetime.now().timestamp() - rotate) >= 10):
        last=False
        if(rotate_last=="1"): rotate_last="2"
        else: rotate_last="1"

Dans le « config.ini »

[oled]
name = RPi-QL
name_posx = 33
name_posy = 0
name_type = 1
icon = logo.jpg
icon_posx = 0
icon_posy = 0
info1 = Ready
info1_type = 2
info1_posx = 33
info1_posy = 12
info2 = IP: :ipaddr:
info2_type = 3
info2_posx = 33
info2_posy = 23
font1_font = DejaVuSansMono.ttf
font1_size = 12
font2_font = DejaVuSansMono.ttf
font2_size = 11
font3_font = DejaVuSansMono.ttf
font3_size = 9

La il suffit d’ajouter dans les script Pythons que j’utilise une fonction pour créer les « ini » des demandes au besoin. Vu le « While », faut j’ajoute un « time.sleep() » pour que l’affichage soit visible un temps minimum.

Dans mon idée de base, j’aurai préféré un « idle mode » ou il traite la demande et tombe « idle » en attente d’une autre. Bref je ne suis pas totalement satisfait de cette solution. Si vous pensez à de quoi de mieux…

J’ai réécrit une partie du code de oled.py pour optimiser l’affichage et prendre en charge un « idle » plus facilement. Ç ressemble plus à ce que je pensait comme processus.

#!/usr/bin/python
# -*- coding:utf-8 -*-

from waveshare_OLED import OLED_0in91
from PIL import Image, ImageDraw, ImageFont
import sys, os, time, datetime
import configparser
import socket
from datetime import datetime

#
# Étiquetteuse Brother-QL
# par Mathieu Légaré <levelkro@yahoo.ca>
#
# v1.2.1217
#

config=False    
def loadConfig():
    global config
    config = configparser.ConfigParser()
    config.read('config.ini', encoding='utf-8')
    
def getConfig(value):
    global config
    try:
        return config['oled'][value]
    except:
        return ""
    
def getIP():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    return s.getsockname()[0]

def getTimeNow():
    return datetime.datetime.timestamp(presentDate)*1000

def getFontFromType(id):
    global font1, font2, font3
    if int(id) == 1: return font1
    elif int(id) == 2: return font2
    elif int(id) == 3: return font3
    else: return False
    
def pasteText(txt):
    txt=txt.replace(":ipaddr:",getIP())
    return str(txt)
    
path=os.path.dirname(os.path.realpath(__file__))

loadConfig()

# Initialize library.
disp = OLED_0in91.OLED_0in91()
disp.Init()

font1 = ImageFont.truetype(getConfig("font1_font"), int(getConfig("font1_size"))) # Main title
font2 = ImageFont.truetype(getConfig("font2_font"), int(getConfig("font2_size"))) # Sub title
font3 = ImageFont.truetype(getConfig("font3_font"), int(getConfig("font3_size"))) # Text

mode="standby"
standby_show=1
standby_last=0
while True:
    display=False
    for x in sorted(os.listdir("oled"), reverse=False):
        if x.endswith(".oled"):
            oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
            oled_draw = ImageDraw.Draw(oled_image)   
            try:
                display = configparser.ConfigParser()
                display.read("oled/"+x, encoding='utf-8')
                mode=display['info']['mode']
                if(mode=="edit"):
                    i=1         
                    while i<=int(display['info']['lines']):
                        oled_draw.text((int(display['line'+str(i)]['posx']),int(display['line'+str(i)]['posy'])), pasteText(display['line'+str(i)]['text']), font = getFontFromType(display['line'+str(i)]['type']), fill = 0)
                        i=int(i + 1)
                    #oled_image=oled_image.rotate(0)       
                    disp.clear()  
                    disp.ShowImage(disp.getbuffer(oled_image))
                    if(display['info']['mintime'] != "False" or display['info']['mintime'] != "0"):
                        time.sleep(int(display['info']['mintime']))
                
            except:
                print("Error with file "+str(x))
            os.remove("oled/"+x)
    if(mode=="standby" and int(datetime.now().timestamp() - standby_last) >= 10):
        oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
        oled_draw = ImageDraw.Draw(oled_image) 
        if(standby_show==1):
            #Standby with custom display
            standby_show=2
            if(config['oled']['icon']!=""):
                img = Image.open(config['oled']['icon'])
                oled_image.paste(img, (int(config['oled']['icon_posx']),int(config['oled']['icon_posy'])))
            if(config['oled']['name']!=""):
                oled_draw.text((int(config['oled']['name_posx']),int(config['oled']['name_posy'])),pasteText(config['oled']['name']), font=getFontFromType(int(config['oled']['name_type'])), fille=0)
            if(config['oled']['info1']!=""):
                oled_draw.text((int(config['oled']['info1_posx']),int(config['oled']['info1_posy'])),pasteText(config['oled']['info1']), font=getFontFromType(int(config['oled']['info1_type'])), fille=0)
            if(config['oled']['info2']!=""):
                oled_draw.text((int(config['oled']['info2_posx']),int(config['oled']['info2_posy'])),pasteText(config['oled']['info2']), font=getFontFromType(int(config['oled']['info2_type'])), fille=0)
        else:
            standby_show=1
            # Default image for standby, force "screensaver" effect
            img = Image.open("oled/oled_standby.jpg")
            oled_image.paste(img, (0,0))
        #oled_image=oled_image.rotate(0)      
        disp.clear()     
        disp.ShowImage(disp.getbuffer(oled_image))
        print("Display standby")
        standby_last=datetime.now().timestamp()

Avec cette version, je prend en charge un mode « standby » et « edit ».

  • Le « edit » permet de faire l’affichage personnalisé. Alors pas de changement pour le reste du display
  • Le « standby » agit comme écran de veille. Il doit être appelé. Alors le changement demandé permet de créer un effet de « idle ».

Par exemple, je lance une impression, le processus du mon projet doit générer une image pour l’envoyer à l’imprimante. Avec mon ancien code, le fait de lancer sur appel le changement de display ajoute un « lag », car je dois attendre la fin de la création pour suivre le script.

Avec cette méthode (que j’implante bientôt dans le projet pour valider), vu que le processus de oled est déjà chargé, mon script de projet n’a qu’a créer une fichier ini dans le dossier du oled. À chaque action je crée ce fichier et je peux dont « buffered » les affichages.

Je supporte le 0, qui demande pas de temps minimum, ce qui peut permettre d’afficher et passer au buffer suivant rapidement, et profiter sinon du « idle ». Avec le temps minimum je peux ainsi forcer l’affichage d’un message important.

Une fois la tâche complété, il peut indiquer, en créant un « ini », de retourner en mode « standby ». Alors je peux garder des états plus longtemps. Par exemple je pourrais créer un script de MaJ du système, avant le « apt », je crée le « ini » avec une information de « Mise à jour en cours… » avec un min time de 5 sec. Je lance la commande, après je passe 2 autres création de « ini »; l’un avec « MaJ terminé » et un min time de 3 sec, et le second avec un mode « standby ». Ce qui fait que peut importe le nombre de mise à jour, l’information sera affiché. Pendant au moins 5sec l’affichage est « protégé ».

Le seul hic est que si une action interfère durant un idle d’une autre action (hors du temps minimum), il ne reviendra pas à cette état. Par exemple si pendant le script de MaJ je fait une impression avec mon projet. Il réaffichera le mode Standby à la fin de la commande d’impression, même si la MaJ est toujours active.

Le moyen de résoudre ça serais de créer un moyen de « monitorer » la tâche qui demande l’affichage, comme un processus, que le « ini » défini. Mais ça reste limité comme problème et je repousse sont implentation.