Principe d'affichage OLED

Bonjour, j’ai eu a travailler avec de petits écran OLED, des 128x32 et des 128x64 et j’ai du trouver un moyen de rendre facile al gestion du rendu. Ici j’expose un concept que j’ai du analyser et faire de zéro (ou presque). C’est rien de « nouveau », mais aucun script « prêt à l’emploi » n’est disponible, alors il faut créer par soit même. C’est dont le résultat actuel de mes recherches.

Le problème

Les modules OLED sont de simple affichage dynamique programmable, au contraire d’une sortie vidéo standard, c’est purement la programmation des commandes qui peuvent changer le rendu. Les modules sont conçu pour afficher de simple informations.

Les demos fournis montre leur usage pour un affichage scripté, le chargement des modules requis cause un délais non désiré entre la demande et le rendu. Sous Python, l’usage est lent « sur demande ». Aucun script explique comment rendre fluide l’affichage et en optimise ce dernier.

Il y a également la différence entre un GUI traditionnel et celui par rendu par ces OLED. Dans une conception d’un GUI d’une application, il est possible de retrouver des éléments choisis par l’utilisateur par « questionnement » du champs concerné.

Les solutions trouvés

J’ai trouvé deux moyens d’afficher un rendu fluide, rapide et pratique. L’un est pour un usage « Passif », soit de simplement afficher des messages, l’autre est « actif », car l’utilisateur va interagir à ce qu’il voit.

1 - La solution « passive »

Cette solution permet d’inclure le support d’un OLED sur un projet facilement. Elle consiste à créer un module Python qui s’occupe de l’affichage et qui attend des ordres d’affichage, qui lui sont envoyé via un fichier créé. Ce fichier à toutes les consignes pour l’affichage.

Cette solution par fichier est la seul qui m’est possible d’exploiter actuellement, la meilleur solution serait de communiquer directement avec le script Python.

Cette méthode inclus un « While » qui va rafraichir, si besoin, l’affichage. Pour limiter les délais, il faut éviter d’envoyer une demande d’effacement directement par le pilote, mais plutôt toujours travailler dans le même « Draw » créé dans le script Python. Dans chaque « While », ont consulte un dossier où les demandes d’affichage attendent. Si aucun message est demandé, une procédure de « Stand by » peu être mis en place. Cette option de « Stand by » devrait être en plus « animé », mis à part qu’elle permet de savoir si le script est toujours actif, il permet de changer l’affichage et ainsi aider à garder en bon état les OLED.

Alors dans chaque « While », le script va actualiser l’image sur demande, si ont veut effacer, ont as qu’a dessiner un rectangle plein. Sinon si ont ajoute des informations à chaque « refresh ». Avec des sauvegardes du dernier affichage, a quel moment (en « unix time »), il est possible de limiter l’actualisation et le travail inutile si l’affichage ne change pas.

2 - La solution « active »

Cette solution est pour les affiches qui servent à l’utilisateur d’une manière immédiate, comme pour un lecteur de musique. Il faut concevoir un affichage selon l’usage, mais souvent elle ce limite à deux chose; un écran statique avec des informations ou un menu.

La solution 1 permet de créer des rendu sur demande, rapidement, mais l’utilisateur n’as pas besoin d’interagir avec. Ici ont à besoin d’un affichage pour l’utilisateur, et le script attend des réponses de l’utilisateur.

Ici ont doit concevoir l’affichage autrement, car ce qui sera affiché est ce que le script devra ce souvenir. Il doit être en mesure de répondre à la demande de l’utilisateur, qui est le résultat de l’affichage actuel.

C’est un principe un peu compliqué à expliquer.

Dans ma réalisation, j’ai créer ceci;

  • Protection par « PIN CODE »,
  • Menu naviguable
  • Affichage selon le choix

1.1 - Affichage dynamique
Quand je démarre mon projet, je veux demander un code PIN, l’utilisateur va entrer 4 touches pour débloquer la combinaison. Ce simple « logon » permet de comprendre le principe de mémoire d’activité.

Alors l’application doit avoir une variable « unlock » mis à False, si l’utilisateur entre le bon code, il deviendra True, et l’application passera à l’autre étape.

IMG_20230102_042722361_HDR

