Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- energy_client_centered.lua
- -- Client: receive energy table from server and display a vertically-centered dashboard
- -- Author: ChatGPT & Beboy
- -- Updated: clamp input/output >=0 & use clamped net for history
- local CHANNEL = 42
- local REFRESH_INTERVAL = 1 -- seconds (UI redraw)
- local MAX_HISTORY_SECONDS = 600 -- keep up to 10 minutes history
- local AVG_FOR_TIME_SEC = 30 -- averaging window for Full/Empty estimate
- local TICKS_PER_SECOND = 20 -- convert RF/tick -> RF/sec
- local monitor = peripheral.find("monitor")
- local modem = peripheral.find("modem")
- if not modem then error("No modem found (place a wireless or ender modem).") end
- if not monitor then error("No monitor found (place a monitor block).") end
- local function safeSetTextScale(s)
- if type(monitor.setTextScale) == "function" then pcall(monitor.setTextScale, s); return true end
- return false
- end
- local function getSize() if type(monitor.getSize) == "function" then return monitor.getSize() end return 51,19 end
- local function safeClear() if type(monitor.clear) == "function" then monitor.clear() end end
- local function safeSetBackground(c) if type(monitor.setBackgroundColor) == "function" then monitor.setBackgroundColor(c) end end
- local function safeSetTextColor(c) if type(monitor.setTextColor) == "function" then monitor.setTextColor(c) end end
- local function safeSetCursor(x,y) if type(monitor.setCursorPos) == "function" then monitor.setCursorPos(x,y) end end
- local function safeWrite(s) if type(monitor.write) == "function" then monitor.write(s) end end
- if type(modem.open) == "function" then pcall(modem.open, CHANNEL) end
- -- history (net per tick samples)
- local history = {}
- local function addHistorySample(net)
- table.insert(history, tonumber(net) or 0)
- while #history > MAX_HISTORY_SECONDS do table.remove(history, 1) end
- end
- local function avgOverSeconds(seconds)
- if seconds <= 0 or #history == 0 then return 0 end
- local n = math.min(seconds, #history)
- local sum = 0
- for i = #history - n + 1, #history do sum = sum + (history[i] or 0) end
- if n == 0 then return 0 end
- return sum / n
- end
- local function formatRF(n)
- n = tonumber(n) or 0
- local sign = n < 0 and "-" or ""
- local absn = math.abs(n)
- if absn >= 1e12 then return string.format("%s%.2fT", sign, absn/1e12) end
- if absn >= 1e9 then return string.format("%s%.2fG", sign, absn/1e9) end
- if absn >= 1e6 then return string.format("%s%.2fM", sign, absn/1e6) end
- if absn >= 1e3 then return string.format("%s%.1fk", sign, absn/1e3) end
- return string.format("%s%d", sign, math.floor(absn + 0.5))
- end
- local function formatTime(seconds)
- if not seconds or seconds ~= seconds or seconds == math.huge or seconds <= 0 then return "N/A" end
- local days = math.floor(seconds / 86400)
- local hours = math.floor((seconds % 86400) / 3600)
- local minutes = math.floor((seconds % 3600) / 60)
- local s = math.floor(seconds % 60)
- if days > 0 then return string.format("%dd %dh %dm", days, hours, minutes) end
- if hours > 0 then return string.format("%dh %dm", hours, minutes) end
- if minutes > 0 then return string.format("%dm %ds", minutes, s) end
- return string.format("%ds", s)
- end
- local function clamp(v,a,b) if v < a then return a end if v > b then return b end return v end
- local function getBarColor(p) if p >= 80 then return colors.green end if p >= 30 then return colors.yellow end return colors.red end
- local function getIOColor(v) if v > 0 then return colors.green end if v < 0 then return colors.red end return colors.orange end
- -- layout constants
- local CONTENT_LINES = 17
- local function chooseBestTextScale(required_lines)
- local scales = {1, 0.75, 0.5}
- local chosen = nil
- for _, s in ipairs(scales) do
- if safeSetTextScale(s) then
- local w,h = getSize()
- if h >= required_lines then chosen = s; break end
- end
- end
- if not chosen then safeSetTextScale(scales[#scales]) end
- return getSize()
- end
- local function drawVerticalBar(leftX, topY, height, width, stored, max)
- local pct = (max>0) and clamp(math.floor(stored/max*100+0.5),0,100) or 0
- local filled = math.floor((pct/100)*height + 0.5)
- for col = 0,width-1 do
- for row = 0,height-1 do
- local drawY = topY + (height - row - 1)
- safeSetCursor(leftX+col, drawY)
- local bg = (row < filled) and getBarColor(pct) or colors.gray
- safeSetBackground(bg)
- safeWrite(" ")
- end
- end
- local txt = (pct>=100 and "FULL") or (pct<=0 and "EMPTY") or string.format("%3d%%", pct)
- local txtColor = (pct>=100 and colors.green) or (pct<=0 and colors.red) or getBarColor(pct)
- local textX = leftX + math.floor((width - #txt) / 2)
- local textY = topY
- safeSetCursor(textX, textY)
- safeSetBackground(colors.black)
- safeSetTextColor(txtColor)
- safeWrite(txt)
- safeSetTextColor(colors.white)
- safeSetBackground(colors.black)
- end
- local function writeLine(label, value, valueColor, x, y, colWidth)
- safeSetCursor(x, y)
- safeSetTextColor(colors.white)
- local lab = tostring(label) .. ": "
- safeWrite(lab)
- local valueStr = tostring(value)
- local vlen = #valueStr
- local valX = x + colWidth - vlen
- if valX <= x + #lab then valX = x + #lab + 1 end
- safeSetCursor(valX, y)
- safeSetTextColor(valueColor)
- safeWrite(valueStr)
- safeSetTextColor(colors.white)
- end
- -- initialize screen
- local screenW, screenH = chooseBestTextScale(2 + CONTENT_LINES)
- safeSetBackground(colors.black)
- safeClear()
- local barWidth = math.max(2, math.floor(screenW * 0.18))
- local barLeft = 2
- local title = "Energy Monitor by ChatGPT and Beboy"
- local titleX = math.floor((screenW - #title) / 2)
- if titleX < 1 then titleX = 1 end
- safeSetCursor(titleX, 1)
- safeSetTextColor(colors.lightGray)
- safeWrite(title)
- safeSetTextColor(colors.white)
- local latestMessage = nil
- local lastMessageTime = 0
- local sampleTimer = os.startTimer(1)
- local refreshTimer = os.startTimer(REFRESH_INTERVAL)
- addHistorySample(0)
- -- MAIN LOOP
- while true do
- local ev = { os.pullEvent() }
- if ev[1] == "modem_message" then
- local ch = ev[3]; local msg = ev[5]
- if ch == CHANNEL and type(msg) == "table" then
- -- normalize and clamp input/output to >= 0
- local raw_input = tonumber(msg.input) or tonumber(msg.inputPerTick) or 0
- local raw_output = tonumber(msg.output) or 0
- local input = (raw_input < 0) and 0 or raw_input
- local output = (raw_output < 0) and 0 or raw_output
- local stored = tonumber(msg.stored) or 0
- local maxv = tonumber(msg.max) or 0
- local net = input - output -- recompute net from clamped values
- latestMessage = {
- stored = stored,
- max = maxv,
- input = input,
- output = output,
- net = net,
- percent = (maxv > 0) and (stored / maxv * 100) or 0
- }
- lastMessageTime = os.time()
- end
- elseif ev[1] == "timer" then
- local tid = ev[2]
- if tid == sampleTimer then
- -- push clamped net to history (per tick)
- local sampleNet = (latestMessage and latestMessage.net) or 0
- addHistorySample(sampleNet)
- sampleTimer = os.startTimer(1)
- elseif tid == refreshTimer then
- -- compute centered vertical start
- local areaTop = 3
- local areaHeight = screenH - (areaTop - 1)
- local contentHeight = CONTENT_LINES
- local startY = areaTop
- if areaHeight > contentHeight then startY = areaTop + math.floor((areaHeight - contentHeight) / 2) end
- if startY < areaTop then startY = areaTop end
- safeClear()
- safeSetCursor(titleX, 1)
- safeSetTextColor(colors.lightGray)
- safeWrite(title)
- safeSetTextColor(colors.white)
- local stored = (latestMessage and latestMessage.stored) or 0
- local maxv = (latestMessage and latestMessage.max) or 0
- local input = (latestMessage and latestMessage.input) or 0
- local output = (latestMessage and latestMessage.output) or 0
- local net = (latestMessage and latestMessage.net) or (input - output)
- local percent = (maxv > 0) and (stored / maxv * 100) or 0
- percent = clamp(math.floor(percent + 0.5), 0, 100)
- local barTopY = startY
- local barHeight = contentHeight
- if barTopY + barHeight - 1 > screenH then barHeight = math.max(1, screenH - barTopY + 1) end
- drawVerticalBar(barLeft, barTopY, barHeight, barWidth, stored, maxv)
- local rx = barLeft + barWidth + 2
- local colW = screenW - rx - 1
- if colW < 12 then colW = 12 end
- local r = barTopY
- -- Capacity above Stored, no blank line between them
- writeLine("Capacity", formatRF(maxv), colors.blue, rx, r, colW)
- r = r + 1
- writeLine("Stored", formatRF(stored), getBarColor(percent), rx, r, colW)
- r = r + 1
- -- small blank line
- r = r + 1
- -- time estimate based on avg30 from history (avg in RF/tick -> convert to RF/sec)
- local avg30 = avgOverSeconds(30)
- local timeLabel = "Stable"
- local timeColor = colors.orange
- local timeStr = "N/A"
- if avg30 and math.abs(avg30) > 1e-9 then
- local rfPerSec = avg30 * TICKS_PER_SECOND
- if avg30 > 0 then
- timeLabel = "Full In"
- local rfToFill = maxv - stored
- if rfToFill > 0 then
- timeStr = formatTime(rfToFill / math.abs(rfPerSec))
- end
- timeColor = colors.green
- else
- timeLabel = "Empty In"
- if stored > 0 then
- timeStr = formatTime(stored / math.abs(rfPerSec))
- end
- timeColor = colors.red
- end
- end
- writeLine(timeLabel, timeStr, timeColor, rx, r, colW)
- r = r + 1
- r = r + 1
- -- Input / Output / Net
- writeLine("Input (RF/t)", formatRF(input), colors.green, rx, r, colW); r = r + 1
- r = r + 1
- writeLine("Output (RF/t)", formatRF(output), colors.red, rx, r, colW); r = r + 1
- r = r + 1
- local netLabel = "Net Stable"; local netColor = colors.orange
- if net > 0 then netLabel = "Net Gain"; netColor = colors.green
- elseif net < 0 then netLabel = "Net Loss"; netColor = colors.red end
- writeLine(netLabel .. " (RF/t)", formatRF(net), netColor, rx, r, colW)
- r = r + 1
- r = r + 1
- -- I/O History (10s,30s,1m,2m,5m,10m)
- local intervals = { {"10s",10}, {"30s",30}, {"1m",60}, {"2m",120}, {"5m",300}, {"10m",600} }
- for _, d in ipairs(intervals) do
- local label, sec = d[1], d[2]
- local val = avgOverSeconds(sec)
- writeLine("I/O (" .. label .. ")", formatRF(val), getIOColor(val), rx, r, colW)
- r = r + 1
- end
- -- offline indicator
- if (not lastMessageTime) or (os.time() - lastMessageTime > REFRESH_INTERVAL * 4) then
- safeSetCursor(rx, math.min(screenH, r + 1))
- safeSetTextColor(colors.red)
- safeWrite("STATUS: OFFLINE")
- safeSetTextColor(colors.white)
- end
- refreshTimer = os.startTimer(REFRESH_INTERVAL)
- end
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment