Απλά γραφικά σε Linux Περιβάλλον άνευ χρήσεως X11 η Wayland.

Κοιτούσα τον προαστιακό και παρατήρησα ότι η πινακίδα ανακοινώσεως τον δρομολογίων χρησιμοποιούσε Linux, ήταν εμφανές διότι είχε εμφανίσει ένα μήνυμα σφάλματος:

Κοιτώντας της μου γεννήθηκε η απορία, αν ήθελα να κάνω Implement και εγώ μια παρόμοια λύση πως θα μπορούσα να το κάνω; Χρειάζομαι Display manager η κάτι παρόμοιο;

ΛΥΣΗ 1: Χρήση Browser

Μια λύση είναι σηκώνω έναν Browser (πχ. Firefox) σε kiosk mode μέσω matchbox. Είναι μια scallable λύση διότι ότι χρειάζεται να εμφανίσω το αναλαμβάνει ο browser ενώ το linux είναι απλά ένα “κουτό” πράγμα.

Όμως αυτό προϋποθέτει:

  1. Είτε να έχω έναν webserver στο ίδιο μηχάνημα που εμφανίζει την πληροφορία.
  2. Είτε να έχω έναν εξωτερικό webserver και το μηχάνημά μου να έχει πρόσβαση στο διαδίκτυο.

Παραδοσιακά μέσω X11 (ΔΕΝ δουλεύει σε ubuntu 26.04, το βάζω για ιστορικούς λόγους)

Παραδοσιακά γινόταν μέχω X11 και ενός lightweigt window manager:

sudo apt-get install matchbox-window-manager firefox xinit

Μετά στο ~/.xintirc τοποθετούμε:

#!/bin/sh

matchbox-window-manager -use_titlebar no &
firefox --kiosk "http://google.com"
logout

Και εκκινάμε τον Χ11 με την εντολή:

startx

Χρήση weston (working)

H εναλλακτικά μέσω weston (wayland compositor) που είχα καλύτερη επιτυχία:

sudo apt-get install weston firefox

Μετά κάνουμε ένα script εκκίνησης firefox:

sudo nano /usr/local/bin/kiosk.sh

και δίδουμε:

#!/bin/sh

# Instead of google.com use your own url
exec /usr/bin/firefox --kiosk 'https://google.com'

Μετά ρυθμίσουμε το weston σε kiosk mode:

nano ~/.config/weston.ini

Και τοποθετούμε:

[core]
shell=kiosk-shell.so

[output]
name = screen0

[autolaunch]
path=/usr/local/bin/kiosk.sh

Αυτό όμως δεν είναι πάντα εφικτό διότι ένα μηχάνημα που απλά εμφανίζει πληροφορίες ίσως να είναι σε απομακρυσμένο περιβάλλον πχ. σε έναν σταθμό στην μέση ενός βουνού. Σε αυτές τις περιπτώσεις το να βάλεις διαδίκτυο μπορεί να είναι είναι ακριβό κοστολογικά, ενώ μπορείς να αξιοποιήσεις άλλες λύσεις πχ. LoRa. Σε αυτήν την περίπτωση η χρήση ενός browser δεν είναι βολική.

Υπάρχει μια εναλλακτική όμως;

ΝΑΙ να τρέξουμε desktop app.

Όμως και πάλι έχεις 2 εμπόδια:

  1. την μνήμη RAM
  2. τον αποθηκευτικό χώρο της ίδια συσκευής.

Πχ. με την χρήση του matchbox έχεις:

  • Χ11 Layer
  • Matchbox
  • Browser

Αυτά αποτελούν επιπλέον εξαρτήσεις που είναι αποθηκευμένες στην συσκευή ενώ αυτό σηκώνει και επιπλέον διεργασίες που καταλαμβάνουν χώρο στην μνήμη. Σε ένα Embeded σύστημα ενδεχομένως να μην είναι θεμιτό αυτό.

Έτσι πάμε στην λύση 2: Να κάνουμε μια desktop εφαρμογή που ο weston θα το σηκώνει:

Λύση 2: Desktop App + weston

Weston Basics

Στο ~/.config/weston.ini Ορίσαμε τα εξής:

[core]
shell=kiosk-shell.so

[output]
name = screen0

[autolaunch]
path=/usr/local/bin/kiosk.sh