Pour commencer, l’affichage va créer une base, dans l’image pour le rendu ont ajoute une demande d’entrer le PIN CODE et 4 espace pour les entrées du PIN CODE. À chaque « refresh du while », ont vérifie si une touche est enfoncé. SI oui, ont l’ajoute au draw actuel.

Vu que chaque « while » ré-envoie le « Draw » en travail, chaque modification sera visible immédiatement.

Pour savoir si l’utilisateur entre bon code, il suffit de sauvegarder chaque entrer et et le comparé avec le mot de passe souhaité.

Ici la difficulté résolution est de recevoir des entrées pour un mot de passe (le PIN CODE). Il est possible d’assigner les boutons pour des valeurs de code. Vu qu’il ne peut pas y avoir de « Input box », il fau créer un moyen de ce souvenir de l’ensemble des entrées pour valider.

IMG_20230102_042735043_HDR

Pour compléter, il faut entre les 4 touches, une fois les 4 présentes, il est validé (unlock) ou non. Si il n’est pas bon, ont réinitialise l’affichage et l’état de la mémoire et ont recommence l’étape.

1.2 - Affichage de navigation

Tout comme les téléphones mobiles des années "90, comme les Nokia, un menu par élément est les plus pratique et simple à mettre en place.

IMG_20230102_042744521_HDR

Ont peut créer plusieurs pages de menu simplement, dans le script faudra sauvegarder le « Stage » affiché. Chaque « Stage » possède un Array des items affiché.

Vous pouvez créer une fonction pour travailler vos affichages de liste, cette fonction va travailler le rendu de manière simple et totalement dynamique, que la liste possède 2 items ou 200. Il faut concevoir que la liste sera affiché en plusieurs « Pages », chaque page contient 4 items (sur une base de 16 pixels de haut par ligne).
Alors ont commence en Page 0, le premier item de la liste afficher est 0 (le dernier sera 3).

Quand l’utilisateur appuie sur le bouton pour changer d’item, ont ajoute 1 au Select. Si le Select de l’item est supérieur à la limite d’affichage (total de 4), il ajoute 1 à la Page, place le Select sur le premier de cette page (0). Faites plus ou moins le contraire pour reculer dans les items/pages.

IMG_20230102_042751631_HDR

À chaque « while » le rendu du menu est actualisé, si le script est bien fait, vous ne recréer pas votre liste tout le temps, mais seulement a l’affichage initiale, après vous la gardez en mémoire. Pour connaitre le choix, il suffit de multiplier la Page active par 4 et d’ajouter le Select, le nombre donné sera la valeur de l’entrer dans le Array.

Exemple
Le menu serait le Array; ["Item menu A","Item menu B","Item menu C","Item menu D","Item menu E","Item menu F","Item menu G"]

Page 0 donnerait

Item menu A (Select 0)
Item menu B (Select 1)
Item menu C (Select 2)
Item menu D (Select 3)

Alors si ont choisi C ont obtient l’item 2 du Array.

Page 1 donnerait

Item menu E (Select 0)
Item menu F (Select 1)
Item menu G (Select 2)

Ici si ont prend le choix F, ce serait Page 1 * 4 = 4, + Select 1, ce qui donne 5, et F est le cinquième élément du Array.

Rappel: Un Array commence à 0.

Après, il suffit de créer des actions selon le choix, selon le bouton choisi. Le choix peux lancer une action, mener à un autre « Stage » avec un autre menu ou simplement afficher une information.

IMG_20230102_042817731_HDR

En contrôlant vos menus, vous pouvez changer le rendu si l’utilisateur change de sélection, vous pouvez par exemple le mettre en fond clair.

IMG_20230102_042801918_HDR

Éventuellement mon code sera disponible, car il est flexible et parfaitement fonctionnel. Il pourra servir de base pour des projets, mais pas ça l’objectif de ce poste.

Le rendu possible avec une bonne compréhension des limitations des modules permet de profiter pleinement des capacités. J’ai songé longtemps a comment mettre le tout en place. Concevoir une interface interactive de cette manière demande beaucoup d’attente aux erreurs possibles et au soins de placer des rappels et effacements de variable au bon moment.

J’espère vous avoir quand même aidé a visualiser comment concevoir un « pilote » pour l’affichage sur ce type de modules.

