-------------------------------------------------------------------- -- 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) niv %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.niveauMoteur or "?", ctx.moteurOk and "ok" or "ARRETE"), ctx.moteurOk and colors.white or colors.red) if ctx.stressPrevu then texte(1, y + 1, ("besoin prevu %.0f su"):format(ctx.stressPrevu), colors.lightGray) return y + 2 end 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", "moteur", "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 moteur (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