cctweaked_drone/drone.lua
2026-07-17 13:54:31 +02:00

610 lines
20 KiB
Lua

--------------------------------------------------------------------
-- drone.lua : controleur central du quadricoptere Create Aeronautics
--
-- Usage :
-- drone lance le controleur
-- drone maj <url_base> installe / met a jour depuis un depot
--
-- Modes : off, inactif, stationnement, vol, drone, atterrissage,
-- auto, position
-- Clavier du PC : auto <x> <z> | position <x> <y> <z>
-- pt save <nom> | pt go <nom> | pt del <nom> | pt list
-- rtb (retour au point de decollage)
--------------------------------------------------------------------
local ARGS = { ... }
local BASE = fs.getDir(shell.getRunningProgram())
--------------------------------------------------------------------
-- INSTALLEUR / MISE A JOUR INTEGRE
--------------------------------------------------------------------
local FICHIERS = {
"drone.lua",
"lib/journal.lua", "lib/conf.lua", "lib/etat.lua", "lib/pid.lua",
"lib/materiel.lua", "lib/pilotage.lua", "lib/navigation.lua",
"lib/ihm.lua", "lib/calibration.lua",
}
if ARGS[1] == "maj" then
local url = ARGS[2]
if not url then error("usage: drone maj <url_base>", 0) end
if not http then error("API http desactivee (config CC:Tweaked)", 0) end
url = url:gsub("/$", "")
for _, fichier in ipairs(FICHIERS) do
write(fichier .. " ... ")
local reponse, err = http.get(url .. "/" .. fichier)
if not reponse then error("echec: " .. tostring(err), 0) end
local contenu = reponse.readAll()
reponse.close()
local chemin = fs.combine(BASE, fichier)
fs.makeDir(fs.getDir(chemin))
local f = fs.open(chemin, "w")
f.write(contenu)
f.close()
print("ok")
end
print("mise a jour terminee, relancer: drone")
return
end
--------------------------------------------------------------------
-- CHARGEMENT DES MODULES
--------------------------------------------------------------------
local function charger(chemin)
local complet = fs.combine(BASE, chemin)
if not fs.exists(complet) then
error(chemin .. " manquant: lancer 'drone maj <url_base>'", 0)
end
return dofile(complet)
end
local journal = charger("lib/journal.lua")
local ModConf = charger("lib/conf.lua")
local conf = ModConf.charger(journal)
local Etat = charger("lib/etat.lua")
local etat = Etat.charger(journal)
local Pid = charger("lib/pid.lua")
local materiel = charger("lib/materiel.lua").initialiser(conf, etat, journal)
-- invalidation de la calibration PID si la conf a change
local empreinte = ModConf.empreintePid(conf)
if etat.pid and etat.empreintePid ~= empreinte then
journal.alerte("conf modifiee: gains PID calibres INVALIDES, "
.. "retour aux gains de la conf (relancer calib pid)")
etat.pid = nil
end
local pilotage = charger("lib/pilotage.lua")
.nouveau(conf, etat, materiel, Pid, journal)
local navigation = charger("lib/navigation.lua")
.nouveau(conf, materiel, journal)
local Calibration = charger("lib/calibration.lua")
local ihm = charger("lib/ihm.lua").nouveau(conf, materiel, journal)
os.setComputerLabel("PC_central")
--------------------------------------------------------------------
-- ETAT DE VOL
--------------------------------------------------------------------
local DT = 0.1
local mode = etat.mode or "off"
local consigneY = etat.consigneY or materiel.lireAltitude()
local cibleNav = nil
local phaseAuto = nil
local stress = nil
local carburantPct, autonomieMin = nil, nil
local saisieTypewriter = ""
local enCalibration = false
local alerteMoteurA = 0
local function sauverEtat()
etat.mode, etat.consigneY = mode, consigneY
Etat.sauver(etat)
end
--------------------------------------------------------------------
-- VERROUS: modes indisponibles tant que des calibrations manquent
--------------------------------------------------------------------
local function verrouDe(m)
local gimbalOk = etat.gimbal ~= nil
local helicesOk = #materiel.helicesManquantes() == 0
local joystickOk = etat.joystick ~= nil
if m == "off" or m == "inactif" then return nil end
if not gimbalOk then return "calib gimbal requise" end
if m == "stationnement" then return nil end
if not helicesOk then return "calib rsc requise" end
if (m == "vol" or m == "drone") and not joystickOk then
return "calib joystick requise"
end
if (m == "auto" or m == "position") and not materiel.lirePosition() then
return "pas de GPS"
end
return nil
end
local function verrous()
local v = {}
for _, m in ipairs({ "off", "inactif", "stationnement", "vol",
"drone", "atterrissage", "auto", "position" }) do
v[m] = verrouDe(m)
end
return v
end
--------------------------------------------------------------------
-- CHANGEMENTS DE MODE
--------------------------------------------------------------------
local function changerMode(nouveau, raison)
if nouveau == mode then return end
local verrou = verrouDe(nouveau)
if verrou then
journal.alerte(nouveau .. " refuse: " .. verrou)
ihm.message(nouveau .. ": " .. verrou)
return
end
if nouveau == "stationnement"
and not (materiel.auSol() and materiel.horizontal()) then
journal.alerte("stationnement refuse: au sol et horizontal requis")
ihm.message("stationnement: sol+plat requis")
return
end
mode = nouveau
pilotage.raz()
navigation.razCap()
if mode == "vol" or mode == "drone" then
consigneY = materiel.lireAltitude()
elseif mode == "auto" then
phaseAuto = "decollage"
local x, y, z = materiel.lirePosition()
etat.decollage = { x = x, y = y, z = z }
consigneY = conf.Y_VOL
elseif mode == "off" then
pilotage.arreter()
end
journal.info("mode: " .. mode .. (raison and (" (" .. raison .. ")") or ""))
sauverEtat()
end
--------------------------------------------------------------------
-- DESCENTE SECURISEE (garde-fou ocean)
-- La descente sous Y_MINI exige un sol POSABLE detecte par
-- l'optical sensor; sinon: maintien a Y_MINI + alerte.
--------------------------------------------------------------------
local function descendre()
local cible = consigneY - conf.VITESSE_ATTERRISSAGE * DT
if cible < conf.Y_MINI and not materiel.solPosable() then
ihm.message("sol non posable: maintien Y_MINI")
return math.max(consigneY, conf.Y_MINI)
end
return cible
end
--------------------------------------------------------------------
-- UN PAS DE CONTROLE PAR MODE
--------------------------------------------------------------------
local function pasControle()
if enCalibration then return end
-- alerte moteur: dans tous les modes ou il devrait tourner
if mode ~= "off" and not materiel.moteurTourne() then
if os.clock() - alerteMoteurA > 5 then
alerteMoteurA = os.clock()
journal.erreur("le moteur ne tourne pas alors qu'il le devrait")
ihm.message("ERREUR: MOTEUR ARRETE", 5)
end
end
if mode == "off" then
pilotage.arreter()
elseif mode == "inactif" then
-- moteur allume, tout a zero, pret pour les calibrations
materiel.toutArreter()
elseif mode == "stationnement" then
if not (materiel.auSol() and materiel.horizontal()) then
changerMode("vol", "conditions de stationnement perdues")
return
end
pilotage.plaquer()
elseif mode == "vol" then
local avance, virage = materiel.lireJoystick()
pilotage.reguler(consigneY, { tangage = 0, roulis = 0 },
avance, virage, DT)
elseif mode == "drone" then
local avance, virage = materiel.lireJoystick()
pilotage.reguler(consigneY, {
tangage = avance * conf.ANGLE_DRONE_MAX,
roulis = -virage * conf.ANGLE_DRONE_MAX,
}, 0, 0, DT)
elseif mode == "atterrissage" then
if materiel.auSol() then
changerMode("stationnement", "sol atteint")
return
end
consigneY = descendre()
pilotage.reguler(consigneY, { tangage = 0, roulis = 0 }, 0, 0, DT)
elseif mode == "auto" then
if phaseAuto == "decollage" then
local alt = pilotage.reguler(conf.Y_VOL,
{ tangage = 0, roulis = 0 }, 0, 0, DT)
if math.abs(alt - conf.Y_VOL) <= 0.5 then
phaseAuto = "croisiere"
journal.info("auto: croisiere vers la cible")
end
elseif phaseAuto == "croisiere" then
local avance, virage, distance = navigation.rallier(
cibleNav.x, cibleNav.z)
if not avance then
changerMode("vol", "GPS perdu")
return
end
pilotage.reguler(conf.Y_VOL, { tangage = 0, roulis = 0 },
avance, virage, DT)
if distance <= conf.NAV.SEUIL_ARRIVEE then
phaseAuto = "atterrissage"
consigneY = materiel.lireAltitude()
journal.info("auto: cible atteinte, atterrissage")
end
else
if materiel.auSol() then
changerMode("stationnement", "mission terminee")
return
end
consigneY = descendre()
pilotage.reguler(consigneY, { tangage = 0, roulis = 0 }, 0, 0, DT)
end
elseif mode == "position" then
local avance, virage = navigation.rallier(cibleNav.x, cibleNav.z)
if not avance then
changerMode("vol", "GPS perdu")
return
end
local cibleY = cibleNav.y
if cibleY < materiel.lireAltitude() then
-- descente limitee par la presence d'un sol
if materiel.lireDistanceSol() <= conf.DIST_SOL + 0.5 then
cibleY = materiel.lireAltitude()
elseif cibleY < conf.Y_MINI and not materiel.solPosable() then
cibleY = conf.Y_MINI
end
end
pilotage.reguler(cibleY, { tangage = 0, roulis = 0 },
avance, virage, DT)
end
end
--------------------------------------------------------------------
-- CALIBRATIONS
--------------------------------------------------------------------
local ui = {
inviter = function(texte)
ihm.message(texte, 3600)
pcall(ihm.rafraichirConduite,
{ mode = mode, consigneY = consigneY })
journal.info("[calib] " .. texte)
end,
attendreToucher = ihm.attendreToucher,
}
local function lancerCalibration(quoi)
enCalibration = true
local ok, err = pcall(function()
if quoi == "gimbal" then
Calibration.gimbal(conf, etat, materiel, journal, Etat, ui)
elseif quoi == "joystick" then
Calibration.joystick(conf, etat, materiel, journal, Etat, ui)
elseif quoi == "typew" then
Calibration.typewriter(conf, etat, materiel, journal, Etat, ui)
elseif quoi == "rsc" then
if mode ~= "inactif" then
journal.alerte("calib rsc: passer en mode inactif d'abord")
else
Calibration.rsc(conf, etat, materiel, journal, Etat, ui)
end
elseif quoi == "capteurs" or quoi == "pid" then
if mode ~= "vol" then
journal.alerte("calib " .. quoi .. ": passer en mode vol d'abord")
elseif quoi == "pid" then
Calibration.pid(conf, etat, materiel, pilotage, journal, Etat,
empreinte)
else
local function tenir(duree, angles, avance)
local cible = materiel.lireAltitude()
return function()
for _ = 1, math.floor(duree / DT) do
pilotage.reguler(cible, angles, avance or 0, 0, DT)
sleep(DT)
end
end
end
Calibration.capteurs(conf, etat, materiel, journal, Etat, ui, {
monter = function()
local cible = materiel.lireAltitude() + 3
for _ = 1, 30 do
pilotage.reguler(cible, { tangage = 0, roulis = 0 },
0, 0, DT)
sleep(DT)
end
end,
avancer = tenir(3, { tangage = 0, roulis = 0 }, 0.6),
gauche = tenir(3,
{ tangage = 0, roulis = -conf.ANGLE_DRONE_MAX / 2 }, 0),
})
end
end
end)
if not ok then journal.erreur("calibration: " .. tostring(err)) end
ihm.message("", 0)
enCalibration = false
end
--------------------------------------------------------------------
-- POINTS NOMMES / RETOUR DECOLLAGE
--------------------------------------------------------------------
local function nbPoints()
local n = 0
for _ in pairs(etat.points) do n = n + 1 end
return n
end
local function commandePoint(mots)
if mots[2] == "save" and mots[3] then
if nbPoints() >= conf.NAV.MAX_POINTS and not etat.points[mots[3]] then
print(("maximum %d points"):format(conf.NAV.MAX_POINTS))
return
end
local x, y, z = materiel.lirePosition()
if not x then print("pas de GPS") return end
etat.points[mots[3]] = { x = x, y = y, z = z }
Etat.sauver(etat)
print(("point '%s' enregistre (%.0f %.0f %.0f)"):format(mots[3], x, y, z))
elseif mots[2] == "go" and mots[3] then
local p = etat.points[mots[3]]
if not p then print("point inconnu") return end
cibleNav = { x = p.x, z = p.z }
changerMode("auto", "pt go " .. mots[3])
elseif mots[2] == "del" and mots[3] then
etat.points[mots[3]] = nil
Etat.sauver(etat)
print("point supprime")
elseif mots[2] == "list" then
for nom, p in pairs(etat.points) do
print((" %s : %.0f %.0f %.0f"):format(nom, p.x, p.y, p.z))
end
else
print("pt save <nom> | pt go <nom> | pt del <nom> | pt list")
end
end
--------------------------------------------------------------------
-- TACHES PARALLELES
--------------------------------------------------------------------
local function tacheControle()
while true do
local ok, err = pcall(pasControle)
if not ok then
journal.erreur("controle: " .. tostring(err))
pilotage.arreter()
end
sleep(DT)
end
end
-- moteur asservi au stressometer; surcharge: reduction propulseurs
local function tacheMoteur()
local niveau = 8
local enSurcharge = false
while true do
if mode == "off" then
materiel.reglerMoteur(0)
else
stress = materiel.lireStress()
if stress then
if stress > conf.MOTEUR.STRESS_HAUT and niveau < 15 then
niveau = niveau + 1
elseif stress < conf.MOTEUR.STRESS_BAS and niveau > 1 then
niveau = niveau - 1
end
-- surcharge malgre moteur au maximum: delester les propulseurs
if stress > conf.MOTEUR.SURCHARGE and niveau >= 15 then
if not enSurcharge then
enSurcharge = true
journal.alerte("surcharge de stress: propulseurs reduits")
ihm.message("SURCHARGE: propulseurs reduits", 10)
end
pilotage.reglerFacteurProp(0.3)
elseif enSurcharge and stress < conf.MOTEUR.STRESS_HAUT then
enSurcharge = false
pilotage.reglerFacteurProp(1.0)
journal.info("surcharge resorbee")
end
else
niveau = 15
end
materiel.reglerMoteur(niveau)
end
sleep(conf.MOTEUR.PERIODE)
end
end
-- carburant: pourcentage de reserve + autonomie estimee par
-- APPRENTISSAGE GLISSANT (EMA) de la consommation. Les hausses de
-- quantite (ravitaillement) sont exclues de l'apprentissage.
local function tacheCarburant()
local quantitePrec, tPrec = nil, nil
local consoLissee = nil -- mB/s apprise
while true do
local quantite, capacite = materiel.lireCarburant()
if quantite then
carburantPct = quantite / capacite
local t = os.clock()
if quantitePrec and t > tPrec then
local conso = (quantitePrec - quantite) / (t - tPrec)
if conso >= 0 then -- conso negative = ravitaillement: ignore
local a = conf.CARBURANT.LISSAGE
consoLissee = consoLissee
and (a * conso + (1 - a) * consoLissee)
or conso
end
end
quantitePrec, tPrec = quantite, t
autonomieMin = nil
if consoLissee and consoLissee > 0 then
autonomieMin = quantite / consoLissee / 60
end
if carburantPct <= conf.CARBURANT.SEUIL_ALERTE then
ihm.message("CARBURANT BAS", 4)
end
pcall(ihm.rafraichirCarburant, carburantPct, autonomieMin)
end
sleep(conf.CARBURANT.PERIODE)
end
end
-- typewriter: saisie de la consigne d'altitude (scrutation des
-- codes ASCII, clavier qwerty). Mappage par defaut integre; la
-- calibration 'typew' (etat.typewriter) le remplace si presente.
-- 0..9 chiffres de la saisie
-- Entree valider la saisie comme nouvelle consigne
-- Backspace effacer le dernier chiffre
-- + / - ajuster la consigne de +-1 (hors saisie en cours)
local ASCII = {
[13] = "valider", [10] = "valider",
[8] = "effacer", [127] = "effacer",
[43] = "plus", [45] = "moins",
}
for code = 48, 57 do ASCII[code] = string.char(code) end
local function tacheTypewriter()
local pressees = {}
while true do
if not enCalibration then
local courantes = {}
for _, code in ipairs(materiel.lireTouches()) do
courantes[code] = true
if not pressees[code] then
local symbole = (etat.typewriter or ASCII)[code]
if symbole == "valider" then
local valeur = tonumber(saisieTypewriter)
if valeur then
consigneY = valeur
journal.info(("typewriter: consigne %d"):format(valeur))
sauverEtat()
end
saisieTypewriter = ""
elseif symbole == "effacer" then
saisieTypewriter = saisieTypewriter:sub(1, -2)
elseif symbole == "plus" and #saisieTypewriter == 0 then
consigneY = consigneY + 1
sauverEtat()
elseif symbole == "moins" and #saisieTypewriter == 0 then
consigneY = consigneY - 1
sauverEtat()
elseif symbole and #symbole == 1 then
saisieTypewriter = (saisieTypewriter .. symbole):sub(1, 6)
end
end
end
pressees = courantes
end
sleep(0.1)
end
end
local function tacheIhm()
local prochainDessin = 0
while true do
if os.clock() >= prochainDessin and not enCalibration then
local px, py, pz = materiel.lirePosition()
pcall(ihm.rafraichirConduite, {
mode = mode, consigneY = consigneY, saisie = saisieTypewriter,
})
pcall(ihm.rafraichirMoniteur, {
mode = mode,
consigneY = consigneY,
stress = stress,
moteurOk = mode == "off" or materiel.moteurTourne(),
gimbalOk = etat.gimbal ~= nil,
carburantPct = carburantPct,
minutes = autonomieMin,
position = px and { x = px, y = py, z = pz } or nil,
points = nbPoints(),
verrous = verrous(),
})
prochainDessin = os.clock() + 0.5
end
local minuteur = os.startTimer(0.5)
local ev, a, b, c = os.pullEvent()
if ev == "monitor_touch" and not enCalibration then
local action = ihm.traiterToucher(b, c)
if action then
if action.type == "mode" then
if action.verrou then
ihm.message(action.valeur .. ": " .. action.verrou)
elseif action.valeur == "auto" or action.valeur == "position" then
ihm.message("cible au clavier: " .. action.valeur .. " x [y] z")
else
changerMode(action.valeur, "moniteur")
end
elseif action.type == "delta" then
consigneY = consigneY + action.valeur
sauverEtat()
elseif action.type == "calib" then
lancerCalibration(action.valeur)
end
end
end
if ev ~= "timer" or a ~= minuteur then os.cancelTimer(minuteur) end
end
end
local function tacheClavier()
while true do
local ligne = read()
local mots = {}
for mot in ligne:gmatch("%S+") do table.insert(mots, mot) end
if mots[1] == "auto" and tonumber(mots[2]) and tonumber(mots[3]) then
cibleNav = { x = tonumber(mots[2]), z = tonumber(mots[3]) }
changerMode("auto", "clavier")
elseif mots[1] == "position" and tonumber(mots[2])
and tonumber(mots[3]) and tonumber(mots[4]) then
cibleNav = { x = tonumber(mots[2]), y = tonumber(mots[3]),
z = tonumber(mots[4]) }
changerMode("position", "clavier")
elseif mots[1] == "pt" then
commandePoint(mots)
elseif mots[1] == "rtb" then
if etat.decollage then
cibleNav = { x = etat.decollage.x, z = etat.decollage.z }
changerMode("auto", "retour decollage")
else
print("pas de point de decollage enregistre")
end
elseif mots[1] then
print("auto <x> <z> | position <x> <y> <z> | pt ... | rtb")
end
end
end
journal.info(("drone: demarrage en mode %s (consigne %.0f)")
:format(mode, consigneY))
if mode == "vol" or mode == "drone" then
consigneY = materiel.lireAltitude()
end
parallel.waitForAny(tacheControle, tacheMoteur, tacheCarburant,
tacheTypewriter, tacheIhm, tacheClavier)