Voici le code de mon « moteur » d’affichage.

Tout le code est a insérer dans un seul document Python (« oled.py » par exemple).

Requirement

Ce moteur est optimisé pour ce produit: https://www.waveshare.com/wiki/1.3inch_OLED_HAT .
Mais sachez que tout écran OLED qui utilise une création d’image pour afficher seraient compatible avec le code, avec quelques ajustements bien sur.

Si vous exécutez le script est vous avez un message vous indiquant un module absent, vous avez juste à l’installer avec PIP. Noter que ce code a été écrit pour Python 3.

Explication du moteur

Ce script est pour générer une interface totalement dynamique. Vous pouvez générer plusieurs pages de navigation et lister des fichiers d’un dossier. Ce menu est protégé par un mot de passe, qui consiste aux boutons. Le code de base permet de montrer les capacités fournis.

Modules et variables de base

Les lignes pour charger les modules, dont le pilote du chapeau, et la définition des variables de base.

#!/usr/bin/python
# -*- coding:utf-8 -*-
from PIL import Image, ImageDraw, ImageFont
import sys, os, time, datetime
import configparser
import socket
from datetime import datetime
# Require the Waveshare Python Librairies for the OLED Hat
import SH1106
import RPi.GPIO as GPIO

#GPIO define
RST_PIN        = 25
CS_PIN         = 8
DC_PIN         = 24

KEY_UP_PIN     = 6 
KEY_DOWN_PIN   = 19
KEY_LEFT_PIN   = 5
KEY_RIGHT_PIN  = 26
KEY_PRESS_PIN  = 13

KEY1_PIN       = 21
KEY2_PIN       = 20
KEY3_PIN       = 16

#Fonts
FONT_LARGE_FAMILY= "DejaVuSansMono.ttf"
FONT_LARGE_SIZE  = 13

FONT_NORMAL_FAMILY= "DejaVuSansMono.ttf"
FONT_NORMAL_SIZE  = 11

FONT_SMALL_FAMILY= "DejaVuSansMono.ttf"
FONT_SMALL_SIZE  = 9

FONT_LARGE = ImageFont.truetype(FONT_LARGE_FAMILY, FONT_LARGE_SIZE)
FONT_NORMAL = ImageFont.truetype(FONT_NORMAL_FAMILY, FONT_NORMAL_SIZE)
FONT_SMALL = ImageFont.truetype(FONT_SMALL_FAMILY, FONT_SMALL_SIZE)

unlock=False
lockDraw=False
lockPass="0123"
timeWait=0
keyLast=False
menuStage=False
menuDraw=False
menuSelect=0
menuPage=0
poweroff=False
menuFavorites=False
menuPasswords=False
menuApps=False
menuBase=["◎ Demo 1","★ Demo 2","☼ Copie Demo 1","♪ Copie Demo 1","☺ Version"]
norefresh=False
keyIdleTime=0
keyIdleMax=60
#Default
path=os.path.dirname(os.path.realpath(__file__))

Ce qui est éditable, c’est;

  • La section « GPIO » si vous boutons sont différents.
  • La section « Fonts », qui permet de choisir la police et la taille (en pixel).
  • « menuBase » qui regroupe le menu principal de vos actions possibles. Vous allez devoir scripter le reste plus loin.
  • « keyIdleTime » est le temps avant que l’écran se remet en mode barré avec le mot de passe.
  • « lockPass » est le mot de passe; « U » pour Haut (Up), « D », pour bas (Down), « L » pour Gauche (Left), « R » pour Droite (Right), Le bouton du baton de direction est « 0 » (KEY0), la touche 1 (KEY1) est « 1 », la touche 2 (KEY2) est « 2 » et la touche 3 (KEY3) est « 3 ». Il doit y avoir quatre (4) caractère (Pin Code).

Les fonctions

Il faut éviter de se répéter dans les codes, alors les fonctions vont nous aider.

#Funcions
def loadConfig(f):
    config = configparser.ConfigParser()
    config.read(f+'.ini', encoding='utf-8')
    return config
    
def getIP():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        return s.getsockname()[0]
    except:
        return "not connected"

def getTimeNow():
    return datetime.now().timestamp()
    
