Κοιτούσα τον προαστιακό και παρατήρησα ότι η πινακίδα ανακοινώσεως τον δρομολογίων χρησιμοποιούσε Linux, ήταν εμφανές διότι είχε εμφανίσει ένα μήνυμα σφάλματος:
Κοιτώντας της μου γεννήθηκε η απορία, αν ήθελα να κάνω Implement και εγώ μια παρόμοια λύση πως θα μπορούσα να το κάνω; Χρειάζομαι Display manager η κάτι παρόμοιο;
ΛΥΣΗ 1: Χρήση Browser
Μια λύση είναι σηκώνω έναν Browser (πχ. Firefox) σε kiosk mode μέσω matchbox. Είναι μια scallable λύση διότι ότι χρειάζεται να εμφανίσω το αναλαμβάνει ο browser ενώ το linux είναι απλά ένα “κουτό” πράγμα.
Όμως αυτό προϋποθέτει:
- Είτε να έχω έναν webserver στο ίδιο μηχάνημα που εμφανίζει την πληροφορία.
- Είτε να έχω έναν εξωτερικό 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 εμπόδια:
- την μνήμη RAM
- τον αποθηκευτικό χώρο της ίδια συσκευής.
Πχ. με την χρήση του 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που αναγράφουμε ποιο εκτελέσιμο θα τρέξει κατα την εκκίνηση του westonoutputορίζουμε την οθόνη που θα εμφανίσει την εκάστοτε εφαρμογή. Στην περίπτωσή μας ορίζουμε ότι θα εμφανίζετε στην οθόνη 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 λογική που ακολουθούμε είναι:
- Λαμβάνουμε την διάσταση της οθόνης
- Λαμβάνουμε το βάθος χρώματος
- Χαρτογραφούμε το 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η θέση του pixelwidth=>αριθμός_στηλώντο πλάτος της οθόνης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.