Σε αυτό ορίσαμε 3 sections:

  • core που εμπεριέχει βασικές ρυθμίσεις. Με την shell=kiosk-shell.so λέμε στον weston να ΜΗΝ σηκώσει κάποιο γραφικό στοιχείο navigation/ μπαρας κοκ
  • autolaunch που αναγράφουμε ποιο εκτελέσιμο θα τρέξει κατα την εκκίνηση του weston
  • output ορίζουμε την οθόνη που θα εμφανίσει την εκάστοτε εφαρμογή. Στην περίπτωσή μας ορίζουμε ότι θα εμφανίζετε στην οθόνη Screen0.

Άρα μπορούμε να ρυθμίσουμε ένα δικό μας desktop app πχ. Electron η Flutter αντί για firefox.

Χρήση Flutter

Ας αξιοποιήσουμε το flutter:

Αν δεν το έχουμε εγκαταστήσει το εγκαθιστούμε

sudo apt update
sudo apt install -y curl git unzip xz-utils zip libglu1-mesa \
  clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev
cd ~
git clone https://github.com/flutter/flutter.git -b stable
echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Και κάνουμε την default εφαρμογή του flutter:

flutter create --empty hello_flutter
cd hello_flutter
flutter build linux --release

Με την εντολή αυτήν στο ~/hello_flutter/build/linux/x64/release/bundle/hello_flutter υπάρχει το εκτελέσιμο. Όμως ΔΕΝ είναι statically build, χάριν ευκολίας το μεταφέρουμε στο /opt:

sudo mkdit /opt/hello_flutter
sudo cp ~/hello_flutter/build/linux/x64/release/bundle/* /opt/hello_flutter

Και ρυθμίζουμε το weston:

[core]
shell=kiosk-shell.so

[output]
name = screen0

[autolaunch]
path=/opt/hello_flutter

Όμως μιας και κάνουμε desktop εφαρμογή υπάρχει μια λύση του να μην αξιοποιώ wayland καθόλου.

Η απάντηση είναι ναι, αν και η λύση desktopapp + weston είναι ικανοποιητική, μπορούμε να ζωγραφίσουμε απευθείας στην οθόνη χωρίς wayland.

Λύση 3: Ζωγραφίσουμε στην οθόνη

Βασικά πράγματα:

Κάθε display manager πχ Χ11,wayland αξιοποιεί τα λεγόμενα framebuffers, δλδ αρχεία συσκευών που εκθέτει το ίδιο το linux για να εμφανίζει γραφικά. H μορφή που ακολουθεί είναι η /dev/fbX (αντικατέστησε το X με έναν αριθμό) η προεπιλεγμένη είναι η /dev/fb0. Για να δούμε όλες τις συσκευές δίδουμε την εντολή:

ls -l /dev/fb*

To αρχείο αυτό αντιστοιχεί σε περιοχή μνήμης που εμπεριέχει τις τιμές των pixel που έχει η οθόνη. Σε συστήματα linux μπορούμε να θέσουμε συσκευές σε αρχείο που σε αυτό μπορείς να γράφεις και να διαβάζεις.

Πέρα από αυτό όμως από αυτό υπάρχουν και άλλα 2 text αρχεία:

  • /sys/class/graphics/fbX/virtual_size το οποίο μας δίδει το μέγεθος της οθόνης.
  • /sys/class/graphics/fbX/bits_per_pixel το οποίο μα δίδει πόσα bit είναι το χρώμα ενός pixel.

Όπου fbX είναι το device μας πχ αν το device είναι το /dev/fb0 τότε τα αρχεία είναι /sys/class/graphics/fb0/virtual_size και /sys/class/graphics/fb0/bits_per_pixel αντίστοιχα.

Το /sys/class/graphics/fbX/virtual_size εμπεριέχει μια εγγραφή με 2 νούμερα χορισμένα με κόμμα πχ στην περίπτωσή μου δίδοντας την εντολή cat:

cat /sys/class/graphics/fb0/virtual_size

Επιστρέφει:

1280,800

Ενώ το /sys/class/graphics/fbX/bits_per_pixel εμπεριέχει μία γραμμή με ένα νούμερο πχ:

cat /sys/class/graphics/fb0/bits_per_pixel
32

Εμφάνιση στην οθόνη

Έτσι αξιοποιώντας python μπορούμε να ζωγραφίσουμε στην οθόνη:

import mmap
import sys

def set_pixel(fb,bpp,width,x, y, r, g, b, a=255):
    if x < 0 or x >= w or y < 0 or y >= h:
        return

    if bpp == 32:
        # Your framebuffer appears to use BGRA byte order
        p = bytes((b, g, r, a))
    else:
        # RGB565
        value = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
        p = bytes((value >> 8, value & 0xff))

    pos = (y * width + x) * len(p)
    fb[pos:pos + len(p)] = p

def fill_rectangle(fb,bpp,width,x1, y1, x2, y2, r, g, b, a=255):
    for y in range(y1, y2 + 1):
        for x in range(x1, x2 + 1):
            set_pixel(fb,bpp,width,x, y, r, g, b, a)

def blank(fb,width,height):
    fb[0:len(fb)] = 0

fbN = 'fb0'

# get width and height
f = open( f"/sys/class/graphics/{fbN}/virtual_size", "r")
wh = f.read()
wa,ha = wh.split( ',')
w = int( wa) # width
h = int( ha) # height
f.close()

# get bits per pixel
f = open( f"/sys/class/graphics/{fbN}/bits_per_pixel", "r")
bpp = int( f.read())
if not bpp in (16,32):
  print( "Unsupported bpp")
  sys.exit()
f.close()

# open framebuffer and map it onto a python bytearray
fbdev = open( f"/dev/{fbN}", mode='r+b') # open R/W
fb = mmap.mmap( fbdev.fileno(), w * h * bpp//8, mmap.MAP_SHARED, mmap.PROT_WRITE|mmap.PROT_READ)

fill_rectangle(fb,bpp,w,0,0,w,h,0,0,0,0)
fill_rectangle(fb, bpp,w, 100, 50, 300, 200, 255, 0, 0)

fb.close()
fbdev.close()

Βασική Λογική

H λογική που ακολουθούμε είναι:

  1. Λαμβάνουμε την διάσταση της οθόνης
  2. Λαμβάνουμε το βάθος χρώματος
  3. Χαρτογραφούμε το framebuffer σε ένα array τιμή που θέτουμε στην θέση i,j είναι η τιμή που θα πάρει το εκάστοτε pixel της οθόνης στη αντίστοιχη θέση.

Σημαντικό στον παραπάνω κώδικα είναι ότι με τη χρήση του mmap χαρτογραφούμε την περιοχή μνήμης του framebuffer στον χώρο διευθύνσεων του προγράμματός μας. Έτσι μπορούμε να την προσπελαύνουμε σαν έναν πίνακα από bytes και να τροποποιούμε απευθείας τα bytes που αντιστοιχούν στα pixels της οθόνης.

Ο χαρτογραφημένος framebuffer είναι μονοδιάστατη περιοχή μνήμης, όπου κάθε θέση αντιστοιχεί σε ένα byte. Επομένως, πρέπει να μετατρέψουμε τις δισδιάστατες συντεταγμένες (x, y) του pixel σε μια θέση (offset) μέσα στον μονοδιάστατο framebuffer.

Επιπλέον, ένα pixel μπορεί να καταλαμβάνει περισσότερα από ένα bytes, ανάλογα με το βάθος χρώματος. Για 16 bit, κάθε pixel καταλαμβάνει 2 bytes, ενώ για 32 bit καταλαμβάνει 4 bytes. Επομένως, αφού υπολογίσουμε τη θέση του pixel, πρέπει να μετακινηθούμε κατά τον αντίστοιχο αριθμό bytes για να βρούμε την ακριβή θέση του pixel στη μνήμη.

Υπολογισμός θέσης pixel

Ας φανταστούμε την οθόνη μας σαν έναν δισδιάστατο πίνακα με w στήλες και h γραμμές πχ. 3*5 (για απλότητα υπολογισμών):

Στήλη 1 Στήλη 2 Στήλη 3
1 2 3
4 5 6
7 8 9
10 11 12
13 14 15

και αυτόν τον κάνουμε μονοδιάστατο δλδ τις θέσεις 0-2 είναι η πρώτη γραμμή και οι θέσεις 3-5 η δεύτερη κοκ.

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Και θέλουμε να λάβουμε το στοιχείο (x,y)=(0,1) δλδ το πρώτο στοιχείο της δεύτερης γραμμής (η αρίθμηση ξεκινά από το 0). Στον μονοδιάστατο πίνακα είναι η θέση 3 αυτό μπορεί να προκύψει με τον τύπο:

y * αριθμός_στηλών+x => 1 * 3 στήλες+0 => 3+0 => 3

Τώρα ας υποθέσουμε πως κάθε 3 θέσεις του μονοδιάστατου πίνακα καταλαμβάνει 1 τιμή του δισδιάστατου πίνακα:

| 1 | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 6 | 6 | 6 | 7 | 7 | 7 | 8 | 8 | 8 | 9 | 9 | 9 | 10 | 10 | 10 | 11 | 11 | 11 | 12 | 12 | 12 | 13 | 13 | 13 | 14 | 14 | 14 | 15 | 15 | 15 |

Έτσι παρατηρούμε πως ο μονοδιάστατος πίνακας τριπλασιάζετε στην τιμή. Άρα ο τύπος:

y * αριθμός_στηλών + x 

δεν μας πάει στο σωστό σημείο.

Για την θέση (0,1) θέλουμε την θέση 9 άρα θα πρέπει να “μετατοπίσουμε” την θέση 3 με βήμα 3 άλλες 3 φορές διότι 3+3+3=3*3=9.

Άρα ο τελικός τύπος είναι

(y*αριθμός_στηλών+x)*bytes_per_color = (1*3στήλες+0)*3 = 9

Έτσι αν πάμε στον κώδικά μας το:

    pos = (y * width + x) * len(p)

Αντιστοιχεί:

  • y,x η θέση του pixel
  • width => αριθμός_στηλών το πλάτος της οθόνης
  • len(p) => bytes_per_color το βάθος του χρώματος

Υπολογισμός χρώματος Pixel

32 βιτ

Αν είναι 32bit το βάθος χρώματος απλά ορίζουμε 4 bytes για τα κανάλια red,green,blue,alpha (διαφάνεια):

p = bytes((b, g, r, a))

16 bit

Στα 16 bit έχουμε διαθέσιμα μόνο 16 bits = 2 bytes για να αποθηκεύσουμε ένα pixel.

Στα 32 bit είχαμε:

RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
   8        8        8        8

δηλαδή 8 bits για κάθε κανάλι χρώματος και 8 bits για το alpha.

Στα 16 bit όμως δεν μπορούμε να δώσουμε 8 bits σε κάθε κανάλι, γιατί:

8 + 8 + 8 + 8 = 32 bits

ενώ διαθέτουμε μόνο 16.

Για αυτό χρησιμοποιούμε το RGB565.

Η διάταξη του RGB565

Το RGB565 μοιράζει τα 16 bits ως εξής:

RRRRR GGGGGG BBBBB
  5      6      5

Δηλαδή:

  • 5 bits για το κόκκινο (Red)
  • 6 bits για το πράσινο (Green)
  • 5 bits για το μπλε (Blue)

Συνολικά:

5 + 6 + 5 = 16 bits

Το alpha channel δεν υπάρχει στο RGB565, επομένως η τιμή a αγνοείται.

Μετατροπή από RGB888 σε RGB565

Οι τιμές r, g, b που έχουμε στη συνάρτηση είναι συνήθως 8-bit:

R = 0..255
G = 0..255
B = 0..255

Δηλαδή κάθε κανάλι αρχικά έχει 8 bits:

RRRRRRRR
GGGGGGGG
BBBBBBBB

Πρέπει όμως να τα χωρέσουμε σε:

RRRRR
GGGGGG
BBBBB

Άρα πρέπει να μειώσουμε:

  • το Red από 8 → 5 bits
  • το Green από 8 → 6 bits
  • το Blue από 8 → 5 bits

Red και Blue: 8 → 5 bits

Για να κρατήσουμε μόνο τα 5 πιο σημαντικά bits, αφαιρούμε τα 3 λιγότερο σημαντικά bits.

Αυτό γίνεται με δεξιά μετατόπιση κατά 3:

r >> 3
b >> 3

Για παράδειγμα:

RRRRRRRR
   ↓ >> 3
000RRRRR

και:

BBBBBBBB
   ↓ >> 3
000BBBBB

Έτσι από 8 bits καταλήγουμε σε τιμές που χωράνε σε 5 bits:

00000 = 0
11111 = 31

Άρα:

8-bit Red   → 5-bit Red
0..255      → 0..31

και το ίδιο για το Blue.

Η μετατόπιση των bit όμως έχει και ένα άλλο αποτέλεσμα την μείωση της ακρίβειας του χρώματος λόγο ότι μειώνουμε τα διαθέσιμα but που είναι για εκάστοτε κανάλι χρώματος.

Green: 8 → 6 bits

Το πράσινο έχει ένα επιπλέον bit σε σχέση με τα άλλα κανάλια.

Από:

GGGGGGGG

θέλουμε:

GGGGGG

οπότε αφαιρούμε τα 2 λιγότερο σημαντικά bits:

g >> 2

δηλαδή:

GGGGGGGG
  ↓ >> 2
00GGGGGG

Έτσι:

8-bit Green → 6-bit Green
0..255      → 0..63

Τοποθέτηση των καναλιών

Μέχρι τώρα έχουμε μειώσει το μέγεθος των καναλιών.

Έχουμε πλέον:

Red   = 5 bits
Green = 6 bits
Blue  = 5 bits

Αλλά δεν αρκεί να τα έχουμε ξεχωριστά.

Πρέπει να δημιουργήσουμε έναν ενιαίο αριθμό 16 bits με τη μορφή:

RRRRR GGGGGG BBBBB

Για αυτό χρησιμοποιούμε τις αριστερές μετατοπίσεις (<<).

Red

Το Red πρέπει να βρίσκεται στην αρχή του 16-bit αριθμού:

RRRRR GGGGGG BBBBB
^^^^^
 Red

Το Red έχει 5 bits και η τελική bitoσειρά έχει 16 bits.

Μετά το Red υπάρχουν:

6 bits Green + 5 bits Blue = 11 bits

Άρα πρέπει να μετακινήσουμε το Red 11 θέσεις προς τα αριστερά:

(r >> 3) << 11

Αν δηλαδή έχουμε:

000RRRRR

με το << 11 παίρνουμε:

RRRRR00000000000

Έτσι το Red βρίσκεται ακριβώς στις θέσεις που πρέπει.

Green

Το Green πρέπει να βρίσκεται στη μέση:

RRRRR GGGGGG BBBBB
      ^^^^^^
      Green

Μετά το Green υπάρχουν μόνο τα 5 bits του Blue.

Άρα πρέπει να μετακινήσουμε το Green 5 θέσεις προς τα αριστερά:

(g >> 2) << 5

Έτσι:

00GGGGGG

γίνεται:

00000GGGGGG00000
Blue

Το Blue πρέπει να βρίσκεται στα τελευταία 5 bits:

RRRRR GGGGGG BBBBB
             ^^^^^
             Blue

Δεν χρειάζεται αριστερή μετατόπιση.

Αρκεί:

b >> 3

οπότε παίρνουμε:

00000000000BBBBB
Ενώνουμε τα τρία κανάλια

Τώρα έχουμε τρεις ξεχωριστούς 16-bit αριθμούς:

Red:

RRRRR 000000 00000


Green:

00000 GGGGGG 00000


Blue:

00000 000000 BBBBB

Θέλουμε να τους ενώσουμε:

RRRRR GGGGGG BBBBB

Για αυτό χρησιμοποιούμε το bitwise OR (|):

value = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)

Το | συγκρίνει τα bits στις ίδιες θέσεις και βάζει 1 όταν τουλάχιστον ένα από τα δύο bits είναι 1.

Εδώ είναι ιδιαίτερα χρήσιμο επειδή τα τρία κανάλια βρίσκονται σε διαφορετικές θέσεις και δεν επικαλύπτονται:

Red:
RRRRR 000000 00000

Green:
00000 GGGGGG 00000

Blue:
00000 000000 BBBBB

Με το |:

RRRRR 000000 00000
00000 GGGGGG 00000
00000 000000 BBBBB
------------------
RRRRR GGGGGG BBBBB

Έτσι δημιουργούμε τον τελικό 16-bit RGB565 αριθμό.

Κόψιμο τιμής se 2 bytes

Παρατηρούμε ποιο κάτω ότι ιο κώδικας εκτελεί:

        p = bytes((value >> 8, value & 0xff))

Ο λόγος που το κάνει αυτό είναι επειδή η τελική τιμή είναι 16Bit και θέλουμε να την κόψουμε σε 2 bytes.

Τo πρώτο byte είναι:

value >> 8

Δηλαδή τα πρώτα 8 bit μετατοπισμένα δεξιά.

Ενώ με το:

value & 0xff 

Μέσω bitwise and, επιτρέπουμε στο τελευταίο byte μόνο να κρατηθεί και το πρώτο απλά να μηδενιστεί, έτσι λαμβάνουμε το τελεταίο byte. Η τιμή 0xFF είναι 8bits που όλοι είναι 1.

3 «Μου αρέσει»

To python script τρέχει με την χρήση sudo. Για να το τρέξετε χωρίς sudo θα πρέπει να ορίσετε τον χρήστη στο group display:

sudo usermod -aG display $USER

Κάντε logout & login με την εντολή:

exit
3 «Μου αρέσει»