def pasteText(txt):
    txt=txt.replace(":ipaddr:",getIP())
    return str(txt)

def getTextSize(text,r,f):
    if(r=="h"):
        return f.getbbox(text)[3]
    elif(r=="w"):
        return f.getbbox(text)[2]
    
def displayMenu(items):
    global menuPage, menuSelect, disp
    try:
        drawClear()
        if(len(items)>=1):
            if(menuPage):
                start=int(menuPage * 4)
                end=int(start + 3)
            else:
                start=0
                end=3
            y=0
            i=0
            x=0
            for item in items:
                if(x>=start and x<=end and x<=len(items)):
                    if(menuSelect==i):
                        drawClearSpace(0,y,disp.width,int(y + 16),0,0)
                        drawText(item,FONT_LARGE,2,y,255)
                    else:
                        drawText(item,FONT_LARGE,2,y)
                    i=int(i + 1)
                    y=int(y + 16)
                x=int(x + 1)
        else:
            displayError("No entry found.")
    except:
        displayError("Can't create menu.")

def displayError(txt):
    global oled_draw, norefresh, timeWait
    drawClear()
    tw=getTextSize(pasteText("ERROR"),"w",FONT_NORMAL)
    tx=int((disp.width - tw) / 2)
    oled_draw.text((tx,25), pasteText("ERROR"), font = FONT_NORMAL, fill = 0)
    tw=getTextSize(pasteText(txt),"w",FONT_SMALL)
    tx=int((disp.width - tw) / 2)
    oled_draw.text((tx,40), pasteText(txt), font = FONT_SMALL, fill = 0)
    norefresh=True
    timeWait=3
            
def drawText(txt,fnt,x="c",y=0,c=0):
    global oled_draw
    if(x=="c"):
        w=getTextSize(pasteText(txt),"w",fnt)
        x=int((disp.width - w) / 2)
    oled_draw.text((x,y), pasteText(txt), font = fnt, fill = c)

def drawClear(c=255,f=1):
    global oled_draw
    oled_draw.rectangle((0, 0,disp.width, disp.height), outline=c, fill=f)
    
def drawClearSpace(x,y,w,h,c=255,f=1):
    global oled_draw
    oled_draw.rectangle((x, y, w, h), outline=c, fill=f)
  • "loadConfig(f) : Mon code permet de charger un fichier de configuration (.ini) il retournera ce fichier comme résultat.
  • « getIP() » : Permet de savoir sont adresse IP fournis par le DHCP (pas celui Internet).
  • « getTimeNow() » : Retourne le temps en version UNIX. Idéal pour calculer le temps entre deux points.
  • « getTextSize(text,r,f) » : Permet de retourner des informations sur le texte affiché; « text » est le texte (string), le « r » est la mesure à obtenir, soit « h » pour la hauteur, ou « w » pour la largeur et « f » est le type de texte à utiliser (FONT_SMALL, FONT_NORMAL ou FONT_LARGE). Ceci permet de positionner le texte de manière plus précise.
  • « displayMenu(items) » : Affiche le menu du Array fournis. Il récupère la valeur de l’affichage courant (menuStage) et la sélection (menuSelect). Il détermine la position à afficher grâce aux pages (menuPage). Il n’affiche que la page active (menuPage).
  • « displayError(txt) » : Permet d’afficher un message d’erreur forcé.
  • « drawText(txt,fnt,x,y,c) » : « text » est le texte (string), le « fnt » est le type de texte à utiliser (FONT_SMALL, FONT_NORMAL ou FONT_LARGE). Le « x » est la position en pixel depuis la gauche (défaut c’est centré), si c’est « c » qui est fournis, il va centrer. Le « y » est la position depuis le haut (défaut 0) en pixel. Le « c » est une valeur entre 0 et 255, soit la différence entre « clair » (0) et foncé (255). (basé sur le résultat sur le OLED)
  • « drawClear(c,f) » : Permet d’effacer l’écran. La valeur de c est la couleur (défaut 255), effacé normal est foncé (éteint) ou clair (tout éclairé). Le « f » est pour remplir ou non (défaut oui avec 1).
  • « drawClearSpace(x,y,w,h,c,f) » : Comme pour « drawClear() » mais permet d’effacer une section seulement, peut permettre de créer un rectangle. Le « x » et « y », sont les position du coin supérieur gauche, le « w », et « h », est la position du coin inférieur droit.

Pré-lancement

On charge les ressources de base, comme les boutons et l’image de travail.

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

#init GPIO
# for P4:
# sudo vi /boot/config.txt
# gpio=6,19,5,26,13,21,20,16=pu
GPIO.setmode(GPIO.BCM) 
GPIO.setup(KEY_UP_PIN,      GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_DOWN_PIN,    GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_LEFT_PIN,    GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_RIGHT_PIN,   GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_PRESS_PIN,   GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY1_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY2_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY3_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up

#Script
oled_image = Image.new('1', (disp.width, disp.height), "WHITE")
oled_draw = ImageDraw.Draw(oled_image)

Le moteur : La grosse partie du script

Ici c’est le moteur du script, il capte les touches, et contrôle l’affichage.

Cette partie est dans un « While », il ce rafraichi dont aussi vite que le système le permet. Il ce divise en deux partie qui ont chacune deux autres parties, mais nous allons ici le découper en trois pour faciliter l’explication.

PIN CODE

while True:
    if(unlock is False):
        #Lock Screen
        if(lockDraw is False):
            try:
                drawClear()
                drawText("piMyKey",FONT_LARGE,"c",2)
                drawText("USB Personal Key locked",FONT_SMALL,"c",18)
                drawText("ENTER PIN CODE",FONT_NORMAL,"c",28)
                oled_draw.line(((29,60),(45,60)),fill = 0, width = 0)
                oled_draw.line(((47,60),(63,60)),fill = 0, width = 0)
                oled_draw.line(((65,60),(81,60)),fill = 0, width = 0)
                oled_draw.line(((83,60),(99,60)),fill = 0, width = 0)
                lockDraw=True
                lockTry=""
                lockKey=0
                lockLast=-1
                lockPress=False
            except:
                displayError("Can't display lock screen.")
        else:
            try:
                if GPIO.input(KEY_UP_PIN) and lockPress=="U": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY_UP_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"U"
                    lockPress="U"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY_LEFT_PIN) and lockPress=="L": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY_LEFT_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"L"
                    lockPress="L"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY_RIGHT_PIN) and lockPress=="R": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY_RIGHT_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"R"
                    lockPress="R"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY_DOWN_PIN) and lockPress=="D": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY_DOWN_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"D"
                    lockPress="D"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY_PRESS_PIN) and lockPress=="0": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY_PRESS_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"0"
                    lockPress="0"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY1_PIN) and lockPress=="1": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY1_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"1"
                    lockPress="1"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY2_PIN) and lockPress=="2": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY2_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"2"
                    lockPress="2"
                    lockKey=int(lockKey + 1)
                if GPIO.input(KEY3_PIN) and lockPress=="3": # button is released
                    lockPress=False
                elif lockPress is False and GPIO.input(KEY3_PIN) == 0: # button is pressed:
                    lockTry=lockTry+"3"
                    lockPress="3"
                    lockKey=int(lockKey + 1)
            except:
                displayError("Can't filter input.")
            try:
                if(lockKey != lockLast and lockPress is not False):
                    if(lockKey == 1):
                        drawText(lockPress,FONT_LARGE,32,46)
                    elif(lockKey == 2):
                        drawText(lockPress,FONT_LARGE,50,46)
                    elif(lockKey == 3):
                        drawText(lockPress,FONT_LARGE,68,46)
                    elif(lockKey == 4):
                        drawText(lockPress,FONT_LARGE,86,46)
                    lockLast=lockKey
            except:
                displayError("Can't display key entry.")
            try:
                if(lockLast >= 4):
                    drawClearSpace(0,0,disp.width,45)
                    if(lockTry == lockPass):
                        drawText("OLED UI",FONT_LARGE,"c",2)
                        drawText("OLED Interface ready",FONT_SMALL,"c",18)
                        drawText("WELCOME",FONT_NORMAL,"c",28)
                        unlock=True
                    else:
                        drawText("OLED UI",FONT_LARGE,"c",2)
                        drawText("OLED Interface locked",FONT_SMALL,"c",18)
                        drawText("TRY AGAIN",FONT_NORMAL,"c",28)
                        unlock=False
                    lockDraw=False
                    timeWait=1
                    keyIdleTime=getTimeNow()
            except:
                displayError("Can't confirm unlock.")

