cctweaked_drone/lib/ihm.lua

451 lines
16 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
local surcharge = ctx.poidsEstime
and ctx.poidsMax and ctx.poidsEstime > ctx.poidsMax
texte(1, y, ("poids est. %s / max %s trim %s/%s"):format(
ctx.poidsEstime and ("%.0f"):format(ctx.poidsEstime) or "?",
ctx.poidsMax and ("%.0f"):format(ctx.poidsMax) or "?",
ctx.trimTangage and ("%+.0f"):format(ctx.trimTangage) or "?",
ctx.trimRoulis and ("%+.0f"):format(ctx.trimRoulis) or "?"),
surcharge and colors.red or colors.white)
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",
"rscman", "capteurs", "pid", "pidmath" }) 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 rscman (mode calibrage)",
colors.lightGray)
texte(1, H, "vol: capteurs pid pidmath (mode vol)",
colors.lightGray)
end
------------------------------------------------------------------
-- PAGE PARAMS (edition tactile; surcharges dans drone.etat)
------------------------------------------------------------------
local function dessinerPageParams(ctx)
texte(1, 2, "surcharges sauvees dans drone.etat", colors.lightGray)
local y = 3
for i2, p in ipairs(ctx.params or {}) do
texte(1, y, ("%-24s"):format(p.label))
texte(26, y, tostring(p.valeur), colors.yellow)
bouton(L - 9, y, "-", { type = "param", idx = i2, sens = -1 })
bouton(L - 4, y, "+", { type = "param", idx = i2, sens = 1 })
y = y + 1
if y > H then break end
end
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",
params = "PARAM", journal = "JOURNAL" }
local x = 1
for _, id in ipairs({ "mode", "calib", "params", "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)
elseif page == "params" then dessinerPageParams(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
-- Art interactif plein ecran: l'utilisateur touche la position de
-- l'helice/du propulseur qui tourne. Retourne le role choisi, ou
-- nil (annulation). `affectes` (role -> nom) grise le deja pris.
function ihm.choisirRole(titre, affectes)
local locales = {}
local function boutonLocal(x, y, texteB, role, couleur)
moniteur.setCursorPos(x, y)
moniteur.setBackgroundColor(couleur)
moniteur.write(" " .. texteB .. " ")
moniteur.setBackgroundColor(colors.black)
table.insert(locales, {
x1 = x, y1 = y, x2 = x + #texteB + 1, y2 = y, role = role,
})
end
moniteur.setBackgroundColor(colors.black)
moniteur.clear()
texte(1, 1, titre:sub(1, L), colors.yellow)
texte(1, 2, "Touchez la position qui TOURNE", colors.lightGray)
local function couleurChoix(role)
if affectes[role] then return colors.gray end
return colors.blue
end
local g, d = 8, L - 12
texte(math.floor(L / 2) - 2, 4, "AVANT", colors.lightGray)
boutonLocal(g, 5, "lf", "lf", couleurChoix("lf"))
boutonLocal(d, 5, "rf", "rf", couleurChoix("rf"))
boutonLocal(1, 7, "prop_l", "prop_l", couleurChoix("prop_l"))
boutonLocal(L - 8, 7, "prop_r", "prop_r", couleurChoix("prop_r"))
boutonLocal(g, 9, "lb", "lb", couleurChoix("lb"))
boutonLocal(d, 9, "rb", "rb", couleurChoix("rb"))
boutonLocal(1, 11, "ANNULER", nil, colors.red)
while true do
local _, _, tx, ty = os.pullEvent("monitor_touch")
for _, z in ipairs(locales) do
if tx >= z.x1 and tx <= z.x2 and ty >= z.y1 and ty <= z.y2 then
if z.role == nil then return nil end
if not affectes[z.role] then return z.role end
texte(1, 12, "deja affecte, choisir une autre position",
colors.orange)
end
end
end
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