-------------------------------------------------------------------- -- lib/calibration.lua : calibrations du drone. -- Les calibrations assistees utilisent `ui` fourni par drone.lua : -- ui.inviter(texte) affiche l'invite sur l'ecran de conduite -- ui.attendreToucher() attend un toucher du moniteur (validation) -- -- gimbal : association des 2 angles bruts aux axes tangage / -- roulis + signes, PROPRE A CHAQUE VAISSEAU. Assistee: -- l'utilisateur incline le drone nez bas puis a gauche. -- joystick : identification des faces du redstone relay. Assistee. -- typewriter : apprentissage des codes de touches. Assistee. -- rsc : affectation des RSC par impulsions (mode inactif, au -- sol, EXIGE gimbal calibre). -- capteurs : axes ET directions des velocity sensors, par -- mouvements commandes (en vol). -- pid : gains d'altitude, zero depassement (en vol). -------------------------------------------------------------------- local Calibration = {} local function copierGains(g) return { kp = g.kp, ki = g.ki, kd = g.kd } end -------------------------------------------------------------------- -- GIMBAL (assistee, drone au sol ou stable) -------------------------------------------------------------------- function Calibration.gimbal(conf, etat, materiel, journal, Etat, ui) local seuil = conf.CALIB.IMPULSION_SEUIL ui.progres(0, "mise a plat") ui.inviter("Drone a plat, touchez l'ecran") ui.attendreToucher() local x0, z0 = materiel.lireAnglesBruts() ui.progres(0.25, "nez bas") ui.inviter("Inclinez NEZ vers le BAS, touchez") ui.attendreToucher() local x1, z1 = materiel.lireAnglesBruts() local dx, dz = x1 - x0, z1 - z0 if math.max(math.abs(dx), math.abs(dz)) < seuil then journal.alerte("calib gimbal: inclinaison insuffisante, abandon") ui.inviter("Echec: inclinaison trop faible") return false end local indexTangage = (math.abs(dx) >= math.abs(dz)) and 1 or 2 local deltaT = (indexTangage == 1) and dx or dz -- convention: nez bas => tangage normalise NEGATIF local signeTangage = (deltaT < 0) and 1 or -1 ui.progres(0.5, "retour a plat") ui.inviter("Revenez a plat, touchez") ui.attendreToucher() x0, z0 = materiel.lireAnglesBruts() ui.progres(0.75, "penche a gauche") ui.inviter("Penchez a GAUCHE, touchez") ui.attendreToucher() x1, z1 = materiel.lireAnglesBruts() local indexRoulis = (indexTangage == 1) and 2 or 1 local deltaR = (indexRoulis == 1) and (x1 - x0) or (z1 - z0) if math.abs(deltaR) < seuil then journal.alerte("calib gimbal: roulis insuffisant, abandon") ui.inviter("Echec: roulis trop faible") return false end -- convention: penche a gauche => roulis normalise NEGATIF local signeRoulis = (deltaR < 0) and 1 or -1 etat.gimbal = { indexTangage = indexTangage, signeTangage = signeTangage, indexRoulis = indexRoulis, signeRoulis = signeRoulis, } Etat.sauver(etat) journal.info(("calib gimbal: tangage=angle%d(x%+d) roulis=angle%d(x%+d)") :format(indexTangage, signeTangage, indexRoulis, signeRoulis)) ui.progres(1, "terminee") ui.inviter("Gimbal calibre, remettez a plat") return true end -------------------------------------------------------------------- -- JOYSTICK (assistee) -------------------------------------------------------------------- function Calibration.joystick(conf, etat, materiel, journal, Etat, ui) if not materiel.relay then journal.alerte("calib joystick: pas de redstone_relay") return false end local seuil = conf.CALIB.JOYSTICK_SEUIL local mapping = {} local function faceActive(exclues) for face, valeur in pairs(materiel.lireFacesRelay()) do if valeur >= seuil and not exclues[face] then return face end end return nil end local function attendreNeutre() while true do local actif = false for _, v in pairs(materiel.lireFacesRelay()) do if v > 0 then actif = true end end if not actif then return end sleep(0.1) end end local exclues = {} for idx, direction in ipairs({ "devant", "derriere", "gauche", "droite" }) do ui.progres((idx - 1) / 4, direction) ui.inviter("Joystick a fond: " .. direction:upper()) local face = nil while not face do face = faceActive(exclues) sleep(0.1) end mapping[direction] = face exclues[face] = true journal.info(("calib joystick: %s = %s"):format(direction, face)) ui.inviter(direction .. " = " .. face .. ", relachez") attendreNeutre() end etat.joystick = mapping Etat.sauver(etat) ui.progres(1, "terminee") ui.inviter("Joystick calibre") journal.info("calib joystick: terminee") return true end -------------------------------------------------------------------- -- TYPEWRITER (assistee): apprentissage des codes de touches -------------------------------------------------------------------- function Calibration.typewriter(conf, etat, materiel, journal, Etat, ui) if not materiel.typewriter then journal.alerte("calib typewriter: pas de linked_typewriter") return false end local mapping = {} local connus = {} local function attendreRelachement() while #materiel.lireTouches() > 0 do sleep(0.1) end end local symboles = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "valider", "effacer" } for idx, symbole in ipairs(symboles) do ui.progres((idx - 1) / #symboles, symbole) ui.inviter("Typewriter: appuyez sur " .. symbole:upper()) local code = nil while not code do for _, c in ipairs(materiel.lireTouches()) do if not connus[c] then code = c end end sleep(0.1) end mapping[code] = symbole connus[code] = true journal.info(("calib typewriter: %s = code %s"):format(symbole, tostring(code))) attendreRelachement() end etat.typewriter = mapping Etat.sauver(etat) ui.progres(1, "terminee") ui.inviter("Typewriter calibre") journal.info("calib typewriter: terminee") return true end -------------------------------------------------------------------- -- RSC (mode inactif, au sol, gimbal deja calibre) -------------------------------------------------------------------- function Calibration.rsc(conf, etat, materiel, journal, Etat, ui) if not etat.gimbal then journal.alerte("calib rsc: calibrer le GIMBAL d'abord") return false end if not materiel.auSol() then journal.alerte("calib rsc: le drone doit etre pose au sol") return false end materiel.toutArreter() local C = conf.CALIB -- RECALIBRATION COMPLETE: on repart de zero (recablage possible) local anciensRoles = etat.roles etat.roles = {} materiel.rafraichirAffectations() local noms = {} for nom in pairs(materiel.rsc) do table.insert(noms, nom) end table.sort(noms) -- reference "a plat" pour le retour a l'etat initial entre mesures sleep(1.0) local refT, refR = materiel.lireAssiette() local function retourInitial() local fin = os.clock() + 10 while os.clock() < fin do local t, r = materiel.lireAssiette() if math.abs(t - refT) < C.IMPULSION_SEUIL / 2 and math.abs(r - refR) < C.IMPULSION_SEUIL / 2 then sleep(0.5) return true end sleep(0.2) end return false end local function abandonner(raison) materiel.toutArreter() etat.roles = anciensRoles materiel.rafraichirAffectations() journal.erreur("calib rsc annulee: " .. raison) ui.progres(0, "ECHEC: " .. raison) return false end local affectes = {} -- role -> nom (controle d'unicite) local propulseurs = {} for i, nom in ipairs(noms) do ui.progres((i - 1) / #noms, "impulsion " .. nom) if not retourInitial() then return abandonner("assiette instable avant " .. nom) end journal.info("calib rsc: impulsion sur " .. nom) local p = materiel.rsc[nom] local t0, r0 = materiel.lireAssiette() local vitesse, dt_, dr = 0, 0, 0 while vitesse < conf.VITESSE_RSC_MAX do vitesse = math.min(vitesse + C.IMPULSION_PAS, conf.VITESSE_RSC_MAX) p.setTargetSpeed(vitesse) sleep(0.5) local t, r = materiel.lireAssiette() dt_, dr = t - t0, r - r0 if math.abs(dt_) >= C.IMPULSION_SEUIL and math.abs(dr) >= C.IMPULSION_SEUIL then break end end p.setTargetSpeed(0) if math.abs(dt_) >= C.IMPULSION_SEUIL and math.abs(dr) >= C.IMPULSION_SEUIL then -- assiette normalisee: tangage>0 = nez haut => helice a l'AVANT -- roulis>0 = penche a droite => helice a GAUCHE local role = ((dr > 0) and "l" or "r") .. ((dt_ > 0) and "f" or "b") if affectes[role] then return abandonner(("conflit: %s et %s -> %s") :format(affectes[role], nom, role)) end affectes[role] = nom etat.roles[nom] = role ui.rscViolet(role) journal.info(" -> helice " .. role) else table.insert(propulseurs, nom) journal.info(" -> pas d'effet d'assiette: propulseur") end end ui.rscViolet(nil) if #propulseurs == 2 then etat.roles[propulseurs[1]] = "prop_l" etat.roles[propulseurs[2]] = "prop_r" journal.alerte("calib rsc: prop_l/prop_r affectes ARBITRAIREMENT: " .. "si un virage gauche part a droite, les echanger dans drone.etat") elseif #propulseurs > 0 then return abandonner(("%d propulseur(s) detecte(s), 2 attendus") :format(#propulseurs)) end Etat.sauver(etat) materiel.rafraichirAffectations() -- prise d'effet immediate ui.progres(1, "terminee") journal.info("calib rsc: terminee") return true end -------------------------------------------------------------------- -- RSC MANUELLE (drone trop lourd pour les impulsions). -- Chaque RSC tourne a RPM_MANUEL; l'utilisateur touche sur l'art la -- position de l'helice / du propulseur qui tourne. -------------------------------------------------------------------- function Calibration.rscManuel(conf, etat, materiel, journal, Etat, ui) -- RECALIBRATION COMPLETE local anciensRoles = etat.roles etat.roles = {} materiel.rafraichirAffectations() materiel.toutArreter() local noms = {} for nom in pairs(materiel.rsc) do table.insert(noms, nom) end table.sort(noms) local affectes = {} -- role -> nom for i, nom in ipairs(noms) do ui.progres((i - 1) / #noms, nom .. " a " .. conf.CALIB.RPM_MANUEL .. " rpm") materiel.rsc[nom].setTargetSpeed(conf.CALIB.RPM_MANUEL) local role = ui.choisirRole( ("%s (%d/%d) tourne a %d rpm"):format(nom, i, #noms, conf.CALIB.RPM_MANUEL), affectes) materiel.rsc[nom].setTargetSpeed(0) if role == nil then etat.roles = anciensRoles materiel.rafraichirAffectations() journal.info("calib rsc manuelle: annulee") ui.progres(0, "annulee") return false end affectes[role] = nom etat.roles[nom] = role ui.rscViolet(role) journal.info(("calib rsc manuelle: %s -> %s"):format(nom, role)) end ui.rscViolet(nil) Etat.sauver(etat) materiel.rafraichirAffectations() ui.progres(1, "terminee") journal.info("calib rsc manuelle: terminee") return true end -------------------------------------------------------------------- -- VELOCITY SENSORS (en vol): axes ET directions. -- mouvements = { monter, avancer, gauche } (fonctions bloquantes qui -- commandent le deplacement pendant que ce module mesure) -------------------------------------------------------------------- function Calibration.capteurs(conf, etat, materiel, journal, Etat, ui, mouvements) local noms = {} for nom in pairs(materiel.velocite) do table.insert(noms, nom) end table.sort(noms) if #noms ~= 3 then journal.alerte(("calib capteurs: %d velocity sensors, 3 attendus") :format(#noms)) return false end -- lance un mouvement en parallele des mesures; retourne, pour le -- capteur non exclu a la plus grande amplitude, (nom, moyenne) local function mesurerPendant(mouvement, exclus) local sommes, maxis, n = {}, {}, 0 parallel.waitForAll(mouvement, function() for _ = 1, 30 do for _, nom in ipairs(noms) do if not exclus[nom] then local v = materiel.velocite[nom].getVelocity() or 0 sommes[nom] = (sommes[nom] or 0) + v maxis[nom] = math.max(maxis[nom] or 0, math.abs(v)) end end n = n + 1 sleep(0.1) end end) local meilleur, amplitude = nil, -1 for _, nom in ipairs(noms) do if not exclus[nom] and (maxis[nom] or 0) > amplitude then meilleur, amplitude = nom, maxis[nom] end end return meilleur, (sommes[meilleur] or 0) / math.max(n, 1) end local function signeDe(moyenne) return (moyenne >= 0) and 1 or -1 end -- RECALIBRATION COMPLETE etat.velocite = {} materiel.rafraichirAffectations() ui.progres(0, "montee...") ui.inviter("Calib capteurs: montee...") local capteur, moyenne = mesurerPendant(mouvements.monter, {}) etat.velocite[capteur] = { axe = "vertical", signe = signeDe(moyenne) } journal.info((" vertical: %s (x%+d)"):format(capteur, signeDe(moyenne))) local exclus = { [capteur] = true } ui.progres(0.33, "avancee...") ui.inviter("Calib capteurs: avancee...") capteur, moyenne = mesurerPendant(mouvements.avancer, exclus) etat.velocite[capteur] = { axe = "avant", signe = signeDe(moyenne) } journal.info((" avant: %s (x%+d)"):format(capteur, signeDe(moyenne))) exclus[capteur] = true ui.progres(0.66, "translation gauche...") ui.inviter("Calib capteurs: translation gauche...") capteur, moyenne = mesurerPendant(mouvements.gauche, exclus) etat.velocite[capteur] = { axe = "lateral", signe = signeDe(moyenne) } journal.info((" lateral: %s (x%+d)"):format(capteur, signeDe(moyenne))) Etat.sauver(etat) materiel.rafraichirAffectations() ui.progres(1, "terminee") ui.inviter("Capteurs calibres") journal.info("calib capteurs: terminee") return true end -------------------------------------------------------------------- -- MOTEUR (au sol, mode calibrage, roles RSC deja affectes) -- 1. Balayage des niveaux 1..15: table capacite(niveau) en su. -- 2. Helices seules a RPM_STRESS -> coefficient Kh (su/rpm), puis -- propulseurs seuls -> Kp. Le moteur predictif et le limiteur de -- stress du pilotage utilisent ces donnees. -------------------------------------------------------------------- function Calibration.moteur(conf, etat, materiel, journal, Etat, ui) if not materiel.stresso then journal.alerte("calib moteur: pas de stressometer") return false end if #materiel.helicesManquantes() > 0 then journal.alerte("calib moteur: calibrer les RSC d'abord") return false end materiel.toutArreter() local C = conf.CALIB -- Phase 1: capacite par niveau local capacites = {} for niveau = 1, 15 do ui.progres(niveau / 15 * 0.6, ("capacite niveau %d/15"):format(niveau)) materiel.reglerMoteur(niveau) sleep(C.ATTENTE_NIVEAU) local _, cap = materiel.lireStressBrut() capacites[niveau] = cap or 0 journal.info(("calib moteur: niveau %d -> %d su"):format( niveau, capacites[niveau])) end if capacites[15] <= 0 then journal.erreur("calib moteur: capacite nulle a pleine puissance " .. "(moteur alimente ?)") materiel.reglerMoteur(0) ui.progres(0, "ECHEC: capacite nulle") return false end -- Phase 2: coefficients de consommation, mesures RSC PAR RSC -- (un seul en rotation a la fois: pas d'overstress possible -- pendant la calibration) materiel.reglerMoteur(15) sleep(C.ATTENTE_NIVEAU) local stressBase = materiel.lireStressBrut() or 0 local function coefficient(roles, base01, libelle) local somme, n = 0, 0 for idx, role in ipairs(roles) do ui.progres(base01 + (idx - 1) / #roles * 0.14, libelle .. " " .. role) materiel.reglerRsc(role, C.RPM_STRESS) sleep(C.ATTENTE_NIVEAU) local s = materiel.lireStressBrut() or 0 materiel.reglerRsc(role, 0) sleep(0.5) local delta = s - stressBase if delta > 0 then somme, n = somme + delta / C.RPM_STRESS, n + 1 end end if n == 0 then return 0 end return somme / n end local kh = coefficient({ "lb", "rb", "lf", "rf" }, 0.65, "coeff helice") local kp = coefficient({ "prop_l", "prop_r" }, 0.82, "coeff propulseur") materiel.toutArreter() if kh <= 0 then journal.erreur("calib moteur: coefficient helices nul") ui.progres(0, "ECHEC: coeff nul") return false end etat.capaciteParNiveau = capacites etat.stressParRpm = { helice = kh, prop = kp } Etat.sauver(etat) journal.info(("calib moteur: Kh=%.3f Kp=%.3f su/rpm, capa max %d su") :format(kh, kp, capacites[15])) ui.progres(1, "terminee") return true end -------------------------------------------------------------------- -- PID MATHEMATIQUE (en vol stationnaire, marge d'altitude requise) -- 1. Identification en boucle ouverte autour du feedforward: un -- echelon de +deltaRpm est applique, la reponse en vitesse -- verticale donne le modele FOPDT (gain K, constante de temps -- tau, retard L). -- 2. Gains candidats calcules par COHEN-COON et ZIEGLER-NICHOLS -- (reponse indicielle), pour la montee puis la descente -- (echelon negatif). -- 3. Validation en boucle fermee de chaque candidat (memes criteres -- que l'auto-tune: zero depassement, assiette bornee): le -- meilleur temps de reponse VALIDE est conserve. -------------------------------------------------------------------- local function identifierFOPDT(pilotage, deltaRpm, dt, duree, journal) -- stabiliser au feedforward pur for _ = 1, math.floor(1.5 / dt) do pilotage.pasOuvert(0, dt) sleep(dt) end -- echelon et enregistrement local temps, vitesses = {}, {} local debut = os.clock() while os.clock() - debut < duree do local v = pilotage.pasOuvert(deltaRpm, dt) table.insert(temps, os.clock() - debut) table.insert(vitesses, v) sleep(dt) end pilotage.pasOuvert(0, dt) -- valeur finale: moyenne du dernier quart local somme, n = 0, 0 for i = math.floor(#vitesses * 0.75), #vitesses do somme, n = somme + vitesses[i], n + 1 end local vFinale = somme / math.max(n, 1) if math.abs(vFinale) < 0.05 then journal.alerte("pidmath: reponse trop faible, augmenter l'echelon") return nil end local K = vFinale / deltaRpm -- retard L: premier passage a 5% de vFinale; tau: 63.2% - L local L, t63 = nil, nil for i, v in ipairs(vitesses) do local ratio = v / vFinale if not L and ratio >= 0.05 then L = temps[i] end if not t63 and ratio >= 0.632 then t63 = temps[i] end end if not L or not t63 or t63 <= L then journal.alerte("pidmath: identification impossible (bruit ?)") return nil end local tau = t63 - L L = math.max(L, dt) -- retard minimal: une periode journal.info(("pidmath: K=%.4f tau=%.2f L=%.2f"):format(K, tau, L)) return K, tau, L end local function gainsCohenCoon(K, tau, L) local r = L / tau local kp = (1 / K) * (tau / L) * (4 / 3 + r / 4) local Ti = L * (32 + 6 * r) / (13 + 8 * r) local Td = L * 4 / (11 + 2 * r) return { kp = kp, ki = kp / Ti, kd = kp * Td } end local function gainsZieglerNichols(K, tau, L) local kp = 1.2 * tau / (K * L) return { kp = kp, ki = kp / (2 * L), kd = kp * 0.5 * L } end function Calibration.mathematique(conf, etat, materiel, pilotage, journal, Etat, empreinte, ui) local C = conf.CALIB local dt = 0.1 local base = materiel.lireAltitude() local gains = pilotage.gains() local deltaRpm = math.max(8, conf.VITESSE_RSC_MAX * 0.05) -- meme critere de validation que l'auto-tune local function valider(cible, direction) local debut = os.clock() local extremum = materiel.lireAltitude() local assietteMax = 0 local dansBande, atteint, tempsReponse = nil, false, nil while os.clock() - debut < C.TIMEOUT do local alt = pilotage.reguler(cible, { tangage = 0, roulis = 0 }, 0, 0, dt) local t, r = materiel.lireAssiette() assietteMax = math.max(assietteMax, math.abs(t), math.abs(r)) if direction > 0 then extremum = math.max(extremum, alt) else extremum = math.min(extremum, alt) end if math.abs(alt - cible) <= C.BANDE then dansBande = dansBande or os.clock() if os.clock() - dansBande >= C.DELAI_STABLE then atteint, tempsReponse = true, dansBande - debut break end else dansBande = nil end sleep(dt) end local depassement = (direction > 0) and math.max(0, extremum - cible) or math.max(0, cible - extremum) local valide = atteint and depassement <= C.TOL_DEPASSEMENT and assietteMax <= conf.ANGLE_MAX return valide, tempsReponse or math.huge, depassement end for iDir, dir in ipairs({ { nom = "montee", sens = 1 }, { nom = "descente", sens = -1 }, }) do ui.progres((iDir - 1) / 2, dir.nom .. ": identification") journal.info("pidmath: identification " .. dir.nom) local K, tau, L = identifierFOPDT(pilotage, dir.sens * deltaRpm, dt, 8, journal) if K then local candidats = { { nom = "cohen-coon", g = gainsCohenCoon(math.abs(K), tau, L) }, { nom = "ziegler-nichols", g = gainsZieglerNichols(math.abs(K), tau, L) }, } local meilleur, meilleurTemps = nil, math.huge for iC, c in ipairs(candidats) do ui.progres((iDir - 1) / 2 + iC * 0.2, dir.nom .. ": " .. c.nom) journal.info(("pidmath %s %s: kp=%.1f ki=%.1f kd=%.1f") :format(dir.nom, c.nom, c.g.kp, c.g.ki, c.g.kd)) gains[dir.nom] = c.g pilotage.reglerGains(gains) -- retour a la base puis echelon de validation valider(base, -dir.sens) local valide, temps, dep = valider( base + dir.sens * C.AMPLITUDE, dir.sens) valider(base, -dir.sens) journal.info((" %s t=%.1f dep=%.2f"):format( valide and "VALIDE" or "rejete", temps == math.huge and -1 or temps, dep)) if valide and temps < meilleurTemps then meilleur, meilleurTemps = c, temps end end if meilleur then gains[dir.nom] = meilleur.g journal.info(("pidmath %s: retenu %s"):format(dir.nom, meilleur.nom)) else -- aucun candidat brut valide: version adoucie de cohen-coon local g = gainsCohenCoon(math.abs(K), tau, L) g.kp, g.ki, g.kd = g.kp * 0.6, g.ki * 0.6, g.kd * 0.6 gains[dir.nom] = g journal.alerte("pidmath " .. dir.nom .. ": candidats rejetes, version adoucie appliquee") end pilotage.reglerGains(gains) end end etat.empreintePid = empreinte etat.poidsCalibration = pilotage.poidsEstime() journal.info(("pidmath: poids de calibration %.0f pN") :format(etat.poidsCalibration)) Etat.sauver(etat) valider(base, 1) ui.progres(1, "terminee") journal.info("pidmath: terminee") return true end -------------------------------------------------------------------- -- PID AUTO-TUNE (en vol stationnaire, marge d'altitude requise) -- Recherche iterative sur la boucle de vitesse verticale. -------------------------------------------------------------------- function Calibration.pid(conf, etat, materiel, pilotage, journal, Etat, empreinte, ui) local C = conf.CALIB local base = materiel.lireAltitude() local gains = pilotage.gains() local dt = 0.1 local function mesurer(cible, direction) local debut = os.clock() local extremum = materiel.lireAltitude() local assietteMax = 0 local dansBande, atteint, tempsReponse = nil, false, nil while os.clock() - debut < C.TIMEOUT do local alt = pilotage.reguler(cible, { tangage = 0, roulis = 0 }, 0, 0, dt) local t, r = materiel.lireAssiette() assietteMax = math.max(assietteMax, math.abs(t), math.abs(r)) if direction > 0 then extremum = math.max(extremum, alt) else extremum = math.min(extremum, alt) end if math.abs(alt - cible) <= C.BANDE then dansBande = dansBande or os.clock() if os.clock() - dansBande >= C.DELAI_STABLE then atteint, tempsReponse = true, dansBande - debut break end else dansBande = nil end sleep(dt) end return { atteint = atteint, tempsReponse = tempsReponse or math.huge, depassement = (direction > 0) and math.max(0, extremum - cible) or math.max(0, cible - extremum), assietteMax = assietteMax, } end for _, dir in ipairs({ { nom = "montee", sens = 1 }, { nom = "descente", sens = -1 }, }) do local g = gains[dir.nom] local meilleurs, meilleurTemps = nil, math.huge for iter = 1, C.MAX_ITER do local base01 = (dir.sens > 0) and 0 or 0.5 ui.progres(base01 + (iter - 1) / C.MAX_ITER / 2, ("%s %d/%d"):format(dir.nom, iter, C.MAX_ITER)) journal.info(("[calib %s %d/%d] kp=%.1f ki=%.1f kd=%.1f") :format(dir.nom, iter, C.MAX_ITER, g.kp, g.ki, g.kd)) pilotage.reglerGains(gains) local m = mesurer(base + dir.sens * C.AMPLITUDE, dir.sens) mesurer(base, -dir.sens) -- retour a la base local valide = m.atteint and m.depassement <= C.TOL_DEPASSEMENT and m.assietteMax <= conf.ANGLE_MAX journal.info((" t=%.1fs dep=%.2f incl=%.2f %s"):format( m.tempsReponse == math.huge and -1 or m.tempsReponse, m.depassement, m.assietteMax, valide and "VALIDE" or "rejete")) if valide and m.tempsReponse < meilleurTemps then meilleurs, meilleurTemps = copierGains(g), m.tempsReponse end if not m.atteint then g.kp = g.kp * 1.3 elseif m.depassement > C.TOL_DEPASSEMENT then g.kd = g.kd * 1.4 g.kp = g.kp * 0.9 elseif m.assietteMax > conf.ANGLE_MAX then g.kp = g.kp * 0.8 else g.kp = g.kp * 1.25 end g.ki = g.kp * 0.15 end if meilleurs then gains[dir.nom] = meilleurs journal.info(("calib %s: kp=%.1f ki=%.1f kd=%.1f (t=%.1fs)") :format(dir.nom, meilleurs.kp, meilleurs.ki, meilleurs.kd, meilleurTemps)) else journal.alerte("calib " .. dir.nom .. ": aucun essai valide") end end pilotage.reglerGains(gains) etat.empreintePid = empreinte etat.poidsCalibration = pilotage.poidsEstime() journal.info(("calib pid: poids de calibration %.0f pN") :format(etat.poidsCalibration)) Etat.sauver(etat) local fin = os.clock() + C.TIMEOUT while os.clock() < fin do local alt = pilotage.reguler(base, { tangage = 0, roulis = 0 }, 0, 0, dt) if math.abs(alt - base) <= C.BANDE then break end sleep(dt) end ui.progres(1, "terminee") journal.info("calib pid: terminee") return true end return Calibration