572 lines
24 KiB
Lua
572 lines
24 KiB
Lua
|
|
-- 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
|