From 54963e5b4232ee96e4bab948a3a0c0a650addf3b Mon Sep 17 00:00:00 2001 From: Sterister Date: Thu, 23 Jul 2026 23:21:54 +0200 Subject: [PATCH] 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 --- .gitignore | 11 + README.md | 70 ++- build.sh | 91 +++- files/autopilot/ap.lua | 10 + files/autopilot/autopilot/calibration.lua | 1 + files/autopilot/autopilot/config.lua | 9 +- files/autopilot/autopilot/main.lua | 466 +++++++++++++----- files/autopilot/calib.txt | 11 - files/autopilot/cockpit.lua | 171 ------- files/autopilot/diag.lua | 50 -- files/autopilot/manifest | 14 +- files/autopilot/pid_gains.txt | 16 - files/autopilot/pilot_startup.lua | 2 - files/autopilot/probe.lua | 30 -- files/autopilot/scan.lua | 45 -- files/autopilot/scan.txt | 27 - files/autopilot/sides.txt | 4 - files/autopilot/stabilize.lua | 571 ---------------------- files/autopilot/stabilizeV2.lua | 11 +- files/autopilot/stabilizev2.lua | 7 +- files/autopilot/startup.lua | 6 +- files/autopilot/target.txt | 1 - files/cockpit/cockpit.lua | 2 +- install.lua | 126 ++++- 24 files changed, 617 insertions(+), 1135 deletions(-) create mode 100644 files/autopilot/ap.lua delete mode 100644 files/autopilot/calib.txt delete mode 100644 files/autopilot/cockpit.lua delete mode 100644 files/autopilot/diag.lua delete mode 100644 files/autopilot/pid_gains.txt delete mode 100644 files/autopilot/pilot_startup.lua delete mode 100644 files/autopilot/probe.lua delete mode 100644 files/autopilot/scan.lua delete mode 100644 files/autopilot/scan.txt delete mode 100644 files/autopilot/sides.txt delete mode 100644 files/autopilot/stabilize.lua delete mode 100644 files/autopilot/target.txt diff --git a/.gitignore b/.gitignore index 1865ace..72f414d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ # CC autopilot repo *.log +*.old .DS_Store + +# per-skip tilstand: maales av setup-wizarden paa skipet, hoerer ALDRI hjemme i repoet +# (shipper vi den, arver et nytt skip forrige skips wiring/pitch-fortegn -> feil vei) +files/*/calib.txt +files/*/sides.txt +files/*/target.txt +files/*/settings.txt +files/*/scan.txt +files/*/pid_gains.txt +files/*/.installed_* diff --git a/README.md b/README.md index 350d69d..8b3d1b2 100644 --- a/README.md +++ b/README.md @@ -3,48 +3,80 @@ Altitude/pitch autopilot for a hot-air airship, delivered as an in-game installer so it runs on any Minecraft server regardless of the computer's id. -## Install on a server +## Install — ONE command, both PCs -On the CC computer, run one command. Pick the role for that machine: +Run the **same** command on each CC computer: ``` --- machine-room PC (full autopilot, boots to standby): -wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua autopilot - --- pilot-house PC (control console): -wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua cockpit +wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua ``` -Omit the role to get a menu. Re-run any time to update to the latest code. +The installer detects which PC it is on (flight sensors → `autopilot`, +monitor → `cockpit`), installs the right role, and continues **straight into +setup** — no reboot, no role names to remember. Re-run the same command any +time to update; it also cleans out files that no longer belong and asks +whether to keep or reset the ship profile. + +(Advanced: append `autopilot` or `cockpit` to override detection.) Requires HTTP enabled on the server (default on). If it is off, set `[http] enabled = true` in `computercraft-server.toml` and restart the server. +## First-time setup (per ship) + +The installer ships **code only** — everything ship-specific (which redstone +side drives which balloon, pitch sign, hover level) is measured on the ship by +the **setup wizard**, which the install flow takes you straight into: + +1. **Wiring** — the wizard pulses each free side; you say which burner fired. +2. **Sign test** — the ship lifts off by itself, finds its own lift power, + pushes the front balloon, verifies the response in both directions + (auto-escalating the push if the response is weak), then descends and holds + hover. No config editing, no freefall. Takes 1–3 minutes. + +After that the ship is ready: `FLY` from the menu or `ENGAGE` from the cockpit +console. Pitch and altitude keep self-learning in flight. The wizard can be +re-run any time: `ap setup`, press `S` in standby, or menu choice 1. + +Balloon orientation does **not** need to match any particular side of the +computer: sensors are found by type, wiring is mapped by the wizard, and the +pitch sign is measured — the only physical requirement is both balloons on the +fore/aft line (the axis the gimbal reports as pitch). + ## Roles -| Role | Source PC | Boots | Purpose | -|-------------|-----------|-----------------|------------------------------------------| -| `autopilot` | 0 | `stabilizeV2 standby` | v3 observer autopilot (13 modules in `autopilot/`) | -| `cockpit` | 4 | `cockpit` | pilot control console | +| Role | Boots | Purpose | +|-------------|----------------------|---------------------------------------------| +| `autopilot` | `ap boot` → wizard/standby | v3 observer autopilot (13 modules in `autopilot/`) | +| `cockpit` | `cockpit` | pilot control console (monitor + modem) | -Boot chain (autopilot): `startup.lua` -> `stabilizeV2` -> `autopilot/main.lua`. -Menu / wiring / sign-test: `Ctrl+T` then `stabilizeV2` for the menu. +Boot chain (autopilot): `startup.lua` → `ap boot` → `autopilot/main.lua`. +Menu any time: `Ctrl+T`, then `ap`. (`stabilizeV2` remains as an alias.) + +Ship state lives on the computer only: `sides.txt`, `calib.txt`, `target.txt`, +`settings.txt`. Reset it via menu `p` → `R`, or when re-running the installer. ## Updating the code -The `files/` tree mirrors the live singleplayer world. After editing the Lua -locally, regenerate the manifests and push: +The repo is the source of truth for the installer. After editing the Lua: ``` -./build.sh +./build.sh # regenerate manifests git commit -am "sync autopilot" git push ``` Then re-run the `wget run` command in-game to pull the update. -`build.sh` reads from the "Modda wii" world by default; pass a different -`computercraft/computer` path as arg 1 if the world moves. +Syncing with the singleplayer dev world (optional): + +``` +./build.sh --from-world # world -> files/ (code only; state/legacy filtered) +./build.sh --to-world # files/ -> world (code only; never touches state) +``` + +Default world path is the "Modda wii" save; pass a different +`computercraft/computer` path as arg 2 if the world moves. ## Notes diff --git a/build.sh b/build.sh index e26d9e2..9cfb49f 100755 --- a/build.sh +++ b/build.sh @@ -1,39 +1,80 @@ #!/usr/bin/env bash -# build.sh -- mirror the live CC autopilot from the singleplayer world into this -# repo and regenerate the per-role manifests that install.lua consumes. +# build.sh -- regenerer per-rolle-manifester fra files/ (REPOET er kilden installeren serverer). +# Vanlig flyt etter kodeendring: ./build.sh && git commit -am "sync" && git push # -# Run this whenever you change the Lua locally and want the server installer to -# pick it up: ./build.sh && git commit -am "sync" && git push +# Valgfri synk mot singleplayer-verdenen (dev-miljoeet): +# ./build.sh --from-world [computer-dir] # verden -> files/ (kun kode; state/legacy filtreres) +# ./build.sh --to-world [computer-dir] # files/ -> verden (kun kode; roerer aldri state) set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SRC_DEFAULT="/home/stian/.local/share/ModrinthApp/profiles/Modda wii/saves/New World/computercraft/computer" -SRC="${1:-$SRC_DEFAULT}" +WORLD_DEFAULT="/home/stian/.local/share/ModrinthApp/profiles/Modda wii/saves/New World/computercraft/computer" -# role name -> source computer id in the world +# rolle -> computer-id i verdenen declare -A ROLE_SRC=( - [autopilot]=0 # maskinrom: startup -> stabilizeV2 -> autopilot/main + [autopilot]=0 # maskinrom: startup -> ap boot -> wizard/standby [cockpit]=4 # pilot-hus: startup -> cockpit ) -if [ ! -d "$SRC" ]; then - echo "error: world computer dir not found: $SRC" >&2 - echo "pass the path as arg 1 if the world moved." >&2 - exit 1 -fi +# per-skip tilstand + utdaterte verktoey: shippes ALDRI (installeren rydder dem ogsaa) +EXCLUDES=(calib.txt sides.txt target.txt settings.txt scan.txt pid_gains.txt + stabilize.lua diag.lua probe.lua scan.lua manifest) +# rolle-spesifikt: cockpit-koden hoerer ikke hjemme paa autopilot-PCen +declare -A ROLE_EXCLUDES=( + [autopilot]="cockpit.lua pilot_startup.lua" + [cockpit]="" +) +is_excluded() { # $1=rolle $2=relativ sti + local base x; base="$(basename "$2")" + case "$base" in *.log|*.old|.installed_*) return 0;; esac + for x in "${EXCLUDES[@]}"; do [ "$base" = "$x" ] && return 0; done + for x in ${ROLE_EXCLUDES[$1]}; do [ "$base" = "$x" ] && return 0; done + return 1 +} + +mode="${1:-manifests}" +world="${2:-$WORLD_DEFAULT}" + +case "$mode" in + manifests) ;; + --from-world|--to-world) + if [ ! -d "$world" ]; then + echo "error: world computer dir not found: $world" >&2 + echo "pass the path as arg 2 if the world moved." >&2 + exit 1 + fi + for role in "${!ROLE_SRC[@]}"; do + id="${ROLE_SRC[$role]}" + src="$world/$id"; dst="$REPO/files/$role" + if [ "$mode" = "--from-world" ]; then from="$src"; to="$dst"; else from="$dst"; to="$src"; fi + if [ ! -d "$from" ]; then + echo "warn: source dir missing for role '$role' ($from), skipping" >&2 + continue + fi + while IFS= read -r rel; do + if ! is_excluded "$role" "$rel"; then + mkdir -p "$to/$(dirname "$rel")" + cp "$from/$rel" "$to/$rel" + fi + done < <(cd "$from" && find . -type f | sed 's|^\./||') + echo "synced role '$role' ($mode)" + done + ;; + *) + echo "usage: ./build.sh [--from-world|--to-world] [computer-dir]" >&2 + exit 1 + ;; +esac + +# manifester: nylinje-separerte relative stier. LC_ALL=C sort er VIKTIG: locale-avhengig +# (case-insensitiv) sortering kan stokke om case-varianter og gi udefinert install-rekkefoelge. for role in "${!ROLE_SRC[@]}"; do - id="${ROLE_SRC[$role]}" dst="$REPO/files/$role" - if [ ! -d "$SRC/$id" ]; then - echo "warn: computer $id (role $role) missing in world, skipping" >&2 - continue - fi - rm -rf "$dst" - cp -r "$SRC/$id" "$dst" - # Manifest = newline-separated relative paths, sorted, excluding the manifest itself. - ( cd "$dst" && find . -type f ! -name manifest | sed 's|^\./||' | sort ) > "$dst/manifest" - echo "role '$role' <- computer $id : $(wc -l < "$dst/manifest") files" + [ -d "$dst" ] || continue + while IFS= read -r rel; do + is_excluded "$role" "$rel" || printf '%s\n' "$rel" + done < <(cd "$dst" && find . -type f ! -name manifest | sed 's|^\./||' | LC_ALL=C sort) > "$dst/manifest" + echo "role '$role': $(wc -l < "$dst/manifest") files" done - -echo "done. files/ and manifests regenerated." +echo "done. manifests regenerated." diff --git a/files/autopilot/ap.lua b/files/autopilot/ap.lua new file mode 100644 index 0000000..4c79706 --- /dev/null +++ b/files/autopilot/ap.lua @@ -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)) diff --git a/files/autopilot/autopilot/calibration.lua b/files/autopilot/autopilot/calibration.lua index dfbcbda..9fe3ccc 100644 --- a/files/autopilot/autopilot/calibration.lua +++ b/files/autopilot/autopilot/calibration.lua @@ -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 diff --git a/files/autopilot/autopilot/config.lua b/files/autopilot/autopilot/config.lua index 0a05124..20c6ace 100644 --- a/files/autopilot/autopilot/config.lua +++ b/files/autopilot/autopilot/config.lua @@ -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. diff --git a/files/autopilot/autopilot/main.lua b/files/autopilot/autopilot/main.lua index 7df17b0..ef4f26c 100644 --- a/files/autopilot/autopilot/main.lua +++ b/files/autopilot/autopilot/main.lua @@ -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 diff --git a/files/autopilot/calib.txt b/files/autopilot/calib.txt deleted file mode 100644 index 9192d9a..0000000 --- a/files/autopilot/calib.txt +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/files/autopilot/cockpit.lua b/files/autopilot/cockpit.lua deleted file mode 100644 index 56d82d9..0000000 --- a/files/autopilot/cockpit.lua +++ /dev/null @@ -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 diff --git a/files/autopilot/diag.lua b/files/autopilot/diag.lua deleted file mode 100644 index 3b3ce7d..0000000 --- a/files/autopilot/diag.lua +++ /dev/null @@ -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() diff --git a/files/autopilot/manifest b/files/autopilot/manifest index 7bb3238..f0808d0 100644 --- a/files/autopilot/manifest +++ b/files/autopilot/manifest @@ -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 diff --git a/files/autopilot/pid_gains.txt b/files/autopilot/pid_gains.txt deleted file mode 100644 index d8d4faa..0000000 --- a/files/autopilot/pid_gains.txt +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/files/autopilot/pilot_startup.lua b/files/autopilot/pilot_startup.lua deleted file mode 100644 index 8521fa7..0000000 --- a/files/autopilot/pilot_startup.lua +++ /dev/null @@ -1,2 +0,0 @@ --- startup for pilot-hus PC: auto-run cockpit -shell.run("cockpit") diff --git a/files/autopilot/probe.lua b/files/autopilot/probe.lua deleted file mode 100644 index 7a968f3..0000000 --- a/files/autopilot/probe.lua +++ /dev/null @@ -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 diff --git a/files/autopilot/scan.lua b/files/autopilot/scan.lua deleted file mode 100644 index 8267046..0000000 --- a/files/autopilot/scan.lua +++ /dev/null @@ -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() diff --git a/files/autopilot/scan.txt b/files/autopilot/scan.txt deleted file mode 100644 index b3ba912..0000000 --- a/files/autopilot/scan.txt +++ /dev/null @@ -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 diff --git a/files/autopilot/sides.txt b/files/autopilot/sides.txt deleted file mode 100644 index 6e8afc1..0000000 --- a/files/autopilot/sides.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - aft = "back", - fore = "right", -} \ No newline at end of file diff --git a/files/autopilot/stabilize.lua b/files/autopilot/stabilize.lua deleted file mode 100644 index a0f805e..0000000 --- a/files/autopilot/stabilize.lua +++ /dev/null @@ -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 diff --git a/files/autopilot/stabilizeV2.lua b/files/autopilot/stabilizeV2.lua index 993d839..097b162 100644 --- a/files/autopilot/stabilizeV2.lua +++ b/files/autopilot/stabilizeV2.lua @@ -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)) diff --git a/files/autopilot/stabilizev2.lua b/files/autopilot/stabilizev2.lua index 89c2544..097b162 100644 --- a/files/autopilot/stabilizev2.lua +++ b/files/autopilot/stabilizev2.lua @@ -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)) diff --git a/files/autopilot/startup.lua b/files/autopilot/startup.lua index e1d7331..3293543 100644 --- a/files/autopilot/startup.lua +++ b/files/autopilot/startup.lua @@ -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") diff --git a/files/autopilot/target.txt b/files/autopilot/target.txt deleted file mode 100644 index 105d7d9..0000000 --- a/files/autopilot/target.txt +++ /dev/null @@ -1 +0,0 @@ -100 \ No newline at end of file diff --git a/files/cockpit/cockpit.lua b/files/cockpit/cockpit.lua index 56d82d9..e78a1d4 100644 --- a/files/cockpit/cockpit.lua +++ b/files/cockpit/cockpit.lua @@ -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 diff --git a/install.lua b/install.lua index 3c89e72..bfd300c 100644 --- a/install.lua +++ b/install.lua @@ -1,19 +1,35 @@ --- install.lua -- role-based installer for the Create: Aeronautics airship autopilot. --- Fetches every file for the chosen role over HTTP and writes it to this computer, --- so it works on any server regardless of the computer's id. Re-run to update. +-- install.lua -- ETT entry-point for hele autopiloten. Samme kommando paa begge PC-er: -- --- Usage on a CC computer: -- wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua --- wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua autopilot --- wget run https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main/install.lua cockpit +-- +-- Installeren DETEKTERER selv hvilken PC den staar paa (flysensorer = autopilot, +-- monitor = cockpit), installerer riktig rolle, og gaar RETT videre i setup/oppstart -- +-- ingen reboot, ingen rollenavn aa huske. Re-run samme kommando for aa oppdatere. +-- Eksplisitt rolle som argument overstyrer deteksjonen (avansert): +-- wget run .../install.lua autopilot | cockpit -- -- Roles: --- autopilot machine-room PC: full v3 observer autopilot (boots to standby) --- cockpit pilot-house PC: control console +-- autopilot machine-room PC: full v3 observer autopilot (setup wizard / standby) +-- cockpit pilot-house PC: control console (monitor + modem) +-- +-- Per-skip TILSTAND (sides/calib/target/settings) shippes ALDRI av installeren -- den maales +-- av setup-wizarden PAA skipet. Ved re-install kan profilen beholdes eller nullstilles. +-- Filer som er fjernet fra manifestet siden sist (eller kjente legacy-filer) ryddes bort. local BASE = "https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main" local ROLES = { "autopilot", "cockpit" } +-- per-skip tilstand: roeres aldri automatisk, men tilbys nullstilt ved install +local STATE_FILES = { "sides.txt", "calib.txt", "target.txt", "settings.txt" } + +-- kjente filer fra eldre versjoner som ikke lenger hoerer til rollen (engangs-rydding; +-- videre oppdateringer ryddes automatisk via installasjons-loggen .installed_) +local LEGACY = { + autopilot = { "stabilize.lua", "diag.lua", "probe.lua", "scan.lua", "scan.txt", + "pid_gains.txt", "cockpit.lua", "pilot_startup.lua" }, + cockpit = {}, +} + local args = { ... } -- ---- helpers --------------------------------------------------------------- @@ -41,9 +57,17 @@ local function writeFile(path, data) f.close() end +local function inList(list, x) + for _, v in ipairs(list) do if v == x then return true end end + return false +end + local function chooseRole() print("Select role to install:") - for i, r in ipairs(ROLES) do print((" %d) %s"):format(i, r)) end + print(" 1) autopilot (machine room -- has the") + print(" altitude + gimbal sensors)") + print(" 2) cockpit (pilot house -- has a") + print(" monitor + modem)") write("> ") local sel = read() local n = tonumber(sel) @@ -52,6 +76,18 @@ local function chooseRole() die("Unknown role: " .. tostring(sel)) end +-- gjett rollen ut fra hva som er koblet til: flysensorer er det sterkeste signalet +-- (maskinrommet kan ogsaa ha monitor), deretter monitor -> cockpit +local function detectRole() + if peripheral.find("altitude_sensor") or peripheral.find("gimbal_sensor") then + return "autopilot", "flight sensors attached" + end + if peripheral.find("monitor") then + return "cockpit", "monitor attached, no flight sensors" + end + return nil +end + -- ---- preflight ------------------------------------------------------------- if not http then @@ -61,12 +97,23 @@ if not http then end local role = args[1] -if not role then - role = chooseRole() -else +if role then local ok = false for _, r in ipairs(ROLES) do if r == role then ok = true end end if not ok then die("Unknown role '" .. role .. "'. Valid: " .. table.concat(ROLES, ", ")) end +else + local guess, why = detectRole() + if guess then + print("This looks like the " .. + (guess == "autopilot" and "MACHINE-ROOM" or "PILOT-HOUSE") .. " PC") + print(" (" .. why .. ")") + write("Install '" .. guess .. "'? [Y/n] ") + role = (read():lower() == "n") and chooseRole() or guess + else + print("Could not auto-detect this PC's role") + print("(no flight sensors or monitor attached).") + role = chooseRole() + end end -- ---- fetch manifest -------------------------------------------------------- @@ -108,8 +155,57 @@ if #fail > 0 then die("Install incomplete -- re-run after fixing the errors above.") end +-- ---- rydd: legacy + filer fjernet fra manifestet siden forrige install ------ + +local removed = 0 +for _, f in ipairs(LEGACY[role] or {}) do + if fs.exists(f) then fs.delete(f); removed = removed + 1; print("removed legacy: " .. f) end +end + +local RECORD = ".installed_" .. role +if fs.exists(RECORD) then + local rf = fs.open(RECORD, "r"); local prev = rf.readAll(); rf.close() + for line in (prev or ""):gmatch("[^\r\n]+") do + local p = line:match("^%s*(.-)%s*$") + if p ~= "" and not inList(files, p) and not inList(STATE_FILES, p) and fs.exists(p) then + fs.delete(p); removed = removed + 1; print("removed obsolete: " .. p) + end + end +end +writeFile(RECORD, table.concat(files, "\n") .. "\n") +if removed > 0 then print(("Cleaned up %d old file(s)."):format(removed)) end + +-- ---- skip-profil: behold eller nullstill (ETTER vellykket install, foer boot) ---- +-- (kun autopilot-rollen har skip-tilstand; sletter aldri noe hvis nedlastingen feilet) + +if role == "autopilot" then + local found = {} + for _, f in ipairs(STATE_FILES) do if fs.exists(f) then found[#found + 1] = f end end + if #found > 0 then + print("Existing ship profile found:") + print(" " .. table.concat(found, ", ")) + write("Keep it? (n = new/rewired ship) [Y/n] ") + if read():lower() == "n" then + for _, f in ipairs(found) do fs.delete(f) end + print("Profile reset -- the setup wizard takes over.") + end + end +end + +-- ---- ferdig: gaa RETT videre (ingen reboot noedvendig) ---------------------- +-- startup.lua er installert, saa alt auto-starter ved fremtidige reboots; naa +-- fortsetter vi direkte inn i samme flyt som boot ville gitt. + term.setTextColor(colors.green) -print("Done. Reboot to auto-start (startup.lua).") +print("Done. (auto-starts on every reboot)") term.setTextColor(colors.white) -write("Reboot now? [y/N] ") -if read():lower() == "y" then os.reboot() end +print("") +if role == "autopilot" then + print("Continuing into setup/standby...") + sleep(1.0) + shell.run("ap", "boot") +else + print("Starting the pilot console...") + sleep(1.0) + shell.run("cockpit") +end