One-command installer + guided setup: role detection, self-flying sign test v2
- install.lua: single entry point -- detects role from peripherals (flight sensors = autopilot, monitor = cockpit), installs, cleans legacy/removed files, then hands off straight into setup/standby. Ship state (sides/calib/target/settings) is never shipped; existing profiles can be kept or reset on re-install. - Sign test v2: fully self-driving -- staircase power search (learns hover), closed-loop climb, both-direction verification with auto- escalating nudge, controlled descent. Never freefalls, also not on Ctrl+T or abort. - Setup wizard (ap setup): wiring pulse-walk + sign test in one guided flow; offered automatically on first boot without a profile. - New launcher 'ap' (stabilizeV2 kept as case-safe identical aliases; removes Windows-host case-collision recursion risk). - build.sh: repo is source of truth; LC_ALL=C sorted manifests; optional --from-world/--to-world dev sync with exclude lists. - Removed legacy v1 files and per-ship state from the repo/manifest (26 -> 17 files in the autopilot role). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
10
files/autopilot/ap.lua
Normal file
10
files/autopilot/ap.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
-- ap.lua -- launcher for v3 observer-autopiloten i autopilot/.
|
||||
-- ap -> meny
|
||||
-- ap setup -> foerstegangs-wizard (wiring + sign test)
|
||||
-- ap fly -> full lukket sloyfe
|
||||
-- ap standby -> vent paa ENGAGE fra pilot-konsollen
|
||||
-- ap boot -> oppstartsvalg (wizard hvis profil mangler, ellers standby)
|
||||
-- ap monitor / manual / cal / observe / signtest / wiring
|
||||
local args = { ... }
|
||||
local unpack = table.unpack or unpack
|
||||
shell.run("autopilot/main", unpack(args))
|
||||
@@ -235,6 +235,7 @@ function M.run(cfg, sensors, drive)
|
||||
end
|
||||
|
||||
drive(calib.uHoverFront, calib.uHoverRear) -- hold hover ved utgang
|
||||
calib.signMeasured = true -- fortegnet er maalt/bekreftet i flukt -> profilen er komplett
|
||||
return calib
|
||||
end
|
||||
|
||||
|
||||
@@ -108,12 +108,11 @@ return {
|
||||
hoverTrim = 0.0, -- self-learned loft-trim (redstone, warm-start); fly-loopen oppdaterer
|
||||
},
|
||||
|
||||
-- ============ SIGNTEST (§13.2) ============
|
||||
-- ============ SIGNTEST (§13.2, selvgaaende) ============
|
||||
-- Loft-nivaa SOEKES automatisk (trapp fra bunnen) og nudgen trappes opp ved svak respons --
|
||||
-- ingen av disse skal trenge redigering for et nytt skip.
|
||||
signtest = {
|
||||
liftLevel = 5, -- felles nivaa for aa loefte skipet luftbaaret (i responsivt omraade)
|
||||
airborneRise = 3.0, -- blokker stigning som regnes som luftbaaret
|
||||
liftTimeout = 15.0, -- s maks paa loft-fasen
|
||||
nudge = 2, -- differanse (front opp / bak ned) under maalingen
|
||||
nudge = 2, -- START-differanse (front opp / bak ned); testen oeker selv ved svak respons
|
||||
settleTime = 1.0, -- (legacy: brukt av full kalibrering 'cal' Stage 3)
|
||||
nudgeTime = 3.0, -- (legacy: brukt av full kalibrering 'cal' Stage 3)
|
||||
-- SETTLE-BASERT maaling: lagrer FOERST naar pitch-RATEN har roet seg, ikke paa fast timer.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
-- main.lua -- loop + tilstandsmaskin for v3 observer-autopiloten.
|
||||
-- All UI-tekst er ENGELSK; kommentarer er norske (husstil).
|
||||
-- Modus (argument eller meny):
|
||||
-- boot oppstart: setup-wizard-prompt hvis profil mangler, ellers standby
|
||||
-- setup foerstegangs-wizard: wiring + sign test i ett (alt skip-spesifikt)
|
||||
-- standby lytter etter ENGAGE fra pilot-konsollen (default etter boot)
|
||||
-- monitor Lag 1: les sensorer+estimator, logg+HUD, styrer INGENTING
|
||||
-- signtest §13.2: nudge front -> anbefal pitchSign
|
||||
-- signtest §13.2: selvgaaende -- loefter, maaler pitch-fortegn, lander igjen
|
||||
-- manual Lag 2: faste nivaaer (piltaster), finn hover
|
||||
-- cal Lag 3: full kalibrering (selv-laerende)
|
||||
-- observe Lag 4: observer ÅPEN SLØYFE -- valider at v_hat sporer v_meas
|
||||
@@ -87,6 +90,13 @@ local function loadSides()
|
||||
end
|
||||
local function saveSides(t) local f = fs.open(SIDES_FILE, "w"); f.write(textutils.serialise(t)); f.close() end
|
||||
|
||||
-- komplett skip-profil = wiring valgt + pitch-fortegn faktisk MAALT paa DETTE skipet
|
||||
-- (signMeasured settes av sign test / full kalibrering -- aldri av en shipped default)
|
||||
local function profileComplete()
|
||||
if not (fs.exists(SIDES_FILE) and fs.exists(CALIB_FILE)) then return false end
|
||||
return loadCalib().signMeasured == true
|
||||
end
|
||||
|
||||
-- live brukerinnstillinger (justeres fra pilot-konsollen), persistert. Syntetiske noekler -> cfg.
|
||||
local SETTINGS_FILE = "settings.txt"
|
||||
local function saveSettings(t) local f = fs.open(SETTINGS_FILE, "w"); f.write(textutils.serialise(t)); f.close() end
|
||||
@@ -135,79 +145,189 @@ local function runMonitor()
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================ SIGN TEST (§13.2) ============================
|
||||
-- Maaler pitch-RESPONSEN paa en nudge (mer front) for aa finne pitchSign. Bruker pitch-RATEN, ikke
|
||||
-- ============================ SIGN TEST (§13.2, v2 -- SELVGAAENDE) ============================
|
||||
-- Maaler pitch-responsen paa en differanse-nudge for aa finne pitchSign. Bruker pitch-RATEN, ikke
|
||||
-- vinkelen: open-loop under et vedvarende moment er skipet en naer-integrator -> VINKELEN settler
|
||||
-- aldri, men RATEN gjor det. Vi venter til raten har roet seg (lag-bevisst: minHold >= loft-
|
||||
-- etterslepet) FOER vi lagrer. Baseline-raten fanger asymmetri-driften; dr = endring pga nudge.
|
||||
-- aldri, men RATEN gjor det. Hele testen er selvgaaende -- INGEN config-redigering:
|
||||
-- 1/4 trapper seg luftbaaren fra bunnen (laerer ~hover paa kjoepet, ingen fast liftLevel)
|
||||
-- 2/4 klatrer kontrollert til trygg maalehoyde (fart-P-hold rundt laert hover)
|
||||
-- 3/4 baseline -> nudge front -> MOTSATT nudge (verifisering); svak respons -> auto sterkere dytt
|
||||
-- 4/4 lagrer, senker skipet kontrollert tilbake og HOLDER hover -- ALDRI frittfall
|
||||
|
||||
-- Driver (fore,aft) til pitch-raten er stabil (lite spenn i et ~1.5 s vindu) ELLER timeout.
|
||||
-- Returnerer snitt pitchRate over vinduet + om den faktisk stabiliserte seg.
|
||||
local function holdTilRateStable(est, fore, aft, h0, label, minHold, maxWait)
|
||||
local t0, win, WINSEC = now(), {}, 1.5
|
||||
while now() - t0 < maxWait do
|
||||
driveValves(fore, aft)
|
||||
est:update(now(), sensors.readAltitude() or h0, sensors.readPitch() or 0)
|
||||
win[#win + 1] = { t = now(), pr = est.pitchRate }
|
||||
while #win > 1 and (now() - win[1].t) > WINSEC do table.remove(win, 1) end
|
||||
local sum, lo, hi = 0, math.huge, -math.huge
|
||||
for _, e in ipairs(win) do sum = sum + e.pr; lo = math.min(lo, e.pr); hi = math.max(hi, e.pr) end
|
||||
local n = #win
|
||||
local avg = sum / n
|
||||
local span = (n > 1) and (hi - lo) or math.huge
|
||||
term.clear(); term.setCursorPos(1, 1); print("SIGN TEST: " .. label)
|
||||
print(string.format(" fore %d aft %d", fore, aft))
|
||||
print(string.format(" pitch %+.1f pRate %+.2f", est.pitch, est.pitchRate))
|
||||
print(string.format(" window avg %+.2f span %.2f deg/s", avg, span))
|
||||
if math.abs(est.pitch) > cfg.signtest.maxTiltAbort then return avg, true end -- ikke roter for langt
|
||||
if (now() - t0) >= minHold and n >= 5 and span < cfg.signtest.rateCalm then return avg, true end
|
||||
sleep(cfg.dt)
|
||||
end
|
||||
local sum = 0; for _, e in ipairs(win) do sum = sum + e.pr end
|
||||
return (#win > 0) and (sum / #win) or est.pitchRate, false
|
||||
end
|
||||
local signtestAirborne = false -- for runMode-cleanup: hold hover hvis luftbaaren, ellers alt av
|
||||
|
||||
local function runSigntest()
|
||||
local s, est = cfg.signtest, Estimator.new(cfg)
|
||||
local h0 = sensors.readAltitude(); if not h0 then print("No altitude sensor."); return end
|
||||
print("SIGN TEST. CTRL+T for emergency stop."); print("1) Lifting airborne..."); sleep(0.5)
|
||||
local liftLevel, airborne, tLift = math.min(cfg.maxSignal, s.liftLevel), false, now()
|
||||
while now() - tLift < s.liftTimeout do
|
||||
driveValves(liftLevel, liftLevel)
|
||||
local h = sensors.readAltitude() or h0
|
||||
est:update(now(), h, sensors.readPitch() or 0)
|
||||
term.clear(); term.setCursorPos(1, 1); print("SIGN TEST: lifting...")
|
||||
print(string.format(" height %.2f (+%.2f) vVel %.2f", h, h - h0, est.verticalVelocity))
|
||||
if h - h0 > s.airborneRise then airborne = true; break end
|
||||
local log = Logger.new("signtest.log", "# signtest: t, phase, fore, aft, height, pitch, vVel, pitchRate")
|
||||
signtestAirborne = false
|
||||
local h0 = sensors.readAltitude()
|
||||
if not h0 then print("No altitude sensor."); return false end
|
||||
local t0 = now()
|
||||
local hover = nil -- laeres i trappa (1/4), trimmes videre av fart-holdet
|
||||
local levelDiff = 0 -- treg nivellering under nedstigning (naar fortegnet er kjent)
|
||||
|
||||
local function tick(fore, aft, phase)
|
||||
local f, a = driveValves(fore, aft)
|
||||
local h, p = sensors.readAltitude(), sensors.readPitch()
|
||||
if h and p then est:update(now(), h, p) end
|
||||
log:row({ now() - t0, phase, f, a, est.altitude, est.pitch, est.verticalVelocity, est.pitchRate })
|
||||
sleep(cfg.dt)
|
||||
end
|
||||
if not airborne then allOff(); print("Did not get airborne -- raise liftLevel / check sides."); return end
|
||||
local climbOk = est.verticalVelocity > 0
|
||||
|
||||
-- baseline-rate (balansert) -- vent til raten roer seg (fanger evt. asymmetri-drift)
|
||||
local r0 = holdTilRateStable(est, liftLevel, liftLevel, h0, "baseline (balanced)", s.baseMinHold, s.baseMaxWait)
|
||||
-- nudge (mer front) -- HOLD til loftet (~5 s) har uttrykt seg OG raten er stabil
|
||||
local r1, settled = holdTilRateStable(est, liftLevel + s.nudge, math.max(0, liftLevel - s.nudge),
|
||||
h0, "nudge (more front)", s.nudgeMinHold, s.nudgeMaxWait)
|
||||
allOff()
|
||||
local dr = r1 - r0 -- endring i stabilisert pitch-RATE pga mer front = robust, lag-tolerant signal
|
||||
|
||||
term.clear(); term.setCursorPos(1, 1); print("==== SIGN TEST RESULT ====")
|
||||
print(string.format("rate baseline %+.2f -> nudged %+.2f (dr %+.2f deg/s)", r0, r1, dr))
|
||||
print(string.format("rate settled: %s", settled and "yes" or "no (timed out -- direction still used)"))
|
||||
print(string.format("climb sign: %s", climbOk and "OK (vVel>0)" or "WRONG -- altitude inverted!"))
|
||||
if math.abs(dr) < s.drMin then
|
||||
print("WEAK response -- NOT saving. Raise signtest.nudge or re-run.")
|
||||
else
|
||||
local c = loadCalib()
|
||||
c.pitchSign = utils.sign(dr)
|
||||
c.pitchGain = 1.0 -- robust default; integral-trimmen gjor magnituden ukritisk
|
||||
c.alphaPitch = 1.0
|
||||
c.pitchTrim = 0 -- nullstill laert trim ved ny fortegns-maaling
|
||||
saveCalib(c)
|
||||
print(string.format(">>> Saved pitchSign=%d (pitchGain=1.0; integral learns the rest)", c.pitchSign))
|
||||
local function hud(step, title, lines)
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== SIGN TEST " .. step .. " ====")
|
||||
print(title)
|
||||
for _, l in ipairs(lines or {}) do print(l) end
|
||||
print("")
|
||||
print(string.format("height +%.1f pitch %+.1f vVel %+.2f",
|
||||
est.altitude - h0, est.pitch, est.verticalVelocity))
|
||||
print("CTRL+T = abort (holds lift, no freefall)")
|
||||
end
|
||||
|
||||
-- fart-P-hold rundt laert hover (+ treg hover-trim), med valgfri differanse (fore - aft)
|
||||
local function velTick(vDes, diff, phase)
|
||||
hover = clamp(hover + 0.05 * (vDes - est.verticalVelocity) * cfg.dt, cfg.absFloor, cfg.maxSignal - 0.5)
|
||||
local u = hover + clamp(1.2 * (vDes - est.verticalVelocity), -1.5, 1.5)
|
||||
local f, a = mixer.preserveDifferenceClamp(u + diff, u - diff, cfg.absFloor, cfg.maxSignal)
|
||||
tick(f, a, phase)
|
||||
end
|
||||
|
||||
-- kontrollert nedstigning + hover-hold -- den ENESTE maaten vi forlater lufta paa
|
||||
local function descendAndHold(pSign)
|
||||
local tD = now()
|
||||
while est.altitude > h0 + 3 and now() - tD < 120 do
|
||||
if pSign ~= 0 then
|
||||
levelDiff = clamp(levelDiff - pSign * 0.2 * est.pitch * cfg.dt, -1.5, 1.5)
|
||||
end
|
||||
velTick(-0.4, levelDiff, "descend")
|
||||
hud("4/4", "Coming back down...", {
|
||||
string.format("descending to +3 hover ~%.1f", hover),
|
||||
})
|
||||
end
|
||||
local tS = now()
|
||||
while now() - tS < 3.0 do velTick(0, levelDiff, "settle") end
|
||||
driveValves(hover, hover) -- bli staaende paa laert hover (aldri null = stup)
|
||||
end
|
||||
|
||||
-- felles avbrudd: luftbaaren -> kontrollert ned foerst; paa bakken -> bare av
|
||||
local function bail(msg)
|
||||
if signtestAirborne then descendAndHold(0) else allOff() end
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== SIGN TEST: STOPPED ====")
|
||||
print(msg)
|
||||
if signtestAirborne then print(""); print("Ship is holding hover near the ground.") end
|
||||
print(""); print("[Enter]"); read()
|
||||
return false
|
||||
end
|
||||
|
||||
local function abortMsg(code)
|
||||
if code == "tilt" then return string.format("Tilted past %.0f deg -- aborted for safety.", s.maxTiltAbort) end
|
||||
if code == "alt" then return "Drifted too far up -- aborted." end
|
||||
if code == "low" then return "Got too close to the ground -- aborted." end
|
||||
return "Aborted."
|
||||
end
|
||||
|
||||
-- ---- 1/4: trappe seg luftbaaren (ingen fast liftLevel -- vi SOEKER effekten) ----
|
||||
local cmd, stepT, riseHold, tLift = cfg.absFloor, now(), 0, now()
|
||||
while true do
|
||||
tick(cmd, cmd, "lift")
|
||||
hud("1/4", "Lifting off (auto power search)...", {
|
||||
string.format("power %.1f / %d", cmd, cfg.maxSignal),
|
||||
})
|
||||
if est.verticalVelocity > 0.25 then riseHold = riseHold + cfg.dt else riseHold = 0 end
|
||||
if riseHold >= 1.0 and est.altitude - h0 > 1.5 then break end
|
||||
if now() - stepT >= 4.0 then
|
||||
if cmd >= cfg.maxSignal then
|
||||
return bail("No lift even at max power.\nCheck wiring (menu: Wiring) and that the\nburners have fuel.")
|
||||
end
|
||||
cmd = math.min(cfg.maxSignal, cmd + 0.5); stepT = now()
|
||||
end
|
||||
if now() - tLift > 120 then return bail("Timed out lifting off.") end
|
||||
end
|
||||
hover = math.max(cfg.absFloor, cmd - 0.25) -- kryssingen er ~her: foerste hover-estimat
|
||||
signtestAirborne = true
|
||||
|
||||
-- ---- 2/4: kontrollert klatring til trygg maalehoyde (hover-trimmen konvergerer underveis) ----
|
||||
local tC = now()
|
||||
while est.altitude < h0 + 8 do
|
||||
velTick(0.4, 0, "climb")
|
||||
hud("2/4", "Climbing to measuring height...", {
|
||||
string.format("target +8 hover ~%.1f", hover),
|
||||
})
|
||||
if math.abs(est.pitch) > s.maxTiltAbort then return bail(abortMsg("tilt")) end
|
||||
if now() - tC > 60 then return bail("Could not reach measuring height.") end
|
||||
end
|
||||
|
||||
-- rate-stabil maaling: hold hoyden (vDes=0) med gitt differanse til pitch-raten roer seg.
|
||||
-- Lag-bevisst: minHold >= loft-etterslepet foer vi godtar. Timeout -> bruk snittet likevel.
|
||||
local function measureRate(diff, step, title, minHold, maxWait)
|
||||
local tS, win = now(), {}
|
||||
local avg = 0
|
||||
while now() - tS < maxWait do
|
||||
velTick(0, diff, title)
|
||||
win[#win + 1] = { t = now(), pr = est.pitchRate }
|
||||
while #win > 1 and (now() - win[1].t) > 1.5 do table.remove(win, 1) end
|
||||
local sum, lo, hi = 0, math.huge, -math.huge
|
||||
for _, e in ipairs(win) do sum = sum + e.pr; lo = math.min(lo, e.pr); hi = math.max(hi, e.pr) end
|
||||
avg = sum / #win
|
||||
local span = (#win > 1) and (hi - lo) or math.huge
|
||||
hud(step, title, {
|
||||
string.format("diff %+.0f rate avg %+.2f span %.2f", diff, avg, span),
|
||||
string.format("stable when span < %.1f deg/s", s.rateCalm),
|
||||
})
|
||||
if math.abs(est.pitch) > s.maxTiltAbort then return nil, "tilt" end
|
||||
if est.altitude - h0 > 35 then return nil, "alt" end
|
||||
if est.altitude - h0 < 2 then return nil, "low" end
|
||||
if now() - tS >= minHold and #win >= 5 and span < s.rateCalm then return avg end
|
||||
end
|
||||
return avg -- timeout: retningen er som regel riktig selv om roen aldri kom
|
||||
end
|
||||
|
||||
-- ---- 3/4: baseline -> nudge -> MOTSATT nudge, med automatisk opptrapping ved svak respons ----
|
||||
local nudge, sign, verified = s.nudge, 0, false
|
||||
local maxNudge = math.max(s.nudge, math.floor(cfg.maxSignal / 2) + 1)
|
||||
for attempt = 1, 3 do
|
||||
local tag = string.format(" (try %d, push %d)", attempt, nudge)
|
||||
local r0, e0 = measureRate(0, "3/4", "Baseline (balanced)" .. tag, s.baseMinHold, s.baseMaxWait)
|
||||
if not r0 then return bail(abortMsg(e0)) end
|
||||
local r1, e1 = measureRate(nudge, "3/4", "Push: more FRONT" .. tag, s.nudgeMinHold, s.nudgeMaxWait)
|
||||
if not r1 then return bail(abortMsg(e1)) end
|
||||
local r2, e2 = measureRate(-nudge, "3/4", "Verify: more AFT" .. tag, s.nudgeMinHold, s.nudgeMaxWait)
|
||||
if not r2 then return bail(abortMsg(e2)) end
|
||||
local dF, dR = r1 - r0, r2 - r0 -- rate-endring ved mer front / mer bak
|
||||
if math.abs(dF) >= s.drMin and utils.sign(dR) == -utils.sign(dF) and math.abs(dR) >= s.drMin * 0.5 then
|
||||
sign, verified = utils.sign(dF), true
|
||||
break
|
||||
end
|
||||
if math.abs(dF) >= s.drMin then sign = utils.sign(dF) end -- kandidat, men uverifisert
|
||||
nudge = math.min(maxNudge, nudge + 1) -- auto: proev sterkere dytt
|
||||
end
|
||||
if sign == 0 then
|
||||
return bail("No measurable pitch response, even with\na stronger push. Are both balloons on\nthe same fore/aft line? Re-check wiring.")
|
||||
end
|
||||
|
||||
-- ---- lagre FOER nedstigningen (Ctrl+T etterpaa mister ingenting) ----
|
||||
local c = loadCalib()
|
||||
c.pitchSign, c.pitchGain, c.alphaPitch = sign, 1.0, 1.0
|
||||
c.pitchTrim = 0 -- nullstill laert trim ved ny fortegns-maaling
|
||||
c.uHoverFront, c.uHoverRear = hover, hover -- grov men EKTE hover-maaling ('cal' kan forfine)
|
||||
c.hoverTrim = 0 -- ny baseline -> gammel loft-trim er ugyldig
|
||||
c.signMeasured = true -- profilen er komplett (wizard/standby sjekker denne)
|
||||
saveCalib(c)
|
||||
|
||||
-- ---- 4/4: kontrollert ned + hold hover (nivellerer med det nymaalte fortegnet) ----
|
||||
descendAndHold(sign)
|
||||
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== SIGN TEST: SUCCESS ====")
|
||||
print(string.format("pitchSign %+d %s", sign,
|
||||
verified and "(verified both directions)" or "(UNVERIFIED -- reverse push unclear)"))
|
||||
print(string.format("hover ~%.1f (saved as baseline)", hover))
|
||||
print("")
|
||||
print("Ship is holding hover. Ready to FLY.")
|
||||
print(""); print("[Enter]"); read()
|
||||
return true
|
||||
end
|
||||
|
||||
-- ============================ WIRING SETUP ============================
|
||||
@@ -245,13 +365,16 @@ local function runWiring()
|
||||
else
|
||||
local level = math.min(cfg.maxSignal, 8)
|
||||
for _, side in ipairs(linkSides) do
|
||||
term.clear(); term.setCursorPos(1, 1); print("==== WIRING SETUP: pulsing ====")
|
||||
print(string.format("Pulsing '%s' at %d -- WATCH THE BURNERS...", side, level))
|
||||
redstone.setAnalogOutput(side, level); sleep(3.0); redstone.setAnalogOutput(side, 0)
|
||||
write(string.format("Which balloon fired on '%s'? [f]ront / [a]ft / [n]one: ", side))
|
||||
local a = read()
|
||||
if a == "f" or a == "F" then fore = side
|
||||
elseif a == "a" or a == "A" then aft = side end
|
||||
while true do -- [r] pulser samme side igjen -- lett aa gaa glipp av et blaff
|
||||
term.clear(); term.setCursorPos(1, 1); print("==== WIRING SETUP: pulsing ====")
|
||||
print(string.format("Pulsing '%s' at %d -- WATCH THE BURNERS...", side, level))
|
||||
redstone.setAnalogOutput(side, level); sleep(3.0); redstone.setAnalogOutput(side, 0)
|
||||
write(string.format("'%s' fired: [f]ront / [a]ft / [n]one / [r]=again: ", side))
|
||||
local a = read():lower()
|
||||
if a == "f" then fore = side; break
|
||||
elseif a == "a" then aft = side; break
|
||||
elseif a ~= "r" then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
allSidesOff()
|
||||
@@ -268,6 +391,7 @@ local function runWiring()
|
||||
print(""); print(">>> Saved to sides.txt and applied.")
|
||||
end
|
||||
print(""); print("[Enter]"); read()
|
||||
return fore ~= nil
|
||||
end
|
||||
|
||||
-- ============================ MANUAL (Lag 2) ============================
|
||||
@@ -378,12 +502,13 @@ local function runFly()
|
||||
if not fs.exists(CALIB_FILE) then
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("No profile for this ship yet.")
|
||||
print("Set it up first (in order):")
|
||||
print(" menu 0 Wiring setup (which side = front/aft valve)")
|
||||
print(" menu 2 Sign test (measures + saves pitch sign)")
|
||||
print("")
|
||||
print("Then run FLY -- pitch & altitude self-learn from there.")
|
||||
print("(Full 'cal' is optional/legacy; controllers no longer need lambda/g.)")
|
||||
print("Run the setup wizard first:")
|
||||
print(" ap setup (or menu choice 1)")
|
||||
print("")
|
||||
print("It maps the wiring and measures the pitch")
|
||||
print("sign in one short guided auto-flight.")
|
||||
print("Pitch & altitude self-learn from there.")
|
||||
print(""); print("[Enter]"); read(); return
|
||||
end
|
||||
|
||||
@@ -500,15 +625,65 @@ local function runFly()
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================ SETUP-WIZARD (foerstegang / nytt skip) ============================
|
||||
-- ETT stoppested for alt skip-spesifikt: wiring (hvilken side driver hvilken ballong) + sign test
|
||||
-- (kort selvgaaende flytur som maaler pitch-fortegn og ~hover). Etterpaa er skipet klart til FLY.
|
||||
local function runSetup()
|
||||
signtestAirborne = false -- nullstill foer wizarden (ellers kan cleanup arve forrige kjoering)
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== SHIP SETUP WIZARD ====")
|
||||
print("")
|
||||
print("Two steps, fully guided:")
|
||||
print(" 1) Wiring -- map front/aft burner sides")
|
||||
print(" 2) Sign test -- short auto-flight that")
|
||||
print(" measures pitch response + hover")
|
||||
print("")
|
||||
print("Ship must be ON THE GROUND, open sky above.")
|
||||
print("")
|
||||
write("[Enter] = start, q = cancel: ")
|
||||
if read():lower() == "q" then return end
|
||||
|
||||
if not runWiring() then
|
||||
print("Wiring was not saved -- wizard stopped."); print("[Enter]"); read(); return
|
||||
end
|
||||
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== STEP 2: SIGN TEST ====")
|
||||
print("")
|
||||
print("The ship now lifts off BY ITSELF, gives")
|
||||
print("the front balloon a small push, verifies")
|
||||
print("the response both ways, and comes back")
|
||||
print("down to hover. Takes 1-3 minutes.")
|
||||
print("")
|
||||
write("[Enter] = lift off, q = cancel: ")
|
||||
if read():lower() == "q" then return end
|
||||
runSigntest()
|
||||
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
if profileComplete() then
|
||||
print("==== SETUP COMPLETE ====")
|
||||
print("")
|
||||
print("Profile saved. The ship is ready:")
|
||||
print(" - FLY from the menu, or")
|
||||
print(" - standby + ENGAGE from the cockpit")
|
||||
else
|
||||
print("==== SETUP INCOMPLETE ====")
|
||||
print("")
|
||||
print("Sign test did not finish -- run the")
|
||||
print("wizard again (menu choice 1).")
|
||||
end
|
||||
print(""); print("[Enter]"); read()
|
||||
end
|
||||
|
||||
-- ============================ STANDBY (pilot-fjernstart) ============================
|
||||
-- Lytter etter ENGAGE fra pilot-konsollen og starter fly. Kringkaster IDLE-heartbeat slik at
|
||||
-- konsollen vet at maskinrommet er online og klart. Kjor denne (eller sett den som startup) saa
|
||||
-- piloten kan starte/stoppe fly selv: stabilizeV2 standby
|
||||
-- konsollen vet at maskinrommet er online og klart. Default etter boot; [S] aapner setup-wizarden.
|
||||
local function awaitCmd(secs)
|
||||
local timer = os.startTimer(secs)
|
||||
while true do
|
||||
local e1, e2, e3, e4 = os.pullEvent()
|
||||
if e1 == "timer" and e2 == timer then return nil
|
||||
elseif e1 == "char" and (e2 == "s" or e2 == "S") then return "setup"
|
||||
elseif e1 == "rednet_message" and e4 == link.CMD and type(e3) == "table" then
|
||||
if e3.cmd == "engage" or e3.cmd == "stop" then return e3.cmd end
|
||||
if e3.cmd == "set" then applySetting(e3.key, e3.value, true) end -- juster innstillinger i standby
|
||||
@@ -519,40 +694,72 @@ end
|
||||
|
||||
local function runStandby()
|
||||
link.open()
|
||||
print("STANDBY -- waiting for ENGAGE from pilot console. CTRL+T for menu.")
|
||||
while true do
|
||||
local h = sensors.readAltitude() or 0
|
||||
local p = sensors.readPitch() or 0
|
||||
local ready = fs.exists(CALIB_FILE)
|
||||
local flyable = fs.exists(CALIB_FILE) -- kan fly (evt. eldre profil uten maalt-flagg)
|
||||
local complete = profileComplete() -- wiring + faktisk MAALT pitch-fortegn
|
||||
link.publishStatus({ state = "IDLE", height = h, target = loadTarget() or h, pitch = p,
|
||||
vVel = 0, hold = false, ready = ready, fore = 0, aft = 0, uSum = 0, iTrim = 0, hTrim = 0,
|
||||
vVel = 0, hold = false, ready = flyable, fore = 0, aft = 0, uSum = 0, iTrim = 0, hTrim = 0,
|
||||
climbSpeed = cfg.vMaxUp, holdBand = cfg.altDeadband, maxPower = cfg.maxSignal })
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== STANDBY (pilot remote) ====")
|
||||
print(ready and "Profile: READY" or "Profile: MISSING -> run Wiring (0) + Sign test (2)")
|
||||
if complete then print("Profile: READY")
|
||||
elseif flyable then print("Profile: LEGACY ([S] = re-run setup)")
|
||||
else print("Profile: NOT SET UP -> press [S]") end
|
||||
print(string.format("Height %.1f Pitch %+.1f", h, p))
|
||||
print("Waiting for ENGAGE from the pilot console...")
|
||||
print("CTRL+T -> quit to menu (for setup).")
|
||||
print("[S] setup wizard CTRL+T then 'ap' = menu")
|
||||
local cmd = awaitCmd(0.5)
|
||||
if cmd == "engage" then
|
||||
if fs.exists(CALIB_FILE) then
|
||||
if cmd == "setup" then
|
||||
runSetup()
|
||||
elseif cmd == "engage" then
|
||||
if flyable then
|
||||
runFly() -- blokkerer til pilot STOP / FAULT / CTRL+T
|
||||
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear) -- hold hover etter stop
|
||||
else
|
||||
link.publishStatus({ state = "SETUP NEEDED", reason = "Wiring + Sign test on AP", ready = false })
|
||||
link.publishStatus({ state = "SETUP NEEDED", reason = "press S on autopilot PC", ready = false })
|
||||
sleep(2.0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================ BOOT (startup-inngang) ============================
|
||||
-- Foerste boot uten komplett profil -> tilby wizarden. Timeout -> standby, slik at en ubemannet
|
||||
-- reboot (chunk-reload) aldri blir staaende og vente paa tastetrykk.
|
||||
local function runBoot()
|
||||
if not profileComplete() then
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("==== AIRSHIP AUTOPILOT ====")
|
||||
print("")
|
||||
print(fs.exists(CALIB_FILE)
|
||||
and "Ship profile: LEGACY (sign not measured)."
|
||||
or "No ship profile on this computer yet.")
|
||||
print("")
|
||||
print("[Enter] = first-time setup wizard")
|
||||
print("(any other key, or 10 s -> standby)")
|
||||
local timer = os.startTimer(10)
|
||||
while true do
|
||||
local e = { os.pullEvent() }
|
||||
if e[1] == "timer" and e[2] == timer then break
|
||||
elseif e[1] == "key" then
|
||||
if e[2] == keys.enter then runSetup() end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
runStandby()
|
||||
end
|
||||
|
||||
-- ============================ PROFIL / MENY ============================
|
||||
local function showProfile()
|
||||
local c = loadCalib()
|
||||
print("==== PROFILE ====")
|
||||
print(string.format("uHoverFront %.2f uHoverRear %.2f", c.uHoverFront, c.uHoverRear))
|
||||
print(string.format("lambda %.3f g %.3f", c.lambda, c.g))
|
||||
print(string.format("pitchSign %d pitchGain %.3f", c.pitchSign, c.pitchGain))
|
||||
print(string.format("pitchSign %d (%s) pitchGain %.3f", c.pitchSign,
|
||||
c.signMeasured and "measured" or "DEFAULT -- run setup", c.pitchGain))
|
||||
print(string.format("wiring: fore=%s aft=%s%s", cfg.foreSide, tostring(cfg.aftSide),
|
||||
fs.exists(SIDES_FILE) and "" or " (config default)"))
|
||||
print("calib.txt: " .. (fs.exists(CALIB_FILE) and "saved" or "none (defaults)"))
|
||||
@@ -563,36 +770,47 @@ end
|
||||
local function menu()
|
||||
while true do
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
print("===== AIRSHIP AUTOPILOT v3 =====\n")
|
||||
print(" 1) Monitor (Layer 1)")
|
||||
print(" 2) Sign test")
|
||||
print(" 3) Manual drive (Layer 2)")
|
||||
print(" 4) Calibrate (Layer 3)")
|
||||
print(" 5) Observe open-loop (Layer 4)")
|
||||
print(" 6) FLY / hold (Layer 5-8)")
|
||||
print(" 7) Set target altitude")
|
||||
print(" 8) Show / reset profile")
|
||||
print(" 0) Wiring setup (front/aft sides)")
|
||||
print(" 9) Exit\n")
|
||||
print(sensors.present() and "Sensors: OK" or "Sensors: MISSING - check wiring")
|
||||
print(fs.exists(CALIB_FILE) and "Calib: saved" or "Calib: none (will auto-cal on fly)")
|
||||
print("===== AIRSHIP AUTOPILOT v3 =====")
|
||||
print(" Setup")
|
||||
print(" 1) Setup wizard (wiring + sign test)")
|
||||
print(" 2) Wiring only 3) Sign test only")
|
||||
print(" Fly")
|
||||
print(" 4) FLY / hold 5) Standby (pilot)")
|
||||
print(" 6) Set target altitude")
|
||||
print(" Tools")
|
||||
print(" 7) Monitor 8) Manual 9) Full calibrate")
|
||||
print(" o) Observe p) Profile/reset q) Quit")
|
||||
print("")
|
||||
local prof = profileComplete() and "Profile: OK"
|
||||
or (fs.exists(CALIB_FILE) and "Profile: LEGACY (1 = re-setup)" or "Profile: NOT SET UP -> 1")
|
||||
print((sensors.present() and "Sensors: OK " or "SENSORS MISSING! ") .. prof)
|
||||
print(string.format("Wiring: fore=%s aft=%s", cfg.foreSide, tostring(cfg.aftSide)))
|
||||
print(""); write("Choice: ")
|
||||
local k = read()
|
||||
if k == "1" then return "monitor" elseif k == "2" then return "signtest"
|
||||
elseif k == "3" then return "manual" elseif k == "4" then return "cal"
|
||||
elseif k == "5" then return "observe" elseif k == "6" then return "fly"
|
||||
elseif k == "0" then return "wiring"
|
||||
elseif k == "7" then
|
||||
write("Choice: ")
|
||||
local k = read():lower()
|
||||
if k == "1" then return "setup"
|
||||
elseif k == "2" then return "wiring"
|
||||
elseif k == "3" then return "signtest"
|
||||
elseif k == "4" then return "fly"
|
||||
elseif k == "5" then return "standby"
|
||||
elseif k == "6" then
|
||||
write("Target (empty = auto): "); local s = read()
|
||||
if s == "" then if fs.exists(TARGET_FILE) then fs.delete(TARGET_FILE) end; print("Target cleared.")
|
||||
elseif tonumber(s) then saveTarget(tonumber(s)); print("Target = " .. s) else print("Invalid number.") end
|
||||
print("[Enter]"); read()
|
||||
elseif k == "8" then
|
||||
elseif k == "7" then return "monitor"
|
||||
elseif k == "8" then return "manual"
|
||||
elseif k == "9" then return "cal"
|
||||
elseif k == "o" then return "observe"
|
||||
elseif k == "p" then
|
||||
term.clear(); term.setCursorPos(1, 1); showProfile()
|
||||
print("\nR = reset calib, Enter = back"); local a = read()
|
||||
if a == "r" or a == "R" then if fs.exists(CALIB_FILE) then fs.delete(CALIB_FILE) end; print("Calib reset."); print("[Enter]"); read() end
|
||||
elseif k == "9" then return "exit" end
|
||||
print("\nR = reset SHIP PROFILE (new ship), Enter = back"); local a = read():lower()
|
||||
if a == "r" then
|
||||
for _, f in ipairs({ CALIB_FILE, SIDES_FILE, TARGET_FILE, SETTINGS_FILE }) do
|
||||
if fs.exists(f) then fs.delete(f) end
|
||||
end
|
||||
print("Ship profile reset -- setup wizard on next boot."); print("[Enter]"); read()
|
||||
end
|
||||
elseif k == "q" then return "exit" end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -614,22 +832,36 @@ if not sensors.present() then print("Sensors missing (altitude_sensor + gimbal_s
|
||||
-- kjor EN modus + opprydding. Returnerer (ok, err) saa menyloopen kan avslutte ved CTRL+T.
|
||||
local function runMode(mode, a2, a3)
|
||||
local ok, err = pcall(function()
|
||||
if mode == "monitor" then runMonitor()
|
||||
if mode == "boot" then runBoot()
|
||||
elseif mode == "setup" then runSetup()
|
||||
elseif mode == "standby" then runStandby()
|
||||
elseif mode == "monitor" then runMonitor()
|
||||
elseif mode == "signtest" then runSigntest()
|
||||
elseif mode == "wiring" then runWiring()
|
||||
elseif mode == "manual" then runManual(a2, a3)
|
||||
elseif mode == "cal" then runCal()
|
||||
elseif mode == "observe" then runObserve()
|
||||
elseif mode == "fly" then runFly()
|
||||
elseif mode == "standby" then runStandby()
|
||||
else print("Unknown mode: " .. tostring(mode)) end
|
||||
else print("Unknown mode: " .. tostring(mode))
|
||||
print("Valid: setup wiring signtest fly standby monitor manual cal observe boot") end
|
||||
end)
|
||||
-- opprydding: aktive driv-modus -> hold hover (aldri etterlat drivende uten beskjed);
|
||||
-- monitor styrer ingenting (la utgangene staa).
|
||||
if mode == "signtest" or mode == "manual" or mode == "wiring" then
|
||||
if mode == "manual" or mode == "wiring" then
|
||||
allOff()
|
||||
elseif mode == "cal" or mode == "fly" or mode == "observe" or mode == "standby" then
|
||||
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear) -- hold, ikke null (aldri stup)
|
||||
elseif mode == "signtest" or mode == "setup" then
|
||||
-- Ctrl+T midt i sign-testen: luftbaaren -> hold hover (ALDRI frittfall); paa bakken -> alt av
|
||||
if signtestAirborne then
|
||||
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear)
|
||||
else
|
||||
allOff()
|
||||
end
|
||||
elseif mode == "cal" or mode == "fly" or mode == "observe" or mode == "standby" or mode == "boot" then
|
||||
if fs.exists(CALIB_FILE) then
|
||||
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear) -- hold, ikke null (aldri stup)
|
||||
else
|
||||
allOff() -- aldri floeyet under vaar kontroll -> skipet staar paa bakken; ikke varm brennere
|
||||
end
|
||||
end
|
||||
if not ok and err ~= "Terminated" then print("ERROR: " .. tostring(err)); print("[Enter]"); read() end
|
||||
return ok, err
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
pitchGain = 1,
|
||||
hoverTrim = 1.7889825868927072,
|
||||
uHoverFront = 3,
|
||||
lambda = 0.33333333333333331,
|
||||
g = 1,
|
||||
pitchTrim = 1.345751507649297,
|
||||
alphaPitch = 1,
|
||||
pitchSign = -1,
|
||||
uHoverRear = 3,
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
-- cockpit.lua -- pilot-hus konsoll. Status + full live-styring av autopiloten over rednet.
|
||||
-- To faner: STATUS (fly/standby-styring) og SETTINGS (live brukerinnstillinger). UI = English.
|
||||
-- ADAPTIV layout: kompakt paa 1x1-monitor, full paa storre.
|
||||
|
||||
local progDir = fs.getDir(shell.getRunningProgram())
|
||||
if package and package.path and not string.find(package.path, progDir, 1, true) then
|
||||
package.path = progDir .. "/?.lua;" .. package.path
|
||||
end
|
||||
local link = require("link")
|
||||
|
||||
local mon = peripheral.find("monitor")
|
||||
if not mon then print("No monitor found. Attach a monitor (Advanced recommended)."); return end
|
||||
if not link.open() then print("No modem found. Attach a wireless modem."); return end
|
||||
|
||||
mon.setTextScale(0.5)
|
||||
local W, H = mon.getSize()
|
||||
|
||||
local C = { bg = colors.black, title = colors.cyan, label = colors.lightGray, val = colors.white,
|
||||
ok = colors.lime, warn = colors.orange, fault = colors.red, btn = colors.gray, btnTxt = colors.white,
|
||||
engage = colors.green, stop = colors.red, tabOn = colors.blue, active = colors.green }
|
||||
|
||||
-- live-justerbare brukerinnstillinger (syntetiske noekler -> autopiloten mapper til cfg)
|
||||
local SETTINGS = {
|
||||
{ key = "climbSpeed", short = "Climb", step = 0.1, fmt = "%.1f", unit = "b/s", def = 0.8 },
|
||||
{ key = "holdBand", short = "Band", step = 1, fmt = "%.0f", unit = "blk", def = 3 },
|
||||
{ key = "maxPower", short = "Power", step = 1, fmt = "%.0f", unit = "", def = 6 },
|
||||
}
|
||||
|
||||
local page = "status"
|
||||
local status, lastRx = nil, -999
|
||||
local buttons = {}
|
||||
local function now() return os.clock() end
|
||||
local function online() return (now() - lastRx) < 2.0 end
|
||||
|
||||
local function button(x1, y1, x2, y2, label, action, col)
|
||||
buttons[#buttons + 1] = { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action }
|
||||
mon.setBackgroundColor(col or C.btn); mon.setTextColor(C.btnTxt)
|
||||
for y = y1, y2 do mon.setCursorPos(x1, y); mon.write(string.rep(" ", x2 - x1 + 1)) end
|
||||
mon.setCursorPos(x1 + math.max(0, math.floor((x2 - x1 + 1 - #label) / 2)), y1 + math.floor((y2 - y1) / 2))
|
||||
mon.write(label); mon.setBackgroundColor(C.bg)
|
||||
end
|
||||
local function center(y, text, color)
|
||||
mon.setTextColor(color or C.val)
|
||||
mon.setCursorPos(math.max(1, math.floor((W - #text) / 2) + 1), y); mon.write(text:sub(1, W))
|
||||
end
|
||||
local function putline(y, text, color)
|
||||
mon.setTextColor(color or C.val); mon.setCursorPos(1, y); mon.write(text:sub(1, W))
|
||||
end
|
||||
|
||||
local function drawTabs()
|
||||
local hw = math.floor(W / 2)
|
||||
button(1, 1, hw, 1, "STATUS", function() page = "status" end, page == "status" and C.tabOn or C.btn)
|
||||
button(hw + 1, 1, W, 1, "SETUP", function() page = "settings" end, page == "settings" and C.tabOn or C.btn)
|
||||
end
|
||||
|
||||
local function curVal(p)
|
||||
if status and status[p.key] ~= nil then return status[p.key] end
|
||||
return p.def
|
||||
end
|
||||
|
||||
local function drawStatus()
|
||||
local compact = W < 28
|
||||
local bRowH = (H >= 16) and 2 or 1
|
||||
local landTop = H - bRowH + 1
|
||||
local tgtTop = landTop - bRowH
|
||||
local top, statusMax = 2, tgtTop - 1
|
||||
|
||||
if not online() then
|
||||
center(math.floor((top + statusMax) / 2), "OFFLINE", C.fault)
|
||||
if not compact then center(math.floor((top + statusMax) / 2) + 1, "no link to machine room", C.warn) end
|
||||
return
|
||||
end
|
||||
local s = status
|
||||
local inFlight = s.state ~= "IDLE" and s.state ~= "SETUP NEEDED"
|
||||
local stateColor = C.fault
|
||||
if s.state == "FLYING" then stateColor = C.ok
|
||||
elseif s.state == "IDLE" then stateColor = (s.ready and C.title or C.warn) end
|
||||
local pcolor = (math.abs(s.pitch or 0) < 3) and C.ok or C.warn
|
||||
|
||||
-- bygg status-linjer
|
||||
local lines = {}
|
||||
lines[#lines + 1] = { tostring(s.state) .. (s.hold and " HOLD" or ""), stateColor }
|
||||
if s.reason and s.reason ~= "" then lines[#lines + 1] = { tostring(s.reason), C.fault } end
|
||||
if compact then
|
||||
lines[#lines + 1] = { string.format("%.0f > %.0f", s.height or 0, s.target or 0), C.val }
|
||||
lines[#lines + 1] = { string.format("PITCH %+.1f", s.pitch or 0), pcolor }
|
||||
if inFlight then lines[#lines + 1] = { string.format("VSPD %+.2f", s.vVel or 0), C.val } end
|
||||
if s.state == "IDLE" then lines[#lines + 1] = { s.ready and "ready -> ENGAGE" or "needs setup", s.ready and C.ok or C.warn } end
|
||||
else
|
||||
if s.state == "IDLE" then lines[#lines + 1] = { s.ready and "ready -> ENGAGE" or "needs setup (Wiring+Sign)", s.ready and C.ok or C.warn } end
|
||||
lines[#lines + 1] = { string.format("ALT %.1f > %.0f", s.height or 0, s.target or 0), C.val }
|
||||
lines[#lines + 1] = { string.format("PITCH %+.1f deg", s.pitch or 0), pcolor }
|
||||
if inFlight then
|
||||
lines[#lines + 1] = { string.format("VSPD %+.2f b/s", s.vVel or 0), C.val }
|
||||
lines[#lines + 1] = { string.format("OUT f%d a%d lift %.1f", s.fore or 0, s.aft or 0, s.uSum or 0), C.val }
|
||||
end
|
||||
end
|
||||
|
||||
-- render: kompakt = sentrert + luftig (fyll den ledige plassen); full = venstrejustert top-down
|
||||
if compact then
|
||||
local avail = statusMax - top + 1
|
||||
local gap = (avail >= 2 * #lines - 1) and 1 or 0
|
||||
local blockH = #lines + (#lines - 1) * gap
|
||||
local startY = top + math.max(0, math.floor((avail - blockH) / 2))
|
||||
for i, L in ipairs(lines) do
|
||||
local y = startY + (i - 1) * (1 + gap)
|
||||
if y >= top and y <= statusMax then center(y, L[1], L[2]) end
|
||||
end
|
||||
else
|
||||
for i, L in ipairs(lines) do local y = top + i - 1; if y <= statusMax then putline(y, L[1], L[2]) end end
|
||||
end
|
||||
|
||||
-- knapper (HOLD/LAND lyser solid naar den modusen er aktiv)
|
||||
if inFlight then
|
||||
local bw = math.floor(W / 4)
|
||||
button(1, tgtTop, bw, tgtTop + bRowH - 1, "-5", function() link.sendCommand({ cmd = "adjust", delta = -5 }) end)
|
||||
button(bw + 1, tgtTop, 2 * bw, tgtTop + bRowH - 1, "-1", function() link.sendCommand({ cmd = "adjust", delta = -1 }) end)
|
||||
button(2 * bw + 1, tgtTop, 3 * bw, tgtTop + bRowH - 1, "+1", function() link.sendCommand({ cmd = "adjust", delta = 1 }) end)
|
||||
button(3 * bw + 1, tgtTop, W, tgtTop + bRowH - 1, "+5", function() link.sendCommand({ cmd = "adjust", delta = 5 }) end)
|
||||
local tw = math.floor(W / 3)
|
||||
button(1, landTop, tw, H, "HOLD", function() link.sendCommand({ cmd = "hold" }) end, (s.mode == "hold") and C.active or C.btn)
|
||||
button(tw + 1, landTop, 2 * tw, H, "LAND", function() link.sendCommand({ cmd = "land" }) end, (s.mode == "land") and C.active or C.btn)
|
||||
button(2 * tw + 1, landTop, W, H, "STOP", function() link.sendCommand({ cmd = "stop" }) end, C.stop)
|
||||
else
|
||||
button(1, tgtTop, W, H, "ENGAGE FLY", function() link.sendCommand({ cmd = "engage" }) end, C.engage)
|
||||
end
|
||||
end
|
||||
|
||||
local function drawSettings()
|
||||
local n = #SETTINGS
|
||||
local areaTop = 2
|
||||
local blockH = math.max(1, math.floor((H - areaTop + 1) / n))
|
||||
local bw = math.max(3, math.floor(W / 5))
|
||||
for i, p in ipairs(SETTINGS) do
|
||||
local y1 = areaTop + (i - 1) * blockH
|
||||
local y2 = (i == n) and H or (y1 + blockH - 1)
|
||||
local cur = curVal(p)
|
||||
button(1, y1, bw, y2, "-", function() link.sendCommand({ cmd = "set", key = p.key, value = cur - p.step }) end)
|
||||
button(W - bw + 1, y1, W, y2, "+", function() link.sendCommand({ cmd = "set", key = p.key, value = cur + p.step }) end)
|
||||
local txt = string.format("%s %s%s", p.short, string.format(p.fmt, cur), p.unit)
|
||||
mon.setTextColor(C.val)
|
||||
mon.setCursorPos(bw + 1 + math.max(0, math.floor((W - 2 * bw - #txt) / 2)), math.floor((y1 + y2) / 2))
|
||||
mon.write(txt)
|
||||
end
|
||||
end
|
||||
|
||||
local function draw()
|
||||
mon.setBackgroundColor(C.bg); mon.clear()
|
||||
buttons = {}
|
||||
drawTabs()
|
||||
if page == "settings" then drawSettings() else drawStatus() end
|
||||
end
|
||||
|
||||
local function handleTouch(x, y)
|
||||
for _, b in ipairs(buttons) do
|
||||
if x >= b.x1 and x <= b.x2 and y >= b.y1 and y <= b.y2 then b.action(); draw(); return end
|
||||
end
|
||||
end
|
||||
|
||||
draw()
|
||||
local redraw = os.startTimer(0.5)
|
||||
while true do
|
||||
local e = { os.pullEvent() }
|
||||
if e[1] == "rednet_message" and e[4] == link.STATUS then
|
||||
status = e[3]; lastRx = now()
|
||||
elseif e[1] == "monitor_touch" then
|
||||
handleTouch(e[3], e[4])
|
||||
elseif e[1] == "timer" and e[2] == redraw then
|
||||
redraw = os.startTimer(0.5); draw()
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
-- diag.lua
|
||||
-- Utforsker alle peripheraler og proever aa kalle hver metode (uten argumenter)
|
||||
-- for aa se hvilke data som er tilgjengelige -- spesielt fra contraption diagram-blokken.
|
||||
-- Skriver til skjerm OG diag.log.
|
||||
|
||||
local OUT = "diag.log"
|
||||
if fs.exists(OUT) then fs.delete(OUT) end
|
||||
local file = fs.open(OUT, "w")
|
||||
|
||||
local function emit(s)
|
||||
print(s)
|
||||
file.writeLine(s)
|
||||
end
|
||||
|
||||
local function dump(v)
|
||||
if type(v) == "table" then
|
||||
return textutils.serialise(v):gsub("%s+", " ")
|
||||
end
|
||||
return tostring(v)
|
||||
end
|
||||
|
||||
local names = peripheral.getNames()
|
||||
emit("=== PERIPHERAL-UTFORSKER ===")
|
||||
emit("Fant " .. #names .. " peripheral(er)")
|
||||
emit("")
|
||||
|
||||
for _, name in ipairs(names) do
|
||||
local ptype = peripheral.getType(name)
|
||||
emit("##########################################")
|
||||
emit("Navn: " .. name .. " Type: " .. tostring(ptype))
|
||||
emit("------------------------------------------")
|
||||
|
||||
local methods = peripheral.getMethods(name)
|
||||
table.sort(methods)
|
||||
|
||||
for _, m in ipairs(methods) do
|
||||
-- proev aa kalle metoden uten argumenter; mange er gettere
|
||||
local ok, res = pcall(function() return peripheral.call(name, m) end)
|
||||
if ok then
|
||||
emit(string.format(" %s() = %s", m, dump(res)))
|
||||
else
|
||||
-- feilet (trenger nok argumenter) -- bare noter signaturen
|
||||
emit(string.format(" %s() -> [krever arg / feil: %s]", m, tostring(res):gsub("%s+"," ")))
|
||||
end
|
||||
end
|
||||
emit("")
|
||||
end
|
||||
|
||||
emit("=== FERDIG -> " .. OUT .. " ===")
|
||||
file.close()
|
||||
@@ -1,3 +1,4 @@
|
||||
ap.lua
|
||||
autopilot/altitude.lua
|
||||
autopilot/calibration.lua
|
||||
autopilot/config.lua
|
||||
@@ -11,17 +12,6 @@ autopilot/pitch.lua
|
||||
autopilot/safety.lua
|
||||
autopilot/sensors.lua
|
||||
autopilot/utils.lua
|
||||
calib.txt
|
||||
cockpit.lua
|
||||
diag.lua
|
||||
pid_gains.txt
|
||||
pilot_startup.lua
|
||||
probe.lua
|
||||
scan.lua
|
||||
scan.txt
|
||||
sides.txt
|
||||
stabilize.lua
|
||||
stabilizev2.lua
|
||||
stabilizeV2.lua
|
||||
stabilizev2.lua
|
||||
startup.lua
|
||||
target.txt
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
A_KI = 0.04,
|
||||
P_KI = 0.05,
|
||||
A_KD = 2.2,
|
||||
P_KD = 0.7,
|
||||
hoverMap = {},
|
||||
altI = -0.27424191075575,
|
||||
kCouple = -0.099857552664046,
|
||||
lag = 5.4,
|
||||
A_KP = 0.22,
|
||||
P_KP = 0.22,
|
||||
TRIM = 1.5,
|
||||
pitchI = 20.305881842314,
|
||||
K_PRATE = 0.08,
|
||||
K_RATE = 1.2,
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
-- startup for pilot-hus PC: auto-run cockpit
|
||||
shell.run("cockpit")
|
||||
@@ -1,30 +0,0 @@
|
||||
-- probe.lua -- list every peripheral this computer can see, with its methods.
|
||||
-- Drop a computer next to a block (or wire it up) and run `probe`. UI text in English.
|
||||
|
||||
local function dump(name)
|
||||
print(name .. " -> type: " .. tostring(peripheral.getType(name)))
|
||||
local methods = peripheral.getMethods(name)
|
||||
if methods and #methods > 0 then
|
||||
table.sort(methods)
|
||||
for _, m in ipairs(methods) do print(" ." .. m) end
|
||||
else
|
||||
print(" (no methods)")
|
||||
end
|
||||
end
|
||||
|
||||
print("=== DIRECT SIDES ===")
|
||||
local any = false
|
||||
for _, side in ipairs({ "top", "bottom", "left", "right", "front", "back" }) do
|
||||
if peripheral.isPresent(side) then any = true; dump(side) else print(side .. " -> (nothing)") end
|
||||
end
|
||||
|
||||
print("")
|
||||
print("=== ALL NAMES (incl. wired modem network) ===")
|
||||
local names = peripheral.getNames()
|
||||
if #names == 0 then print("(none)") end
|
||||
for _, n in ipairs(names) do dump(n) end
|
||||
|
||||
if not any and #names == 0 then
|
||||
print("")
|
||||
print(">>> Nothing exposes a peripheral here.")
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
-- scan.lua
|
||||
-- Oppdager peripheraler paa det monterte luftskipet.
|
||||
-- Skriver til skjerm OG til scan.txt slik at resultatet kan deles.
|
||||
-- Kjor paa CC-computeren NAR skipet er montert/lever.
|
||||
|
||||
local OUT = "scan.txt"
|
||||
local file = fs.open(OUT, "w")
|
||||
|
||||
local function emit(line)
|
||||
print(line)
|
||||
file.writeLine(line)
|
||||
end
|
||||
|
||||
emit("=== PERIPHERAL-SCAN ===")
|
||||
emit("")
|
||||
|
||||
local names = peripheral.getNames()
|
||||
|
||||
if #names == 0 then
|
||||
emit("Ingen peripheraler funnet.")
|
||||
emit("Sjekk at skipet er montert og at computeren sitter paa contraptionen.")
|
||||
file.close()
|
||||
return
|
||||
end
|
||||
|
||||
for _, name in ipairs(names) do
|
||||
local ptype = peripheral.getType(name)
|
||||
emit("------------------------------------------")
|
||||
emit("Navn: " .. tostring(name))
|
||||
emit("Type: " .. tostring(ptype))
|
||||
emit("Metoder:")
|
||||
|
||||
local methods = peripheral.getMethods(name)
|
||||
table.sort(methods)
|
||||
for _, m in ipairs(methods) do
|
||||
emit(" " .. m)
|
||||
end
|
||||
emit("")
|
||||
end
|
||||
|
||||
emit("==========================================")
|
||||
emit("Totalt " .. #names .. " peripheral(er).")
|
||||
emit("Lagret til " .. OUT)
|
||||
|
||||
file.close()
|
||||
@@ -1,27 +0,0 @@
|
||||
=== PERIPHERAL-SCAN ===
|
||||
|
||||
------------------------------------------
|
||||
Navn: bottom
|
||||
Type: create:fluid_tank
|
||||
Metoder:
|
||||
pullFluid
|
||||
pushFluid
|
||||
tanks
|
||||
|
||||
------------------------------------------
|
||||
Navn: top
|
||||
Type: gimbal_sensor
|
||||
Metoder:
|
||||
getAngles
|
||||
getAnglesRad
|
||||
|
||||
------------------------------------------
|
||||
Navn: right
|
||||
Type: altitude_sensor
|
||||
Metoder:
|
||||
getAirPressure
|
||||
getHeight
|
||||
|
||||
==========================================
|
||||
Totalt 3 peripheral(er).
|
||||
Lagret til scan.txt
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
aft = "back",
|
||||
fore = "right",
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
-- 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
|
||||
@@ -1,9 +1,6 @@
|
||||
-- stabilizeV2.lua -- launcher for v3 observer-autopiloten i autopilot/.
|
||||
-- Videresender argumenter:
|
||||
-- stabilizeV2 -> meny
|
||||
-- stabilizeV2 monitor -> Lag 1
|
||||
-- stabilizeV2 signtest -> §13.2
|
||||
-- stabilizeV2 manual 4 3 -> Lag 2 (fore=4, aft=3)
|
||||
-- alias: videresender til 'ap' (beholdt for gammel muskelhukommelse).
|
||||
-- VIKTIG: begge case-variantene (stabilizeV2/stabilizev2) har IDENTISK innhold,
|
||||
-- saa et case-insensitivt filsystem (Windows-hostet server) kan aldri gi rekursjon.
|
||||
local args = { ... }
|
||||
local unpack = table.unpack or unpack
|
||||
shell.run("autopilot/main", unpack(args))
|
||||
shell.run("ap", unpack(args))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- stabilizev2.lua -- lowercase alias for stabilizeV2 (CC is case-sensitive on Linux).
|
||||
-- Forwards all args to the real launcher so either case works.
|
||||
-- alias: videresender til 'ap' (beholdt for gammel muskelhukommelse).
|
||||
-- VIKTIG: begge case-variantene (stabilizeV2/stabilizev2) har IDENTISK innhold,
|
||||
-- saa et case-insensitivt filsystem (Windows-hostet server) kan aldri gi rekursjon.
|
||||
local args = { ... }
|
||||
local unpack = table.unpack or unpack
|
||||
shell.run("stabilizeV2", unpack(args))
|
||||
shell.run("ap", unpack(args))
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
-- Maskinrom autopilot: STANDBY er default paa -- staar klar og lytter etter ENGAGE
|
||||
-- fra pilot-konsollen. CTRL+T -> kjor "stabilizeV2" for menyen (wiring/sign test/setup).
|
||||
shell.run("stabilizeV2", "standby")
|
||||
-- Maskinrom-PC: boot -> setup-wizard-prompt hvis skipet mangler profil, ellers standby
|
||||
-- (lytter etter ENGAGE fra pilot-konsollen). CTRL+T og kjor "ap" for menyen.
|
||||
shell.run("ap", "boot")
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
100
|
||||
@@ -87,7 +87,7 @@ local function drawStatus()
|
||||
if inFlight then lines[#lines + 1] = { string.format("VSPD %+.2f", s.vVel or 0), C.val } end
|
||||
if s.state == "IDLE" then lines[#lines + 1] = { s.ready and "ready -> ENGAGE" or "needs setup", s.ready and C.ok or C.warn } end
|
||||
else
|
||||
if s.state == "IDLE" then lines[#lines + 1] = { s.ready and "ready -> ENGAGE" or "needs setup (Wiring+Sign)", s.ready and C.ok or C.warn } end
|
||||
if s.state == "IDLE" then lines[#lines + 1] = { s.ready and "ready -> ENGAGE" or "needs setup (wizard on AP)", s.ready and C.ok or C.warn } end
|
||||
lines[#lines + 1] = { string.format("ALT %.1f > %.0f", s.height or 0, s.target or 0), C.val }
|
||||
lines[#lines + 1] = { string.format("PITCH %+.1f deg", s.pitch or 0), pcolor }
|
||||
if inFlight then
|
||||
|
||||
Reference in New Issue
Block a user