scarlabeboy

energy monitoring FINAL (display)

Sep 6th, 2025 (edited)
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 11.62 KB | None | 0 0
  1. -- energy_client_centered.lua
  2. -- Client: receive energy table from server and display a vertically-centered dashboard
  3. -- Author: ChatGPT & Beboy
  4. -- Updated: clamp input/output >=0 & use clamped net for history
  5.  
  6. local CHANNEL = 42
  7. local REFRESH_INTERVAL = 1       -- seconds (UI redraw)
  8. local MAX_HISTORY_SECONDS = 600  -- keep up to 10 minutes history
  9. local AVG_FOR_TIME_SEC = 30      -- averaging window for Full/Empty estimate
  10. local TICKS_PER_SECOND = 20      -- convert RF/tick -> RF/sec
  11.  
  12. local monitor = peripheral.find("monitor")
  13. local modem = peripheral.find("modem")
  14. if not modem then error("No modem found (place a wireless or ender modem).") end
  15. if not monitor then error("No monitor found (place a monitor block).") end
  16.  
  17. local function safeSetTextScale(s)
  18.     if type(monitor.setTextScale) == "function" then pcall(monitor.setTextScale, s); return true end
  19.     return false
  20. end
  21. local function getSize() if type(monitor.getSize) == "function" then return monitor.getSize() end return 51,19 end
  22. local function safeClear() if type(monitor.clear) == "function" then monitor.clear() end end
  23. local function safeSetBackground(c) if type(monitor.setBackgroundColor) == "function" then monitor.setBackgroundColor(c) end end
  24. local function safeSetTextColor(c) if type(monitor.setTextColor) == "function" then monitor.setTextColor(c) end end
  25. local function safeSetCursor(x,y) if type(monitor.setCursorPos) == "function" then monitor.setCursorPos(x,y) end end
  26. local function safeWrite(s) if type(monitor.write) == "function" then monitor.write(s) end end
  27.  
  28. if type(modem.open) == "function" then pcall(modem.open, CHANNEL) end
  29.  
  30. -- history (net per tick samples)
  31. local history = {}
  32. local function addHistorySample(net)
  33.     table.insert(history, tonumber(net) or 0)
  34.     while #history > MAX_HISTORY_SECONDS do table.remove(history, 1) end
  35. end
  36. local function avgOverSeconds(seconds)
  37.     if seconds <= 0 or #history == 0 then return 0 end
  38.     local n = math.min(seconds, #history)
  39.     local sum = 0
  40.     for i = #history - n + 1, #history do sum = sum + (history[i] or 0) end
  41.     if n == 0 then return 0 end
  42.     return sum / n
  43. end
  44.  
  45. local function formatRF(n)
  46.     n = tonumber(n) or 0
  47.     local sign = n < 0 and "-" or ""
  48.     local absn = math.abs(n)
  49.     if absn >= 1e12 then return string.format("%s%.2fT", sign, absn/1e12) end
  50.     if absn >= 1e9  then return string.format("%s%.2fG", sign, absn/1e9) end
  51.     if absn >= 1e6  then return string.format("%s%.2fM", sign, absn/1e6) end
  52.     if absn >= 1e3  then return string.format("%s%.1fk", sign, absn/1e3) end
  53.     return string.format("%s%d", sign, math.floor(absn + 0.5))
  54. end
  55.  
  56. local function formatTime(seconds)
  57.     if not seconds or seconds ~= seconds or seconds == math.huge or seconds <= 0 then return "N/A" end
  58.     local days = math.floor(seconds / 86400)
  59.     local hours = math.floor((seconds % 86400) / 3600)
  60.     local minutes = math.floor((seconds % 3600) / 60)
  61.     local s = math.floor(seconds % 60)
  62.     if days > 0 then return string.format("%dd %dh %dm", days, hours, minutes) end
  63.     if hours > 0 then return string.format("%dh %dm", hours, minutes) end
  64.     if minutes > 0 then return string.format("%dm %ds", minutes, s) end
  65.     return string.format("%ds", s)
  66. end
  67.  
  68. local function clamp(v,a,b) if v < a then return a end if v > b then return b end return v end
  69. local function getBarColor(p) if p >= 80 then return colors.green end if p >= 30 then return colors.yellow end return colors.red end
  70. local function getIOColor(v) if v > 0 then return colors.green end if v < 0 then return colors.red end return colors.orange end
  71.  
  72. -- layout constants
  73. local CONTENT_LINES = 17
  74.  
  75. local function chooseBestTextScale(required_lines)
  76.     local scales = {1, 0.75, 0.5}
  77.     local chosen = nil
  78.     for _, s in ipairs(scales) do
  79.         if safeSetTextScale(s) then
  80.             local w,h = getSize()
  81.             if h >= required_lines then chosen = s; break end
  82.         end
  83.     end
  84.     if not chosen then safeSetTextScale(scales[#scales]) end
  85.     return getSize()
  86. end
  87.  
  88. local function drawVerticalBar(leftX, topY, height, width, stored, max)
  89.     local pct = (max>0) and clamp(math.floor(stored/max*100+0.5),0,100) or 0
  90.     local filled = math.floor((pct/100)*height + 0.5)
  91.     for col = 0,width-1 do
  92.         for row = 0,height-1 do
  93.             local drawY = topY + (height - row - 1)
  94.             safeSetCursor(leftX+col, drawY)
  95.             local bg = (row < filled) and getBarColor(pct) or colors.gray
  96.             safeSetBackground(bg)
  97.             safeWrite(" ")
  98.         end
  99.     end
  100.     local txt = (pct>=100 and "FULL") or (pct<=0 and "EMPTY") or string.format("%3d%%", pct)
  101.     local txtColor = (pct>=100 and colors.green) or (pct<=0 and colors.red) or getBarColor(pct)
  102.     local textX = leftX + math.floor((width - #txt) / 2)
  103.     local textY = topY
  104.     safeSetCursor(textX, textY)
  105.     safeSetBackground(colors.black)
  106.     safeSetTextColor(txtColor)
  107.     safeWrite(txt)
  108.     safeSetTextColor(colors.white)
  109.     safeSetBackground(colors.black)
  110. end
  111.  
  112. local function writeLine(label, value, valueColor, x, y, colWidth)
  113.     safeSetCursor(x, y)
  114.     safeSetTextColor(colors.white)
  115.     local lab = tostring(label) .. ": "
  116.     safeWrite(lab)
  117.     local valueStr = tostring(value)
  118.     local vlen = #valueStr
  119.     local valX = x + colWidth - vlen
  120.     if valX <= x + #lab then valX = x + #lab + 1 end
  121.     safeSetCursor(valX, y)
  122.     safeSetTextColor(valueColor)
  123.     safeWrite(valueStr)
  124.     safeSetTextColor(colors.white)
  125. end
  126.  
  127. -- initialize screen
  128. local screenW, screenH = chooseBestTextScale(2 + CONTENT_LINES)
  129. safeSetBackground(colors.black)
  130. safeClear()
  131.  
  132. local barWidth = math.max(2, math.floor(screenW * 0.18))
  133. local barLeft = 2
  134. local title = "Energy Monitor by ChatGPT and Beboy"
  135. local titleX = math.floor((screenW - #title) / 2)
  136. if titleX < 1 then titleX = 1 end
  137.  
  138. safeSetCursor(titleX, 1)
  139. safeSetTextColor(colors.lightGray)
  140. safeWrite(title)
  141. safeSetTextColor(colors.white)
  142.  
  143. local latestMessage = nil
  144. local lastMessageTime = 0
  145. local sampleTimer = os.startTimer(1)
  146. local refreshTimer = os.startTimer(REFRESH_INTERVAL)
  147.  
  148. addHistorySample(0)
  149.  
  150. -- MAIN LOOP
  151. while true do
  152.     local ev = { os.pullEvent() }
  153.  
  154.     if ev[1] == "modem_message" then
  155.         local ch = ev[3]; local msg = ev[5]
  156.         if ch == CHANNEL and type(msg) == "table" then
  157.             -- normalize and clamp input/output to >= 0
  158.             local raw_input = tonumber(msg.input) or tonumber(msg.inputPerTick) or 0
  159.             local raw_output = tonumber(msg.output) or 0
  160.             local input = (raw_input < 0) and 0 or raw_input
  161.             local output = (raw_output < 0) and 0 or raw_output
  162.             local stored = tonumber(msg.stored) or 0
  163.             local maxv = tonumber(msg.max) or 0
  164.             local net = input - output -- recompute net from clamped values
  165.  
  166.             latestMessage = {
  167.                 stored = stored,
  168.                 max = maxv,
  169.                 input = input,
  170.                 output = output,
  171.                 net = net,
  172.                 percent = (maxv > 0) and (stored / maxv * 100) or 0
  173.             }
  174.             lastMessageTime = os.time()
  175.         end
  176.  
  177.     elseif ev[1] == "timer" then
  178.         local tid = ev[2]
  179.         if tid == sampleTimer then
  180.             -- push clamped net to history (per tick)
  181.             local sampleNet = (latestMessage and latestMessage.net) or 0
  182.             addHistorySample(sampleNet)
  183.             sampleTimer = os.startTimer(1)
  184.  
  185.         elseif tid == refreshTimer then
  186.             -- compute centered vertical start
  187.             local areaTop = 3
  188.             local areaHeight = screenH - (areaTop - 1)
  189.             local contentHeight = CONTENT_LINES
  190.             local startY = areaTop
  191.             if areaHeight > contentHeight then startY = areaTop + math.floor((areaHeight - contentHeight) / 2) end
  192.             if startY < areaTop then startY = areaTop end
  193.  
  194.             safeClear()
  195.             safeSetCursor(titleX, 1)
  196.             safeSetTextColor(colors.lightGray)
  197.             safeWrite(title)
  198.             safeSetTextColor(colors.white)
  199.  
  200.             local stored = (latestMessage and latestMessage.stored) or 0
  201.             local maxv = (latestMessage and latestMessage.max) or 0
  202.             local input = (latestMessage and latestMessage.input) or 0
  203.             local output = (latestMessage and latestMessage.output) or 0
  204.             local net = (latestMessage and latestMessage.net) or (input - output)
  205.             local percent = (maxv > 0) and (stored / maxv * 100) or 0
  206.             percent = clamp(math.floor(percent + 0.5), 0, 100)
  207.  
  208.             local barTopY = startY
  209.             local barHeight = contentHeight
  210.             if barTopY + barHeight - 1 > screenH then barHeight = math.max(1, screenH - barTopY + 1) end
  211.  
  212.             drawVerticalBar(barLeft, barTopY, barHeight, barWidth, stored, maxv)
  213.  
  214.             local rx = barLeft + barWidth + 2
  215.             local colW = screenW - rx - 1
  216.             if colW < 12 then colW = 12 end
  217.             local r = barTopY
  218.  
  219.             -- Capacity above Stored, no blank line between them
  220.             writeLine("Capacity", formatRF(maxv), colors.blue, rx, r, colW)
  221.             r = r + 1
  222.             writeLine("Stored", formatRF(stored), getBarColor(percent), rx, r, colW)
  223.             r = r + 1
  224.             -- small blank line
  225.             r = r + 1
  226.  
  227.             -- time estimate based on avg30 from history (avg in RF/tick -> convert to RF/sec)
  228.             local avg30 = avgOverSeconds(30)
  229.             local timeLabel = "Stable"
  230.             local timeColor = colors.orange
  231.             local timeStr = "N/A"
  232.  
  233.             if avg30 and math.abs(avg30) > 1e-9 then
  234.                 local rfPerSec = avg30 * TICKS_PER_SECOND
  235.  
  236.                 if avg30 > 0 then
  237.                     timeLabel = "Full In"
  238.                     local rfToFill = maxv - stored
  239.                     if rfToFill > 0 then
  240.                         timeStr = formatTime(rfToFill / math.abs(rfPerSec))
  241.                     end
  242.                     timeColor = colors.green
  243.                 else
  244.                     timeLabel = "Empty In"
  245.                     if stored > 0 then
  246.                         timeStr = formatTime(stored / math.abs(rfPerSec))
  247.                     end
  248.                     timeColor = colors.red
  249.                 end
  250.             end
  251.  
  252.             writeLine(timeLabel, timeStr, timeColor, rx, r, colW)
  253.             r = r + 1
  254.             r = r + 1
  255.  
  256.             -- Input / Output / Net
  257.             writeLine("Input (RF/t)", formatRF(input), colors.green, rx, r, colW); r = r + 1
  258.             r = r + 1
  259.             writeLine("Output (RF/t)", formatRF(output), colors.red, rx, r, colW); r = r + 1
  260.             r = r + 1
  261.             local netLabel = "Net Stable"; local netColor = colors.orange
  262.             if net > 0 then netLabel = "Net Gain"; netColor = colors.green
  263.             elseif net < 0 then netLabel = "Net Loss"; netColor = colors.red end
  264.             writeLine(netLabel .. " (RF/t)", formatRF(net), netColor, rx, r, colW)
  265.             r = r + 1
  266.             r = r + 1
  267.  
  268.             -- I/O History (10s,30s,1m,2m,5m,10m)
  269.             local intervals = { {"10s",10}, {"30s",30}, {"1m",60}, {"2m",120}, {"5m",300}, {"10m",600} }
  270.             for _, d in ipairs(intervals) do
  271.                 local label, sec = d[1], d[2]
  272.                 local val = avgOverSeconds(sec)
  273.                 writeLine("I/O (" .. label .. ")", formatRF(val), getIOColor(val), rx, r, colW)
  274.                 r = r + 1
  275.             end
  276.  
  277.             -- offline indicator
  278.             if (not lastMessageTime) or (os.time() - lastMessageTime > REFRESH_INTERVAL * 4) then
  279.                 safeSetCursor(rx, math.min(screenH, r + 1))
  280.                 safeSetTextColor(colors.red)
  281.                 safeWrite("STATUS: OFFLINE")
  282.                 safeSetTextColor(colors.white)
  283.             end
  284.  
  285.             refreshTimer = os.startTimer(REFRESH_INTERVAL)
  286.         end
  287.     end
  288. end
  289.  
Advertisement
Add Comment
Please, Sign In to add comment