changement complet de logiciel

This commit is contained in:
Ploush 2026-07-19 20:08:24 +02:00
parent 9137a98af3
commit 810b1c5e27
13 changed files with 2328 additions and 3063 deletions

338
README.md
View file

@ -1,231 +1,131 @@
# Quadricoptere Create Aeronautics : controleur centralise # Drone V2 : quadricoptere Create Aeronautics / CC:Tweaked
Nouvelle architecture: un SEUL ordinateur (PC central) pilote tout via Reecriture complete, concue contre les oscillations permanentes de
le reseau filaire. Les anciens scripts multi-PC (helice.lua, la V1. Installation: copier drone.lua puis `drone maj <url_base>`
central.lua, input.lua, console.lua, propulsion.lua) sont obsoletes. (Forgejo raw, domaine autorise dans la config http de CC:Tweaked).
## Materiel ## Pourquoi la V1 oscillait, et les parades de la V2
Directement sur le PC central: 1. QUANTIFICATION: les RSC acceptent des rpm ENTIERS. A ~20 rpm de
- altitude_sensor, Create_DisplayLink (affichage de conduite) sustentation, 1 rpm = 5% de poussee par coin: aucun regulateur ne
- ender modem (uniquement pour le GPS) peut converger, il ne peut qu'osciller autour (cycle limite).
- sortie redstone BOTTOM vers le moteur (15 = arret, 14 = vitesse PARADE MATERIELLE FORTEMENT RECOMMANDEE: engrener les helices
minimale, 0 = vitesse maximale) pour que la sustentation tombe vers 60-100 rpm (meme poussee,
3 a 5 fois plus de resolution de commande).
2. RETARD DE BOUCLE (ticks, transitions de rotation): la calibration
par RELAIS mesure la periode ultime REELLE retard compris, et
TYREUS-LUYBEN laisse ~3x de marge de gain: stabilite garantie
pour la dynamique identifiee.
3. BRUIT DE MESURE: derivee FILTREE dans le PID; vitesse verticale
par velocity sensor AUTO-DETECTE a la calib altitude (repli:
derivee filtree de l'altitude).
4. FILET DE SECURITE: un SUPERVISEUR detecte l'oscillation de
chaque boucle en vol (passages a zero repetes au-dela d'un seuil
d'amplitude) et reduit ses gains de 20% automatiquement, avec
journalisation et persistance: meme mal regle, le systeme
converge vers la stabilite.
Via le reseau filaire (wired modems): ## Modes
- 6 Create_RotationSpeedController: 4 helices + 2 propulseurs
(roles affectes par `calib rsc`, persistes dans drone.etat)
- 3 velocity_sensor, un par composante (axes affectes par
`calib capteurs`)
- 1 gimbal_sensor, 1 optical_sensor (vers le sol, 15.5 = rien),
1 stressometer, 1 redstone_relay (joystick), 1 advanced monitor
(tactile), 1 linked typewriter (EN ATTENTE: relever ses signatures
avec test_signatures, voir lib/ihm.lua)
## Installation - `off` : moteur coupe, RSC a 0
- `sol` : moteur allume, RSC a 0
1. Copier drone.lua sur le PC, puis: `drone maj <url_base>` pour - `stationnement` : helices a pleine puissance INVERSEE (plaquage),
telecharger les modules depuis votre depot (Forgejo: URL "raw" du propulseurs a 0. Entree: au sol (optical <= DIST_SOL) ET
dossier). Autoriser le domaine dans la config http de CC:Tweaked. horizontal (<= ANGLE_MAX). Sortie AUTO vers vol si perdu.
Ou copier a la main drone.lua + lib/*.lua. - `vol` : assiette horizontale, altitude vers la consigne (+-1/+-10
2. Premier lancement: `drone` cree `drone.conf`. RELIRE chaque au moniteur), deplacements au joystick (propulseurs
parametre (Y_VOL, Y_MINI, ANGLE_MAX, DIST_SOL, offsets, joystick, differentiels avance/virage)
moteur...) puis passer `verifie = true`. Un warning s'affiche tant - `atterrissage` : descente a VITESSE_ATTERRISSAGE puis
que ce n'est pas fait. stationnement. La verification d'un sol posable est au pilote.
3. Calibrations depuis l'onglet CALIB, en mode CALIBRAGE pour
celles au sol, DANS CET ORDRE (les modes non calibres sont
verrouilles en rouge). Toutes sont RE-EXECUTABLES (recablage:
relancer la calibration remet ses affectations a zero et prend
effet sans relancer le programme). La calib rsc verifie la
coherence (retour de l'assiette a l'etat initial entre chaque
impulsion, detection des conflits deux RSC -> une meme helice)
et s'annule proprement en cas d'incoherence. Les calibrations
assistees affichent leurs consignes sur l'ecran de conduite et se
valident en touchant le moniteur:
- `gimbal` (assistee, en premier): incliner le nez vers le bas
puis pencher a gauche; associe les 2 angles bruts au tangage et
au roulis (l'association CHANGE d'un vaisseau a l'autre).
- `joystick` (assistee): pousser a fond dans chaque direction pour
identifier les faces du redstone relay.
- `typew` (assistee, OPTIONNELLE): le typewriter emet des codes
ASCII (qwerty), le mappage est integre par defaut (0..9,
Entree = valider, Backspace = effacer, + / - = consigne +-1).
Ne calibrer que si le clavier a un mappage exotique.
- `rsc` (drone pose): impulsion sur chaque RSC; l'effet
d'assiette designe le coin; sans effet = propulseur. prop_l /
prop_r arbitraires: si un virage gauche part a droite, les
echanger dans drone.etat. Pour un DRONE TROP LOURD (impulsions
sans effet), utiliser `rscman`.
- `rscman` (drone pose, manuelle): chaque RSC tourne a
CALIB.RPM_MANUEL (10 rpm); regarder quelle helice tourne et
toucher sa position sur l'art (4 coins + prop_l / prop_r);
unicite garantie, ANNULER restaure l'etat precedent. Fiabilise
aussi prop_l / prop_r (plus d'affectation arbitraire).
- `capteurs` (en vol): montee, avancee puis translation gauche
identifient l'axe ET la direction de chaque velocity sensor
(repere du drone: avant / lateral / vertical, positif =
avant / gauche / haut).
- `pid` (en vol stationnaire, marge d'altitude): recherche
iterative, critere ZERO depassement + temps de reponse minimal +
assiette <= ANGLE_MAX. EXPERIMENTAL. Toute modification des
parametres de conf dont dependent ces gains (VITESSE_RSC_MAX,
VITESSE_RAMPE, ANGLE_MAX) INVALIDE la calibration au prochain
demarrage (retour aux gains de la conf + alerte).
## Interface (moniteur tactile)
Echelle de texte automatique (la plus grande qui loge le contenu).
Navigation par onglets:
- PARAM : edition tactile des parametres (depassement tolere, angles
max, POIDS et POUSSEE de l'helice en pixelNewton, vitesses
verticales max, rampe, Y_VOL, Y_MINI). Les valeurs modifiees sont
persistees dans drone.etat et PRIMENT sur drone.conf sans
l'ecraser; modifier un parametre dont depend la calibration PID
affiche un rappel de recalibration.
- MODE : boutons de modes + contenu adapte au mode courant (voir
ci-dessous), art du quadricoptere (cercles des helices: blanc
rpm=0, rouge rpm max, jaune en changement, vert stable; rpm des
propulseurs sur les cotes)
- CALIB : boutons des calibrations (gris = a faire, jaune = en
cours, vert = faite), consignes et barre de progression, art avec
le RSC en cours en VIOLET
- JOURNAL : dernieres lignes du journal de vol
## Modes (verrouilles en rouge si une calibration manque, la raison
s'affiche au toucher)
- `off` : moteur coupe (bottom a 15), tous les RSC a 0
- `inactif` : moteur ALLUME, tous les RSC a 0, commandes desactivees.
Dans tous les modes sauf off, une erreur s'affiche si le moteur ne
tourne pas (stressometer a capacite nulle) alors qu'il le devrait
- `calibrage` : identique a inactif mais AUTORISE les calibrations
au sol (gimbal, joystick, typew, rsc); capteurs et pid se lancent
en mode vol
- `stationnement` : helices a pleine puissance inversee (plaquage),
propulseurs a 0, commandes desactivees. Conditions d'entree: au sol
(optical <= DIST_SOL) ET horizontal (<= ANGLE_MAX). Sortie
automatique vers `vol` si les conditions sont perdues.
- `vol` : assiette maintenue horizontale (PID tangage/roulis a 0),
altitude au PID double regime, deplacement horizontal au joystick
via les PROPULSEURS (avance/virage differentiels)
- `drone` : le joystick commande l'INCLINAISON (jusqu'a
ANGLE_DRONE_MAX), deplacement par les helices, propulseurs a 0
- `atterrissage` : descente douce (VITESSE_ATTERRISSAGE) jusqu'au
sol, puis stationnement. Sous Y_MINI, la descente exige un sol
POSABLE (optical: bloc detecte, ni eau ni lave): garde-fou ocean
- `auto` : cible a 2 coordonnees (`auto <x> <z>` au clavier, pave
tactile, ou bouton d'un point enregistre): decollage a Y_VOL,
ralliement en avancant (cap estime par GPS), atterrissage sur
place. Cible a 3 coordonnees (`auto <x> <y> <z>`): rallie le point
et s'y MAINTIENT a l'altitude y (bornee par le sol et Y_MINI).
Bouton ANNULER: retour en vol stabilise a la position courante
La consigne d'altitude se regle aux boutons +-1/+-10 du moniteur ou
au TYPEWRITER: taper les chiffres (la saisie s'affiche sur l'ecran
de conduite) puis Entree; Backspace corrige; + et - ajustent la
consigne de 1 quand aucune saisie n'est en cours.
Points nommes (clavier du PC, 10 max):
`pt save <nom>` | `pt go <nom>` | `pt del <nom>` | `pt list` | `rtb`
(retour au point de decollage de la derniere mission auto).
## Regulation ## Regulation
- ASSERVISSEMENT EN CASCADE + FEEDFORWARD, ADAPTATIF (transport a - ALTITUDE en cascade: erreur -> vitesse verticale cible (P bornee
charge et centrage variables a chaque vol): V_MONTEE_MAX / V_DESCENTE_MAX) -> PID vitesse -> delta rpm autour
1. feedforward de sustentation: rpm d'equilibre calcule depuis du rpm de sustentation MESURE (corrige par la pression).
POIDS (A VIDE) et POUSSEE_HELICE_MAX (poussee ~ rpm^EXPOSANT), Adaptation lente a la charge en quasi-stationnaire.
CORRIGE EN CONTINU PAR LA PRESSION mesuree (la poussee diminue - ASSIETTE: PID par axe, derivee filtree, autorite bornee a
avec l'altitude): pousseeEffective = POUSSEE * pression / ASSIETTE_PLAFOND * rpm de sustentation.
PRESSION_REF. Mesurer poids a vide et poussee d'une helice et - MOTEUR: bouton on/off ou adaptatif au moniteur. En adaptatif, le
les saisir (conf ou page PARAM). besoin est PREVU (coefficients su/rpm mesures pendant la calib
1b. ADAPTATION DE POIDS: en quasi-stationnaire, le residu du PID rsc) et le niveau est regle AVANT l'ecriture des RSC en montee de
de vitesse est transfere lentement vers le feedforward: le regime, apres en descente; garde: besoin borne a capacite max *
drone SE PESE tout seul (poids estime affiche en vol). Remise a SECURITE.
zero a chaque decollage (nouvelle cargaison). Plafond: poids
total max = RATIO_CHARGE_MAX (60%%) * 4 * poussee de reference,
alerte SURCHARGE au dela.
1c. TRIM D'ASSIETTE ADAPTATIF: en stationnaire, la composante
statique des corrections d'assiette (centre de gravite decale
par la cargaison) est absorbee par un trim par axe; alerte
"chargement desequilibre" si |trim| depasse
ADAPTATION.SEUIL_DESEQUILIBRE.
1d. GAIN SCHEDULING: la boucle de vitesse est mise a l'echelle du
ratio poids estime / poids memorise a la calibration (borne
0.5..2): calibrer avec n'importe quelle charge, les gains
suivent la cargaison.
Procedure par vol: charger -> mode vol -> 5-10 s de stationnaire
(pesee + trim + alertes) -> partir. Aucune recalibration.
2. boucle externe: erreur d'altitude -> consigne de vitesse
verticale (P, bornee V_MONTEE_MAX / V_DESCENTE_MAX)
3. boucle interne: PID sur la vitesse verticale mesuree (velocity
sensor vertical calibre; a defaut, derivee filtree de
l'altitude), deux jeux de gains montee / descente
Consigne rampee (VITESSE_RAMPE), derivee sur la mesure et
integrale conditionnelle (zero-depassement).
- Deux calibrations PID complementaires (onglet CALIB, mode vol):
- `pid` (auto-tune): recherche iterative en boucle fermee, garde
le meilleur essai valide (zero depassement, assiette bornee)
- `pidmath` (methodes mathematiques): identification du modele en
boucle ouverte autour du feedforward (echelon -> K, tau, L),
gains candidats par COHEN-COON et ZIEGLER-NICHOLS, validation en
boucle fermee des deux, conservation du meilleur valide (version
adoucie appliquee si aucun ne passe)
Conseil: regler POIDS/POUSSEE d'abord, puis pidmath (rapide), puis
pid si besoin d'affiner.
- PID assiette: tangage et roulis vers leurs cibles; mixage par coin:
base + corrections signees.
- Moteur asservi au stressometer: augmente si l'utilisation depasse
STRESS_HAUT, diminue sous STRESS_BAS (economie). Le moteur est
dimensionne pour suffire; en cas de SURCHARGE malgre le maximum,
les PROPULSEURS sont reduits en priorite (facteur 0.3) + alerte,
puis retablis quand le stress redescend.
- Carburant: deux valeurs sur le 2e display link: POURCENTAGE de
reserve (ligne 1) et AUTONOMIE en minutes (ligne 2), estimee par
apprentissage glissant (EMA, CARBURANT.LISSAGE) de la
consommation; les ravitaillements et le mode off sont exclus de
l'apprentissage. Alerte sous SEUIL_ALERTE.
- Moteur PREDICTIF (apres `calib moteur`, au sol en mode calibrage):
la calibration mesure la capacite produite a chacun des 15 niveaux
redstone puis les coefficients de consommation Kh (helices) et Kp
(propulseurs) en su/rpm. En vol, le besoin est PREVU d'apres les
rpm commandes et le moteur est regle au PLUS PETIT niveau couvrant
besoin * (1 + MARGE): capacite au plus pres de la consommation,
donc carburant minimal. Apprentissage continu de la table, montee
immediate, descente d'un cran par periode. Repli sur le servo
reactif tant que la calib n'est pas faite.
- LIMITEUR DE STRESS (anti-overstress transitoire): a chaque pas de
pilotage, les commandes sont bornees pour que le besoin prevu
reste sous la capacite MESUREE * SECURITE, par priorites: la
sustentation n'est jamais reduite, puis la montee, puis les
propulseurs. Pendant la montee en regime du diesel, le drone
grimpe exactement aussi vite que la capacite le permet: le
depassement transitoire devient structurellement impossible.
Alerte STRESS INSUFFISANT si meme la sustentation depasse le
budget (moteur sous-dimensionne pour la charge).
- Pre-boost a pleine puissance (MOTEUR.BOOST secondes) conserve aux
changements de regime, et garde-fou ultime SURCHARGE (delestage
propulseurs) sur la mesure.
- TYPE DE MOTEUR: le diesel repond instantanement au signal; avec
lui, BOOST peut etre mis a 0, MARGE reduite (~0.10) et
CALIB.ATTENTE_NIVEAU a 0.5 s (calib moteur en ~15 s). Ces trois
valeurs sont sur la page PARAM. Pour un moteur A TRANSITION,
garder BOOST et MARGE plus larges: le limiteur de stress couvre le
delai de montee en regime, l'architecture est inchangee.
- Reactivite: le GPS (appel bloquant jusqu'a 2 s) est isole dans une
tache dediee alimentant un cache; l'interface et la navigation ne
bloquent jamais dessus. Messages longs de l'ecran de conduite:
defilement par segments de mots entiers (indicateur i/n).
## Fichiers ## Calibrations (onglet CALIB), dans l'ordre
- `drone.conf` : parametres utilisateur (cree si absent) 1. `gimbal` (mode sol): a plat, nez bas, a plat, penche a gauche.
- `drone.etat` : mode, consigne, roles RSC, axes capteurs, gains 2. `joystick` (mode sol): pousser a fond dans chaque direction.
calibres, point de decollage (restaures au reboot; en vol, la 3. `rsc` (mode sol): chaque RSC tourne a RPM_MANUEL, toucher sur
consigne repart de l'altitude reelle) l'art la position qui tourne; mesure AUSSI les coefficients
- `drone.log` : journal de vol (rotation a 64 Ko) su/rpm, la capacite moteur par niveau (mode adaptatif) et
l'association helice <-> speedometer.
3b. `boite` (mode sol, optionnelle, rsc requis): association de
CHAQUE face du second relay a SON helice (chaque helice tourne a
son tour; la face dont le signal 13 = rapport 2/16 reduit la
vitesse lue par le speedometer de cette helice est la sienne).
En vol, le meme signal est ecrit sur les 4 faces: rapport
identique sur les 4 helices, indispensable a la symetrie.
4. `altitude` (mode sol, AUTO-AMORCANTE): rampe lente jusqu'au
decollage -> rpm de sustentation MESURE. Si les boites sont
calibrees: retour au sol, CHOIX DU RAPPORT (k = plus petit k/16
tel que la sustentation RSC <= BOITE.RSC_CIBLE, soit ~70% de la
plage avec x1.4 de marge de montee), re-decollage au rapport
definitif, verification par speedometer. Puis gains d'assiette
par defaut dimensionnes, detection du velocity sensor vertical,
relais d'Astrom-Hagglund sur la boucle de vitesse -> gains
Tyreus-Luyben, retour au sol automatique.
5. `inclinaison` (mode vol, stationnaire): relais par axe ->
gains Tyreus-Luyben.
## Points d'attention Toutes re-executables; gains persistes en surcharges, visibles et
retouchables page PARAM (parametrage manuel guide). Le superviseur
peut ensuite les affiner a la baisse en vol si necessaire.
- tanks(): si la capacite n'est pas fournie par le jeu, la conf ## Affichages
CARBURANT.CAPACITE_TANK sert de repli.
- Moniteur: onglets MODE / CALIB / PARAM / JOURNAL, echelle de
texte auto, art du quadricoptere (blanc 0, rouge max, jaune en
changement, vert stable, violet en calibration), autonomie en
minutes, choix du regime moteur.
- Conduite (2x22): ALT reelle > consigne, VIT globale
(sqrt(v1^2+v2^2+v3^2)); messages longs en segments de mots.
- Carburant (2x4): pourcentage de reserve.
## Boites de vitesse (analog transmission, optionnelles)
Une par helice, CHACUNE sur SA face du second redstone relay
(association apprise par la calib boite), plus un speedometer par
helice. Le meme signal est ecrit sur les 4 faces: rapport identique
pour les 4, indispensable a la symetrie. Rapport = (15-signal)/16
(signal 0 = 1/1; signal 15 INTERDIT = rapport 0). Interet: a ~20 rpm
de sustentation, 1 rpm RSC entier = 5%% de poussee (oscillation
inevitable); au rapport 2/16 la sustentation RSC passe a ~160 rpm et
la resolution est multipliee par 8.
BOITE AUTOMATIQUE: le regulateur travaille en RPM HELICE (unites
physiques); la conversion RSC = helice / rapport n'a lieu qu'a la
frontiere de l'actionneur. Le gain de boucle est donc INVARIANT au
rapport: les gains calibres une fois valent a tous les rapports. En
vol, si la commande RSC des helices sort de la bande [RSC_BAS,
RSC_HAUT] (charge accrochee en vol, perte de poussee en altitude),
le rapport change d'UN cran, uniquement en quasi-stationnaire et a
plat, avec ATTENTE de temps mort, et dans un ORDRE SUR: le
transitoire d'un tick est toujours un CREUX de poussee symetrique
(jamais un pic): montee de k = RSC reecrits puis redstone; descente
= redstone puis RSC. k=15 est saute (signal 15 = rapport 0,
interdit). Les speedometers surveillent en continu que vitesse
reelle = commande * rapport (ecart > 25%% persistant 2 s -> alerte
BOITES INCOHERENTES + reassertion du signal).
## Renumerotation des relays
Les NOMS des peripheriques peuvent changer a la reconnexion du
reseau; les FACES des relays sont stables. Si les noms stockes ne
correspondent plus au reseau, les modes de vol sont verrouilles et
la REIDENTIFICATION AUTOMATIQUE se lance en mode sol (~3 s:
l'helice lf tourne a bas regime, le signal 13 sur sa face designe
le relay des boites via le speedometer; le joystick est l'autre
relay). Aucune action manuelle.
Non utilises: GPS, typewriter.

1104
drone.lua

File diff suppressed because it is too large Load diff

735
lib/calib.lua Normal file
View file

@ -0,0 +1,735 @@
--------------------------------------------------------------------
-- lib/calib.lua : calibrations.
-- gimbal : conventions d'assiette (nez bas, penche a gauche)
-- joystick : faces du redstone relay
-- rsc : procedure MANUELLE (chaque RSC tourne a
-- RPM_MANUEL, toucher sa position sur l'art); mesure
-- au passage le coefficient su/rpm de chaque RSC et
-- la capacite du moteur par niveau (mode adaptatif)
-- altitude : AUTO-AMORCANTE depuis le sol: rampe lente jusqu'au
-- decollage -> rpm de sustentation mesure; detection
-- du velocity sensor VERTICAL; gains d'assiette par
-- defaut dimensionnes sur ce rpm; puis relais
-- d'Astrom-Hagglund sur la boucle de vitesse ->
-- gains Tyreus-Luyben; retour au sol
-- inclinaison : relais par axe (tangage, roulis) en stationnaire
-- Toutes persistees (etat + surcharges page PARAM), re-executables.
--------------------------------------------------------------------
local Calib = {}
--------------------------------------------------------------------
-- OUTILS PARTAGES
--------------------------------------------------------------------
-- persiste des gains PID dans les surcharges (visibles page PARAM)
local function persisterGains(etat, Etat, axe, g)
etat.surcharges = etat.surcharges or {}
etat.surcharges["PID." .. axe .. ".kp"] = g.kp
etat.surcharges["PID." .. axe .. ".ki"] = g.ki
etat.surcharges["PID." .. axe .. ".kd"] = g.kd
Etat.sauver(etat)
end
-- Relais d'Astrom-Hagglund sur une boucle de la regulation.
-- boucle : "vitesse" | "tangage" | "roulis"
-- mesure : function() -> valeur regulee (v verticale ou angle)
-- pas : function() -> un pas de regulation (le relais y est
-- injecte via reglerInjection)
-- d : amplitude du relais (rpm)
-- garde : function(valeur) -> false pour interrompre (securite)
-- Retourne Ku, Tu ou nil, raison ("amplitude" | "oscillation")
local function relais(conf, regulation, boucle, mesure, pas, d, garde)
local croisements, pics = {}, {}
local signePrec, picCourant = nil, 0
local debut = os.clock()
while os.clock() - debut < conf.CALIB.RELAIS_DUREE do
local valeur = mesure()
if not garde(valeur) then
regulation.reglerInjection(boucle, nil)
return nil, "amplitude"
end
picCourant = math.max(picCourant, math.abs(valeur))
local signe = valeur >= 0
if signePrec ~= nil and signe ~= signePrec then
table.insert(croisements, os.clock())
table.insert(pics, picCourant)
picCourant = 0
if #croisements >= 9 then break end
end
signePrec = signe
-- relais oppose a la valeur (meme sens que la correction)
regulation.reglerInjection(boucle, (valeur < 0) and d or -d)
pas()
end
regulation.reglerInjection(boucle, nil)
if #croisements < 6 then return nil, "oscillation" end
-- moyennes sur les cycles etablis (2 premiers ecartes)
local sommeT, n = 0, 0
for i = 4, #croisements - 1, 2 do
sommeT = sommeT + (croisements[i + 1] - croisements[i - 1])
n = n + 1
end
local sommeA = 0
for i = 3, #pics do sommeA = sommeA + pics[i] end
local Tu = sommeT / math.max(n, 1)
local a = sommeA / math.max(#pics - 2, 1)
if a <= 0.02 or Tu <= 0 then return nil, "oscillation" end
return 4 * d / (math.pi * a), Tu
end
-- essais de relais avec auto-ajustement de d, puis Tyreus-Luyben
local function accorder(conf, regulation, journal, ui, boucle,
mesure, pas, d, garde, stabiliser)
local Ku, Tu
for essai = 1, 4 do
ui.progres(nil, ("%s: relais d=%.1f rpm (essai %d)")
:format(boucle, d, essai))
journal.info(("calib %s: relais d=%.1f"):format(boucle, d))
local raison
Ku, Tu = relais(conf, regulation, boucle, mesure, pas, d, garde)
raison = Tu
if Ku then break end
if raison == "amplitude" then d = d / 2 else d = d * 1.7 end
stabiliser()
end
if not Ku then return nil end
journal.info(("calib %s: Ku=%.2f Tu=%.2f"):format(boucle, Ku, Tu))
-- Tyreus-Luyben PID (conservateur, ~3x de marge de gain)
return { kp = Ku / 2.2, ki = (Ku / 2.2) / (2.2 * Tu),
kd = (Ku / 2.2) * Tu / 6.3 }, Tu
end
--------------------------------------------------------------------
-- GIMBAL (au sol): conventions d'assiette
--------------------------------------------------------------------
function Calib.gimbal(conf, etat, materiel, journal, Etat, ui)
ui.progres(0, "mise a plat")
ui.inviter("Drone a plat, touchez l'ecran")
ui.attendreToucher()
local plat1, plat2 = materiel.lireAnglesBruts()
ui.progres(0.25, "nez bas")
ui.inviter("Inclinez NEZ vers le BAS, touchez")
ui.attendreToucher()
local nez1, nez2 = materiel.lireAnglesBruts()
local d1, d2 = nez1 - plat1, nez2 - plat2
local indexTangage = (math.abs(d1) >= math.abs(d2)) and 1 or 2
local deltaTangage = (indexTangage == 1) and d1 or d2
if math.abs(deltaTangage) < 2 then
journal.erreur("calib gimbal: inclinaison insuffisante")
ui.progres(0, "ECHEC: incliner davantage")
return false
end
ui.progres(0.5, "retour a plat")
ui.inviter("Revenez a plat, touchez")
ui.attendreToucher()
plat1, plat2 = materiel.lireAnglesBruts()
ui.progres(0.75, "penche a gauche")
ui.inviter("Penchez a GAUCHE, touchez")
ui.attendreToucher()
local gauche1, gauche2 = materiel.lireAnglesBruts()
local indexRoulis = (indexTangage == 1) and 2 or 1
local deltaRoulis = (indexRoulis == 1) and (gauche1 - plat1)
or (gauche2 - plat2)
if math.abs(deltaRoulis) < 2 then
journal.erreur("calib gimbal: penchement insuffisant")
ui.progres(0, "ECHEC: pencher davantage")
return false
end
-- conventions: nez bas -> tangage NEGATIF, gauche -> roulis NEGATIF
etat.gimbal = {
indexTangage = indexTangage,
signeTangage = (deltaTangage < 0) and 1 or -1,
indexRoulis = indexRoulis,
signeRoulis = (deltaRoulis < 0) and 1 or -1,
}
Etat.sauver(etat)
ui.progres(1, "terminee")
ui.inviter("Gimbal calibre, remettez a plat")
journal.info("calib gimbal: terminee")
return true
end
--------------------------------------------------------------------
-- JOYSTICK (au sol): faces du redstone relay
--------------------------------------------------------------------
function Calib.joystick(conf, etat, materiel, journal, Etat, ui)
local FACES = { "front", "back", "left", "right", "top", "bottom" }
local relais, faces, exclues = nil, {}, {}
for idx, direction in ipairs({ "devant", "derriere", "gauche",
"droite" }) do
ui.progres((idx - 1) / 4, direction)
ui.inviter("Joystick a fond: " .. direction:upper()
.. ", puis touchez")
ui.attendreToucher()
local meilleure, meilleurRelais = nil, nil
local niveau = conf.CALIB.SEUIL_JOYSTICK - 1
for nomRelais in pairs(materiel.relays) do
if not relais or relais == nomRelais then
for _, face in ipairs(FACES) do
if not exclues[face] then
local n = materiel.lireFaceRelayDe(nomRelais, face)
if n > niveau then
meilleure, meilleurRelais, niveau = face, nomRelais, n
end
end
end
end
end
if not meilleure then
journal.erreur("calib joystick: aucune face active pour "
.. direction)
ui.progres(0, "ECHEC " .. direction)
return false
end
relais = meilleurRelais
faces[direction] = meilleure
exclues[meilleure] = true
journal.info(("calib joystick: %s -> %s / %s"):format(direction,
relais, meilleure))
end
etat.joystick = { relais = relais, faces = faces }
Etat.sauver(etat)
ui.progres(1, "terminee")
ui.inviter("Joystick calibre")
journal.info("calib joystick: terminee")
return true
end
--------------------------------------------------------------------
-- RSC (au sol, moteur allume): procedure MANUELLE + coefficients de
-- stress + capacites moteur par niveau (pour le mode adaptatif)
--------------------------------------------------------------------
function Calib.rsc(conf, etat, materiel, journal, Etat, ui)
local anciensRoles = etat.roles
etat.roles = {}
materiel.rafraichirAffectations()
materiel.toutArreter()
materiel.reglerMoteur(15)
sleep(0.5)
local noms = {}
for nom in pairs(materiel.rsc) do table.insert(noms, nom) end
table.sort(noms)
local stressBase = materiel.lireStressBrut() or 0
local coeffs, affectes = {}, {}
for i, nom in ipairs(noms) do
ui.progres((i - 1) / (#noms + 1), nom .. " a "
.. conf.CALIB.RPM_MANUEL .. " rpm")
materiel.rsc[nom].setTargetSpeed(conf.CALIB.RPM_MANUEL)
sleep(0.6)
-- coefficient su/rpm (un seul RSC tourne: mesure propre)
local stress = materiel.lireStressBrut() or 0
coeffs[nom] = math.max(0,
(stress - stressBase) / conf.CALIB.RPM_MANUEL)
-- speedometer associe: le seul qui tourne en ce moment
local speedoActif = nil
for nomSpeedo in pairs(materiel.speedo) do
if math.abs(materiel.lireSpeedo(nomSpeedo) or 0) > 1 then
speedoActif = nomSpeedo
break
end
end
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: annulee")
ui.progres(0, "annulee")
return false
end
affectes[role] = nom
etat.roles[nom] = role
if speedoActif then
etat.speedoParRole = etat.speedoParRole or {}
etat.speedoParRole[role] = speedoActif
end
ui.rscViolet(role)
journal.info(("calib rsc: %s -> %s (%.1f su/rpm%s)")
:format(nom, role, coeffs[nom],
speedoActif and (", speedo " .. speedoActif) or ""))
sleep(0.3)
end
ui.rscViolet(nil)
-- capacite du moteur a chaque niveau (mode adaptatif)
local capacites = {}
for niveau = 1, 15 do
ui.progres(#noms / (#noms + 1)
+ niveau / 15 / (#noms + 1), ("capacite niveau %d"):format(niveau))
materiel.reglerMoteur(niveau)
sleep(conf.CALIB.ATTENTE_NIVEAU)
local _, capacite = materiel.lireStressBrut()
capacites[niveau] = capacite or 0
end
materiel.reglerMoteur(15)
etat.coeffRsc = coeffs
etat.capaciteParNiveau = capacites
Etat.sauver(etat)
materiel.rafraichirAffectations()
ui.progres(1, "terminee")
journal.info(("calib rsc: terminee (capa max %d su)")
:format(capacites[15] or 0))
return true
end
--------------------------------------------------------------------
-- BOITES DE VITESSE (au sol, rsc requis): association de CHAQUE
-- face du relay a SON helice. Chaque helice tourne a un RSC fixe;
-- la face qui, recevant le signal 13 (rapport 2/16), reduit la
-- vitesse lue par le speedometer de cette helice, est la sienne.
-- Le meme signal sera ensuite ecrit sur les 4 faces (rapport
-- identique sur les 4: symetrie).
--------------------------------------------------------------------
function Calib.boite(conf, etat, materiel, journal, Etat, ui)
if #materiel.helicesManquantes() > 0 then
journal.alerte("calib boite: calibrer les RSC d'abord")
return false
end
local ROLES = { "lb", "rb", "lf", "rf" }
for _, role in ipairs(ROLES) do
if not (etat.speedoParRole and etat.speedoParRole[role]) then
journal.alerte("calib boite: speedometer de " .. role
.. " non associe (refaire la calib rsc avec les "
.. "speedometers en place)")
return false
end
end
local FACES = { "front", "back", "left", "right", "top", "bottom" }
local RPM_TEST = 64
materiel.toutArreter()
materiel.reglerMoteur(15)
-- relays candidats: tous sauf celui du joystick
local relaisJoystick = etat.joystick and etat.joystick.relais
local candidats = {}
for nomRelais in pairs(materiel.relays) do
if nomRelais ~= relaisJoystick then
table.insert(candidats, nomRelais)
end
end
table.sort(candidats)
if #candidats == 0 then
journal.erreur("calib boite: aucun relay disponible (celui du "
.. "joystick est exclu)")
ui.progres(0, "ECHEC: pas de relay")
return false
end
-- association: chaque helice tourne a son tour; la face qui,
-- recevant le signal 13 (rapport 2/16), REDUIT la vitesse lue par
-- le speedometer de CETTE helice, est la sienne
local relaisRetenu, faces = nil, {}
local facesPrises = {}
for iRole, role in ipairs(ROLES) do
ui.progres((iRole - 1) / (#ROLES + 1), role .. " a "
.. RPM_TEST .. " rpm")
materiel.reglerRsc(role, RPM_TEST)
sleep(1.0)
local reference = materiel.lireVitesseHelice(role) or 0
if math.abs(reference) < RPM_TEST * 0.5 then
materiel.toutArreter()
journal.erreur("calib boite: le speedometer de " .. role
.. " ne lit pas la rotation")
ui.progres(0, "ECHEC: speedometer " .. role)
return false
end
local trouvee = nil
for _, nomRelais in ipairs(candidats) do
if (not relaisRetenu or relaisRetenu == nomRelais)
and not trouvee then
for _, face in ipairs(FACES) do
if not facesPrises[face] then
materiel.reglerSignalBoite(nomRelais, face, 13)
sleep(0.8)
local mesure = materiel.lireVitesseHelice(role) or 0
materiel.reglerSignalBoite(nomRelais, face, 0)
sleep(0.4)
local rapport = math.abs(mesure) / math.abs(reference)
if rapport > 0.06 and rapport < 0.25 then -- ~0.125
trouvee = face
relaisRetenu = nomRelais
journal.info(("calib boite: %s -> %s / %s "
.. "(rapport observe %.3f)"):format(role,
nomRelais, face, rapport))
break
end
end
end
end
end
materiel.reglerRsc(role, 0)
if not trouvee then
materiel.toutArreter()
journal.erreur("calib boite: face introuvable pour " .. role
.. " (cablage ?)")
ui.progres(0, "ECHEC: face de " .. role)
return false
end
faces[role] = trouvee
facesPrises[trouvee] = true
end
materiel.toutArreter()
-- rapport 1/1 par defaut; la calib altitude choisira le rapport
etat.boite = { relais = relaisRetenu, faces = faces,
signal = 0, rapport = 1 }
Etat.sauver(etat)
materiel.appliquerBoite()
ui.progres(1, "terminee (rapport choisi par calib altitude)")
journal.info("calib boite: terminee (relay " .. relaisRetenu .. ")")
return true
end
--------------------------------------------------------------------
-- REIDENTIFICATION DES RELAYS (au sol, moteur allume, ~3 s).
-- Les noms des relays changent a la renumerotation du reseau mais
-- les FACES sont stables: l'helice lf tourne a bas regime, le
-- signal 13 est envoye sur SA face de chaque relay candidat, et le
-- speedometer designe le relay des boites; celui du joystick est
-- l'autre. Lancee AUTOMATIQUEMENT en mode sol si les noms stockes
-- ne correspondent plus au reseau.
--------------------------------------------------------------------
function Calib.reidentifierRelais(conf, etat, materiel, journal, Etat)
local b = etat.boite
if not (b and b.faces and b.faces.lf) then return false end
if not (etat.speedoParRole and etat.speedoParRole.lf) then
return false
end
materiel.reglerMoteur(15)
materiel.reglerRsc("lf", conf.CALIB.RPM_MANUEL * b.rapport)
sleep(1.0)
local reference = materiel.lireVitesseHelice("lf") or 0
if math.abs(reference) < 2 then
materiel.reglerRsc("lf", 0)
journal.alerte("reidentification relays: speedometer lf muet")
return false
end
local noms = {}
for nom in pairs(materiel.relays) do table.insert(noms, nom) end
table.sort(noms)
local trouve = nil
for _, nom in ipairs(noms) do
materiel.reglerSignalBoite(nom, b.faces.lf, 13)
sleep(0.8)
local mesure = materiel.lireVitesseHelice("lf") or 0
materiel.reglerSignalBoite(nom, b.faces.lf, 0)
sleep(0.4)
local rapportObserve = math.abs(mesure)
/ math.max(math.abs(reference), 0.1)
if rapportObserve > 0.06 and rapportObserve < 0.5 then
trouve = nom
break
end
end
materiel.reglerRsc("lf", 0)
if not trouve then
journal.erreur("reidentification relays: relay des boites "
.. "introuvable (cablage ?)")
return false
end
b.relais = trouve
-- le joystick est un relay qui n'est pas celui des boites
if etat.joystick then
local autre = nil
local nb = 0
for nom in pairs(materiel.relays) do
if nom ~= trouve then autre, nb = nom, nb + 1 end
end
if nb == 1 then
etat.joystick.relais = autre
elseif not materiel.relays[etat.joystick.relais or ""] then
journal.alerte("reidentification relays: plusieurs candidats "
.. "pour le joystick, refaire sa calibration")
etat.joystick.relais = nil
end
end
Etat.sauver(etat)
materiel.appliquerBoite()
journal.info(("relays reidentifies: boites = %s, joystick = %s")
:format(trouve, etat.joystick and tostring(etat.joystick.relais)
or "n/a"))
return true
end
--------------------------------------------------------------------
-- ALTITUDE (depuis le sol, gimbal + rsc requis): auto-amorcante
--------------------------------------------------------------------
function Calib.altitude(conf, etat, materiel, regulation, journal,
Etat, ui)
if #materiel.helicesManquantes() > 0 then
journal.alerte("calib altitude: calibrer les RSC d'abord")
return false
end
if not etat.gimbal then
journal.alerte("calib altitude: calibrer le gimbal d'abord")
return false
end
if not materiel.auSol() then
journal.alerte("calib altitude: le drone doit etre au sol")
return false
end
local dt = 0.1
regulation.raz()
regulation.razAdaptation()
materiel.reglerMoteur(15)
-- rampe jusqu'au decollage; retourne le rpm RSC de sustentation
local function rampeDecollage(depart)
local distSol0 = materiel.lireDistanceSol()
local rpm = depart or 0
while rpm < conf.VMAX do
rpm = rpm + conf.CALIB.RAMPE_DECOLLAGE * dt
for _, role in ipairs({ "lb", "rb", "lf", "rf" }) do
materiel.reglerRsc(role, rpm)
end
if materiel.lireDistanceSol() > distSol0 + 0.4 then
return rpm
end
sleep(dt)
end
return nil
end
-- Phase 1: rampe a rapport 1/1 -> sustentation cote helices
ui.progres(0.05, "rampe de decollage (rapport 1/1)")
ui.inviter("Calib altitude: decollage automatique")
if etat.boite then
etat.boite.signal, etat.boite.rapport = 0, 1
materiel.appliquerBoite()
sleep(0.3)
end
local rpm = rampeDecollage(0)
if not rpm then
materiel.toutArreter()
journal.erreur("calib altitude: pas de decollage a VMAX "
.. "(poussee insuffisante ?)")
ui.progres(0, "ECHEC: pas de decollage")
return false
end
-- rpm est en unites HELICE (rampe faite a rapport 1/1: RSC =
-- helice). etat.rpmHover est TOUJOURS en unites helice: le
-- regulateur est invariant au rapport de boite.
etat.rpmHover = rpm
etat.pressionHover = materiel.lirePression()
Etat.sauver(etat)
journal.info(("calib altitude: sustentation %.0f rpm helice "
.. "(pression %.3f)"):format(rpm, etat.pressionHover))
-- Phase 1b (boites presentes): retour au sol, choix du rapport
-- INITIAL (vise RSC_CIBLE en sustentation: resolution maximale
-- avec marge), puis re-decollage PAR LA REGULATION (le
-- feedforward est desormais connu, en unites helice). En vol, la
-- boite automatique ajustera ensuite le rapport d'un cran a la
-- fois selon la charge et l'altitude.
if etat.boite then
ui.progres(0.15, "retour au sol (choix du rapport)")
regulation.plaquer()
local fin = os.clock() + 8
while os.clock() < fin and not materiel.auSol() do sleep(0.2) end
sleep(1.0)
materiel.toutArreter()
local k = math.max(1, math.min(16,
math.ceil(16 * rpm / conf.BOITE.RSC_CIBLE)))
if k == 15 then k = 16 end
etat.boite.signal = materiel.signalPourRapportK(k)
etat.boite.rapport = k / 16
materiel.appliquerBoite()
Etat.sauver(etat)
journal.info(("calib altitude: rapport initial %d/16 (signal "
.. "%d), sustentation ~%.0f rpm RSC"):format(k,
etat.boite.signal, rpm * 16 / k))
sleep(0.5)
ui.progres(0.2, ("re-decollage au rapport %d/16"):format(k))
regulation.raz()
local cible = materiel.lireAltitude() + 2
local finDecollage = os.clock() + 15
while os.clock() < finDecollage
and materiel.lireAltitude() < cible - 0.3 do
regulation.reguler(cible, 0, 0, dt)
sleep(dt)
end
if materiel.lireAltitude() < cible - 0.5 then
materiel.toutArreter()
journal.erreur("calib altitude: pas de re-decollage au "
.. "nouveau rapport")
ui.progres(0, "ECHEC: re-decollage")
return false
end
-- verification par speedometer: helice reelle ~ commande
local commande = materiel.rpm.lf and materiel.rpm.lf.v
local reel = materiel.lireVitesseHelice("lf")
if commande and reel then
local attendu = math.abs(commande) * etat.boite.rapport
if math.abs(math.abs(reel) - attendu)
> math.max(3, 0.25 * attendu) then
journal.alerte(("calib altitude: speedometer lf %.0f rpm "
.. "vs attendu %.0f: verifier les boites"):format(reel,
attendu))
end
end
end
local releves = {} -- nom -> somme des vitesses (capteur vertical)
-- gains d'assiette par defaut DIMENSIONNES sur ce rpm (si la
-- calib inclinaison n'a pas encore ete faite)
if not etat.calibInclinaison then
for _, axe in ipairs({ "tangage", "roulis" }) do
local g = conf.PID[axe]
g.kp = math.max(0.5, 0.08 * rpm)
g.ki = math.max(0.05, 0.008 * rpm)
g.kd = math.max(0.5, 0.15 * rpm)
persisterGains(etat, Etat, axe, g)
end
journal.info("calib altitude: gains d'assiette par defaut "
.. "dimensionnes sur la sustentation")
end
-- Phase 2: montee reguliere de 3 blocs pour identifier le
-- velocity sensor vertical (celui qui suit la montee)
ui.progres(0.3, "detection capteur vertical")
local altDepart = materiel.lireAltitude()
local consigne = altDepart + 3
local debut = os.clock()
while os.clock() - debut < 8
and materiel.lireAltitude() < consigne - 0.3 do
regulation.reguler(consigne, 0, 0, dt)
for nom in pairs(materiel.velocite) do
releves[nom] = (releves[nom] or 0)
+ materiel.lireVitesseCapteur(nom)
end
sleep(dt)
end
local meilleur, somme = nil, 0.5
for nom, s in pairs(releves) do
if math.abs(s) > math.abs(somme) then meilleur, somme = nom, s end
end
if meilleur then
etat.veloVertical = { nom = meilleur, signe = (somme > 0) and 1 or -1 }
journal.info("calib altitude: capteur vertical = " .. meilleur)
else
etat.veloVertical = nil
journal.alerte("calib altitude: capteur vertical non detecte, "
.. "repli sur la derivee d'altitude")
end
Etat.sauver(etat)
-- Phase 3: relais sur la boucle de vitesse -> gains Tyreus-Luyben
ui.progres(0.5, "relais boucle de vitesse")
local altRef = materiel.lireAltitude()
local d = math.max(2, conf.CALIB.RELAIS_FRACTION * rpm)
local gains = accorder(conf, regulation, journal, ui, "vitesse",
function() return regulation.vitesseVerticale(dt) end,
function()
regulation.reguler(altRef, 0, 0, dt)
sleep(dt)
end,
d,
function()
return math.abs(materiel.lireAltitude() - altRef) < 4
end,
function()
local fin = os.clock() + 4
while os.clock() < fin do
regulation.reguler(altRef, 0, 0, dt)
sleep(dt)
end
end)
if gains then
conf.PID.vitesse.kp = gains.kp
conf.PID.vitesse.ki = gains.ki
conf.PID.vitesse.kd = gains.kd
persisterGains(etat, Etat, "vitesse", gains)
else
journal.alerte("calib altitude: relais non concluant, gains "
.. "par defaut conserves")
end
-- Phase 4: retour au sol en douceur
ui.progres(0.9, "retour au sol")
local fin = os.clock() + 60
local cible = materiel.lireAltitude()
while os.clock() < fin and not materiel.auSol() do
cible = cible - conf.VITESSE_ATTERRISSAGE * dt
regulation.reguler(cible, 0, 0, dt)
sleep(dt)
end
regulation.plaquer()
etat.calibAltitude = true
Etat.sauver(etat)
ui.progres(1, "terminee")
journal.info("calib altitude: terminee")
return true
end
--------------------------------------------------------------------
-- INCLINAISON (en vol stationnaire): relais par axe
--------------------------------------------------------------------
function Calib.inclinaison(conf, etat, materiel, regulation, journal,
Etat, ui)
local dt = 0.1
local altRef = materiel.lireAltitude()
local hover = regulation.rpmHover()
local function pas()
regulation.reguler(altRef, 0, 0, dt)
sleep(dt)
end
for iAxe, axe in ipairs({ "tangage", "roulis" }) do
local base01 = (iAxe - 1) * 0.5
ui.progres(base01, axe)
local mesure = function()
local t, r = materiel.lireAssiette()
return (axe == "tangage") and t or r
end
local gains = accorder(conf, regulation, journal, ui, axe,
mesure, pas,
math.max(1, conf.CALIB.RELAIS_FRACTION * hover),
function(valeur) return math.abs(valeur) < conf.ANGLE_MAX * 2 end,
function()
local fin = os.clock() + 3
while os.clock() < fin do pas() end
end)
if not gains then
journal.erreur("calib inclinaison " .. axe .. ": relais "
.. "impossible")
ui.progres(0, "ECHEC " .. axe)
return false
end
conf.PID[axe].kp = gains.kp
conf.PID[axe].ki = gains.ki
conf.PID[axe].kd = gains.kd
persisterGains(etat, Etat, axe, gains)
journal.info(("calib inclinaison %s: kp=%.2f ki=%.3f kd=%.2f")
:format(axe, gains.kp, gains.ki, gains.kd))
end
etat.calibInclinaison = true
Etat.sauver(etat)
ui.progres(1, "terminee")
journal.info("calib inclinaison: terminee")
return true
end
return Calib

View file

@ -1,790 +0,0 @@
--------------------------------------------------------------------
-- 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

View file

@ -1,211 +1,85 @@
-------------------------------------------------------------------- --------------------------------------------------------------------
-- lib/conf.lua : fichier de configuration utilisateur (drone.conf) -- lib/conf.lua : configuration (drone.conf), cree avec des valeurs
-- Cree le fichier avec des valeurs par defaut commentees s'il -- par defaut commentees. Les surcharges de la page PARAM (persistees
-- n'existe pas; avertit tant que `verifie` n'est pas passe a true. -- dans drone.etat) PRIMENT sur ce fichier sans l'ecraser.
-------------------------------------------------------------------- --------------------------------------------------------------------
local Conf = {} local Conf = {}
local CHEMIN = "drone.conf" local DEFAUT = [[
-- drone.conf : configuration du drone V2. Relire puis passer
-- verifie a true pour supprimer l'alerte au demarrage.
{
verifie = false,
local MODELE = [[ DIST_SOL = 1.6, -- optical: distance "au sol" (blocs)
-- drone.conf : parametres utilisateur du quadricoptere. ANGLE_MAX = 1.5, -- inclinaison max toleree (degres)
-- RELIRE ET AJUSTER chaque valeur, puis passer verifie a true. VITESSE_ATTERRISSAGE = 1.0,-- descente du mode atterrissage (b/s)
-- NB: les mappages (gimbal, joystick, typewriter, capteurs, RSC) V_MONTEE_MAX = 3.0, -- vitesse verticale max montee (b/s)
-- ne sont PAS ici: ils sont calibres en jeu et stockes dans drone.etat. V_DESCENTE_MAX = 2.0, -- vitesse verticale max descente (b/s)
return { RAMPE = 3.0, -- rampe de consigne d'altitude (b/s)
verifie = false, -- passer a true apres relecture VMAX = 256, -- borne des RSC
ASSIETTE_PLAFOND = 0.5, -- autorite assiette: fraction du rpm
-- de sustentation
------------------------------------------------------------------ -- gains par defaut; les calibrations altitude / inclinaison les
-- Altitudes (plan de l'altitude sensor du PC central) -- remplacent (persistes en surcharges, retouchables page PARAM)
------------------------------------------------------------------ PID = {
Y_VOL = 90, -- altitude de croisiere du mode auto alt = { kp = 1.0 }, -- err alt -> v cible (1/s)
Y_MINI = 70, -- plancher de securite: en dessous, la vitesse = { kp = 8, ki = 12, kd = 1 }, -- v -> delta rpm
-- descente exige un sol POSABLE detecte tangage = { kp = 2, ki = 0.2, kd = 3 },
-- (evite d'atterrir dans un ocean) roulis = { kp = 2, ki = 0.2, kd = 3 },
------------------------------------------------------------------
-- Assiette
------------------------------------------------------------------
ANGLE_MAX = 1.5, -- inclinaison max toleree hors mode drone (deg)
ANGLE_DRONE_MAX = 15, -- inclinaison commandee max en mode drone (deg)
------------------------------------------------------------------
-- Sol
------------------------------------------------------------------
DIST_SOL = 1.2, -- optical sensor: distance <= => au sol
-- (15.5 = aucun bloc detecte)
VITESSE_ATTERRISSAGE = 1.0, -- descente du mode atterrissage (blocs/s)
BLOCS_INTERDITS = { -- blocs sur lesquels ne PAS se poser
["minecraft:water"] = true,
["minecraft:lava"] = true,
}, },
------------------------------------------------------------------
-- Position reelle affichee = position GPS + ces offsets
------------------------------------------------------------------
OFFSET_X = 0,
OFFSET_Y = 0,
OFFSET_Z = 0,
------------------------------------------------------------------
-- Regulation (cascade altitude -> vitesse verticale -> rpm)
------------------------------------------------------------------
VITESSE_RAMPE = 3.0, -- rampe de consigne d'altitude (blocs/s)
VITESSE_RSC_MAX = 256, -- borne des rotation speed controllers
V_MONTEE_MAX = 3.0, -- vitesse verticale max en montee (b/s)
V_DESCENTE_MAX = 2.0, -- vitesse verticale max en descente (b/s)
-- Feedforward de sustentation: le rpm d'equilibre est calcule
-- depuis le poids et la poussee, le PID ne corrige qu'autour.
-- Transport a charge variable: POIDS est le poids A VIDE; la
-- charge est estimee en vol (adaptation) et la poussee est
-- corrigee par la pression mesuree (elle diminue avec l'altitude).
POIDS = 32000, -- poids A VIDE du drone (pixelNewton)
POUSSEE_HELICE_MAX = 10000, -- poussee d'UNE helice a
-- VITESSE_RSC_MAX et PRESSION_REF (pN)
POUSSEE_EXPOSANT = 1, -- poussee ~ rpm^exp (1 lineaire, 2 carre)
PRESSION_REF = 1.0, -- pression a laquelle POUSSEE_HELICE_MAX
-- est mesuree (getAirPressure)
RATIO_CHARGE_MAX = 0.60, -- poids total max = ratio * 4 * poussee
-- de reference (alerte SURCHARGE au dela)
-- Adaptation en vol (le drone se pese et se trime en stationnaire)
ADAPTATION = {
TAUX_POIDS = 0.05, -- vitesse de transfert vers le
-- feedforward (1/s); petit = stable
TAUX_TRIM = 0.05, -- idem pour le trim d'assiette
SEUIL_DESEQUILIBRE = 15, -- |trim| (rpm) declenchant l'alerte
-- "chargement desequilibre"
},
PID = { -- gains par defaut (remplaces par calib)
alt = { kp = 1.2 }, -- erreur alt -> v cible (1/s)
montee = { kp = 40, ki = 60, kd = 5 }, -- v -> delta rpm
descente = { kp = 40, ki = 60, kd = 5 },
-- gains d'ASSIETTE: toujours lus ici (editables en direct page
-- PARAM), JAMAIS remplaces par les calibrations pid/pidmath
tangage = { kp = 2, ki = 0.2, kd = 3 },
roulis = { kp = 2, ki = 0.2, kd = 3 },
},
-- Autorite maximale de la correction d'assiette (et du trim), en
-- FRACTION du rpm de sustentation courant: l'autorite suit le
-- point de fonctionnement du drone (20 rpm comme 200 rpm) au lieu
-- d'etre figee a VMAX/2.
ASSIETTE_PLAFOND = 0.5,
------------------------------------------------------------------
-- Ecrans (faces des deux Create_DisplayLink sur le PC)
------------------------------------------------------------------
ECRANS = {
conduite = "front", -- vitesse, altitudes, invites
carburant = "back", -- reserve + autonomie estimee
},
------------------------------------------------------------------
-- Carburant (2 cuves create:fluid_tank sur le reseau)
------------------------------------------------------------------
CARBURANT = {
CAPACITE_TANK = 8000, -- mB par cuve, si tanks() ne donne pas
-- la capacite
SEUIL_ALERTE = 0.15, -- alerte sous ce ratio de reserve
PERIODE = 5, -- periode d'echantillonnage (s)
LISSAGE = 0.1, -- apprentissage glissant de la conso
-- (EMA: 0.1 = lent et stable, 0.5 = reactif)
},
------------------------------------------------------------------
-- Moteur (sortie redstone bottom: 15=arret, 14=mini, 0=maxi)
-- Le moteur est dimensionne pour suffire; en cas de surcharge
-- malgre tout, les PROPULSEURS sont reduits en priorite + alerte.
------------------------------------------------------------------
MOTEUR = { MOTEUR = {
-- PREDICTIF (apres calib moteur): le niveau est choisi d'apres MARGE = 0.15, -- adaptatif: capacite >= besoin*(1+m)
-- le besoin PREVU des commandes rpm, pour une capacite au plus SECURITE = 0.95, -- garde: besoin borne a capacite*s
-- pres de la consommation sans jamais etre depassee.
-- TYPE DE MOTEUR: le diesel repond INSTANTANEMENT au signal;
-- pour lui, BOOST peut etre mis a 0 et MARGE reduite (0.10).
-- Pour un moteur A TRANSITION (autres moteurs du modpack),
-- garder BOOST > 0 et une MARGE plus large: le limiteur de
-- stress du pilotage couvre le delai de montee en regime.
MARGE = 0.15, -- capacite visee = besoin * (1 + marge)
SECURITE = 0.92, -- le pilotage borne ses commandes pour
-- que besoin <= capacite mesuree * sec.
PERIODE = 0.25, -- periode de l'asservissement (s)
-- repli REACTIF (tant que calib moteur non faite)
STRESS_HAUT = 0.90,
STRESS_BAS = 0.60,
SURCHARGE = 0.98, -- garde-fou ultime: delestage propulseurs
BOOST = 4.0, -- pre-boost aux changements de regime
}, },
------------------------------------------------------------------ CARBURANT = {
-- Navigation (modes auto / position, points enregistres) LISSAGE = 0.1, -- EMA de la consommation
------------------------------------------------------------------ PERIODE = 5, -- periode de mesure (s)
NAV = { SEUIL_ALERTE = 0.15, -- alerte sous 15%%
SEUIL_ARRIVEE = 2.0, -- distance a la cible consideree atteinte },
VITESSE_GPS_MIN = 0.5, -- vitesse mini pour estimer le cap (b/s)
MAX_POINTS = 10, -- points nommes enregistrables BOITE = {
RSC_CIBLE = 180, -- calib: rpm RSC vise en sustentation
RSC_HAUT = 200, -- vol: au-dela -> rapport suivant (k+1)
RSC_BAS = 90, -- vol: en-deca -> rapport precedent
-- (k-1, meilleure resolution)
ATTENTE = 3.0, -- temps mort entre deux changements (s)
}, },
------------------------------------------------------------------
-- Calibration (voir README)
------------------------------------------------------------------
CALIB = { CALIB = {
AMPLITUDE = 4, -- pid: hauteur des echelons de test (blocs) RPM_MANUEL = 10, -- rscman: rpm de reperage
TOL_DEPASSEMENT = 0.0, -- pid: depassement tolere (0 = strict) SEUIL_JOYSTICK = 8, -- niveau declarant une face (0..15)
BANDE = 0.3, -- pid: bande de convergence (blocs) ATTENTE_NIVEAU = 0.6, -- balayage moteur: attente par niveau
DELAI_STABLE = 2.0, -- pid: duree dans la bande = converge (s) RAMPE_DECOLLAGE = 2, -- calib altitude: rpm/s de la rampe
TIMEOUT = 25, -- pid: duree max d'un echelon (s) RELAIS_DUREE = 25, -- relais: duree max d'un essai (s)
MAX_ITER = 6, -- pid: essais par direction RELAIS_FRACTION = 0.15, -- relais: d = fraction du rpm hover
IMPULSION_PAS = 16, -- rsc: increment de vitesse (rpm)
RPM_MANUEL = 10, -- rsc manuel: vitesse de reperage (rpm)
IMPULSION_SEUIL = 0.8, -- rsc/gimbal: variation d'angle declarante (deg)
JOYSTICK_SEUIL = 8, -- joystick: niveau declarant une face (0..15)
RPM_STRESS = 32, -- calib moteur: rpm de mesure des coeff.
ATTENTE_NIVEAU = 2.5, -- calib moteur: stabilisation par niveau
-- (s); 0.5 suffit pour un moteur
-- instantane (diesel)
}, },
} }
]] ]]
function Conf.charger(journal) function Conf.charger(journal)
if not fs.exists(CHEMIN) then if not fs.exists("drone.conf") then
local f = fs.open(CHEMIN, "w") local f = fs.open("drone.conf", "w")
f.write(MODELE) f.write(DEFAUT)
f.close() f.close()
journal.alerte(CHEMIN .. " cree avec les valeurs par defaut: A PERSONNALISER") journal.alerte("drone.conf cree avec les valeurs par defaut: "
.. "A PERSONNALISER")
end end
local f = fs.open("drone.conf", "r")
local chargeur, err = loadfile(CHEMIN) local contenu = f.readAll()
if not chargeur then f.close()
error("drone.conf invalide: " .. tostring(err), 0) local conf = textutils.unserialize(contenu)
if not conf then
error("drone.conf illisible: corriger ou supprimer le fichier", 0)
end end
local conf = chargeur() if not conf.verifie then
if type(conf) ~= "table" then journal.alerte("drone.conf non verifie: relire les parametres "
error("drone.conf doit retourner une table", 0) .. "puis passer verifie a true")
end
if conf.verifie ~= true then
journal.alerte("drone.conf non verifie: relire les parametres puis passer verifie a true")
end end
return conf return conf
end end
-- Empreinte des parametres dont dependent les gains PID calibres.
-- Si elle change, la calibration PID est invalidee.
function Conf.empreintePid(conf)
return textutils.serialize({
vmax = conf.VITESSE_RSC_MAX,
rampe = conf.VITESSE_RAMPE,
angle = conf.ANGLE_MAX,
poids = conf.POIDS,
poussee = conf.POUSSEE_HELICE_MAX,
exposant = conf.POUSSEE_EXPOSANT,
vmont = conf.V_MONTEE_MAX,
vdesc = conf.V_DESCENTE_MAX,
})
end
return Conf return Conf

375
lib/ecran.lua Normal file
View file

@ -0,0 +1,375 @@
--------------------------------------------------------------------
-- lib/ecran.lua : affichages.
-- Moniteur tactile: echelle auto, onglets MODE / CALIB / PARAM /
-- JOURNAL, art du quadricoptere, choix de role interactif.
-- Display conduite (2x22): ALT reelle -> consigne, VIT globale,
-- messages en segments de mots entiers.
-- Display carburant (2x4): pourcentage.
--------------------------------------------------------------------
local Ecran = {}
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 Ecran.nouveau(conf, materiel, journal)
local e = {}
local moniteur = materiel.moniteur
local zones = {}
local page = "mode"
local defilParams = 0
local L, H = 0, 0
-- la PLUS GRANDE echelle offrant au moins 38x14
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 break end
end
------------------------------------------------------------------
-- PRIMITIVES
------------------------------------------------------------------
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
local function bouton(x, y, libelle, action, couleur, couleurTexte)
moniteur.setCursorPos(x, y)
moniteur.setBackgroundColor(couleur or colors.gray)
moniteur.setTextColor(couleurTexte or colors.white)
moniteur.write(" " .. libelle .. " ")
moniteur.setBackgroundColor(colors.black)
moniteur.setTextColor(colors.white)
table.insert(zones, { x1 = x, y1 = y, x2 = x + #libelle + 1,
y2 = y, action = action })
end
------------------------------------------------------------------
-- CONDUITE (messages en segments de mots entiers)
------------------------------------------------------------------
local segments, segmentFin = nil, 0
function e.message(contenu, duree)
if not contenu or contenu == "" then segments = nil return end
segments = {}
local courant = ""
for mot in contenu: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
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
local i = 1 + math.floor((os.clock() % (#segments * 2)) / 2)
i = math.min(i, #segments)
return segments[i]
.. (#segments > 1 and (" " .. i .. "/" .. #segments) or "")
end
-- ctx: { mode, consigneY }
function e.rafraichirConduite(ctx)
local d = materiel.ecranConduite
if not d then return end
d.clear()
d.setCursorPos(1, 1)
local seg = segmentCourant()
if seg then
d.write(seg:sub(1, 22))
elseif ctx.mode == "vol" or ctx.mode == "atterrissage" then
d.write(("ALT %.1f > %.0f"):format(materiel.lireAltitude(),
ctx.consigneY or 0):sub(1, 22))
else
d.write(("MODE %s"):format(ctx.mode:upper()):sub(1, 22))
end
d.setCursorPos(1, 2)
d.write(("VIT %.2f b/s"):format(materiel.vitesseGlobale())
:sub(1, 22))
d.update()
end
function e.rafraichirCarburant(pct)
local d = materiel.ecranCarburant
if not d then return end
local largeur = d.getSize()
d.clear()
d.setCursorPos(1, 1)
if pct == nil then d.write(("n/a"):sub(1, largeur))
else
d.write(("%d%%"):format(math.floor(pct * 100 + 0.5))
:sub(1, largeur))
end
d.update()
end
------------------------------------------------------------------
-- ART DU QUADRICOPTERE (7 lignes)
------------------------------------------------------------------
local function rpmDe(role)
local suivi = materiel.rpm[role]
return suivi and tostring(suivi.v) or "?"
end
local function couleurDe(role, ctx)
if ctx.rscViolet == role then return colors.purple end
return couleurRpm(materiel.rpm[role], conf.VMAX)
end
local function dessinerArt(x, y, ctx)
local function coin(cx, cy, role, aGauche)
local couleur = couleurDe(role, ctx)
texte(cx, cy, "O", couleur)
local r = rpmDe(role)
if aGauche then texte(cx - #r - 1, cy, r, couleur)
else texte(cx + 2, cy, r, couleur) end
end
local g, d = x + 6, x + 19
texte(x + 10, y, "AVANT", colors.lightGray)
coin(g, y + 1, "lf", true)
coin(d, y + 1, "rf", false)
texte(g + 1, y + 1, ("-"):rep(d - g - 1))
for i = 2, 4 do
texte(g, y + i, "|")
texte(d, y + i, "|")
end
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)
texte(g + 1, y + 5, ("-"):rep(d - g - 1))
return y + 7
end
------------------------------------------------------------------
-- PAGES
------------------------------------------------------------------
local function dessinerPageMode(ctx)
local x, y = 1, 2
for _, m in ipairs({ "off", "sol", "stationnement", "vol",
"atterrissage" }) do
local couleur = colors.gray
if m == ctx.mode then couleur = colors.green
elseif ctx.verrous[m] 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 = ctx.verrous[m] }, couleur)
x = x + #m + 3
end
y = y + 2
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 "--"))
y = y + 1
if ctx.mode ~= "off" then
local stress, capacite = materiel.lireStressBrut()
texte(1, y, ("stress %s/%s niv %d moteur %s"):format(
stress and ("%d"):format(stress) or "?",
capacite and ("%d"):format(capacite) or "?",
materiel.niveauMoteur(),
ctx.moteurOk and "ok" or "ARRETE"),
ctx.moteurOk and colors.white or colors.red)
y = y + 1
end
-- choix du mode moteur depuis le moniteur
texte(1, y, "regime moteur:")
bouton(16, y, "on/off", { type = "modemoteur", valeur = "onoff" },
ctx.modeMoteur == "onoff" and colors.green or colors.gray)
bouton(25, y, "adaptatif",
{ type = "modemoteur", valeur = "adaptatif" },
ctx.modeMoteur == "adaptatif" and colors.green or colors.gray)
y = y + 2
if ctx.mode == "stationnement" or ctx.mode == "vol"
or ctx.mode == "atterrissage" then
y = dessinerArt(1, y, ctx)
end
if ctx.mode == "vol" then
texte(1, y, ("vit %.2f b/s alt %.1f -> %.0f boite %s")
:format(materiel.vitesseGlobale(), materiel.lireAltitude(),
ctx.consigneY or 0, ctx.rapportBoite or "1/1"))
y = y + 1
texte(1, y, "consigne:")
bouton(11, y, "-10", { type = "delta", valeur = -10 })
bouton(17, y, "-1", { type = "delta", valeur = -1 })
bouton(22, y, "+1", { type = "delta", valeur = 1 })
bouton(27, y, "+10", { type = "delta", valeur = 10 })
elseif ctx.mode == "atterrissage" then
texte(1, y, ("distance sol %.1f (sol posable: au pilote)")
:format(materiel.lireDistanceSol()))
end
end
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", "rsc", "boite",
"altitude", "inclinaison" }) 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
y = dessinerArt(1, y, ctx)
end
texte(1, H - 1, "sol: gimbal joystick rsc boite altitude",
colors.lightGray)
texte(1, H, "vol stationnaire: inclinaison", colors.lightGray)
end
local function dessinerPageParams(ctx)
local params = ctx.params or {}
local parPage = H - 3
local maxDefil = math.max(0, #params - parPage)
if defilParams > maxDefil then defilParams = maxDefil end
texte(1, 2, ("surcharges dans drone.etat (%d-%d/%d)"):format(
defilParams + 1,
math.min(defilParams + parPage, #params), #params),
colors.lightGray)
bouton(L - 9, 2, "^", { type = "defil", sens = -1 })
bouton(L - 4, 2, "v", { type = "defil", sens = 1 })
local y = 3
for i = defilParams + 1,
math.min(defilParams + parPage, #params) do
local p = params[i]
texte(1, y, ("%-22s"):format(p.label))
texte(24, y, tostring(p.valeur), colors.yellow)
bouton(L - 9, y, "-", { type = "param", idx = i, sens = -1 })
bouton(L - 4, y, "+", { type = "param", idx = i, sens = 1 })
y = y + 1
end
end
local function dessinerPageJournal()
for i, ligne in ipairs(journal.recents(H - 2)) 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 ET TOUCHERS
------------------------------------------------------------------
function e.rafraichirMoniteur(ctx)
zones = {}
moniteur.setBackgroundColor(colors.black)
moniteur.clear()
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
if page == "mode" then dessinerPageMode(ctx)
elseif page == "calib" then dessinerPageCalib(ctx)
elseif page == "params" then dessinerPageParams(ctx)
else dessinerPageJournal() end
end
function e.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" }
elseif z.action.type == "defil" then
defilParams = math.max(0, defilParams + z.action.sens * 4)
return { type = "redessiner" }
end
return z.action
end
end
return nil
end
function e.reglerPage(p) page = p end
function e.attendreToucher()
os.pullEvent("monitor_touch")
end
-- art interactif plein ecran pour la calib rsc
function e.choisirRole(titre, affectes)
local locales = {}
local function boutonLocal(x, y, libelle, role, couleur)
moniteur.setCursorPos(x, y)
moniteur.setBackgroundColor(couleur)
moniteur.write(" " .. libelle .. " ")
moniteur.setBackgroundColor(colors.black)
table.insert(locales, { x1 = x, y1 = y, x2 = x + #libelle + 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)
return affectes[role] and colors.gray or 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
return e
end
return Ecran

View file

@ -1,63 +1,49 @@
-------------------------------------------------------------------- --------------------------------------------------------------------
-- lib/etat.lua : persistance de l'etat du drone (drone.etat) -- lib/etat.lua : etat persistant (drone.etat), ecrit par le
-- Restaure apres un redemarrage: mode, consigne, et tous les -- programme: calibrations, surcharges de parametres, mode.
-- MAPPAGES CALIBRES (propres a chaque vaisseau):
-- roles nom RSC -> lb|rb|lf|rf|prop_l|prop_r
-- gimbal { indexTangage, signeTangage, indexRoulis, signeRoulis }
-- convention normalisee: tangage negatif = nez bas,
-- roulis negatif = penche a gauche
-- joystick direction (devant|derriere|gauche|droite) -> face relay
-- typewriter code de touche -> symbole ("0".."9", "valider", "effacer")
-- velocite nom capteur -> { axe = avant|lateral|vertical,
-- signe = 1|-1 } (positif = avant/gauche/haut)
-- points nom -> { x, y, z } (max NAV.MAX_POINTS)
-- empreintePid parametres de conf au moment de la calib PID
-------------------------------------------------------------------- --------------------------------------------------------------------
local Etat = {} local Etat = {}
local CHEMIN = "drone.etat"
local DEFAUT = { local DEFAUT = {
mode = "off", mode = "off",
consigneY = nil, consigneY = nil,
roles = {}, roles = {}, -- nom RSC -> lb|rb|lf|rf|prop_l|prop_r
gimbal = nil, gimbal = nil, -- { indexTangage, signeTangage, indexRoulis,
joystick = nil, -- signeRoulis }
typewriter = nil, joystick = nil, -- { relais, faces = direction -> face }
velocite = {}, rpmHover = nil, -- rpm de sustentation mesure (calib altitude)
points = {}, pressionHover = nil, -- pression lors de cette mesure
pid = nil, coeffRsc = nil, -- nom RSC -> su/rpm (mesure pendant rscman)
empreintePid = nil, speedoParRole = nil, -- role -> nom du speedometer (calib rsc)
capaciteParNiveau = nil, -- calib moteur: niveau (1..15) -> su boite = nil, -- { relais, faces = role -> face, signal,
stressParRpm = nil, -- calib moteur: { helice, prop } su/rpm -- rapport } boites de vitesse (une face
decollage = nil, -- PAR boite, meme signal sur les 4)
capaciteParNiveau = nil, -- niveau moteur (1..15) -> su
modeMoteur = "onoff", -- "onoff" | "adaptatif" (bouton du moniteur)
calibAltitude = nil, -- calib altitude effectuee
calibInclinaison = nil,
surcharges = {}, -- chemin conf -> valeur (page PARAM)
} }
function Etat.charger(journal) function Etat.charger(journal)
local etat = {} local etat = {}
for k, v in pairs(DEFAUT) do etat[k] = v end for cle, valeur in pairs(DEFAUT) do etat[cle] = valeur end
if fs.exists("drone.etat") then
if fs.exists(CHEMIN) then local f = fs.open("drone.etat", "r")
local f = fs.open(CHEMIN, "r") local lu = textutils.unserialize(f.readAll() or "")
local contenu = f.readAll()
f.close() f.close()
local lu = textutils.unserialize(contenu) if lu then
if type(lu) == "table" then for cle, valeur in pairs(lu) do etat[cle] = valeur end
for k, v in pairs(lu) do etat[k] = v end journal.info("etat restaure (mode " .. tostring(etat.mode) .. ")")
journal.info(("etat restaure (mode %s)"):format(etat.mode))
else
journal.alerte("drone.etat illisible, etat par defaut")
end end
end end
return etat return etat
end end
function Etat.sauver(etat) function Etat.sauver(etat)
local f = fs.open(CHEMIN, "w") local f = fs.open("drone.etat", "w")
if f then f.write(textutils.serialize(etat))
f.write(textutils.serialize(etat)) f.close()
f.close()
end
end end
return Etat return Etat

View file

@ -1,470 +0,0 @@
--------------------------------------------------------------------
-- 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 defilParams = 0
local function dessinerPageParams(ctx)
local params = ctx.params or {}
local parPage = H - 3
local maxDefil = math.max(0, #params - parPage)
if defilParams > maxDefil then defilParams = maxDefil end
texte(1, 2, ("surcharges dans drone.etat (%d-%d/%d)"):format(
defilParams + 1, math.min(defilParams + parPage, #params),
#params), colors.lightGray)
bouton(L - 9, 2, "^", { type = "defilparam", sens = -1 })
bouton(L - 4, 2, "v", { type = "defilparam", sens = 1 })
local y = 3
for i2 = defilParams + 1, math.min(defilParams + parPage, #params) do
local p = params[i2]
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
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" }
elseif z.action.type == "defilparam" then
defilParams = math.max(0, defilParams + z.action.sens * 4)
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

View file

@ -1,59 +1,86 @@
-------------------------------------------------------------------- --------------------------------------------------------------------
-- lib/materiel.lua : resolution des peripheriques et adaptateurs. -- lib/materiel.lua : acces au materiel.
-- Tous les formats sont VERIFIES en jeu (inventaire du 17/07/2026). -- Peripheriques: 6 RSC (4 helices + 2 propulseurs), gimbal,
-- -- altitude (top), optical (vers le sol), stressometer, redstone
-- Directs : altitude_sensor (top), 2 Create_DisplayLink (faces dans -- relay (joystick), moniteur tactile, 2 display links (conduite
-- conf.ECRANS), ender modem (gps), moteur diesel en bottom pilote -- 2x22, carburant 2x4), velocity sensors (vitesse globale), moteur
-- par redstone analogique (15=arret, 14=mini, 0=maxi). -- diesel pilote par redstone analogique bottom (15=arret, 0=maxi).
-- Reseau : RSC (roles calibres), velocity_sensor x3 (axes/directions
-- calibres), gimbal_sensor (axes calibres), optical_sensor,
-- Create_Stressometer, redstone_relay (joystick calibre),
-- linked_typewriter, monitor, 2 cuves create:fluid_tank.
-------------------------------------------------------------------- --------------------------------------------------------------------
local Materiel = {} local Materiel = {}
local function trouver(type_, obligatoire, journal, note)
local p = peripheral.find(type_)
if not p and obligatoire then
error("peripherique manquant: " .. type_, 0)
end
if not p and journal then
journal.alerte(type_ .. " introuvable" .. (note and (": " .. note) or ""))
end
return p
end
function Materiel.initialiser(conf, etat, journal) function Materiel.initialiser(conf, etat, journal)
local m = {} local m = {}
m.rpm = {} -- role -> { v, tChange } (pour l'affichage) m.rpm = {} -- role -> { v, tChange } (affichage + besoin moteur)
local posCache = nil, nil
local posDate = 0
m.altitude = trouver("altitude_sensor", true) local function premier(type_)
m.gimbal = trouver("gimbal_sensor", true) for _, nom in ipairs(peripheral.getNames()) do
m.optical = trouver("optical_sensor", true) if peripheral.getType(nom) == type_ then
m.moniteur = trouver("monitor", true) return peripheral.wrap(nom), nom
m.relay = trouver("redstone_relay", false, journal, "joystick inactif") end
m.stresso = trouver("Create_Stressometer", false, journal,
"detection moteur et surcharge inactives")
m.typewriter = trouver("linked_typewriter", false, journal,
"consigne au moniteur uniquement")
-- les deux display links, identifies par leur face (conf.ECRANS)
local function ecran(face, usage)
local p = peripheral.wrap(face)
if not p or peripheral.getType(face) ~= "Create_DisplayLink" then
error(("Create_DisplayLink attendu en '%s' (%s), voir conf.ECRANS")
:format(face, usage), 0)
end end
return p
end end
m.ecranConduite = ecran(conf.ECRANS.conduite, "conduite")
m.ecranCarburant = ecran(conf.ECRANS.carburant, "carburant")
-- rotation speed controllers et velocity sensors: les affectations m.altitude = premier("altitude_sensor")
-- (etat.roles / etat.velocite) sont rechargeables A CHAUD apres m.gimbal = premier("gimbal_sensor")
-- une recalibration via m.rafraichirAffectations() m.optical = premier("optical_sensor")
m.stresso = premier("Create_Stressometer")
m.moniteur = premier("monitor")
-- relays multiples: joystick sur l'un, boites de vitesse sur un
-- autre (identifies par leurs calibrations respectives)
m.relays = {}
for _, nom in ipairs(peripheral.getNames()) do
if peripheral.getType(nom) == "redstone_relay" then
m.relays[nom] = peripheral.wrap(nom)
end
end
-- Les NOMS des relays peuvent changer (renumerotation reseau);
-- les FACES, elles, sont stables. Resolution robuste:
-- joystick: nom stocke s'il existe encore, sinon "un relay qui
-- n'est pas celui des boites", sinon le premier
local function relayJoystick()
local j = etat.joystick
if j and j.relais and m.relays[j.relais] then
return m.relays[j.relais]
end
local boite = etat.boite and etat.boite.relais
for nom, p in pairs(m.relays) do
if nom ~= boite then return p end
end
for _, p in pairs(m.relays) do return p end
end
-- les noms stockes correspondent-ils encore au reseau ?
function m.relaisValides()
if etat.boite and not m.relays[etat.boite.relais] then
return false
end
if etat.joystick and etat.joystick.relais
and not m.relays[etat.joystick.relais] then
return false
end
return true
end
for _, obligatoire in ipairs({ "altitude", "gimbal", "moniteur" }) do
if not m[obligatoire] then
error("peripherique manquant: " .. obligatoire, 0)
end
end
-- display links: conduite = le plus large, carburant = l'autre
local liens = {}
for _, nom in ipairs(peripheral.getNames()) do
if peripheral.getType(nom) == "Create_DisplayLink" then
table.insert(liens, peripheral.wrap(nom))
end
end
table.sort(liens, function(a, b)
return (a.getSize() or 0) > (b.getSize() or 0)
end)
m.ecranConduite, m.ecranCarburant = liens[1], liens[2]
-- RSC et velocity sensors
m.rsc, m.velocite = {}, {} m.rsc, m.velocite = {}, {}
for _, nom in ipairs(peripheral.getNames()) do for _, nom in ipairs(peripheral.getNames()) do
local type_ = peripheral.getType(nom) local type_ = peripheral.getType(nom)
@ -64,29 +91,82 @@ function Materiel.initialiser(conf, etat, journal)
end end
end end
-- speedometers (vitesse REELLE des helices, apres boite)
m.speedo = {}
for _, nom in ipairs(peripheral.getNames()) do
if peripheral.getType(nom) == "Create_Speedometer" then
m.speedo[nom] = peripheral.wrap(nom)
end
end
function m.lireSpeedo(nom)
local p = m.speedo[nom]
if not p then return nil end
return p.getSpeed() or 0
end
-- vitesse reelle d'une helice via son speedometer associe
function m.lireVitesseHelice(role)
local assoc = etat.speedoParRole and etat.speedoParRole[role]
if not assoc then return nil end
return m.lireSpeedo(assoc)
end
------------------------------------------------------------------
-- BOITES DE VITESSE (analog transmission):
-- signal 0 -> rapport 16/16 ; sinon rapport = (15-signal)/16
-- signal 15 -> rapport 0: INTERDIT (helices stoppees)
-- Les 4 boites partagent UNE face d'UN relay (meme rapport pour
-- les 4: indispensable a la symetrie). Le rapport est choisi AU
-- SOL (calib altitude) et JAMAIS change en vol.
------------------------------------------------------------------
-- etat.boite = { relais, faces = { lb, rb, lf, rf -> face },
-- signal, rapport } : une face PAR boite, meme
-- signal (donc meme rapport) sur les 4
function m.rapportBoite()
local b = etat.boite
if not b then return 1 end
return b.rapport or 1
end
function m.signalPourRapportK(k)
if k >= 16 then return 0 end
return 15 - k
end
-- ecrit le signal sur les 4 faces des boites
function m.appliquerBoite()
local b = etat.boite
if not b or not m.relays[b.relais] or not b.faces then return end
for _, face in pairs(b.faces) do
m.relays[b.relais].setAnalogOutput(face, b.signal or 0)
end
end
function m.reglerSignalBoite(nomRelais, face, signal)
local p = m.relays[nomRelais]
if p then p.setAnalogOutput(face, signal) end
end
-- affectations rechargeables A CHAUD apres recalibration
function m.rafraichirAffectations() function m.rafraichirAffectations()
m.rscParRole, m.veloParAxe = {}, {} m.rscParRole, m.nomParRole = {}, {}
for nom, p in pairs(m.rsc) do for nom, p in pairs(m.rsc) do
local role = etat.roles[nom] local role = etat.roles[nom]
if role then m.rscParRole[role] = p end if role then
end m.rscParRole[role] = p
for nom, p in pairs(m.velocite) do m.nomParRole[role] = nom
local aff = etat.velocite[nom] end
if aff then m.veloParAxe[aff.axe] = { p = p, signe = aff.signe } end
end end
end end
m.rafraichirAffectations() m.rafraichirAffectations()
-- cuves de carburant (create:fluid_tank; exclut le moteur diesel, function m.helicesManquantes()
-- qui est fluid_storage mais pas fluid_tank) local manquantes = {}
m.cuves = {} for _, role in ipairs({ "lb", "rb", "lf", "rf" }) do
for _, nom in ipairs(peripheral.getNames()) do if not m.rscParRole[role] then table.insert(manquantes, role) end
if peripheral.hasType(nom, "create:fluid_tank") then
table.insert(m.cuves, peripheral.wrap(nom))
end end
end return manquantes
if #m.cuves == 0 then
journal.alerte("aucune cuve create:fluid_tank: suivi carburant inactif")
end end
------------------------------------------------------------------ ------------------------------------------------------------------
@ -100,163 +180,124 @@ function Materiel.initialiser(conf, etat, journal)
return m.altitude.getAirPressure() or 1.0 return m.altitude.getAirPressure() or 1.0
end end
-- angles bruts du gimbal (table de 2) -- angles bruts du gimbal: { a1, a2 } (mapping variable par
-- vaisseau, d'ou la calibration)
function m.lireAnglesBruts() function m.lireAnglesBruts()
local a = m.gimbal.getAngles() local a = m.gimbal.getAngles()
return a[1] or 0, a[2] or 0 return a[1] or 0, a[2] or 0
end end
-- assiette NORMALISEE via la calibration gimbal : -- assiette NORMALISEE par la calib gimbal:
-- tangage negatif = nez bas, roulis negatif = penche a gauche -- tangage NEGATIF = nez bas, roulis NEGATIF = penche a gauche
function m.lireAssiette() function m.lireAssiette()
local g = etat.gimbal local g = etat.gimbal
if not g then return m.lireAnglesBruts() end if not g then return 0, 0 end
local a = m.gimbal.getAngles() local a = m.gimbal.getAngles()
return (a[g.indexTangage] or 0) * g.signeTangage, return (a[g.indexTangage] or 0) * g.signeTangage,
(a[g.indexRoulis] or 0) * g.signeRoulis (a[g.indexRoulis] or 0) * g.signeRoulis
end end
-- distance au sol (optical vers le bas); 15.5 = rien devant
function m.lireDistanceSol() function m.lireDistanceSol()
return m.optical.getDistance() if not m.optical then return 99 end
local d = m.optical.getDistance()
if not d or d >= 15.4 then return 99 end
return d
end end
function m.auSol() function m.auSol()
return m.lireDistanceSol() <= conf.DIST_SOL return m.lireDistanceSol() <= conf.DIST_SOL
end end
-- sol detecte ET posable (pas d'eau/lave: garde-fou ocean)
function m.solPosable()
if not m.optical.hasHit() then return false end
local bloc = m.optical.getBlock()
return not conf.BLOCS_INTERDITS[bloc]
end
function m.horizontal()
local t, r = m.lireAssiette()
return math.abs(t) <= conf.ANGLE_MAX and math.abs(r) <= conf.ANGLE_MAX
end
-- vitesse le long d'un axe calibre ("avant"|"lateral"|"vertical")
-- signe: positif = avant / gauche / haut
function m.lireVitesseAxe(axe)
local c = m.veloParAxe[axe]
if not c then return 0 end
return (c.p.getVelocity() or 0) * c.signe
end
function m.vitesseHorizontale()
local va = m.lireVitesseAxe("avant")
local vl = m.lireVitesseAxe("lateral")
return math.sqrt(va * va + vl * vl)
end
-- joystick calibre: avance (devant-derriere), virage (gauche-droite)
-- dans -1..1; pas de zone morte (joystick de mod, zero franc)
function m.lireJoystick()
local j = etat.joystick
if not (m.relay and j) then return 0, 0 end
local function lire(direction)
local face = j[direction]
if not face then return 0 end
return m.relay.getAnalogInput(face) / 15
end
return lire("devant") - lire("derriere"),
lire("gauche") - lire("droite")
end
function m.lireFacesRelay()
if not m.relay then return {} end
local lectures = {}
for _, face in ipairs({ "top", "bottom", "left", "right",
"front", "back" }) do
lectures[face] = m.relay.getAnalogInput(face)
end
return lectures
end
-- typewriter: table des codes de touches actuellement pressees
function m.lireTouches()
if not m.typewriter then return {} end
return m.typewriter.getPressedKeyCodes() or {}
end
-- stressometer (formats verifies): ratio 0..1, ou nil
function m.lireStress()
if not m.stresso then return nil end
local cap = m.stresso.getStressCapacity()
if not cap or cap <= 0 then return nil end
return m.stresso.getStress() / cap
end
-- valeurs brutes (su) pour l'affichage: stress, capacite
function m.lireStressBrut() function m.lireStressBrut()
if not m.stresso then return nil end if not m.stresso then return nil end
return m.stresso.getStress(), m.stresso.getStressCapacity() return m.stresso.getStress(), m.stresso.getStressCapacity()
end end
-- besoin de stress PREVU (su) d'apres les rpm commandes et les -- capacite > 0 <=> le moteur tourne (verifie: 0 a l'arret)
-- coefficients de la calib moteur; nil si non calibree
function m.stressPrevu()
local k = etat.stressParRpm
if not k then return nil end
local besoin = 0
for role, suivi in pairs(m.rpm) do
local coeff = (role == "prop_l" or role == "prop_r")
and k.prop or k.helice
besoin = besoin + (coeff or 0) * math.abs(suivi.v)
end
return besoin
end
-- capacite > 0 <=> le moteur tourne (verifie: capacite 0 a l'arret)
function m.moteurTourne() function m.moteurTourne()
if not m.stresso then return true end -- indetectable: on suppose oui if not m.stresso then return true end
return (m.stresso.getStressCapacity() or 0) > 0 local _, capacite = m.lireStressBrut()
return (capacite or 0) > 0
end end
-- carburant: quantite totale (mB) et capacite totale -- vitesse GLOBALE = sqrt(v1^2 + v2^2 + v3^2): les directions des
-- capteurs n'ont pas d'importance
function m.vitesseGlobale()
local somme = 0
for _, p in pairs(m.velocite) do
local v = p.getVelocity() or 0
somme = somme + v * v
end
return math.sqrt(somme)
end
function m.lireVitesseCapteur(nom)
local p = m.velocite[nom]
return p and (p.getVelocity() or 0) or 0
end
-- joystick via redstone relay (calib: direction -> face)
-- retourne avance (-1..1, devant positif) et virage (-1..1,
-- droite positif); 0,0 si non calibre
function m.lireJoystick()
local j = etat.joystick
local relay = relayJoystick()
if not j or not j.faces or not relay then return 0, 0 end
local function niveau(direction)
local face = j.faces[direction]
if not face then return 0 end
return (relay.getAnalogInput(face) or 0) / 15
end
return niveau("devant") - niveau("derriere"),
niveau("droite") - niveau("gauche")
end
-- lecture d'une face d'un relay nomme (calibrations)
function m.lireFaceRelayDe(nomRelais, face)
local p = m.relays[nomRelais]
if not p then return 0 end
return p.getAnalogInput(face) or 0
end
-- carburant: cuves du reseau (le moteur diesel en bottom est
-- aussi un fluid_storage: exclu du comptage)
function m.lireCarburant() function m.lireCarburant()
if #m.cuves == 0 then return nil end
local quantite, capacite = 0, 0 local quantite, capacite = 0, 0
for _, cuve in ipairs(m.cuves) do for _, nom in ipairs(peripheral.getNames()) do
for _, t in ipairs(cuve.tanks() or {}) do if nom ~= "bottom"
quantite = quantite + (t.amount or 0) and peripheral.hasType(nom, "fluid_storage") then
capacite = capacite + (t.capacity or conf.CARBURANT.CAPACITE_TANK) local p = peripheral.wrap(nom)
for _, cuve in ipairs(p.tanks() or {}) do
quantite = quantite + (cuve.amount or 0)
end
if p.getInfo then
local info = p.getInfo()
capacite = capacite + (info and info.capacity or 0)
else
capacite = capacite + 16000
end
end end
end end
if capacite <= 0 then capacite = conf.CARBURANT.CAPACITE_TANK * #m.cuves end if capacite <= 0 then return nil end
return quantite, capacite return quantite / capacite, quantite
end
-- ACTUALISATION GPS (BLOQUANTE, jusqu'a 2 s): a appeler depuis une
-- tache dediee uniquement
function m.actualiserPosition()
local x, y, z = gps.locate(2)
if x then
posCache = { x = x + conf.OFFSET_X, y = y + conf.OFFSET_Y,
z = z + conf.OFFSET_Z }
posDate = os.clock()
end
end
-- position GPS en CACHE (non bloquante); nil si absente ou > 8 s
function m.lirePosition()
if not posCache or os.clock() - posDate > 8 then return nil end
return posCache.x, posCache.y, posCache.z
end end
------------------------------------------------------------------ ------------------------------------------------------------------
-- SORTIES -- COMMANDES
------------------------------------------------------------------ ------------------------------------------------------------------
function m.reglerMoteur(niveau) -- 0 (arret) .. 15 (maxi) local HELICES = { lb = true, rb = true, lf = true, rf = true }
niveau = math.max(0, math.min(15, math.floor(niveau + 0.5)))
redstone.setAnalogOutput("bottom", 15 - niveau)
end
-- vitesse en RPM HELICE pour les 4 helices (conversion RSC =
-- helice / rapport a la frontiere de l'actionneur: le regulateur
-- travaille en unites physiques, invariantes au rapport), en rpm
-- RSC pour les propulseurs (pas de boite)
function m.reglerRsc(role, vitesse) function m.reglerRsc(role, vitesse)
local p = m.rscParRole[role] local p = m.rscParRole[role]
if not p then return end if not p then return end
vitesse = math.floor(vitesse + 0.5) if HELICES[role] then vitesse = vitesse / m.rapportBoite() end
vitesse = math.max(-256, math.min(256,
math.floor(vitesse + 0.5)))
local suivi = m.rpm[role] local suivi = m.rpm[role]
if not suivi or suivi.v ~= vitesse then if not suivi or suivi.v ~= vitesse then
m.rpm[role] = { v = vitesse, tChange = os.clock() } m.rpm[role] = { v = vitesse, tChange = os.clock() }
@ -264,38 +305,76 @@ function Materiel.initialiser(conf, etat, journal)
p.setTargetSpeed(vitesse) p.setTargetSpeed(vitesse)
end end
------------------------------------------------------------------
-- CHANGEMENT DE RAPPORT EN VOL (ordre SUR: le transitoire d'un
-- tick est toujours un CREUX de poussee, jamais un pic)
-- k monte (rapport augmente): RSC reecrits D'ABORD (avec le
-- nouveau rapport: valeurs plus basses, creux), redstone
-- ENSUITE (restaure)
-- k descend: redstone D'ABORD (creux), RSC reecrits ENSUITE
-- rpmHelices: role -> rpm HELICE a maintenir pendant la bascule
------------------------------------------------------------------
function m.changerRapport(k, rpmHelices)
local b = etat.boite
if not b then return false end
if k == 15 then k = (b.rapport * 16 < 15) and 14 or 16 end
k = math.max(1, math.min(16, k))
local nouveauRapport = k / 16
if nouveauRapport == b.rapport then return false end
local monte = nouveauRapport > b.rapport
local function ecrireRsc()
for role in pairs(HELICES) do
local p = m.rscParRole[role]
local v = rpmHelices[role]
if p and v then
local rsc = math.max(-256, math.min(256,
math.floor(v / nouveauRapport + 0.5)))
m.rpm[role] = { v = rsc, tChange = os.clock() }
p.setTargetSpeed(rsc)
end
end
end
local signal = m.signalPourRapportK(k)
local function ecrireFaces()
for _, face in pairs(b.faces or {}) do
m.reglerSignalBoite(b.relais, face, signal)
end
end
if monte then
ecrireRsc()
ecrireFaces()
else
ecrireFaces()
ecrireRsc()
end
b.signal, b.rapport = signal, nouveauRapport
return true
end
function m.toutArreter() function m.toutArreter()
for _, p in pairs(m.rsc) do p.setTargetSpeed(0) end for _, p in pairs(m.rsc) do p.setTargetSpeed(0) end
for role, suivi in pairs(m.rpm) do for role, suivi in pairs(m.rpm) do
if suivi.v ~= 0 then m.rpm[role] = { v = 0, tChange = os.clock() } end if suivi.v ~= 0 then
m.rpm[role] = { v = 0, tChange = os.clock() }
end
end end
end end
------------------------------------------------------------------ -- niveau 0 = arret, 15 = pleine puissance (redstone inverse:
-- DIAGNOSTIC DES CALIBRATIONS -- signal bottom 15 = arret, 0 = maxi)
------------------------------------------------------------------ local niveauCourant = -1
function m.helicesManquantes() function m.reglerMoteur(niveau)
local manquantes = {} niveau = math.max(0, math.min(15, math.floor(niveau + 0.5)))
for _, role in ipairs({ "lb", "rb", "lf", "rf" }) do if niveau ~= niveauCourant then
if not m.rscParRole[role] then table.insert(manquantes, role) end redstone.setAnalogOutput("bottom", 15 - niveau)
niveauCourant = niveau
end end
return manquantes
end end
function m.propulseursManquants() function m.niveauMoteur()
local manquants = {} return math.max(0, niveauCourant)
for _, role in ipairs({ "prop_l", "prop_r" }) do
if not m.rscParRole[role] then table.insert(manquants, role) end
end
return manquants
end
function m.axesManquants()
local manquants = {}
for _, axe in ipairs({ "avant", "lateral", "vertical" }) do
if not m.veloParAxe[axe] then table.insert(manquants, axe) end
end
return manquants
end end
return m return m

View file

@ -1,77 +0,0 @@
--------------------------------------------------------------------
-- lib/navigation.lua : position GPS, estimation du cap, ralliement.
--
-- Le cap n'est pas mesurable directement (pas de boussole): il est
-- estime a partir du deplacement GPS quand la vitesse est
-- suffisante. Le ralliement avance donc "en aveugle" au depart puis
-- corrige le cap des que le drone bouge.
--------------------------------------------------------------------
local Navigation = {}
function Navigation.nouveau(conf, materiel, journal)
local n = {}
local posPrec, tPrec = nil, nil
local cap = nil -- radians, atan2(dx, dz) du deplacement
function n.position()
return materiel.lirePosition()
end
-- A appeler periodiquement: met a jour l'estimation du cap
function n.actualiser()
local x, y, z = materiel.lirePosition()
if not x then return nil end
local t = os.clock()
if posPrec and t > tPrec then
local dx, dz = x - posPrec.x, z - posPrec.z
local dist = math.sqrt(dx * dx + dz * dz)
local vitesse = dist / (t - tPrec)
if vitesse >= conf.NAV.VITESSE_GPS_MIN then
cap = math.atan2(dx, dz)
end
end
posPrec, tPrec = { x = x, y = y, z = z }, t
return x, y, z
end
-- Commandes propulseurs pour rallier (cibleX, cibleZ).
-- Retourne avance, virage, distance (ou nil si pas de GPS).
function n.rallier(cibleX, cibleZ)
local x, y, z = n.actualiser()
if not x then return nil end
local dx, dz = cibleX - x, cibleZ - z
local distance = math.sqrt(dx * dx + dz * dz)
if distance <= conf.NAV.SEUIL_ARRIVEE then
return 0, 0, distance
end
local capCible = math.atan2(dx, dz)
local virage = 0
if cap then
local ecart = capCible - cap
while ecart > math.pi do ecart = ecart - 2 * math.pi end
while ecart < -math.pi do ecart = ecart + 2 * math.pi end
-- proportionnel borne: plein virage au dela de 45 degres
virage = math.max(-1, math.min(1, ecart / (math.pi / 4)))
end
-- avance reduite tant que le cap est incertain ou tres desaxe
local avance = 0.8
if cap == nil then
avance = 0.4 -- demarrage: avancer pour estimer le cap
elseif math.abs(virage) > 0.7 then
avance = 0.2 -- virage serre: privilegier la rotation
end
return avance, virage, distance
end
function n.razCap()
cap, posPrec, tPrec = nil, nil, nil
end
return n
end
return Navigation

View file

@ -1,28 +1,31 @@
-------------------------------------------------------------------- --------------------------------------------------------------------
-- lib/pid.lua : regulateur PID concu pour le zero-depassement -- lib/pid.lua : regulateur PID concu pour le zero-depassement.
-- - derivee calculee sur la MESURE (pas de coup de derivee au -- - derivee sur la MESURE (pas de coup au changement de consigne)
-- changement de consigne) -- et FILTREE (EMA) contre le bruit des capteurs
-- - integrale conditionnelle: gelee quand la sortie sature dans le -- - integrale conditionnelle: gelee quand la sortie sature dans
-- sens de l'erreur (anti-emballement) -- le sens de l'erreur (anti-emballement)
-------------------------------------------------------------------- --------------------------------------------------------------------
local Pid = {} local Pid = {}
Pid.__index = Pid Pid.__index = Pid
-- gains: { kp, ki, kd } ; bornes de sortie ; borne d'integrale -- gains { kp, ki, kd } lus PAR REFERENCE a chaque pas (edition vive)
function Pid.nouveau(gains, borneMin, borneMax, iMax) function Pid.nouveau(gains, borneMin, borneMax, iMax, filtreD)
return setmetatable({ return setmetatable({
gains = gains, gains = gains,
borneMin = borneMin, borneMin = borneMin,
borneMax = borneMax, borneMax = borneMax,
iMax = iMax or 40, iMax = iMax or 40,
filtreD = filtreD or 0.5, -- 1 = brut, 0.5 = lisse
integrale = 0, integrale = 0,
mesurePrec = nil, mesurePrec = nil,
deriveeF = 0,
}, Pid) }, Pid)
end end
function Pid:raz() function Pid:raz()
self.integrale = 0 self.integrale = 0
self.mesurePrec = nil self.mesurePrec = nil
self.deriveeF = 0
end end
function Pid:reglerGains(gains) function Pid:reglerGains(gains)
@ -33,21 +36,21 @@ function Pid:calculer(consigne, mesure, dt)
local g = self.gains local g = self.gains
local erreur = consigne - mesure local erreur = consigne - mesure
local derivee = 0
if self.mesurePrec and dt > 0 then if self.mesurePrec and dt > 0 then
derivee = (mesure - self.mesurePrec) / dt local brute = (mesure - self.mesurePrec) / dt
self.deriveeF = self.deriveeF
+ self.filtreD * (brute - self.deriveeF)
end end
self.mesurePrec = mesure self.mesurePrec = mesure
-- tentative d'integration bornee
local integrale = self.integrale + erreur * dt local integrale = self.integrale + erreur * dt
if integrale > self.iMax then integrale = self.iMax end if integrale > self.iMax then integrale = self.iMax end
if integrale < -self.iMax then integrale = -self.iMax end if integrale < -self.iMax then integrale = -self.iMax end
local sortie = g.kp * erreur + g.ki * integrale - g.kd * derivee local sortie = g.kp * erreur + g.ki * integrale
- g.kd * self.deriveeF
if sortie > self.borneMax then if sortie > self.borneMax then
-- sature haut: on n'integre que si l'erreur tire vers le bas
if erreur < 0 then self.integrale = integrale end if erreur < 0 then self.integrale = integrale end
return self.borneMax return self.borneMax
elseif sortie < self.borneMin then elseif sortie < self.borneMin then

View file

@ -1,332 +0,0 @@
--------------------------------------------------------------------
-- lib/pilotage.lua : regulation et mixage.
--
-- ASSERVISSEMENT EN CASCADE + FEEDFORWARD :
-- 1. feedforward : rpm d'equilibre (sustentation) calcule depuis
-- POIDS et POUSSEE_HELICE_MAX; le PID ne corrige qu'autour de
-- ce point au lieu de devoir le trouver par integrale
-- 2. boucle externe : erreur d'altitude -> consigne de vitesse
-- verticale (P simple, bornee V_MONTEE_MAX / V_DESCENTE_MAX)
-- 3. boucle interne : PID sur la vitesse verticale MESUREE
-- (velocity sensor vertical calibre; repli: derivee filtree de
-- l'altitude) -> delta rpm autour du feedforward
-- 4. assiette : PID tangage + roulis, corrections signees par coin
-- 5. propulseurs : mixage differentiel avance / virage
--------------------------------------------------------------------
local Pilotage = {}
local SIGNES = {
lb = { bf = -1, lr = 1 },
rb = { bf = -1, lr = -1 },
lf = { bf = 1, lr = 1 },
rf = { bf = 1, lr = -1 },
}
-- Assiette normalisee (calib gimbal): tangage NEGATIF = nez bas,
-- roulis NEGATIF = penche a gauche.
-- Algebre du signe: nez bas => mesure < 0 => erreur (consigne 0 -
-- mesure) > 0 => sortie PID > 0. Il faut alors POUSSER PLUS a
-- l'AVANT (bf = +1) pour relever le nez, donc la correction
-- s'applique telle quelle: signes +1. (Un -1 ici AMPLIFIE
-- l'inclinaison au lieu de la corriger.)
local SIGNE_TANGAGE, SIGNE_ROULIS = 1, 1
function Pilotage.nouveau(conf, etat, materiel, Pid, journal)
local p = {}
local VMAX = conf.VITESSE_RSC_MAX
local function borner(v, mini, maxi)
if v < mini then return mini elseif v > maxi then return maxi end
return v
end
-- Gains: montee/descente viennent des calibrations (etat.pid),
-- l'assiette vient TOUJOURS de la conf (reference vive: la page
-- PARAM agit immediatement)
local gains = {
alt = conf.PID.alt,
montee = (etat.pid and etat.pid.montee) or conf.PID.montee,
descente = (etat.pid and etat.pid.descente) or conf.PID.descente,
tangage = conf.PID.tangage,
roulis = conf.PID.roulis,
}
local pidMontee = Pid.nouveau(gains.montee, -VMAX, VMAX, 20)
local pidDescente = Pid.nouveau(gains.descente, -VMAX, VMAX, 20)
local pidTangage = Pid.nouveau(gains.tangage, -VMAX / 2, VMAX / 2)
local pidRoulis = Pid.nouveau(gains.roulis, -VMAX / 2, VMAX / 2)
local consigneRampe = nil
local regimePrec = nil
local facteurProp = 1.0
local altPrec, vFiltre = nil, 0 -- repli de mesure de vitesse
-- ADAPTATION (transport a charge variable, remise a zero au
-- decollage via razAdaptation)
local rpmAdapt = 0 -- correction adaptative du ff
local trimTangage, trimRoulis = 0, 0 -- trim d'assiette adaptatif
local limitation = nil -- info du limiteur de stress
------------------------------------------------------------------
-- FEEDFORWARD : rpm d'equilibre d'UNE helice
-- poussee(rpm) = pousseeEffective * (rpm/VMAX)^exp, avec la
-- poussee CORRIGEE PAR LA PRESSION (elle diminue avec l'altitude):
-- pousseeEffective = POUSSEE_HELICE_MAX * pression / PRESSION_REF
-- equilibre: 4 * poussee(rpm) = POIDS (a vide) + charge estimee
------------------------------------------------------------------
local function pousseeEffective()
local facteur = materiel.lirePression() / conf.PRESSION_REF
return conf.POUSSEE_HELICE_MAX * math.max(facteur, 0.05)
end
function p.rpmSustentation()
local ratio = (conf.POIDS / 4) / pousseeEffective()
if ratio <= 0 then return 0 end
if ratio >= 1 then return VMAX end
return VMAX * ratio ^ (1 / conf.POUSSEE_EXPOSANT)
end
-- poids total estime (a vide + charge), inverse de la courbe de
-- poussee au rpm de sustentation adapte, a la pression courante
function p.poidsEstime()
local rpm = math.max(0, math.min(VMAX, p.rpmSustentation() + rpmAdapt))
return 4 * pousseeEffective() * (rpm / VMAX) ^ conf.POUSSEE_EXPOSANT
end
-- plafond de poids total (regle des 60% a la pression de reference)
function p.poidsMax()
return conf.RATIO_CHARGE_MAX * 4 * conf.POUSSEE_HELICE_MAX
end
function p.trim()
return trimTangage, trimRoulis
end
-- derniere limitation de stress appliquee ({besoin, budget}) ou nil
function p.limitationStress()
return limitation
end
------------------------------------------------------------------
-- LIMITEUR DE STRESS (anti-overstress transitoire)
-- Borne les commandes pour que le besoin PREVU reste sous la
-- capacite MESUREE * SECURITE. Priorites: la sustentation n'est
-- jamais reduite, puis la montee, puis les propulseurs.
-- Retourne: base bornee, facteur propulseurs (0..1)
------------------------------------------------------------------
local function limiterStress(base, propTotal)
limitation = nil
local k = etat.stressParRpm
if not k or not k.helice then return base, 1 end
local _, capacite = materiel.lireStressBrut()
if not capacite or capacite <= 0 then return base, 1 end
local budget = capacite * conf.MOTEUR.SECURITE
local sust = borner(p.rpmSustentation() + rpmAdapt, 0, VMAX)
local besoinHelices = k.helice * 4 * base
local besoinProps = (k.prop or 0) * propTotal
local total = besoinHelices + besoinProps
if total <= budget then return base, 1 end
limitation = { besoin = total, budget = budget }
local excedent = total - budget
-- 1. delester les propulseurs
local facteurP = 1
if besoinProps > 0 then
local reduc = math.min(besoinProps, excedent)
facteurP = (besoinProps - reduc) / besoinProps
excedent = excedent - reduc
end
-- 2. rogner la part au-dessus de la sustentation (la montee)
if excedent > 0 then
local margeMontee = k.helice * 4 * math.max(0, base - sust)
local reduc = math.min(margeMontee, excedent)
base = base - reduc / (k.helice * 4)
excedent = excedent - reduc
end
-- 3. il reste un excedent: la sustentation elle-meme depasse le
-- budget; on la GARDE (securite > stress), signale via limitation
limitation.critique = excedent > 0
return base, facteurP
end
function p.razAdaptation()
rpmAdapt, trimTangage, trimRoulis = 0, 0, 0
end
------------------------------------------------------------------
-- MESURE DE VITESSE VERTICALE
-- capteur calibre si present, sinon derivee filtree de l'altitude
------------------------------------------------------------------
function p.vitesseVerticale(altitude, dt)
if materiel.veloParAxe["vertical"] then
return materiel.lireVitesseAxe("vertical")
end
if altPrec and dt > 0 then
local brute = (altitude - altPrec) / dt
vFiltre = vFiltre + 0.3 * (brute - vFiltre)
end
altPrec = altitude
return vFiltre
end
function p.gains()
return gains
end
function p.reglerGains(nouveaux)
gains.alt = nouveaux.alt or gains.alt
gains.montee = nouveaux.montee or gains.montee
gains.descente = nouveaux.descente or gains.descente
-- seuls les gains verticaux sont persistes: l'assiette reste
-- pilotee par la conf / page PARAM
etat.pid = { montee = gains.montee, descente = gains.descente }
pidMontee:reglerGains(gains.montee)
pidDescente:reglerGains(gains.descente)
end
function p.reglerFacteurProp(f)
facteurProp = math.max(0, math.min(1, f))
end
function p.raz()
pidMontee:raz()
pidDescente:raz()
pidTangage:raz()
pidRoulis:raz()
consigneRampe = nil
regimePrec = nil
altPrec, vFiltre = nil, 0
end
-- applique base + trim + corrections d'assiette aux 4 helices.
-- `adapter` (booleen): en quasi-stationnaire, le trim absorbe
-- lentement la composante statique des corrections (CG decale par
-- la cargaison); le PID reste centre sur la dynamique.
local function appliquerHelices(base, tangage, roulis, dt,
anglesCibles, adapter)
-- autorite d'assiette proportionnelle au point de fonctionnement
local plafond = math.max(2,
conf.ASSIETTE_PLAFOND * (p.rpmSustentation() + rpmAdapt))
pidTangage.borneMin, pidTangage.borneMax = -plafond, plafond
pidRoulis.borneMin, pidRoulis.borneMax = -plafond, plafond
local corrTangage = SIGNE_TANGAGE
* pidTangage:calculer(anglesCibles.tangage or 0, tangage, dt)
local corrRoulis = SIGNE_ROULIS
* pidRoulis:calculer(anglesCibles.roulis or 0, roulis, dt)
if adapter then
local taux = conf.ADAPTATION.TAUX_TRIM * dt
trimTangage = borner(trimTangage + taux * corrTangage,
-plafond, plafond)
trimRoulis = borner(trimRoulis + taux * corrRoulis,
-plafond, plafond)
end
for role, s in pairs(SIGNES) do
materiel.reglerRsc(role, borner(base
+ s.bf * (trimTangage + corrTangage)
+ s.lr * (trimRoulis + corrRoulis), 0, VMAX))
end
end
------------------------------------------------------------------
-- UN PAS DE REGULATION EN VOL
------------------------------------------------------------------
function p.reguler(consigneY, angles, avance, virage, dt)
local altitude = materiel.lireAltitude()
local tangage, roulis = materiel.lireAssiette()
-- rampe de consigne
if consigneRampe == nil then consigneRampe = altitude end
local pas = conf.VITESSE_RAMPE * dt
consigneRampe = consigneRampe
+ borner(consigneY - consigneRampe, -pas, pas)
-- boucle externe: erreur alt -> vitesse verticale cible
local vCible = borner(gains.alt.kp * (consigneRampe - altitude),
-conf.V_DESCENTE_MAX, conf.V_MONTEE_MAX)
-- boucle interne: vitesse verticale -> delta rpm (double regime)
local vMesuree = p.vitesseVerticale(altitude, dt)
local montee = vCible >= 0
local pid = montee and pidMontee or pidDescente
if regimePrec ~= nil and regimePrec ~= montee then pid:raz() end
regimePrec = montee
local deltaRpm = pid:calculer(vCible, vMesuree, dt)
-- GAIN SCHEDULING: la boucle de vitesse est mise a l'echelle du
-- poids (delta de poussee requis proportionnel a la masse)
if etat.poidsCalibration and etat.poidsCalibration > 0 then
deltaRpm = deltaRpm
* borner(p.poidsEstime() / etat.poidsCalibration, 0.5, 2.0)
end
-- ADAPTATION DE POIDS: en quasi-stationnaire, le residu du PID
-- est transfere lentement vers le feedforward (le drone se pese)
local stationnaire = math.abs(vCible) < 0.2
and math.abs(vMesuree) < 0.3
if stationnaire then
rpmAdapt = borner(
rpmAdapt + conf.ADAPTATION.TAUX_POIDS * dt * deltaRpm,
-VMAX / 2, VMAX / 2)
end
local base = borner(p.rpmSustentation() + rpmAdapt + deltaRpm,
0, VMAX)
-- propulseurs (differentiel, delestables en surcharge)
local function b1(v) return borner(v, -1, 1) end
local propR = b1(avance + virage) * VMAX * facteurProp
local propL = b1(avance - virage) * VMAX * facteurProp
-- limiteur de stress: base et propulseurs bornes au budget
local facteurStress
base, facteurStress = limiterStress(base,
math.abs(propR) + math.abs(propL))
appliquerHelices(base, tangage, roulis, dt, angles, stationnaire
and math.abs(tangage) < conf.ANGLE_MAX
and math.abs(roulis) < conf.ANGLE_MAX)
materiel.reglerRsc("prop_r", propR * facteurStress)
materiel.reglerRsc("prop_l", propL * facteurStress)
return altitude, consigneRampe
end
------------------------------------------------------------------
-- PAS EN BOUCLE OUVERTE (identification pour pidmath)
-- applique rpmSustentation + deltaRpm (assiette toujours asservie)
-- et retourne la vitesse verticale mesuree
------------------------------------------------------------------
function p.pasOuvert(deltaRpm, dt)
local altitude = materiel.lireAltitude()
local tangage, roulis = materiel.lireAssiette()
local base = borner(p.rpmSustentation() + rpmAdapt + deltaRpm,
0, VMAX)
appliquerHelices(base, tangage, roulis, dt,
{ tangage = 0, roulis = 0 }, false)
materiel.reglerRsc("prop_l", 0)
materiel.reglerRsc("prop_r", 0)
return p.vitesseVerticale(altitude, dt), altitude
end
function p.plaquer()
for role in pairs(SIGNES) do
materiel.reglerRsc(role, -VMAX)
end
materiel.reglerRsc("prop_l", 0)
materiel.reglerRsc("prop_r", 0)
end
function p.arreter()
materiel.toutArreter()
p.raz()
end
return p
end
return Pilotage

346
lib/regulation.lua Normal file
View file

@ -0,0 +1,346 @@
--------------------------------------------------------------------
-- lib/regulation.lua : stabilisation et mixage.
--
-- STRUCTURE (concue contre les oscillations constatees en V1):
-- ALTITUDE en cascade: erreur -> vitesse verticale cible (P,
-- bornee) -> PID vitesse -> delta rpm autour du rpm de
-- sustentation MESURE (calib altitude). La vitesse verticale
-- vient du velocity sensor vertical auto-detecte a la calib
-- (repli: derivee filtree de l'altitude).
-- ASSIETTE: PID par axe a derivee filtree, autorite bornee a
-- ASSIETTE_PLAFOND * rpm de sustentation, zone morte reglable.
-- SUPERVISEUR D'OSCILLATION: si une boucle oscille (passages a
-- zero repetes au-dela d'un seuil d'amplitude), ses gains sont
-- reduits automatiquement de 20%% et l'evenement journalise:
-- le systeme converge vers la stabilite meme mal regle.
-- MOTEUR: mode on/off (pleine puissance) ou adaptatif (niveau
-- regle AVANT l'ecriture des RSC en montee, apres en descente).
--------------------------------------------------------------------
local Regulation = {}
-- coefficients de mixage par coin
local SIGNES = {
lb = { bf = -1, lr = 1 },
rb = { bf = -1, lr = -1 },
lf = { bf = 1, lr = 1 },
rf = { bf = 1, lr = -1 },
}
-- Algebre du signe (demontree en V1): nez bas => tangage mesure < 0
-- => erreur (0 - mesure) > 0 => sortie PID > 0, a APPLIQUER en plus
-- a l'AVANT (bf = +1) pour relever le nez. Signes +1. Un -1 ici
-- AMPLIFIE l'inclinaison au lieu de la corriger.
local SIGNE_TANGAGE, SIGNE_ROULIS = 1, 1
function Regulation.nouveau(conf, etat, materiel, Pid, journal)
local r = {}
local VMAX = conf.VMAX
-- borne HELICE courante: 256 RSC * rapport de boite
local function vmaxHelice()
return VMAX * materiel.rapportBoite()
end
local function borner(v, mini, maxi)
if v < mini then return mini elseif v > maxi then return maxi end
return v
end
-- gains: conf.PID est la table VIVE (surcharges appliquees au
-- demarrage, page PARAM et calibrations la modifient en place)
local pidVitesse = Pid.nouveau(conf.PID.vitesse, -VMAX, VMAX, 30)
local pidTangage = Pid.nouveau(conf.PID.tangage, -VMAX, VMAX, 15)
local pidRoulis = Pid.nouveau(conf.PID.roulis, -VMAX, VMAX, 15)
local consigneRampe = nil
local ffAdapt = 0 -- adaptation lente (charge variable)
local altPrec, vFiltre = nil, 0
local injection = {} -- relais de calibration par axe
local gainsModifies = false -- superviseur: a persister
------------------------------------------------------------------
-- SUSTENTATION: rpm mesure par la calib altitude, corrige par la
-- pression (la poussee diminue avec l'altitude)
------------------------------------------------------------------
-- rpm HELICE de sustentation (etat.rpmHover est en unites
-- helice: invariant au rapport de boite)
function r.rpmHover()
local base = etat.rpmHover or VMAX * 0.3
if etat.pressionHover then
local facteur = etat.pressionHover
/ math.max(materiel.lirePression(), 0.05)
base = base * facteur
end
return borner(base + ffAdapt, 0, vmaxHelice())
end
------------------------------------------------------------------
-- VITESSE VERTICALE: capteur auto-detecte, repli derivee filtree
------------------------------------------------------------------
function r.vitesseVerticale(dt)
local vv = etat.veloVertical
if vv then
return materiel.lireVitesseCapteur(vv.nom) * vv.signe
end
local altitude = materiel.lireAltitude()
if altPrec and dt and dt > 0 then
local brute = (altitude - altPrec) / dt
vFiltre = vFiltre + 0.3 * (brute - vFiltre)
end
altPrec = altitude
return vFiltre
end
------------------------------------------------------------------
-- SUPERVISEUR D'OSCILLATION
-- Une boucle qui traverse zero >= 8 fois en 6 s avec une
-- amplitude significative oscille: ses gains sont reduits de 20%%
------------------------------------------------------------------
local surveillance = {
vitesse = { seuil = 0.4, gains = conf.PID.vitesse },
tangage = { seuil = 0.4, gains = conf.PID.tangage },
roulis = { seuil = 0.4, gains = conf.PID.roulis },
}
for _, s in pairs(surveillance) do
s.croisements, s.signePrec, s.arme = {}, nil, false
end
local function surveiller(nom, valeur)
local s = surveillance[nom]
local maintenant = os.clock()
if math.abs(valeur) > s.seuil then s.arme = true end
local signe = valeur >= 0
if s.signePrec ~= nil and signe ~= s.signePrec and s.arme then
table.insert(s.croisements, maintenant)
s.arme = false
end
s.signePrec = signe
while s.croisements[1]
and maintenant - s.croisements[1] > 6 do
table.remove(s.croisements, 1)
end
if #s.croisements >= 8 then
s.croisements = {}
s.gains.kp = s.gains.kp * 0.8
s.gains.ki = s.gains.ki * 0.8
s.gains.kd = s.gains.kd * 0.85
gainsModifies = true
journal.alerte(("oscillation %s: gains reduits (kp=%.2f)")
:format(nom, s.gains.kp))
end
end
-- le superviseur a-t-il modifie des gains (a persister) ?
function r.gainsModifies()
if gainsModifies then gainsModifies = false return true end
return false
end
------------------------------------------------------------------
-- MOTEUR: besoin prevu et niveau anticipe
------------------------------------------------------------------
-- cibles: helices en rpm HELICE, propulseurs en rpm RSC; les
-- coefficients (mesures a rapport 1/1) sont en su par rpm helice
local function besoinPrevu(cibles)
if not etat.coeffRsc then return nil end
local besoin = 0
for role, v in pairs(cibles) do
local nom = materiel.nomParRole[role]
local coeff = nom and etat.coeffRsc[nom]
if coeff then besoin = besoin + coeff * math.abs(v) end
end
return besoin
end
------------------------------------------------------------------
-- BOITE AUTOMATIQUE: maintient la commande RSC des helices dans
-- la bande [RSC_BAS, RSC_HAUT] en changeant le rapport d'UN cran,
-- uniquement en quasi-stationnaire et a plat, avec temps mort.
-- Le regulateur (unites helice) n'y voit RIEN: gains invariants.
------------------------------------------------------------------
local prochainChangement = 0
local boiteModifiee = false
local function gererBoite(cibles, calme)
local b = etat.boite
if not b or not calme then return end
if os.clock() < prochainChangement then return end
local base = (cibles.lb + cibles.rb + cibles.lf + cibles.rf) / 4
local rsc = base / b.rapport
local k = math.floor(b.rapport * 16 + 0.5)
local nouveauK = nil
if rsc > conf.BOITE.RSC_HAUT and k < 16 then
nouveauK = (k == 14) and 16 or (k + 1)
elseif rsc < conf.BOITE.RSC_BAS and k > 1 then
nouveauK = (k == 16) and 14 or (k - 1)
end
if nouveauK then
local rpmHelices = { lb = cibles.lb, rb = cibles.rb,
lf = cibles.lf, rf = cibles.rf }
if materiel.changerRapport(nouveauK, rpmHelices) then
prochainChangement = os.clock() + conf.BOITE.ATTENTE
boiteModifiee = true
journal.info(("boite: rapport %d/16 (RSC sustentation "
.. "~%.0f)"):format(nouveauK, base * 16 / nouveauK))
end
end
end
-- le rapport a-t-il change (etat a persister) ?
function r.boiteModifiee()
if boiteModifiee then boiteModifiee = false return true end
return false
end
local function niveauPour(besoin)
local capacites = etat.capaciteParNiveau
if not capacites then return 15 end
local vise = besoin * (1 + conf.MOTEUR.MARGE)
for n = 1, 15 do
if (capacites[n] or 0) >= vise then return n end
end
return 15
end
------------------------------------------------------------------
-- APPLICATION DES COMMANDES (moteur anticipe puis RSC)
------------------------------------------------------------------
local function appliquer(cibles)
if etat.modeMoteur == "adaptatif" then
local besoin = besoinPrevu(cibles)
if besoin then
-- garde de stress: jamais plus que la capacite maximale
local capacites = etat.capaciteParNiveau
local capaMax = capacites and capacites[15]
if capaMax and capaMax > 0
and besoin > capaMax * conf.MOTEUR.SECURITE then
local facteur = capaMax * conf.MOTEUR.SECURITE / besoin
for role, v in pairs(cibles) do
cibles[role] = v * facteur
end
besoin = besoin * facteur
end
local niveau = niveauPour(besoin)
if niveau > materiel.niveauMoteur() then
-- MONTEE de regime AVANT les RSC (anticipation)
materiel.reglerMoteur(niveau)
for role, v in pairs(cibles) do
materiel.reglerRsc(role, v)
end
return
else
-- descente: RSC d'abord, moteur ensuite
for role, v in pairs(cibles) do
materiel.reglerRsc(role, v)
end
materiel.reglerMoteur(niveau)
return
end
end
end
for role, v in pairs(cibles) do
materiel.reglerRsc(role, v)
end
end
------------------------------------------------------------------
-- UN PAS DE REGULATION EN VOL
-- avance / virage: joystick (-1..1), propulseurs differentiels
------------------------------------------------------------------
function r.reguler(consigneY, avance, virage, dt)
local altitude = materiel.lireAltitude()
local tangage, roulis = materiel.lireAssiette()
local hover = r.rpmHover()
-- rampe de consigne
if consigneRampe == nil then consigneRampe = altitude end
local pas = conf.RAMPE * dt
consigneRampe = consigneRampe
+ borner(consigneY - consigneRampe, -pas, pas)
-- cascade: erreur -> v cible -> delta rpm
local vCible = borner(conf.PID.alt.kp * (consigneRampe - altitude),
-conf.V_DESCENTE_MAX, conf.V_MONTEE_MAX)
local vMesuree = r.vitesseVerticale(dt)
pidVitesse.borneMin = -vmaxHelice()
pidVitesse.borneMax = vmaxHelice()
local deltaRpm = injection.vitesse
or pidVitesse:calculer(vCible, vMesuree, dt)
surveiller("vitesse", vCible - vMesuree)
-- adaptation lente: en quasi-stationnaire le residu du PID est
-- transfere vers la sustentation (charge variable)
local calme = math.abs(vCible) < 0.2 and math.abs(vMesuree) < 0.3
if not injection.vitesse and calme then
ffAdapt = borner(ffAdapt + 0.05 * dt * deltaRpm,
-vmaxHelice() / 2, vmaxHelice() / 2)
end
-- assiette: autorite proportionnelle a la sustentation
local plafond = math.max(2, conf.ASSIETTE_PLAFOND * hover)
pidTangage.borneMin, pidTangage.borneMax = -plafond, plafond
pidRoulis.borneMin, pidRoulis.borneMax = -plafond, plafond
local corrTangage = injection.tangage or (SIGNE_TANGAGE
* pidTangage:calculer(0, tangage, dt))
local corrRoulis = injection.roulis or (SIGNE_ROULIS
* pidRoulis:calculer(0, roulis, dt))
surveiller("tangage", tangage)
surveiller("roulis", roulis)
-- mixage (helices en rpm HELICE)
local base = borner(hover + deltaRpm, 0, vmaxHelice())
local cibles = {}
for role, s in pairs(SIGNES) do
cibles[role] = borner(base + s.bf * corrTangage
+ s.lr * corrRoulis, 0, vmaxHelice())
end
cibles.prop_r = borner(avance + virage, -1, 1) * VMAX
cibles.prop_l = borner(avance - virage, -1, 1) * VMAX
appliquer(cibles)
-- boite automatique (apres application: bascule sur des cibles
-- fraiches, uniquement au calme et a plat)
gererBoite(cibles, calme
and math.abs(tangage) < conf.ANGLE_MAX / 2
and math.abs(roulis) < conf.ANGLE_MAX / 2)
return altitude, consigneRampe
end
------------------------------------------------------------------
-- MODES SPECIAUX
------------------------------------------------------------------
-- stationnement: helices a pleine puissance INVERSEE (plaquage)
function r.plaquer()
local v = vmaxHelice()
appliquer({ lb = -v, rb = -v, lf = -v, rf = -v,
prop_l = 0, prop_r = 0 })
end
function r.arreter()
materiel.toutArreter()
r.raz()
end
function r.raz()
pidVitesse:raz()
pidTangage:raz()
pidRoulis:raz()
consigneRampe = nil
altPrec, vFiltre = nil, 0
end
function r.razAdaptation()
ffAdapt = 0
end
-- calibrations: forcer une boucle ("vitesse", "tangage",
-- "roulis") a `valeur` (nil = rendre la main au PID)
function r.reglerInjection(boucle, valeur)
injection[boucle] = valeur
end
return r
end
return Regulation