Le système du « PIN CODE » est simple. Ont va créer en premier lieux l’affichage et une fois fait ont va la réutiliser, jusqu’à ce que le code soit entré (success ou fail). Chaque touche va éditer trois valeurs, une pour éviter la répétition involontaire, une autre pour composer le code essayé et une autre qui calcul le total des entrées.

Quand il détecte une changement, il va ajouter le caractère à l’écran, vous pouvez remplacer par un « * », si vous désirez être plus secret, en autant que « lockTry » ne soit pas altéré.

Quand il détecte la rpésence de quatres entrées, il va tester le code avec celui défini et afficher le résultat. Si il est bon (le code), il chage le « unlock » pour « True » ET permet de passer à la section suivante.

Les entrées (boutons)
Ici vous travailler l’action faites par les boutons, ceci va changer dans la dernière partie l’affichage.

else:
    #Menu
    try:
        pos=int(((menuPage * 4) + 1) + menuSelect)
        if GPIO.input(KEY_UP_PIN): # button is released
            if(keyLast=="U"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="U"):
                if(menuStage == "demo1"):
                    print("Demo 1")
                    
                else:
                    menuSelect=int(menuSelect - 1)               
                    if(menuSelect>3 or menuSelect<=0):
                        if(menuPage<=0):
                            menuPage=0
                            menuSelect=0
                        else:
                            menuPage=int(menuPage - 1)
                            menuSelect=3
                keyLast="U"
                print("UP")
            
        if GPIO.input(KEY_LEFT_PIN): # button is released
            if(keyLast=="L"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="L"):
                if(menuStage == "demo1"):
                    print("Demo 1")
                keyLast="L"
                print("LEFT")
            
        if GPIO.input(KEY_RIGHT_PIN): # button is released
            if(keyLast=="R"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="R"):
                if(menuStage == "demo1"):
                    print("Demo 1")
                keyLast="R"
                print("RIGHT")
            
        if GPIO.input(KEY_DOWN_PIN): # button is released
            if(keyLast=="D"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="D"):
                                    
                if(menuStage == "demo1"):
                    print("Demo 1")
                    
                else:
                    if(menuPasswords): menuList=menuPasswords
                    elif(menuFavorites): menuList=menuFavorites
                    elif(menuApps): menuList=menuApps
                    else: menuList=menuBase
                    menuSelect=int(menuSelect + 1)                
                    pos=int(((menuPage * 4) + 1) + menuSelect)
                    if(menuSelect>=4 and pos<=len(menuList)):
                        menuPage=int(menuPage + 1)
                        menuSelect=0
                    elif(pos>len(menuList)):
                        menuSelect=int(menuSelect - 1)
                        if(menuSelect<=0):
                            menuSelect=0
                keyLast="D"
                print("DOWN")

        if GPIO.input(KEY_PRESS_PIN): # button is released
            if(keyLast=="0"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="0"):
                
                if(menuStage == "poweroff"):
                    print("Powering off the Raspberry Pi")
                    poweroff=True
                    drawClear()
                    drawText("Power Off",FONT_LARGE,"c",0)
                    drawText("Shutting down...",FONT_NORMAL,"c",20)
                    drawText("Please unplug when done.",FONT_SMALL,"c",36)
                    timeWait=2
                    norefresh=True
                    
                elif(menuStage == "demo1"):
                    print("Demo 1")                  
                keyLast="0"
                print("KEY0")
            
        if GPIO.input(KEY1_PIN): # button is released
            if(keyLast=="1"):
                keyLast=False
        else: # button is pressed:
            if(menuStage!="demo2"):
                keyIdleTime=getTimeNow()
            if(keyLast!="1"):
                if(menuStage==0):
                    unlock=False
                    lockDraw=False
                menuDraw=False
                menuSelect=0
                menuStage=0
                menuPage=0
                norefresh=False
                keyLast="1"
                print("KEY1")
            
        if GPIO.input(KEY2_PIN): # button is released
            if(keyLast=="2"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="2"):
                if(menuStage==0):
                    menuStage="poweroff"
                    
                elif(menuStage == "demo1"):
                    print("Demo 1")
                keyLast="2"
                print("KEY2")
            
        if GPIO.input(KEY3_PIN): # button is released
            if(keyLast=="3"):
                keyLast=False
        else: # button is pressed:
            keyIdleTime=getTimeNow()
            if(keyLast!="3"):                
                if(menuStage == "demo1"):
                    print("Demo 1")
                    
                elif(menuStage==0):
                    if(pos==1):
                        menuStage="demo1"
                    elif(pos==2):
                        menuStage="demo2"
                    elif(pos==3):
                        menuStage="demo1"
                    elif(pos==4):
                        menuStage="demo2"
                    elif(pos==5):
                        menuStage="version"
                    else:
                        menuDraw=False
                        menuStage=0
                    menuPage=0
                    menuSelect=0
                keyLast="3"
                print("KEY3")
    except:
        displayError("Key input fail.")

