Files
airship-autopilot/files/autopilot/stabilize.lua

572 lines
24 KiB
Lua
Raw Normal View History

-- stabilize.lua -- GENERELT selvkalibrerende hoyde+pitch PID for ballong-luftskip.
--
-- Ingen ballong-spesifikke tall hardkodet:
-- * svevegass laeres av hoyde-integralet
-- * trim-skjevhet (volum/CoM) laeres av pitch-integralet
-- * pitch-fortegn auto-detekteres ved oppstart
-- Det eneste skip-spesifikke er hvilken redstone-side hver ventil sitter paa.
--
-- Sensorer (auto-funnet paa type): altitude_sensor (getHeight), gimbal_sensor (getAngles)
-- Aktuator: redstone analog 0..MAX_SIGNAL -> ventil (mer = mer gass = mer loft)
--
-- BRUK:
-- stabilize cal -> logger sensoravlesninger til cal.log (styrer ingenting)
-- stabilize test -> kjente ventil-monstre -> test.log
-- stabilize -> kjorer regulatoren -> run.log
-- ============================ KONFIG ============================
-- Skip-spesifikt: hvilke redstone-sider mater ventilene ("left"/"right"/"front"/"back")
local FORE_SIDE = "right"
local AFT_SIDE = "back" -- nil hvis bare EN ventil (da deaktiveres pitch-styring)
local TARGET_PITCH = 0 -- onsket pitch (vannrett)
local TAKEOFF_OFFSET = 5 -- mal settes saa mange blokker over startposisjon
-- Start-gains (dempet; auto-tuneren justerer og lagrer dem videre)
local A_KP, A_KI, A_KD = 0.22, 0.04, 2.2 -- hoyde
local P_KP, P_KI, P_KD = 0.22, 0.05, 0.70 -- pitch
local AUTOTUNE = true -- maaler egen svingning og justerer gains
local TUNE_INTERVAL = 12 -- sekunder mellom hver justering (KI laerer trim-skjevheten)
local MAX_SIGNAL = 7 -- maks redstone. Hold i responsivt omraade (loftet ditt topper ~5).
local MIN_SIGNAL = 1 -- gulv paa TOTAL-loft (collective) -> mykt fall, aldri fritt fall.
local LAG = 2.0 -- sek: tregheten i loft-respons. Regulatoren forutser saa langt fram.
local SLEW = 2 -- maks signal-endring per tick (demper voldsomme hopp)
local HOVER_SEED = 4.5 -- start-gjetning paa svevegass (naer ekte hover; kartet finjusterer)
-- Pitch-fortegn: 0 = auto-detekter ved oppstart, ellers +1/-1 manuelt
local PITCH_SIGN = 0
local PITCH_INDEX = 1 -- hvilket tall fra getAngles er pitch
-- Rock-solid hold (Tier 1) + place-and-forget (Tier 2)
local DEADBAND_H = 1.5 -- blokker: innenfor dette holdes hoyden rolig (ingen twitch)
local DEADBAND_P = 2.0 -- grader: innenfor dette holdes pitch rolig
local DERIV_FILTER = 0.3 -- lavpass paa avledning (0..1; lavere = mykere)
local SETTLE_TIME = 3.0 -- sek innenfor dodband for "STABIL"
-- KASKADE-regulering for hoyde (hindrer fartsoppbygging -> oversving)
local MAX_RATE = 2.0 -- blokker/sek: maks klatre-/synkerate
local K_POS = 0.4 -- posisjonsfeil -> onsket rate (5 blokker feil -> maks rate)
local K_RATE = 1.2 -- rate-feil -> gass (auto-tunes)
-- Grasios bevegelse + pitch-kaskade + robusthet
local MAX_ACCEL = 1.0 -- blokker/sek^2: myk ease-in/ut paa rate-endring
local MAX_PITCH_RATE = 4.0 -- grader/sek: maks pitch-endringsrate (grasios attityde)
local K_POS_P = 0.5 -- pitch-feil -> onsket pitch-rate
local K_PRATE = 0.08 -- pitch-rate-feil -> differanse (auto-tunes), mildt mot lag
local MAX_DIFF = 4.0 -- maks differanse: pitch kan ALDRI pumpe all gass til en ende
local GLITCH_H = 25 -- avvis hoyde-hopp storre enn dette per tick (sensor-glitch)
local DT = 0.2
-- ===============================================================
-- ---- selvlaert modell (Fase C/D): persisteres i profilen ----
local BIN_SIZE = 10 -- blokker per kart-bin
local hoverMap = {} -- hoyde-bin -> svevegass (laert kurve)
local kCouple = 0 -- collective->pitch koblingskoeffisient (laert)
local TRIM = 0 -- statisk balanse-differanse (maalt i balance-modus)
local altS = peripheral.find("altitude_sensor")
local gimS = peripheral.find("gimbal_sensor")
-- hoverFF: interpoler laert svevegass for en hoyde (fallback = HOVER_SEED)
local function hoverFF(h)
local b = h / BIN_SIZE
local lo, hi = math.floor(b), math.ceil(b)
local vlo, vhi = hoverMap[lo], hoverMap[hi]
if vlo and vhi then
if lo == hi then return vlo end
return vlo + (vhi - vlo) * (b - lo)
elseif vlo then return vlo
elseif vhi then return vhi end
local best, bestd
for k, v in pairs(hoverMap) do
local d = math.abs(k - b)
if not bestd or d < bestd then bestd, best = d, v end
end
return best or HOVER_SEED
end
-- learnHover: oppdater kart-bin med glidende snitt naar skipet er stabilt
local function learnHover(h, c)
local b = math.floor(h / BIN_SIZE + 0.5)
hoverMap[b] = hoverMap[b] and (hoverMap[b]*0.9 + c*0.1) or c
end
local function mapCount()
local n = 0; for _ in pairs(hoverMap) do n = n + 1 end; return n
end
local function logline(path, text)
local f = fs.open(path, "a"); if f then f.writeLine(text); f.close() end
end
local function angles()
local r = { gimS.getAngles() }
if type(r[1]) == "table" then r = r[1] end
return r
end
local function pitchRaw() return angles()[PITCH_INDEX] or 0 end
local function clampSig(x)
x = math.floor(x + 0.5)
return math.max(0, math.min(MAX_SIGNAL, x))
end
-- slew-begrenset utgang (mot forrige verdi)
local curFore, curAft = 0, 0
local function driveValves(fore, aft)
fore = clampSig(fore); aft = clampSig(aft)
if fore - curFore > SLEW then fore = curFore + SLEW end
if fore - curFore < -SLEW then fore = curFore - SLEW end
if aft - curAft > SLEW then aft = curAft + SLEW end
if aft - curAft < -SLEW then aft = curAft - SLEW end
curFore, curAft = fore, aft
redstone.setAnalogOutput(FORE_SIDE, fore)
if AFT_SIDE then redstone.setAnalogOutput(AFT_SIDE, aft) end
return fore, aft
end
local function allOff()
redstone.setAnalogOutput(FORE_SIDE, 0)
if AFT_SIDE then redstone.setAnalogOutput(AFT_SIDE, 0) end
curFore, curAft = 0, 0
end
-- ---- persistens: lagrede gains overlever omstart ----
local GAINS_FILE = "pid_gains.txt"
-- Lagrer gains (respons), arbeidspunkt OG laert modell (kart/kobling/lag).
local function saveState(altI, pitchI)
local f = fs.open(GAINS_FILE, "w")
f.write(textutils.serialise({A_KP=A_KP, A_KI=A_KI, A_KD=A_KD, P_KP=P_KP, P_KI=P_KI, P_KD=P_KD,
K_RATE=K_RATE, K_PRATE=K_PRATE, altI=altI, pitchI=pitchI, hoverMap=hoverMap, kCouple=kCouple, lag=LAG, TRIM=TRIM}))
f.close()
end
local function loadState()
if not fs.exists(GAINS_FILE) then return nil end
local f = fs.open(GAINS_FILE, "r"); local s = f.readAll(); f.close()
local g = textutils.unserialise(s)
if type(g) ~= "table" then return nil end
A_KP, A_KI, A_KD = g.A_KP or A_KP, g.A_KI or A_KI, g.A_KD or A_KD
P_KP, P_KI, P_KD = g.P_KP or P_KP, g.P_KI or P_KI, g.P_KD or P_KD
-- sikkerhet: kapp lagrede gains saa en korrupt profil ikke kan laste inn vanvittige verdier
A_KP, A_KD = math.min(A_KP, 0.6), math.min(A_KD, 3.0)
P_KP, P_KD = math.min(P_KP, 0.5), math.min(P_KD, 2.0)
K_RATE = math.min(g.K_RATE or K_RATE, 4.0)
K_PRATE = math.min(g.K_PRATE or K_PRATE, 1.0)
if type(g.hoverMap) == "table" then
for k, v in pairs(g.hoverMap) do hoverMap[tonumber(k) or k] = v end
end
kCouple = g.kCouple or kCouple
LAG = g.lag or LAG
TRIM = g.TRIM or TRIM
return g
end
-- ---- malhoyde: egen fil saa den overlever reload (place-and-forget) ----
local TARGET_FILE = "target.txt"
local function saveTarget(h)
local f = fs.open(TARGET_FILE, "w"); f.write(tostring(h)); f.close()
end
local function loadTarget()
if not fs.exists(TARGET_FILE) then return nil end
local f = fs.open(TARGET_FILE, "r"); local s = f.readAll(); f.close()
return tonumber(s)
end
-- ---- analyse av et feilvindu: amplitude (topp-til-topp) + null-kryssinger ----
local function analyze(win)
local mn, mx, cross = math.huge, -math.huge, 0
for i, v in ipairs(win) do
if v < mn then mn = v end
if v > mx then mx = v end
if i > 1 and ((win[i-1] < 0) ~= (v < 0)) then cross = cross + 1 end
end
return (mx - mn), cross
end
-- ============================ MENY (UI) ============================
local function pause() print(""); print("[ Enter for menu ]"); read() end
local function menu()
while true do
term.clear(); term.setCursorPos(1,1)
print("====== BALLOON AUTOPILOT ======")
print("")
print(" 1) Start stabilization")
print(" 2) Set target altitude")
print(" 3) Balance check (level the ship)")
print(" 4) Calibrate sensors")
print(" 5) Actuator test")
print(" 6) Reset profile (delete learned data)")
print(" 7) Show saved profile")
print(" 8) Exit")
print("")
local sensorsOk = peripheral.find("altitude_sensor") and peripheral.find("gimbal_sensor")
print(sensorsOk and "Sensors: OK" or "Sensors: MISSING - check wiring")
print(fs.exists(GAINS_FILE) and "Profile: saved" or "Profile: none (fresh)")
local tH = loadTarget()
print("Target: " .. (tH and string.format("%.1f", tH) or "where I start"))
print("")
write("Choice: ")
local k = read()
if k == "1" then return "run"
elseif k == "2" then
write("Target altitude (empty = where I start): ")
local s = read()
if s == "" then
if fs.exists(TARGET_FILE) then fs.delete(TARGET_FILE) end
print("Target cleared - holds where it starts.")
elseif tonumber(s) then
saveTarget(tonumber(s)); print("Target altitude set to " .. s)
else print("Invalid number.") end
pause()
elseif k == "3" then return "balance"
elseif k == "4" then return "cal"
elseif k == "5" then return "test"
elseif k == "6" then
if fs.exists(GAINS_FILE) then fs.delete(GAINS_FILE) end
print("Profile reset - fresh calibration next start."); pause()
elseif k == "7" then
if fs.exists(GAINS_FILE) then
local f = fs.open(GAINS_FILE, "r"); print(f.readAll()); f.close()
else print("No saved profile yet.") end
pause()
elseif k == "8" then return "exit"
end
end
end
-- ============================ AUTO-SIGN ============================
-- Maaler hvilken vei pitch beveger seg av positiv differanse + dodtid (lag).
-- Krever at skipet kan tilte (helst i lufta). Faller tilbake til +1 hvis ingen respons.
local function detectPitchSign()
if not AFT_SIDE then return 1 end
print("Auto-sign: measuring lag, lifting, nudging...")
local h0 = altS.getHeight()
local stepLvl = math.min(MAX_SIGNAL, 5)
local lvl = stepLvl
local t0 = os.clock()
local lagMeasured = nil
while os.clock() - t0 < 12 do
driveValves(stepLvl, stepLvl)
local rise = altS.getHeight() - h0
if rise > 1 and not lagMeasured then lagMeasured = os.clock() - t0 end
if rise > 2 then break end -- luftbaaren
sleep(0.2)
end
if lagMeasured then
LAG = math.max(0.5, math.min(6, lagMeasured))
print(string.format("Measured lag = %.1f s", LAG))
end
sleep(0.5)
local p0 = pitchRaw()
local t1 = os.clock()
while os.clock() - t1 < 3 do
driveValves(math.min(MAX_SIGNAL, lvl + 3), math.max(0, lvl - 3))
sleep(0.2)
end
local p1 = pitchRaw()
allOff()
local dp = p1 - p0
if math.abs(dp) < 1.0 then
print("Weak response -- using +1. Override PITCH_SIGN if wrong.")
return 1
end
local s = dp >= 0 and 1 or -1 -- sign(dp): gir negativ tilbakekobling
print(string.format("dpitch %.1f -> PITCH_SIGN = %d", dp, s))
sleep(1)
return s
end
-- modus fra argument, ellers vis meny
local mode = ({...})[1]
if not mode then mode = menu() end
if mode == "exit" then return end
-- alle modus trenger sensorene
if not (altS and gimS) then
print("Sensors missing (altitude_sensor + gimbal_sensor). Check wiring.")
return
end
-- ============================ AUTO-BALANCE (BRUTE-FORCE SWEEP) ============================
-- Sveiper TRIM gjennom hele omraadet, maaler pitch ved hver verdi, velger den flateste.
-- Trenger INGEN fortegn-deteksjon -> enkelt og robust. Lagres som permanent TRIM.
if mode == "balance" then
local g0 = loadState() -- behold eksisterende laerte data
local ground = altS.getHeight()
local target = ground + 10
local AIRBORNE = ground + 4 -- under dette = staar paa bakken (ugyldig)
local SWEEP_MAX, STEP = 6, 0.5
local MAXWAIT, WIN, SETTLE = 18, 14, 1.2
-- 1) LOFT skipet til luftbaaren (begge ventiler likt) FOR vi maaler noe
print("AUTO-BALANCE - lifting ship first...")
local liftOk = false
local tLift = os.clock()
while os.clock() - tLift < 18 do
local h = altS.getHeight()
local g = clampSig(HOVER_SEED + 0.7*(target - h))
driveValves(g, g)
if h > ground + 6 then liftOk = true; break end
term.clear(); term.setCursorPos(1,1)
print("AUTO-BALANCE")
print(string.format("lifting... %.1f / %.0f", h, target))
sleep(0.2)
end
if not liftOk then
allOff(); print("Could not get airborne - balloons too weak?"); return
end
-- 2) SVEIP trim, men KUN gyldig naar skipet faktisk svever (h >= AIRBORNE)
local best, bestAbs = nil, math.huge
local ok, err = pcall(function()
local t = -SWEEP_MAX
while t <= SWEEP_MAX + 0.001 do
local hist, tStart, settledP = {}, os.clock(), nil
while os.clock() - tStart < MAXWAIT do
local h = altS.getHeight()
local coll = clampSig(HOVER_SEED + 0.7*(target - h)) -- hold hoyden
local fore, aft = driveValves(coll + t, coll - t)
local p = pitchRaw()
hist[#hist+1] = p; if #hist > WIN then table.remove(hist, 1) end
local mn, mx = math.huge, -math.huge
for _, v in ipairs(hist) do mn = math.min(mn, v); mx = math.max(mx, v) end
local spread = (#hist >= WIN) and (mx - mn) or 999
if spread < SETTLE and h >= AIRBORNE then settledP = (mn + mx)/2 end -- gyldig KUN luftbaaren
term.clear(); term.setCursorPos(1,1)
print("==== AUTO-BALANCE SWEEP ====")
print(string.format(" TRIM %+.1f pitch %+.1f", t, p))
print(string.format(" fore %d/aft %d height %.1f", fore, aft, h))
print(" " .. (h < AIRBORNE and "SINKING - invalid trim" or (settledP and "settled" or "settling...")))
print(string.format(" best: %s", best and string.format("TRIM %+.1f (|p| %.1f)", best, bestAbs) or "none yet"))
if settledP then break end
sleep(0.2)
end
-- bare gyldig hvis den satte seg MENS luftbaaren
if settledP and math.abs(settledP) < bestAbs then
best, bestAbs = t, math.abs(settledP)
TRIM = best; saveState(g0 and g0.altI or 0, g0 and g0.pitchI or 0) -- lagre lopende
end
t = t + STEP
end
end)
allOff()
if best then
TRIM = best; saveState(g0 and g0.altI or 0, g0 and g0.pitchI or 0)
print(string.format("Best TRIM = %+.2f (|pitch| %.1f). Saved.", best, bestAbs))
else
print("No valid (airborne) trim found.")
end
if not ok and err ~= "Terminated" then error(err, 0) end
return
end
-- ============================ CAL ============================
if mode == "cal" then
local LOG = "cal.log"; if fs.exists(LOG) then fs.delete(LOG) end
logline(LOG, "# cal: t, height, airpressure, a1, a2, a3")
print("CALIBRATION -> cal.log (controls nothing). CTRL+T to stop.")
while true do
local h = altS.getHeight()
local press = altS.getAirPressure and altS.getAirPressure() or 0
local a = angles()
logline(LOG, string.format("%.2f, %.2f, %.2f, %.3f, %.3f, %.3f",
os.clock(), h, press or 0, a[1] or 0, a[2] or 0, a[3] or 0))
term.clear(); term.setCursorPos(1,1)
print(string.format("Height %.1f Pressure %.2f", h, press or 0))
print(string.format("Angles %.2f / %.2f / %.2f", a[1] or 0, a[2] or 0, a[3] or 0))
sleep(0.3)
end
end
-- ============================ TEST ============================
if mode == "test" then
local LOG = "test.log"; if fs.exists(LOG) then fs.delete(LOG) end
logline(LOG, "# test: t, phase, fore, aft, height, pitch")
local steps = {
{"balanced", 6, 6, 8}, {"more_FRONT", 10, 3, 8},
{"balanced", 6, 6, 6}, {"more_AFT", 3, 10, 8}, {"off", 0, 0, 4},
}
print("TEST MODE -> test.log. CTRL+T to stop.")
local ok, err = pcall(function()
for _, s in ipairs(steps) do
local phase, fore, aft, secs = s[1], s[2], s[3], s[4]
local t0 = os.clock()
while os.clock() - t0 < secs do
driveValves(fore, aft)
local h, p = altS.getHeight(), pitchRaw()
logline(LOG, string.format("%.2f, %s, %d, %d, %.2f, %.2f", os.clock(), phase, fore, aft, h, p))
term.clear(); term.setCursorPos(1,1)
print("Phase: "..phase); print(string.format("fore %d aft %d", fore, aft))
print(string.format("Height %.1f Pitch %.1f", h, p))
sleep(0.2)
end
end
end)
allOff(); print("Valves 0. Test done -> test.log.")
if not ok and err ~= "Terminated" then error(err, 0) end
return
end
-- ============================ REGULERING ============================
local loaded = loadState()
if loaded then print(string.format("Loaded profile (%d map pts, lag %.1f, kC %.2f)",
mapCount(), LAG, kCouple)) end
local sign = PITCH_SIGN
if sign == 0 then sign = detectPitchSign() end -- kan over-skrive lag med fersk maling
-- Malhoyde: lagret (place-and-forget) ELLER der skipet starter
local TARGET_FINAL = loadTarget() or (altS.getHeight() + TAKEOFF_OFFSET)
print(string.format("Target = %.1f (sign %d)", TARGET_FINAL, sign))
local LOG = "run.log"; if fs.exists(LOG) then fs.delete(LOG) end
logline(LOG, string.format("# run: target_h=%.1f sign=%d A(%.2f,%.3f,%.2f) P(%.2f,%.3f,%.2f) max=%d",
TARGET_FINAL, sign, A_KP, A_KI, A_KD, P_KP, P_KI, P_KD, MAX_SIGNAL))
logline(LOG, "# t, height, pitch, eH, eP, collective, diff, fore, aft")
-- Integralet trimmer nå bare RESTEN (hoverFF gir grunngassen) -> start nær 0
local altI = (loaded and loaded.altI) or 0
local pitchI = (loaded and loaded.pitchI) or 0
local lastH = altS.getHeight()
local lastP = pitchRaw()
local fVspd, fPr = 0, 0 -- filtrerte avledninger (lavpass)
local cmdRate = 0 -- akselerasjons-begrenset onsket rate (grasios bevegelse)
local badCount = 0 -- online fortegns-vakt
local winH, winP = {}, {} -- feilvinduer for auto-tuning
local lastTune = os.clock()
local tuneMsg = "waiting..."
local settledFor = 0 -- sek innenfor dodband
print("Self-calibrating PID active -> run.log. CTRL+T to stop.")
local ok, err = pcall(function()
while true do
-- glitch-vern: avvis urealistiske sensor-hopp (robusthet)
local rawH = altS.getHeight()
local height = (lastH and math.abs(rawH - lastH) > GLITCH_H) and lastH or rawH
local pitch = pitchRaw()
-- HOYDE KASKADE: posisjon -> begrenset+myk onsket rate -> gass
local vspd = (height - lastH) / DT; lastH = height
fVspd = DERIV_FILTER*vspd + (1-DERIV_FILTER)*fVspd -- lavpass paa stigerate
local eH = TARGET_FINAL - height
local inH = math.abs(eH) < DEADBAND_H
local ff = hoverFF(TARGET_FINAL) -- laert svevegass for malet
local desRate = math.max(-MAX_RATE, math.min(MAX_RATE, K_POS*eH)) -- ytre: begrenset rate
-- akselerasjons-grense: ramp cmdRate mot desRate -> myk ease-in/ease-out
local dr = math.max(-MAX_ACCEL*DT, math.min(MAX_ACCEL*DT, desRate - cmdRate))
cmdRate = cmdRate + dr
local rateErr = cmdRate - fVspd -- indre: hold raten
local rawColl = ff + A_KI*altI + K_RATE*rateErr
local collective = math.max(0, math.min(MAX_SIGNAL, rawColl)) -- KLEM til ventilomraade
local excess = collective - ff -- klatrekraft (for avkobling)
-- integral paa RATE-feil (IKKE posisjon!) -> trimmer FF-gass, kan ikke vinde opp paa stor eH
local satLow, satHigh = rawColl <= 0, rawColl >= MAX_SIGNAL
if (not satLow and not satHigh) or (satLow and rateErr > 0) or (satHigh and rateErr < 0) then
altI = math.max(-150, math.min(150, altI + rateErr*DT))
end
-- PITCH KASKADE: pitch-feil -> begrenset onsket pitch-rate -> differanse (grasios attityde)
local diff, eP = 0, 0
local inP = true
if AFT_SIDE then
local pr = (pitch - lastP) / DT; lastP = pitch
fPr = DERIV_FILTER*pr + (1-DERIV_FILTER)*fPr
eP = TARGET_PITCH - pitch
inP = math.abs(eP) < DEADBAND_P
local desPRate = math.max(-MAX_PITCH_RATE, math.min(MAX_PITCH_RATE, K_POS_P*eP))
local pRateErr = desPRate - fPr
-- avkobling (kCouple*excess) motvirker dreiemoment fra gass-endring + laert trim
-- dynamisk pitch-korreksjon klemmes (mot bang-bang), saa legges statisk TRIM oppaa
local dyn = sign * (K_PRATE*pRateErr + P_KI*pitchI + kCouple*excess)
dyn = math.max(-MAX_DIFF, math.min(MAX_DIFF, dyn))
diff = TRIM + dyn -- per-ventil clampSig i driveValves binder sluttverdien
-- laer decoupling: RIKTIG fortegn (gradient) -> kansellerer klatre-pitchen
if math.abs(excess) > 0.5 then
kCouple = math.max(-2, math.min(2, kCouple + 0.002 * eP * excess))
end
local fSat = (collective+diff) <= 0 or (collective+diff) >= MAX_SIGNAL
local aSat = (collective-diff) <= 0 or (collective-diff) >= MAX_SIGNAL
if not inP and not (fSat or aSat) then pitchI = math.max(-200, math.min(200, pitchI + sign*eP*DT)) end
if math.abs(pitch) > 25 then badCount = badCount + 1 else badCount = 0 end
if badCount >= 20 then
sign = -sign; pitchI = 0; badCount = 0
print("Pitch diverging -> flipping sign to " .. sign)
end
end
-- settle-deteksjon: innenfor dodband paa begge akser + naa malet
local atTarget = math.abs(TARGET_FINAL - height) < DEADBAND_H
if inH and inP and atTarget then settledFor = settledFor + DT else settledFor = 0 end
local settled = settledFor >= SETTLE_TIME
-- LAER svevegass-kart naar stabilt -> kartet overtar, integralet lekkes mot 0
if inH and inP and math.abs(fVspd) < 0.4 then
learnHover(height, collective)
altI = altI * 0.97
end
-- gulv paa TOTAL-loft (ikke per ventil): kan synke til mal + beholder pitch-differanse
collective = math.max(MIN_SIGNAL, collective)
local fore, aft = driveValves(collective + diff, collective - diff)
-- ---- AUTO-TUNING: KUN naar nær malet (hold) -> reise-transienter lurer den ikke ----
local tuneNear = math.abs(TARGET_FINAL - height) < 5
and (not AFT_SIDE or math.abs(TARGET_PITCH - pitch) < 8)
if AUTOTUNE and not tuneNear then
winH, winP = {}, {}; lastTune = os.clock(); tuneMsg = "traveling (no tune)"
elseif AUTOTUNE then
winH[#winH+1] = eH
winP[#winP+1] = (TARGET_PITCH - pitch)
if os.clock() - lastTune >= TUNE_INTERVAL then
local ampH, crH = analyze(winH)
if crH >= 4 and ampH > 4 then -- oscillating -> damp inner rate gain
K_RATE = math.max(0.2, K_RATE*0.82)
tuneMsg = "alt: damping"
elseif ampH > 5 and crH < 2 then -- genuinely sluggish near target -> mild sharpen
K_RATE = math.min(4.0, K_RATE*1.08)
tuneMsg = "alt: sharpening"
else tuneMsg = "alt: ok" end
if AFT_SIDE then
local ampP, crP = analyze(winP)
if crP >= 4 and ampP > 6 then
K_PRATE = math.max(0.03, K_PRATE*0.82)
tuneMsg = tuneMsg .. " | pitch: damping"
elseif ampP > 7 and crP < 2 then
K_PRATE = math.min(0.15, K_PRATE*1.05) -- pitch: hold mildt, svingning er verre enn treghet
tuneMsg = tuneMsg .. " | pitch: sharpening"
else tuneMsg = tuneMsg .. " | pitch: ok" end
end
saveState(altI, pitchI)
logline(LOG, string.format("# TUNE %s -> A(%.3f,%.3f,%.2f) P(%.3f,%.3f,%.2f)",
tuneMsg, A_KP, A_KI, A_KD, P_KP, P_KI, P_KD))
winH, winP = {}, {}
lastTune = os.clock()
end
end
logline(LOG, string.format("%.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %d, %d",
os.clock(), height, pitch, eH, TARGET_PITCH - pitch, collective, diff, fore, aft))
term.clear(); term.setCursorPos(1,1)
local maxed = collective >= MAX_SIGNAL-0.5 and math.abs(eH) > 5 and math.abs(fVspd) < 0.3
print(settled and "*** STABLE ***"
or (maxed and "!! MAX LIFT - target unreachable")
or (math.abs(eH) > DEADBAND_H and ">> moving..." or ".. holding .."))
print(string.format("Height %6.1f / %.0f (e %.1f)", height, TARGET_FINAL, TARGET_FINAL-height))
print(string.format("Pitch %6.1f / %.0f (e %.1f)", pitch, TARGET_PITCH, TARGET_PITCH-pitch))
print(string.format("fore %2d aft %2d %s", fore, aft, (inH and inP) and "[hold]" or ""))
print(string.format("model: ff~%.1f kR %.2f kPR %.2f kC %.2f", ff, K_RATE, K_PRATE, kCouple))
print(string.format("rate %.1f/%.1f | map %d | trim~%.1f", fVspd, cmdRate, mapCount(), sign*P_KI*pitchI))
print("tune: " .. tuneMsg)
sleep(DT)
end
end)
allOff()
saveState(altI, pitchI) -- lagre nyeste arbeidspunkt + gains
print("Valves set to 0. Operating point saved. Exited.")
if not ok and err ~= "Terminated" then error(err, 0) end