2026-07-23 12:53:41 +02:00
|
|
|
-- 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
|
2026-07-23 23:21:54 +02:00
|
|
|
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
|
2026-07-23 12:53:41 +02:00
|
|
|
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
|
|
|
|
|
|
2026-07-23 23:45:11 +02:00
|
|
|
-- terminal-diagnostikk: monitoren viser pilot-UI, terminalen FORKLARER link-status --
|
|
|
|
|
-- "OFFLINE" skal aldri vaere et mysterium (vanligste aarsak: AP-PCen er ikke i standby/fly)
|
|
|
|
|
local function drawTerm()
|
|
|
|
|
term.clear(); term.setCursorPos(1, 1)
|
|
|
|
|
print("==== COCKPIT CONSOLE ====")
|
|
|
|
|
print(string.format("Monitor %dx%d modem: open", W, H))
|
|
|
|
|
print("")
|
|
|
|
|
if online() then
|
|
|
|
|
local s = status
|
|
|
|
|
print(string.format("Link: ONLINE (heartbeat %.1fs ago)", now() - lastRx))
|
|
|
|
|
print("State: " .. tostring(s.state)
|
|
|
|
|
.. ((s.reason and s.reason ~= "") and (" (" .. tostring(s.reason) .. ")") or ""))
|
|
|
|
|
print(string.format("Height %.1f -> %.0f Pitch %+.1f", s.height or 0, s.target or 0, s.pitch or 0))
|
|
|
|
|
print(s.ready == false and "AP needs setup (wizard on the AP PC)" or "Control via the monitor buttons.")
|
|
|
|
|
else
|
|
|
|
|
print("Link: OFFLINE -- no heartbeat from the")
|
|
|
|
|
print("machine-room PC. Checklist over there:")
|
|
|
|
|
print(" 1) It must be in STANDBY or FLY mode")
|
|
|
|
|
print(" (menu/setup/shell do NOT broadcast)")
|
|
|
|
|
print(" 2) It needs a modem attached -- its")
|
|
|
|
|
print(" standby screen shows 'Link: modem OK'")
|
|
|
|
|
print(" 3) Wireless out of range? Use wired")
|
|
|
|
|
print(" modems + network cable instead.")
|
|
|
|
|
end
|
|
|
|
|
print("")
|
|
|
|
|
print("CTRL+T = quit")
|
|
|
|
|
end
|
|
|
|
|
|
2026-07-23 12:53:41 +02:00
|
|
|
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()
|
2026-07-23 23:45:11 +02:00
|
|
|
drawTerm()
|
2026-07-23 12:53:41 +02:00
|
|
|
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
|
2026-07-23 23:45:11 +02:00
|
|
|
redraw = os.startTimer(0.5); draw(); drawTerm()
|
2026-07-23 12:53:41 +02:00
|
|
|
end
|
|
|
|
|
end
|