Files
airship-autopilot/files/autopilot/cockpit.lua
Stian 4cd2573f56 Airship autopilot: role-based wget installer for server
Mirrors the live CC autopilot (computer 0) and cockpit (computer 4) from the
Modda wii world. install.lua fetches each role's manifest over HTTP so the
system installs on any server regardless of computer id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:53:41 +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 (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