Le code ce répète une peu, mais c’est un mal nécessaire. Chaque touche est basé sur le même principe.
Le « if » est le relâchement du bouton, pour détecter si c’est le dernier utilisé ont l’ajoute en variable dans le « else » qui suit, si il est utilisé.

Note: Le script n’a pas été conçu pour une utilisation avec des combinaisons de touches. Mais il serait possible de le faire.

Dans le « else » ont inclus chaque « menuStage ». Le « menuStage » est la position dans le menu ou ce trouve l’utilisateur, le menu principal est le « 0 » (ou « False »), et dans el script il existe « demo1 », « demo2 » et « version », un menu caché existe pour la fermeture, nommé « poweroff ». Selon la touche et la position dans le menu, il est possible de choisir l’action.

Dans cet exemple, la touche « UP » et « DOWN » présente le script pour effectuer le mouvement dans le menu. il change deux valeurs. Le « menuSelect » est la sélection de l’item à l’écran, il est possible d’avoir quatre éléments visible à la fois, il est dans un range de 0 à 3. Si le Array (fournis au menu et gardé en mémoire) est plus petit que 0 ou plus grand que 3, il va changer de page (inférieur ou supérieur).

En mode menu, la touche 3 (KEY3) sert de « OK/Entrer » pour le choix. Dans le menu principal, la touche 2 permet de fermer l’appareil proprement (Power Off), cette touche peut servir dans un sous-menu ou une autre activité pour d’autres utilités. La touche 1 (KEY1) est celle permettant de retourner en arrière (quand dans une activité/sous-menu) ou de barrer quand ont est dans le menu principal.

À chaque touche, le « keyIdleTime » est changé, c’est pour la fonction de retour au « PIN CODE » automatique.

L’affichage et fin de boucle
Ici ont traite la grande partie de l’informations à afficher et finalise les actions de la boucle de rafraichissement.

if(menuStage and norefresh is False):
    try:
        if(menuStage == "demo1"):
            if(menuPasswords is False):
                menuPasswords=[]
                for x in os.listdir("demo"):
                    if x.endswith(".ini"):
                        menuPasswords.append(x.replace(".ini",""))
            menuPasswords.sort()
            displayMenu(menuPasswords)
            
        elif(menuStage == "poweroff"):
            drawClear()
            drawText("Power Off",FONT_LARGE,"c",0)
            drawText("Are you sure ?",FONT_NORMAL,"c",20)
            drawText("Press ● To confirm",FONT_SMALL,"c",36)
            
        elif(menuStage == "demo2"):
            drawClear()
            drawText("Demo 2",FONT_LARGE,"c",0)
            drawText("▲: Up",FONT_NORMAL,0,16)
            drawText("▼: Down",FONT_NORMAL,64,16)
            drawText("◄: Left",FONT_NORMAL,0,28)
            drawText("►: Right",FONT_NORMAL,64,28)
            drawText("●: KEY0",FONT_NORMAL,0,40)
            drawText("■¹: KEY1",FONT_NORMAL,64,40)
            drawText("■²: KEY2",FONT_NORMAL,0,52)
            drawText("■³: KEY3",FONT_NORMAL,64,52)
            norefresh=True
            
        elif(menuStage=="version"):
            drawClear()
            drawText("OLED UI",FONT_LARGE,"c",0)
            drawText("OLED Interface",FONT_NORMAL,"c",16)
            drawText("version 1.0.23.1.5",FONT_SMALL,"c",30)
            drawText("Mathieu 'levelKro' Légaré",FONT_SMALL,"c",40)
            drawText("https://levelkro.com",FONT_SMALL,"c",50)
            
    except:
        displayError("Can't display interface.")
        
