375 lines
13 KiB
Lua
375 lines
13 KiB
Lua
--------------------------------------------------------------------
|
|
-- lib/ihm.lua : interfaces homme-machine.
|
|
-- Moniteur tactile : navigation par onglets (MODE / CALIB /
|
|
-- JOURNAL), echelle de texte auto (la plus grande qui loge le
|
|
-- contenu), pages adaptees au mode, art du quadricoptere.
|
|
-- Display Link conduite : altitudes / vitesse / messages longs
|
|
-- defiles par segments de mots entiers.
|
|
-- Display Link carburant : % + autonomie.
|
|
--------------------------------------------------------------------
|
|
local Ihm = {}
|
|
|
|
-- couleur d'un rpm: blanc=0, rouge=max, jaune=en changement, vert=stable
|
|
local function couleurRpm(suivi, vmax)
|
|
if not suivi then return colors.lightGray end
|
|
if os.clock() - suivi.tChange < 1.0 then return colors.yellow end
|
|
if suivi.v == 0 then return colors.white end
|
|
if math.abs(suivi.v) >= vmax then return colors.red end
|
|
return colors.green
|
|
end
|
|
|
|
function Ihm.nouveau(conf, materiel, journal)
|
|
local ihm = {}
|
|
local moniteur = materiel.moniteur
|
|
local zones = {}
|
|
local page = "mode" -- mode | calib | journal
|
|
local L, H = 0, 0 -- taille en caracteres
|
|
|
|
-- Echelle auto: la PLUS GRANDE echelle offrant au moins 38x14
|
|
local function reglerEchelle()
|
|
for _, s in ipairs({ 5, 4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1, 0.5 }) do
|
|
moniteur.setTextScale(s)
|
|
L, H = moniteur.getSize()
|
|
if L >= 38 and H >= 14 then return end
|
|
end
|
|
end
|
|
reglerEchelle()
|
|
|
|
------------------------------------------------------------------
|
|
-- MESSAGES DE CONDUITE (defilement par segments de mots entiers)
|
|
------------------------------------------------------------------
|
|
local segments, segmentFin, segmentIdx = nil, 0, 1
|
|
|
|
function ihm.message(texte, duree)
|
|
if texte == "" then segments = nil return end
|
|
-- decoupe en segments <= 22 caracteres sans couper les mots
|
|
segments = {}
|
|
local courant = ""
|
|
for mot in texte:gmatch("%S+") do
|
|
if #courant == 0 then courant = mot
|
|
elseif #courant + 1 + #mot <= 22 then courant = courant .. " " .. mot
|
|
else table.insert(segments, courant) courant = mot end
|
|
end
|
|
if #courant > 0 then table.insert(segments, courant) end
|
|
segmentIdx = 1
|
|
segmentFin = os.clock() + (duree or math.max(5, #segments * 2))
|
|
end
|
|
|
|
local function segmentCourant()
|
|
if not segments or os.clock() > segmentFin then return nil end
|
|
-- avance d'un segment toutes les 2 s
|
|
local i = 1 + math.floor((os.clock() % (#segments * 2)) / 2)
|
|
return segments[math.min(i, #segments)]
|
|
.. (#segments > 1 and (" " .. i .. "/" .. #segments) or "")
|
|
end
|
|
|
|
-- ctx: { mode, consigneY, saisie }
|
|
function ihm.rafraichirConduite(ctx)
|
|
local e = materiel.ecranConduite
|
|
e.clear()
|
|
e.setCursorPos(1, 1)
|
|
local seg = segmentCourant()
|
|
if seg then
|
|
e.write(seg:sub(1, 22))
|
|
elseif ctx.saisie and #ctx.saisie > 0 then
|
|
e.write(("SAISIE: " .. ctx.saisie):sub(1, 22))
|
|
elseif ctx.mode == "vol" or ctx.mode == "drone"
|
|
or ctx.mode == "atterrissage" or ctx.mode == "auto" then
|
|
e.write(("ALT %.1f / %.0f"):format(
|
|
materiel.lireAltitude(), ctx.consigneY or 0))
|
|
else
|
|
e.write("MODE " .. ctx.mode:upper())
|
|
end
|
|
e.setCursorPos(1, 2)
|
|
e.write(("VIT %.2f b/s"):format(materiel.vitesseHorizontale()))
|
|
e.update()
|
|
end
|
|
|
|
function ihm.rafraichirCarburant(pct, minutes)
|
|
local e = materiel.ecranCarburant
|
|
local largeur = e.getSize()
|
|
e.clear()
|
|
e.setCursorPos(1, 1)
|
|
if pct == nil then
|
|
e.write(("n/a"):sub(1, largeur))
|
|
else
|
|
e.write(("%d%%"):format(math.floor(pct * 100 + 0.5)):sub(1, largeur))
|
|
e.setCursorPos(1, 2)
|
|
e.write((minutes and ("%dm"):format(math.floor(minutes)) or "--m")
|
|
:sub(1, largeur))
|
|
end
|
|
e.update()
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- PRIMITIVES MONITEUR
|
|
------------------------------------------------------------------
|
|
local function bouton(x, y, texte, action, couleur, couleurTexte)
|
|
moniteur.setCursorPos(x, y)
|
|
moniteur.setBackgroundColor(couleur or colors.gray)
|
|
moniteur.setTextColor(couleurTexte or colors.white)
|
|
moniteur.write(" " .. texte .. " ")
|
|
moniteur.setBackgroundColor(colors.black)
|
|
moniteur.setTextColor(colors.white)
|
|
table.insert(zones, {
|
|
x1 = x, y1 = y, x2 = x + #texte + 1, y2 = y, action = action,
|
|
})
|
|
end
|
|
|
|
local function texte(x, y, contenu, couleur)
|
|
moniteur.setTextColor(couleur or colors.white)
|
|
moniteur.setCursorPos(x, y)
|
|
moniteur.write(contenu)
|
|
moniteur.setTextColor(colors.white)
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- ART DU QUADRICOPTERE
|
|
-- rectangle, 4 cercles aux coins (helices, rpm a cote), rpm des
|
|
-- propulseurs sur les cotes; violet = RSC en cours de calibration
|
|
------------------------------------------------------------------
|
|
local function couleurDe(role, ctx)
|
|
if ctx.rscViolet == role then return colors.purple end
|
|
return couleurRpm(materiel.rpm[role], conf.VITESSE_RSC_MAX)
|
|
end
|
|
|
|
local function rpmDe(role)
|
|
local s = materiel.rpm[role]
|
|
return s and tostring(s.v) or "?"
|
|
end
|
|
|
|
-- occupe 7 lignes a partir de y; largeur ~ 30
|
|
local function dessinerArt(x, y, ctx)
|
|
local function coin(cx, cy, role, rpmAGauche)
|
|
texte(cx, cy, "O", couleurDe(role, ctx))
|
|
local r = rpmDe(role)
|
|
if rpmAGauche then texte(cx - #r - 1, cy, r, couleurDe(role, ctx))
|
|
else texte(cx + 2, cy, r, couleurDe(role, ctx)) end
|
|
end
|
|
local g, d = x + 5, x + 18 -- colonnes des coins
|
|
texte(x + 9, y, "AVANT", colors.lightGray)
|
|
coin(g, y + 1, "lf", true)
|
|
coin(d, y + 1, "rf", false)
|
|
for i = 2, 4 do
|
|
texte(g, y + i, "|")
|
|
texte(d, y + i, "|")
|
|
end
|
|
texte(g + 1, y + 1, ("-"):rep(d - g - 1))
|
|
texte(g + 1, y + 5, ("-"):rep(d - g - 1))
|
|
texte(x, y + 3, rpmDe("prop_l"), couleurDe("prop_l", ctx))
|
|
texte(d + 2, y + 3, rpmDe("prop_r"), couleurDe("prop_r", ctx))
|
|
coin(g, y + 5, "lb", true)
|
|
coin(d, y + 5, "rb", false)
|
|
return y + 7
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- PAGE MODE (contenu adapte au mode courant)
|
|
------------------------------------------------------------------
|
|
local function blocCommun(y, ctx)
|
|
texte(1, y, ("carburant %s autonomie %s"):format(
|
|
ctx.carburantPct and ("%.0f%%"):format(ctx.carburantPct * 100)
|
|
or "n/a",
|
|
ctx.minutes and ("%dmin"):format(math.floor(ctx.minutes)) or "--"))
|
|
if ctx.position then
|
|
texte(1, y + 1, ("gps %.0f / %.0f"):format(
|
|
ctx.position.x, ctx.position.z))
|
|
else
|
|
texte(1, y + 1, "gps n/a", colors.red)
|
|
end
|
|
return y + 2
|
|
end
|
|
|
|
local function blocStress(y, ctx)
|
|
local brut, cap = materiel.lireStressBrut()
|
|
texte(1, y, ("stress %s / %s (%s) moteur %s"):format(
|
|
brut and ("%d"):format(brut) or "?",
|
|
cap and ("%d"):format(cap) or "?",
|
|
ctx.stress and ("%.0f%%"):format(ctx.stress * 100) or "n/a",
|
|
ctx.moteurOk and "ok" or "ARRETE"),
|
|
ctx.moteurOk and colors.white or colors.red)
|
|
return y + 1
|
|
end
|
|
|
|
local function dessinerPageMode(ctx)
|
|
local modes = { "off", "inactif", "calibrage", "stationnement",
|
|
"vol", "drone", "atterrissage", "auto" }
|
|
local x, y = 1, 2
|
|
for _, m in ipairs(modes) do
|
|
local verrou = ctx.verrous[m]
|
|
local couleur = colors.gray
|
|
if m == ctx.mode then couleur = colors.green
|
|
elseif verrou then couleur = colors.red end
|
|
if x + #m + 2 > L then x, y = 1, y + 1 end
|
|
bouton(x, y, m, { type = "mode", valeur = m, verrou = verrou },
|
|
couleur)
|
|
x = x + #m + 3
|
|
end
|
|
y = y + 2
|
|
|
|
local mode = ctx.mode
|
|
y = blocCommun(y, ctx)
|
|
if mode ~= "off" then y = blocStress(y, ctx) end
|
|
y = y + 1
|
|
|
|
if mode == "stationnement" or mode == "vol" or mode == "drone"
|
|
or mode == "auto" then
|
|
y = dessinerArt(1, y, ctx)
|
|
end
|
|
|
|
if mode == "vol" or mode == "drone" then
|
|
texte(1, y, ("vit %.2f b/s alt %.1f -> %.0f"):format(
|
|
materiel.vitesseHorizontale(), materiel.lireAltitude(),
|
|
ctx.consigneY or 0))
|
|
y = y + 1
|
|
bouton(1, y, "-10", { type = "delta", valeur = -10 })
|
|
bouton(7, y, "-1", { type = "delta", valeur = -1 })
|
|
bouton(12, y, "+1", { type = "delta", valeur = 1 })
|
|
bouton(17, y, "+10", { type = "delta", valeur = 10 })
|
|
y = y + 1
|
|
if mode == "drone" then
|
|
texte(1, y, "INCLINAISON LIBRE (joystick = assiette)",
|
|
colors.orange)
|
|
y = y + 1
|
|
end
|
|
elseif mode == "atterrissage" then
|
|
texte(1, y, ("distance au sol: %.1f"):format(
|
|
materiel.lireDistanceSol()))
|
|
y = y + 1
|
|
elseif mode == "auto" then
|
|
texte(1, y, ("vit %.2f b/s alt %.1f cible %s dist %s"):format(
|
|
materiel.vitesseHorizontale(), materiel.lireAltitude(),
|
|
ctx.cible or "-", ctx.distanceCible
|
|
and ("%.0f"):format(ctx.distanceCible) or "-"))
|
|
y = y + 1
|
|
-- points enregistres en boutons + annulation
|
|
local x2 = 1
|
|
for nom in pairs(ctx.points or {}) do
|
|
if x2 + #nom + 2 > L - 10 then break end
|
|
bouton(x2, y, nom, { type = "ptgo", nom = nom }, colors.blue)
|
|
x2 = x2 + #nom + 3
|
|
end
|
|
bouton(L - 9, y, "ANNULER", { type = "annuler" }, colors.red)
|
|
y = y + 1
|
|
-- zone de saisie + pave numerique (coordonnees: "x z" ou "x y z")
|
|
texte(1, y, "coord: " .. (ctx.saisieCible or "") .. "_")
|
|
y = y + 1
|
|
local paves = { "1","2","3","4","5","6","7","8","9","0","-" }
|
|
local x3 = 1
|
|
for _, p in ipairs(paves) do
|
|
bouton(x3, y, p, { type = "pave", valeur = p })
|
|
x3 = x3 + 4
|
|
end
|
|
bouton(x3, y, "esp", { type = "pave", valeur = " " })
|
|
x3 = x3 + 6
|
|
bouton(x3, y, "eff", { type = "pave", valeur = "eff" })
|
|
x3 = x3 + 6
|
|
bouton(x3, y, "GO", { type = "pave", valeur = "go" }, colors.green)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- PAGE CALIBRATIONS
|
|
------------------------------------------------------------------
|
|
local function dessinerPageCalib(ctx)
|
|
texte(1, 2, "gris: a faire jaune: en cours vert: fait",
|
|
colors.lightGray)
|
|
local x, y = 1, 3
|
|
for _, c in ipairs({ "gimbal", "joystick", "typew", "rsc",
|
|
"capteurs", "pid" }) do
|
|
local statut = ctx.calibStatuts[c]
|
|
local couleur = colors.gray
|
|
if statut == "encours" then couleur = colors.yellow
|
|
elseif statut == "fait" then couleur = colors.green end
|
|
if x + #c + 2 > L then x, y = 1, y + 1 end
|
|
bouton(x, y, c, { type = "calib", valeur = c }, couleur,
|
|
statut == "encours" and colors.black or colors.white)
|
|
x = x + #c + 3
|
|
end
|
|
y = y + 2
|
|
|
|
if ctx.calibTexte then
|
|
texte(1, y, ctx.calibTexte:sub(1, L), colors.yellow)
|
|
y = y + 1
|
|
end
|
|
if ctx.calibProgres then
|
|
local largeur = L - 2
|
|
local rempli = math.floor(ctx.calibProgres * largeur + 0.5)
|
|
moniteur.setCursorPos(1, y)
|
|
moniteur.setBackgroundColor(colors.green)
|
|
moniteur.write((" "):rep(rempli))
|
|
moniteur.setBackgroundColor(colors.gray)
|
|
moniteur.write((" "):rep(largeur - rempli))
|
|
moniteur.setBackgroundColor(colors.black)
|
|
y = y + 2
|
|
end
|
|
if ctx.rscViolet then
|
|
texte(1, y, "en violet: RSC en cours", colors.purple)
|
|
y = dessinerArt(1, y + 1, ctx)
|
|
end
|
|
texte(1, H - 1, "sol: gimbal joystick typew rsc (mode calibrage)",
|
|
colors.lightGray)
|
|
texte(1, H, "vol: capteurs pid (mode vol)", colors.lightGray)
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- PAGE JOURNAL
|
|
------------------------------------------------------------------
|
|
local function dessinerPageJournal()
|
|
local lignes = journal.recents(H - 2)
|
|
for i, ligne in ipairs(lignes) do
|
|
texte(1, 1 + i, ligne:sub(1, L),
|
|
ligne:find("ERREUR") and colors.red
|
|
or ligne:find("ALERTE") and colors.orange or colors.white)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- RENDU GLOBAL
|
|
------------------------------------------------------------------
|
|
function ihm.rafraichirMoniteur(ctx)
|
|
zones = {}
|
|
moniteur.setBackgroundColor(colors.black)
|
|
moniteur.clear()
|
|
|
|
-- barre d'onglets
|
|
local onglets = { mode = "MODE", calib = "CALIB", journal = "JOURNAL" }
|
|
local x = 1
|
|
for _, id in ipairs({ "mode", "calib", "journal" }) do
|
|
bouton(x, 1, onglets[id], { type = "page", valeur = id },
|
|
page == id and colors.green or colors.gray)
|
|
x = x + #onglets[id] + 3
|
|
end
|
|
texte(math.min(x + 1, L - #ctx.mode), 1, ctx.mode, colors.lightGray)
|
|
|
|
if page == "mode" then dessinerPageMode(ctx)
|
|
elseif page == "calib" then dessinerPageCalib(ctx)
|
|
else dessinerPageJournal() end
|
|
end
|
|
|
|
function ihm.traiterToucher(tx, ty)
|
|
for _, z in ipairs(zones) do
|
|
if tx >= z.x1 and tx <= z.x2 and ty >= z.y1 and ty <= z.y2 then
|
|
if z.action.type == "page" then
|
|
page = z.action.valeur
|
|
return { type = "redessiner" }
|
|
end
|
|
return z.action
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ihm.reglerPage(p) page = p end
|
|
|
|
function ihm.attendreToucher()
|
|
while true do
|
|
local ev = os.pullEvent("monitor_touch")
|
|
if ev then return end
|
|
end
|
|
end
|
|
|
|
return ihm
|
|
end
|
|
|
|
return Ihm
|