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:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,3 +1,14 @@
|
|||||||
# CC autopilot repo
|
# CC autopilot repo
|
||||||
*.log
|
*.log
|
||||||
|
*.old
|
||||||
.DS_Store
|
.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_*
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -3,48 +3,80 @@
|
|||||||
Altitude/pitch autopilot for a hot-air airship, delivered as an in-game installer
|
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.
|
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
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
Requires HTTP enabled on the server (default on). If it is off, set
|
||||||
`[http] enabled = true` in `computercraft-server.toml` and restart the server.
|
`[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
|
## Roles
|
||||||
|
|
||||||
| Role | Source PC | Boots | Purpose |
|
| Role | Boots | Purpose |
|
||||||
|-------------|-----------|-----------------|------------------------------------------|
|
|-------------|----------------------|---------------------------------------------|
|
||||||
| `autopilot` | 0 | `stabilizeV2 standby` | v3 observer autopilot (13 modules in `autopilot/`) |
|
| `autopilot` | `ap boot` → wizard/standby | v3 observer autopilot (13 modules in `autopilot/`) |
|
||||||
| `cockpit` | 4 | `cockpit` | pilot control console |
|
| `cockpit` | `cockpit` | pilot control console (monitor + modem) |
|
||||||
|
|
||||||
Boot chain (autopilot): `startup.lua` -> `stabilizeV2` -> `autopilot/main.lua`.
|
Boot chain (autopilot): `startup.lua` → `ap boot` → `autopilot/main.lua`.
|
||||||
Menu / wiring / sign-test: `Ctrl+T` then `stabilizeV2` for the menu.
|
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
|
## Updating the code
|
||||||
|
|
||||||
The `files/` tree mirrors the live singleplayer world. After editing the Lua
|
The repo is the source of truth for the installer. After editing the Lua:
|
||||||
locally, regenerate the manifests and push:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
./build.sh
|
./build.sh # regenerate manifests
|
||||||
git commit -am "sync autopilot"
|
git commit -am "sync autopilot"
|
||||||
git push
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
Then re-run the `wget run` command in-game to pull the update.
|
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
|
Syncing with the singleplayer dev world (optional):
|
||||||
`computercraft/computer` path as arg 1 if the world moves.
|
|
||||||
|
```
|
||||||
|
./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
|
## Notes
|
||||||
|
|
||||||
|
|||||||
89
build.sh
89
build.sh
@@ -1,39 +1,80 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# build.sh -- mirror the live CC autopilot from the singleplayer world into this
|
# build.sh -- regenerer per-rolle-manifester fra files/ (REPOET er kilden installeren serverer).
|
||||||
# repo and regenerate the per-role manifests that install.lua consumes.
|
# 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
|
# Valgfri synk mot singleplayer-verdenen (dev-miljoeet):
|
||||||
# pick it up: ./build.sh && git commit -am "sync" && git push
|
# ./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
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
SRC_DEFAULT="/home/stian/.local/share/ModrinthApp/profiles/Modda wii/saves/New World/computercraft/computer"
|
WORLD_DEFAULT="/home/stian/.local/share/ModrinthApp/profiles/Modda wii/saves/New World/computercraft/computer"
|
||||||
SRC="${1:-$SRC_DEFAULT}"
|
|
||||||
|
|
||||||
# role name -> source computer id in the world
|
# rolle -> computer-id i verdenen
|
||||||
declare -A ROLE_SRC=(
|
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
|
[cockpit]=4 # pilot-hus: startup -> cockpit
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ ! -d "$SRC" ]; then
|
# per-skip tilstand + utdaterte verktoey: shippes ALDRI (installeren rydder dem ogsaa)
|
||||||
echo "error: world computer dir not found: $SRC" >&2
|
EXCLUDES=(calib.txt sides.txt target.txt settings.txt scan.txt pid_gains.txt
|
||||||
echo "pass the path as arg 1 if the world moved." >&2
|
stabilize.lua diag.lua probe.lua scan.lua manifest)
|
||||||
exit 1
|
# rolle-spesifikt: cockpit-koden hoerer ikke hjemme paa autopilot-PCen
|
||||||
fi
|
declare -A ROLE_EXCLUDES=(
|
||||||
|
[autopilot]="cockpit.lua pilot_startup.lua"
|
||||||
|
[cockpit]=""
|
||||||
|
)
|
||||||
|
|
||||||
for role in "${!ROLE_SRC[@]}"; do
|
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]}"
|
id="${ROLE_SRC[$role]}"
|
||||||
dst="$REPO/files/$role"
|
src="$world/$id"; dst="$REPO/files/$role"
|
||||||
if [ ! -d "$SRC/$id" ]; then
|
if [ "$mode" = "--from-world" ]; then from="$src"; to="$dst"; else from="$dst"; to="$src"; fi
|
||||||
echo "warn: computer $id (role $role) missing in world, skipping" >&2
|
if [ ! -d "$from" ]; then
|
||||||
|
echo "warn: source dir missing for role '$role' ($from), skipping" >&2
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
rm -rf "$dst"
|
while IFS= read -r rel; do
|
||||||
cp -r "$SRC/$id" "$dst"
|
if ! is_excluded "$role" "$rel"; then
|
||||||
# Manifest = newline-separated relative paths, sorted, excluding the manifest itself.
|
mkdir -p "$to/$(dirname "$rel")"
|
||||||
( cd "$dst" && find . -type f ! -name manifest | sed 's|^\./||' | sort ) > "$dst/manifest"
|
cp "$from/$rel" "$to/$rel"
|
||||||
echo "role '$role' <- computer $id : $(wc -l < "$dst/manifest") files"
|
fi
|
||||||
done
|
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
|
||||||
|
|
||||||
echo "done. files/ and manifests regenerated."
|
# 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
|
||||||
|
dst="$REPO/files/$role"
|
||||||
|
[ -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. manifests regenerated."
|
||||||
|
|||||||
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
|
end
|
||||||
|
|
||||||
drive(calib.uHoverFront, calib.uHoverRear) -- hold hover ved utgang
|
drive(calib.uHoverFront, calib.uHoverRear) -- hold hover ved utgang
|
||||||
|
calib.signMeasured = true -- fortegnet er maalt/bekreftet i flukt -> profilen er komplett
|
||||||
return calib
|
return calib
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,11 @@ return {
|
|||||||
hoverTrim = 0.0, -- self-learned loft-trim (redstone, warm-start); fly-loopen oppdaterer
|
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 = {
|
signtest = {
|
||||||
liftLevel = 5, -- felles nivaa for aa loefte skipet luftbaaret (i responsivt omraade)
|
nudge = 2, -- START-differanse (front opp / bak ned); testen oeker selv ved svak respons
|
||||||
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
|
|
||||||
settleTime = 1.0, -- (legacy: brukt av full kalibrering 'cal' Stage 3)
|
settleTime = 1.0, -- (legacy: brukt av full kalibrering 'cal' Stage 3)
|
||||||
nudgeTime = 3.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.
|
-- 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.
|
-- main.lua -- loop + tilstandsmaskin for v3 observer-autopiloten.
|
||||||
-- All UI-tekst er ENGELSK; kommentarer er norske (husstil).
|
-- All UI-tekst er ENGELSK; kommentarer er norske (husstil).
|
||||||
-- Modus (argument eller meny):
|
-- 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
|
-- 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
|
-- manual Lag 2: faste nivaaer (piltaster), finn hover
|
||||||
-- cal Lag 3: full kalibrering (selv-laerende)
|
-- cal Lag 3: full kalibrering (selv-laerende)
|
||||||
-- observe Lag 4: observer ÅPEN SLØYFE -- valider at v_hat sporer v_meas
|
-- observe Lag 4: observer ÅPEN SLØYFE -- valider at v_hat sporer v_meas
|
||||||
@@ -87,6 +90,13 @@ local function loadSides()
|
|||||||
end
|
end
|
||||||
local function saveSides(t) local f = fs.open(SIDES_FILE, "w"); f.write(textutils.serialise(t)); f.close() 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.
|
-- live brukerinnstillinger (justeres fra pilot-konsollen), persistert. Syntetiske noekler -> cfg.
|
||||||
local SETTINGS_FILE = "settings.txt"
|
local SETTINGS_FILE = "settings.txt"
|
||||||
local function saveSettings(t) local f = fs.open(SETTINGS_FILE, "w"); f.write(textutils.serialise(t)); f.close() end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================ SIGN TEST (§13.2) ============================
|
-- ============================ SIGN TEST (§13.2, v2 -- SELVGAAENDE) ============================
|
||||||
-- Maaler pitch-RESPONSEN paa en nudge (mer front) for aa finne pitchSign. Bruker pitch-RATEN, ikke
|
-- 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
|
-- 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-
|
-- aldri, men RATEN gjor det. Hele testen er selvgaaende -- INGEN config-redigering:
|
||||||
-- etterslepet) FOER vi lagrer. Baseline-raten fanger asymmetri-driften; dr = endring pga nudge.
|
-- 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.
|
local signtestAirborne = false -- for runMode-cleanup: hold hover hvis luftbaaren, ellers alt av
|
||||||
-- 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 function runSigntest()
|
local function runSigntest()
|
||||||
local s, est = cfg.signtest, Estimator.new(cfg)
|
local s, est = cfg.signtest, Estimator.new(cfg)
|
||||||
local h0 = sensors.readAltitude(); if not h0 then print("No altitude sensor."); return end
|
local log = Logger.new("signtest.log", "# signtest: t, phase, fore, aft, height, pitch, vVel, pitchRate")
|
||||||
print("SIGN TEST. CTRL+T for emergency stop."); print("1) Lifting airborne..."); sleep(0.5)
|
signtestAirborne = false
|
||||||
local liftLevel, airborne, tLift = math.min(cfg.maxSignal, s.liftLevel), false, now()
|
local h0 = sensors.readAltitude()
|
||||||
while now() - tLift < s.liftTimeout do
|
if not h0 then print("No altitude sensor."); return false end
|
||||||
driveValves(liftLevel, liftLevel)
|
local t0 = now()
|
||||||
local h = sensors.readAltitude() or h0
|
local hover = nil -- laeres i trappa (1/4), trimmes videre av fart-holdet
|
||||||
est:update(now(), h, sensors.readPitch() or 0)
|
local levelDiff = 0 -- treg nivellering under nedstigning (naar fortegnet er kjent)
|
||||||
term.clear(); term.setCursorPos(1, 1); print("SIGN TEST: lifting...")
|
|
||||||
print(string.format(" height %.2f (+%.2f) vVel %.2f", h, h - h0, est.verticalVelocity))
|
local function tick(fore, aft, phase)
|
||||||
if h - h0 > s.airborneRise then airborne = true; break end
|
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)
|
sleep(cfg.dt)
|
||||||
end
|
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 function hud(step, title, lines)
|
||||||
local r0 = holdTilRateStable(est, liftLevel, liftLevel, h0, "baseline (balanced)", s.baseMinHold, s.baseMaxWait)
|
term.clear(); term.setCursorPos(1, 1)
|
||||||
-- nudge (mer front) -- HOLD til loftet (~5 s) har uttrykt seg OG raten er stabil
|
print("==== SIGN TEST " .. step .. " ====")
|
||||||
local r1, settled = holdTilRateStable(est, liftLevel + s.nudge, math.max(0, liftLevel - s.nudge),
|
print(title)
|
||||||
h0, "nudge (more front)", s.nudgeMinHold, s.nudgeMaxWait)
|
for _, l in ipairs(lines or {}) do print(l) end
|
||||||
allOff()
|
print("")
|
||||||
local dr = r1 - r0 -- endring i stabilisert pitch-RATE pga mer front = robust, lag-tolerant signal
|
print(string.format("height +%.1f pitch %+.1f vVel %+.2f",
|
||||||
|
est.altitude - h0, est.pitch, est.verticalVelocity))
|
||||||
term.clear(); term.setCursorPos(1, 1); print("==== SIGN TEST RESULT ====")
|
print("CTRL+T = abort (holds lift, no freefall)")
|
||||||
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))
|
|
||||||
end
|
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()
|
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
|
end
|
||||||
|
|
||||||
-- ============================ WIRING SETUP ============================
|
-- ============================ WIRING SETUP ============================
|
||||||
@@ -245,13 +365,16 @@ local function runWiring()
|
|||||||
else
|
else
|
||||||
local level = math.min(cfg.maxSignal, 8)
|
local level = math.min(cfg.maxSignal, 8)
|
||||||
for _, side in ipairs(linkSides) do
|
for _, side in ipairs(linkSides) do
|
||||||
|
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 ====")
|
term.clear(); term.setCursorPos(1, 1); print("==== WIRING SETUP: pulsing ====")
|
||||||
print(string.format("Pulsing '%s' at %d -- WATCH THE BURNERS...", side, level))
|
print(string.format("Pulsing '%s' at %d -- WATCH THE BURNERS...", side, level))
|
||||||
redstone.setAnalogOutput(side, level); sleep(3.0); redstone.setAnalogOutput(side, 0)
|
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))
|
write(string.format("'%s' fired: [f]ront / [a]ft / [n]one / [r]=again: ", side))
|
||||||
local a = read()
|
local a = read():lower()
|
||||||
if a == "f" or a == "F" then fore = side
|
if a == "f" then fore = side; break
|
||||||
elseif a == "a" or a == "A" then aft = side end
|
elseif a == "a" then aft = side; break
|
||||||
|
elseif a ~= "r" then break end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
allSidesOff()
|
allSidesOff()
|
||||||
@@ -268,6 +391,7 @@ local function runWiring()
|
|||||||
print(""); print(">>> Saved to sides.txt and applied.")
|
print(""); print(">>> Saved to sides.txt and applied.")
|
||||||
end
|
end
|
||||||
print(""); print("[Enter]"); read()
|
print(""); print("[Enter]"); read()
|
||||||
|
return fore ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================ MANUAL (Lag 2) ============================
|
-- ============================ MANUAL (Lag 2) ============================
|
||||||
@@ -378,12 +502,13 @@ local function runFly()
|
|||||||
if not fs.exists(CALIB_FILE) then
|
if not fs.exists(CALIB_FILE) then
|
||||||
term.clear(); term.setCursorPos(1, 1)
|
term.clear(); term.setCursorPos(1, 1)
|
||||||
print("No profile for this ship yet.")
|
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("")
|
||||||
print("Then run FLY -- pitch & altitude self-learn from there.")
|
print("Run the setup wizard first:")
|
||||||
print("(Full 'cal' is optional/legacy; controllers no longer need lambda/g.)")
|
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
|
print(""); print("[Enter]"); read(); return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -500,15 +625,65 @@ local function runFly()
|
|||||||
end
|
end
|
||||||
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) ============================
|
-- ============================ STANDBY (pilot-fjernstart) ============================
|
||||||
-- Lytter etter ENGAGE fra pilot-konsollen og starter fly. Kringkaster IDLE-heartbeat slik at
|
-- 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
|
-- konsollen vet at maskinrommet er online og klart. Default etter boot; [S] aapner setup-wizarden.
|
||||||
-- piloten kan starte/stoppe fly selv: stabilizeV2 standby
|
|
||||||
local function awaitCmd(secs)
|
local function awaitCmd(secs)
|
||||||
local timer = os.startTimer(secs)
|
local timer = os.startTimer(secs)
|
||||||
while true do
|
while true do
|
||||||
local e1, e2, e3, e4 = os.pullEvent()
|
local e1, e2, e3, e4 = os.pullEvent()
|
||||||
if e1 == "timer" and e2 == timer then return nil
|
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
|
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 == "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
|
if e3.cmd == "set" then applySetting(e3.key, e3.value, true) end -- juster innstillinger i standby
|
||||||
@@ -519,40 +694,72 @@ end
|
|||||||
|
|
||||||
local function runStandby()
|
local function runStandby()
|
||||||
link.open()
|
link.open()
|
||||||
print("STANDBY -- waiting for ENGAGE from pilot console. CTRL+T for menu.")
|
|
||||||
while true do
|
while true do
|
||||||
local h = sensors.readAltitude() or 0
|
local h = sensors.readAltitude() or 0
|
||||||
local p = sensors.readPitch() 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,
|
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 })
|
climbSpeed = cfg.vMaxUp, holdBand = cfg.altDeadband, maxPower = cfg.maxSignal })
|
||||||
term.clear(); term.setCursorPos(1, 1)
|
term.clear(); term.setCursorPos(1, 1)
|
||||||
print("==== STANDBY (pilot remote) ====")
|
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(string.format("Height %.1f Pitch %+.1f", h, p))
|
||||||
print("Waiting for ENGAGE from the pilot console...")
|
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)
|
local cmd = awaitCmd(0.5)
|
||||||
if cmd == "engage" then
|
if cmd == "setup" then
|
||||||
if fs.exists(CALIB_FILE) then
|
runSetup()
|
||||||
|
elseif cmd == "engage" then
|
||||||
|
if flyable then
|
||||||
runFly() -- blokkerer til pilot STOP / FAULT / CTRL+T
|
runFly() -- blokkerer til pilot STOP / FAULT / CTRL+T
|
||||||
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear) -- hold hover etter stop
|
local c = loadCalib(); driveValves(c.uHoverFront, c.uHoverRear) -- hold hover etter stop
|
||||||
else
|
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)
|
sleep(2.0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
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 ============================
|
-- ============================ PROFIL / MENY ============================
|
||||||
local function showProfile()
|
local function showProfile()
|
||||||
local c = loadCalib()
|
local c = loadCalib()
|
||||||
print("==== PROFILE ====")
|
print("==== PROFILE ====")
|
||||||
print(string.format("uHoverFront %.2f uHoverRear %.2f", c.uHoverFront, c.uHoverRear))
|
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("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),
|
print(string.format("wiring: fore=%s aft=%s%s", cfg.foreSide, tostring(cfg.aftSide),
|
||||||
fs.exists(SIDES_FILE) and "" or " (config default)"))
|
fs.exists(SIDES_FILE) and "" or " (config default)"))
|
||||||
print("calib.txt: " .. (fs.exists(CALIB_FILE) and "saved" or "none (defaults)"))
|
print("calib.txt: " .. (fs.exists(CALIB_FILE) and "saved" or "none (defaults)"))
|
||||||
@@ -563,36 +770,47 @@ end
|
|||||||
local function menu()
|
local function menu()
|
||||||
while true do
|
while true do
|
||||||
term.clear(); term.setCursorPos(1, 1)
|
term.clear(); term.setCursorPos(1, 1)
|
||||||
print("===== AIRSHIP AUTOPILOT v3 =====\n")
|
print("===== AIRSHIP AUTOPILOT v3 =====")
|
||||||
print(" 1) Monitor (Layer 1)")
|
print(" Setup")
|
||||||
print(" 2) Sign test")
|
print(" 1) Setup wizard (wiring + sign test)")
|
||||||
print(" 3) Manual drive (Layer 2)")
|
print(" 2) Wiring only 3) Sign test only")
|
||||||
print(" 4) Calibrate (Layer 3)")
|
print(" Fly")
|
||||||
print(" 5) Observe open-loop (Layer 4)")
|
print(" 4) FLY / hold 5) Standby (pilot)")
|
||||||
print(" 6) FLY / hold (Layer 5-8)")
|
print(" 6) Set target altitude")
|
||||||
print(" 7) Set target altitude")
|
print(" Tools")
|
||||||
print(" 8) Show / reset profile")
|
print(" 7) Monitor 8) Manual 9) Full calibrate")
|
||||||
print(" 0) Wiring setup (front/aft sides)")
|
print(" o) Observe p) Profile/reset q) Quit")
|
||||||
print(" 9) Exit\n")
|
print("")
|
||||||
print(sensors.present() and "Sensors: OK" or "Sensors: MISSING - check wiring")
|
local prof = profileComplete() and "Profile: OK"
|
||||||
print(fs.exists(CALIB_FILE) and "Calib: saved" or "Calib: none (will auto-cal on fly)")
|
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(string.format("Wiring: fore=%s aft=%s", cfg.foreSide, tostring(cfg.aftSide)))
|
||||||
print(""); write("Choice: ")
|
write("Choice: ")
|
||||||
local k = read()
|
local k = read():lower()
|
||||||
if k == "1" then return "monitor" elseif k == "2" then return "signtest"
|
if k == "1" then return "setup"
|
||||||
elseif k == "3" then return "manual" elseif k == "4" then return "cal"
|
elseif k == "2" then return "wiring"
|
||||||
elseif k == "5" then return "observe" elseif k == "6" then return "fly"
|
elseif k == "3" then return "signtest"
|
||||||
elseif k == "0" then return "wiring"
|
elseif k == "4" then return "fly"
|
||||||
elseif k == "7" then
|
elseif k == "5" then return "standby"
|
||||||
|
elseif k == "6" then
|
||||||
write("Target (empty = auto): "); local s = read()
|
write("Target (empty = auto): "); local s = read()
|
||||||
if s == "" then if fs.exists(TARGET_FILE) then fs.delete(TARGET_FILE) end; print("Target cleared.")
|
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
|
elseif tonumber(s) then saveTarget(tonumber(s)); print("Target = " .. s) else print("Invalid number.") end
|
||||||
print("[Enter]"); read()
|
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()
|
term.clear(); term.setCursorPos(1, 1); showProfile()
|
||||||
print("\nR = reset calib, Enter = back"); local a = read()
|
print("\nR = reset SHIP PROFILE (new ship), Enter = back"); local a = read():lower()
|
||||||
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
|
if a == "r" then
|
||||||
elseif k == "9" then return "exit" end
|
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
|
||||||
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.
|
-- kjor EN modus + opprydding. Returnerer (ok, err) saa menyloopen kan avslutte ved CTRL+T.
|
||||||
local function runMode(mode, a2, a3)
|
local function runMode(mode, a2, a3)
|
||||||
local ok, err = pcall(function()
|
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 == "signtest" then runSigntest()
|
||||||
elseif mode == "wiring" then runWiring()
|
elseif mode == "wiring" then runWiring()
|
||||||
elseif mode == "manual" then runManual(a2, a3)
|
elseif mode == "manual" then runManual(a2, a3)
|
||||||
elseif mode == "cal" then runCal()
|
elseif mode == "cal" then runCal()
|
||||||
elseif mode == "observe" then runObserve()
|
elseif mode == "observe" then runObserve()
|
||||||
elseif mode == "fly" then runFly()
|
elseif mode == "fly" then runFly()
|
||||||
elseif mode == "standby" then runStandby()
|
else print("Unknown mode: " .. tostring(mode))
|
||||||
else print("Unknown mode: " .. tostring(mode)) end
|
print("Valid: setup wiring signtest fly standby monitor manual cal observe boot") end
|
||||||
end)
|
end)
|
||||||
-- opprydding: aktive driv-modus -> hold hover (aldri etterlat drivende uten beskjed);
|
-- opprydding: aktive driv-modus -> hold hover (aldri etterlat drivende uten beskjed);
|
||||||
-- monitor styrer ingenting (la utgangene staa).
|
-- monitor styrer ingenting (la utgangene staa).
|
||||||
if mode == "signtest" or mode == "manual" or mode == "wiring" then
|
if mode == "manual" or mode == "wiring" then
|
||||||
allOff()
|
allOff()
|
||||||
elseif mode == "cal" or mode == "fly" or mode == "observe" or mode == "standby" then
|
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)
|
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
|
end
|
||||||
if not ok and err ~= "Terminated" then print("ERROR: " .. tostring(err)); print("[Enter]"); read() end
|
if not ok and err ~= "Terminated" then print("ERROR: " .. tostring(err)); print("[Enter]"); read() end
|
||||||
return ok, err
|
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/altitude.lua
|
||||||
autopilot/calibration.lua
|
autopilot/calibration.lua
|
||||||
autopilot/config.lua
|
autopilot/config.lua
|
||||||
@@ -11,17 +12,6 @@ autopilot/pitch.lua
|
|||||||
autopilot/safety.lua
|
autopilot/safety.lua
|
||||||
autopilot/sensors.lua
|
autopilot/sensors.lua
|
||||||
autopilot/utils.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
|
||||||
|
stabilizev2.lua
|
||||||
startup.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/.
|
-- alias: videresender til 'ap' (beholdt for gammel muskelhukommelse).
|
||||||
-- Videresender argumenter:
|
-- VIKTIG: begge case-variantene (stabilizeV2/stabilizev2) har IDENTISK innhold,
|
||||||
-- stabilizeV2 -> meny
|
-- saa et case-insensitivt filsystem (Windows-hostet server) kan aldri gi rekursjon.
|
||||||
-- stabilizeV2 monitor -> Lag 1
|
|
||||||
-- stabilizeV2 signtest -> §13.2
|
|
||||||
-- stabilizeV2 manual 4 3 -> Lag 2 (fore=4, aft=3)
|
|
||||||
local args = { ... }
|
local args = { ... }
|
||||||
local unpack = table.unpack or unpack
|
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).
|
-- alias: videresender til 'ap' (beholdt for gammel muskelhukommelse).
|
||||||
-- Forwards all args to the real launcher so either case works.
|
-- VIKTIG: begge case-variantene (stabilizeV2/stabilizev2) har IDENTISK innhold,
|
||||||
|
-- saa et case-insensitivt filsystem (Windows-hostet server) kan aldri gi rekursjon.
|
||||||
local args = { ... }
|
local args = { ... }
|
||||||
local unpack = table.unpack or unpack
|
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
|
-- Maskinrom-PC: boot -> setup-wizard-prompt hvis skipet mangler profil, ellers standby
|
||||||
-- fra pilot-konsollen. CTRL+T -> kjor "stabilizeV2" for menyen (wiring/sign test/setup).
|
-- (lytter etter ENGAGE fra pilot-konsollen). CTRL+T og kjor "ap" for menyen.
|
||||||
shell.run("stabilizeV2", "standby")
|
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 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
|
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
|
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("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 }
|
lines[#lines + 1] = { string.format("PITCH %+.1f deg", s.pitch or 0), pcolor }
|
||||||
if inFlight then
|
if inFlight then
|
||||||
|
|||||||
126
install.lua
126
install.lua
@@ -1,19 +1,35 @@
|
|||||||
-- install.lua -- role-based installer for the Create: Aeronautics airship autopilot.
|
-- install.lua -- ETT entry-point for hele autopiloten. Samme kommando paa begge PC-er:
|
||||||
-- 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.
|
|
||||||
--
|
--
|
||||||
-- 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
|
||||||
-- 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:
|
-- Roles:
|
||||||
-- autopilot machine-room PC: full v3 observer autopilot (boots to standby)
|
-- autopilot machine-room PC: full v3 observer autopilot (setup wizard / standby)
|
||||||
-- cockpit pilot-house PC: control console
|
-- 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 BASE = "https://gt.zyon.no/Stian/airship-autopilot/raw/branch/main"
|
||||||
local ROLES = { "autopilot", "cockpit" }
|
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_<role>)
|
||||||
|
local LEGACY = {
|
||||||
|
autopilot = { "stabilize.lua", "diag.lua", "probe.lua", "scan.lua", "scan.txt",
|
||||||
|
"pid_gains.txt", "cockpit.lua", "pilot_startup.lua" },
|
||||||
|
cockpit = {},
|
||||||
|
}
|
||||||
|
|
||||||
local args = { ... }
|
local args = { ... }
|
||||||
|
|
||||||
-- ---- helpers ---------------------------------------------------------------
|
-- ---- helpers ---------------------------------------------------------------
|
||||||
@@ -41,9 +57,17 @@ local function writeFile(path, data)
|
|||||||
f.close()
|
f.close()
|
||||||
end
|
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()
|
local function chooseRole()
|
||||||
print("Select role to install:")
|
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("> ")
|
write("> ")
|
||||||
local sel = read()
|
local sel = read()
|
||||||
local n = tonumber(sel)
|
local n = tonumber(sel)
|
||||||
@@ -52,6 +76,18 @@ local function chooseRole()
|
|||||||
die("Unknown role: " .. tostring(sel))
|
die("Unknown role: " .. tostring(sel))
|
||||||
end
|
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 -------------------------------------------------------------
|
-- ---- preflight -------------------------------------------------------------
|
||||||
|
|
||||||
if not http then
|
if not http then
|
||||||
@@ -61,12 +97,23 @@ if not http then
|
|||||||
end
|
end
|
||||||
|
|
||||||
local role = args[1]
|
local role = args[1]
|
||||||
if not role then
|
if role then
|
||||||
role = chooseRole()
|
|
||||||
else
|
|
||||||
local ok = false
|
local ok = false
|
||||||
for _, r in ipairs(ROLES) do if r == role then ok = true end end
|
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
|
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
|
end
|
||||||
|
|
||||||
-- ---- fetch manifest --------------------------------------------------------
|
-- ---- fetch manifest --------------------------------------------------------
|
||||||
@@ -108,8 +155,57 @@ if #fail > 0 then
|
|||||||
die("Install incomplete -- re-run after fixing the errors above.")
|
die("Install incomplete -- re-run after fixing the errors above.")
|
||||||
end
|
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)
|
term.setTextColor(colors.green)
|
||||||
print("Done. Reboot to auto-start (startup.lua).")
|
print("Done. (auto-starts on every reboot)")
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
write("Reboot now? [y/N] ")
|
print("")
|
||||||
if read():lower() == "y" then os.reboot() end
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user