elif(norefresh is False):
    menuFavorites=False
    menuPasswords=False
    menuApps=False
    displayMenu(menuBase)
    
if(int(getTimeNow() - keyIdleTime) >= keyIdleMax and norefresh is False):
    unlock=False
    lockDraw=False
    menuDraw=False
    menuSelect=0
    menuStage=0
    menuPage=0

disp.ShowImage(disp.getbuffer(oled_image))
norefresh=False

if(timeWait):
time.sleep(timeWait)
timeWait=0

if(poweroff):
os.system('sudo poweroff')

Il faut savoir que certains événement demande priorité, par exemple un message d’erreur, ou si un rendu est créé par un bouton mais qui ne doit pas être actualisé par cette section, la variable « norefresh » quand est à « True », force à sauter cette section. La fonction « displayError() » et la touche « KEY0 » en affichage du « poweroff » l’utilise. Elle doit idéalement être utilisé avec « timeWait », qui est le temps en seconde durant lequel l’affichage reste avant une actualisation (pause du « While »). Il est utilisé aussi avec « demo2 », qui à pour effet de garder l’affichage sans être affecté par le retour automatique du « PIN CODE ». Mais quand ont le quitte (KEY1) et le temps est dépassé, le « PIN CODE » s’affichera.

La structure de « menuStage » est simple, soit il est « 0 » (False), ou il est défini sur une nom. Alors la structure du menu est selon comment vous la désirez, il suffit que de bien coder vos actions selon la position ou ce trouve l’utilisateur sur l’affichage. Cependant, le retour avec « KEY1 » sera l’affichage demandé vers menu principal, et menu principal vers « PIN CODE », et ce tout le temps, vous devrez dont aussi modifier cette partie si vous désirez créer une effet de dossier.

À la fin ont a un « if » pour vérifier le temps da la dernière activité sur une touche, si c’est positif, il va activer le retour du « PIN CODE » en remettant les valeurs à zéro.

Après il affiche le rendu (commandes « disp.ShowImage() ») et remet le « norefresh » à False pour permettre le prochain rendu. Ont effectue la pause (si demandé) et après ont fait l’action de fermeture (si confirmé par le « menuStage » à « poweroff » et le boutton « KEY0 »).

Conclusion

Ceci est une version totalement exploitable du script. Je l’utilise (avec du code en plus pour mes besoins) dans un projet et j’ai aucun problème, les bugs sont plus du à des caprices connus, pour ça le nombre de « try » dans le script, pour éviter une plantage complet. Avec les fonctions ajoutés, il est facile de créer les éléments de base d’un affichage et de le manipuler pour créer plusieurs choses.

Avec le principe d’affichage il serait possible de créer des applications plus interactive, comme un jeu du serpent, l’affichage est quand même assez rapide (sous un Raspberry Pi Zero v1).

Le texte dans le script est en anglais, car c’est ma manière de coder (désolé pour certains, mais en debug, j’évite les problème du aux accents comme du passage de AINSI à UTF-8 et vice-versa ou caractère absent dans la police de caractère).

Pour utiliser le « demo1 », il faudra créer un dossier nommé" demo" et y ajouter un (ou des) fichier(s) avec l’extension « .ini ».

Pour utiliser police personnalisé, installer un package ou copier le fichier « .ttf » dans le dossier du script.

L’affichage se ferme quand le Raspberry s’éteint, si vous utilisez un OLED en SPI, il restera allumé avec un modèle en I2C.

Si vous avez un effet de neige au lancement du script attendez quelques secondes, si ça ne change pas c’est que le script est planté ou qu’il n’arrive pas a produire un affichage (aucun draw envoyé à l’affichage mais initialisé).

Pas obligé de garder mon nom dans la version, mais au minimum le garder en commentaire dans les sources. Il est libre d’usage, si je suis cité.