Files
airship-autopilot/files/cockpit/cockpit.lua
Sterister 54963e5b42 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>
2026-07-23 23:21:54 +02:00

172 lines
7.8 KiB
Lua

-- 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 (wizard on AP)", s.ready and C.ok or C.warn } end
lines[#lines + 1] = { string.format("ALT %.1f > %.0f", s.height or 0, s.target or 0), C.val }
lines[#lines + 1] = { string.format("PITCH %+.1f deg", s.pitch or 0), pcolor }
if inFlight then
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