Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- ==========================================
- -- UU DASHBOARD v4 - Persistent + Scrap Stats + Fast Warmup
- -- ==========================================
- os.loadAPI("ocs/apis/sensor")
- local SAVE_FILE = "/uu_save.dat"
- local LONG_WINDOW = 6 -- 6 x 5s = 30s pro Long-Kerze
- local UUDashboard = {}
- UUDashboard.__index = UUDashboard
- function UUDashboard:new(sensorSide, monitorSide)
- local obj = {
- sen = sensor.wrap(sensorSide),
- mon = peripheral.wrap(monitorSide),
- w = 0, h = 0,
- pollInterval = 0.5,
- sampleWindow = 5,
- smoothPoints = 24,
- -- UU Stats
- totalProduced = 0,
- targetTotal = 10000,
- currentStock = 0,
- currentPerMin = 0,
- smoothPerMin = 0,
- minPerMin = 0,
- maxPerMin = 0,
- historyShort = {},
- historyLong = {},
- longAccum = 0,
- longCount = 0,
- -- Scrap Stats
- currentScrap = 0,
- scrapInPerMin = 0,
- smoothScrapPerMin = 0,
- minScrapPerMin = 0,
- maxScrapPerMin = 0,
- scrapHistShort = {},
- scrapHistLong = {},
- scrapLongAccum = 0,
- scrapLongCount = 0,
- -- Scrap Pulse
- scrapOutputSide = "bottom",
- scrapMin = 64,
- pulseDuration = 5,
- pulseCooldown = 30,
- pulseTimer = 0,
- cooldownTimer = 999,
- isPulsing = false,
- -- UI
- zoomMode = 1,
- lastProgress= 0,
- statusText = "Lade...",
- spinner = {"|", "/", "-", "\\"},
- spinIndex = 1,
- }
- setmetatable(obj, UUDashboard)
- return obj
- end
- -- ============================================================
- -- PERSISTENZ
- -- ============================================================
- function UUDashboard:save()
- local data = {
- totalProduced = self.totalProduced,
- historyShort = self.historyShort,
- historyLong = self.historyLong,
- scrapHistShort = self.scrapHistShort,
- scrapHistLong = self.scrapHistLong,
- smoothPerMin = self.smoothPerMin,
- smoothScrapPerMin= self.smoothScrapPerMin,
- minPerMin = self.minPerMin,
- maxPerMin = self.maxPerMin,
- minScrapPerMin = self.minScrapPerMin,
- maxScrapPerMin = self.maxScrapPerMin,
- }
- local f = fs.open(SAVE_FILE, "w")
- if f then
- f.write(textutils.serialize(data))
- f.close()
- end
- end
- function UUDashboard:load()
- if not fs.exists(SAVE_FILE) then return end
- local f = fs.open(SAVE_FILE, "r")
- if not f then return end
- local raw = f.readAll()
- f.close()
- local ok, data = pcall(textutils.unserialize, raw)
- if not ok or type(data) ~= "table" then return end
- self.totalProduced = data.totalProduced or 0
- self.historyShort = data.historyShort or {}
- self.historyLong = data.historyLong or {}
- self.scrapHistShort = data.scrapHistShort or {}
- self.scrapHistLong = data.scrapHistLong or {}
- self.smoothPerMin = data.smoothPerMin or 0
- self.smoothScrapPerMin = data.smoothScrapPerMin or 0
- self.minPerMin = data.minPerMin or 0
- self.maxPerMin = data.maxPerMin or 0
- self.minScrapPerMin = data.minScrapPerMin or 0
- self.maxScrapPerMin = data.maxScrapPerMin or 0
- end
- -- ============================================================
- -- HILFSFUNKTIONEN
- -- ============================================================
- function UUDashboard:fmt(n)
- n = math.floor(tonumber(n) or 0)
- local s = tostring(n)
- local sign = ""
- if string.sub(s,1,1) == "-" then sign="-"; s=string.sub(s,2) end
- local out = ""
- while string.len(s) > 3 do
- out = "." .. string.sub(s,-3) .. out
- s = string.sub(s,1,-4)
- end
- return sign .. s .. out
- end
- function UUDashboard:avgTable(tbl, count)
- if #tbl == 0 then return 0 end
- local from = math.max(1, #tbl - count + 1)
- local sum, c = 0, 0
- for i = from, #tbl do sum = sum + tbl[i]; c = c + 1 end
- return c > 0 and (sum/c) or 0
- end
- function UUDashboard:minTable(tbl, count)
- if #tbl == 0 then return 0 end
- local from = math.max(1, #tbl - count + 1)
- local m = tbl[from]
- for i = from+1, #tbl do if tbl[i] < m then m = tbl[i] end end
- return m
- end
- function UUDashboard:maxTable(tbl, count)
- if #tbl == 0 then return 0 end
- local from = math.max(1, #tbl - count + 1)
- local m = tbl[from]
- for i = from+1, #tbl do if tbl[i] > m then m = tbl[i] end end
- return m
- end
- function UUDashboard:getPeak(arr)
- local m = 1
- for i=1,#arr do if arr[i]>m then m=arr[i] end end
- return m
- end
- function UUDashboard:pushTable(tbl, v, limit)
- table.insert(tbl, v)
- while #tbl > limit do table.remove(tbl, 1) end
- end
- function UUDashboard:etaText()
- if self.totalProduced >= self.targetTotal then return "Fertig!" end
- if self.smoothPerMin <= 0 then return "Warte..." end
- local remaining = self.targetTotal - self.totalProduced
- local mins = math.ceil(remaining / self.smoothPerMin)
- local h = math.floor(mins/60); local m = mins%60
- if h > 0 then return h.."h "..m.."m" else return m.."m" end
- end
- -- ============================================================
- -- ZEICHEN ENGINE (Zero-Flicker)
- -- ============================================================
- function UUDashboard:tc(col)
- pcall(function() self.mon.setTextColor(col) end)
- end
- function UUDashboard:bc(col)
- pcall(function() self.mon.setBackgroundColor(col) end)
- end
- function UUDashboard:fillRect(x1,y1,x2,y2,bg)
- self:bc(bg)
- local line = string.rep(" ", x2-x1+1)
- for y=y1,y2 do self.mon.setCursorPos(x1,y); self.mon.write(line) end
- end
- function UUDashboard:writeAt(x,y,text,tc,bg)
- local pad = self.w - x + 1
- if pad < 1 then return end
- self.mon.setCursorPos(x,y)
- self:bc(bg); self:tc(tc)
- self.mon.write(string.sub(tostring(text),1,pad))
- end
- -- Kachel: Label oben links, Wert unten rechts, Unterzeile fuer Stats
- function UUDashboard:drawTile(x, y, tw, label, value, minV, avgV, maxV, lblCol, valCol)
- self:fillRect(x, y, x+tw-1, y+2, colors.gray)
- -- Label
- self:writeAt(x+1, y, label, lblCol, colors.gray)
- -- Hauptwert (gross, rechts-buendig)
- local valStr = type(value)=="number" and self:fmt(value) or tostring(value)
- local vx = x + tw - string.len(valStr) - 1
- if vx < x+1 then vx = x+1 end
- self:writeAt(vx, y+1, valStr, valCol, colors.gray)
- -- Min / Avg / Max (kleine Zeile)
- local stats = "v"..self:fmt(minV).." ~"..self:fmt(avgV).." ^"..self:fmt(maxV)
- -- kuerzen wenn zu lang
- if string.len(stats) > tw-2 then
- stats = "~"..self:fmt(avgV)
- end
- self:writeAt(x+1, y+2, stats, colors.lightGray, colors.gray)
- end
- -- Einfache 2-Zeilen-Kachel ohne Stats (fuer Werte ohne Verlauf)
- function UUDashboard:drawTile2(x, y, tw, label, value, sub, lblCol, valCol, subCol)
- self:fillRect(x, y, x+tw-1, y+2, colors.gray)
- self:writeAt(x+1, y, label, lblCol, colors.gray)
- local valStr = type(value)=="number" and self:fmt(value) or tostring(value)
- local vx = x + tw - string.len(valStr) - 1
- if vx < x+1 then vx = x+1 end
- self:writeAt(vx, y+1, valStr, valCol, colors.gray)
- local sub2 = tostring(sub)
- self:writeAt(x+1, y+2, sub2, subCol or colors.lightGray, colors.gray)
- end
- function UUDashboard:drawHeader()
- local text = " UU-MATTER & SCRAP HUB "
- local lp = math.max(0, math.floor((self.w - string.len(text))/2))
- local rp = math.max(0, self.w - lp - string.len(text))
- self.mon.setCursorPos(1,1)
- self:bc(colors.cyan); self:tc(colors.white)
- self.mon.write(string.rep(" ",lp)..text..string.rep(" ",rp))
- end
- function UUDashboard:drawProgress(y, ratio)
- local width = self.w - 2
- if width < 1 then return end
- local fill = math.floor(width * ratio + 0.5)
- local empty = width - fill
- self.mon.setCursorPos(2, y)
- if fill > 0 then self:bc(colors.lime); self.mon.write(string.rep(" ",fill)) end
- if empty> 0 then self:bc(colors.lightGray); self.mon.write(string.rep(" ",empty)) end
- self:bc(colors.black)
- end
- function UUDashboard:drawGraph(x1,y1,x2,y2, arr, smoothVal)
- local gw = x2-x1+1; local gh = y2-y1+1
- if gw < 10 or gh < 4 then return end
- local plotX1=x1+2; local plotY1=y1+1
- local plotX2=x2-1; local plotY2=y2-1
- local peak = self:getPeak(arr)
- if peak < 1 then peak = 1 end
- local avg = smoothVal or 0
- local plotH = plotY2 - plotY1
- if plotH < 1 then plotH = 1 end
- local avgY = plotY2 - math.floor((avg/peak)*plotH + 0.5)
- local capacity = plotX2 - plotX1 + 1
- local startIndex = math.max(1, #arr - capacity + 1)
- for y = plotY1, plotY2 do
- self.mon.setCursorPos(plotX1, y)
- for i = 1, capacity do
- local val = arr[startIndex+i-1]
- local col = colors.black
- if val then
- local barH = math.floor((val/peak)*plotH + 0.5)
- local barTopY = plotY2 - barH
- if y >= barTopY then
- col = colors.blue
- if (startIndex+i-1) == #arr then col = colors.orange
- elseif val >= avg then col = colors.lightBlue end
- elseif y == avgY then col = colors.gray end
- elseif y == avgY then col = colors.gray end
- self:bc(col); self.mon.write(" ")
- end
- end
- self:bc(colors.black)
- self:writeAt(plotX1, plotY1, "max "..self:fmt(peak), colors.white, colors.black)
- self:writeAt(plotX1, plotY2, "0", colors.white, colors.black)
- local at = "avg "..self:fmt(math.floor(avg+0.5))
- self:writeAt(plotX2 - string.len(at)+1, plotY1, at, colors.lightGray, colors.black)
- end
- -- ============================================================
- -- SENSOR
- -- ============================================================
- function UUDashboard:getInventories()
- if not self.sen then return 0,0 end
- local targets = self.sen.getTargets()
- if not targets then return 0,0 end
- local tid = nil
- for id, info in pairs(targets) do
- if info.Name == "ME Wireless Access Point" then tid=id; break end
- end
- if not tid then self.statusText="Fehler: Kein AP"; return 0,0 end
- local det = self.sen.getTargetDetails(tid)
- local uu,sc = 0,0
- local uf,sf = false,false
- if det and det.Items then
- self.statusText = "Online"
- for _,item in pairs(det.Items) do
- local n = string.lower(tostring(item.Name))
- if string.find(n,"uu") then uu=item.Size; uf=true end
- if string.find(n,"scrap") then sc=item.Size; sf=true end
- if uf and sf then break end
- end
- end
- return uu, sc
- end
- -- ============================================================
- -- PULSE LOGIK
- -- ============================================================
- function UUDashboard:updatePulseLogic()
- if self.isPulsing then
- self.pulseTimer = self.pulseTimer + self.pollInterval
- if self.pulseTimer >= self.pulseDuration then
- self.isPulsing = false
- self.cooldownTimer = 0
- redstone.setOutput(self.scrapOutputSide, false)
- end
- else
- self.cooldownTimer = self.cooldownTimer + self.pollInterval
- if self.cooldownTimer >= self.pulseCooldown and self.currentScrap >= self.scrapMin then
- self.isPulsing = true
- self.pulseTimer = 0
- redstone.setOutput(self.scrapOutputSide, true)
- end
- end
- end
- -- ============================================================
- -- EVENT LOOP (Touch Support)
- -- ============================================================
- function UUDashboard:sleepWithTouch(duration)
- local tid = os.startTimer(duration)
- while true do
- local ev,p1 = os.pullEvent()
- if ev=="timer" and p1==tid then break
- elseif ev=="monitor_touch" then
- self.zoomMode = (self.zoomMode==1) and 2 or 1
- self:render(self.lastProgress, true)
- end
- end
- end
- -- ============================================================
- -- RENDER
- -- ============================================================
- function UUDashboard:render(progressRatio, drawFullGraph)
- self.lastProgress = progressRatio
- self:drawHeader()
- -- Kachel-Layout
- local tw = math.floor((self.w - 3) / 2)
- local t1X = 2
- local t2X = t1X + tw + 1
- -- Zeile A: Lager UU | Lager Scrap (y=2..4)
- self:drawTile2(t1X, 2, tw,
- "Lager UU", self.currentStock,
- "Prod: "..self:fmt(self.totalProduced),
- colors.lightGray, colors.white, colors.cyan)
- -- Scrap Pulse Status
- local pulseStr
- if self.isPulsing then
- pulseStr = "PUMPEN "..math.ceil(self.pulseDuration - self.pulseTimer).."s"
- elseif self.cooldownTimer < self.pulseCooldown then
- pulseStr = "WARTE "..math.ceil(self.pulseCooldown - self.cooldownTimer).."s"
- elseif self.currentScrap < self.scrapMin then
- pulseStr = "Scrap fehlt"
- else
- pulseStr = "BEREIT"
- end
- self:drawTile2(t2X, 2, tw,
- "Lager Scrap", self.currentScrap,
- pulseStr,
- colors.lightGray, colors.yellow,
- self.isPulsing and colors.lime or colors.orange)
- self:fillRect(1,5,self.w,5, colors.black)
- -- Zeile B: UU/Min Stats | Scrap In/Min Stats (y=6..8)
- self:drawTile(t1X, 6, tw,
- "UU/Min",
- math.floor(self.currentPerMin+0.5),
- self.minPerMin, math.floor(self.smoothPerMin+0.5), self.maxPerMin,
- colors.green, colors.lime)
- self:drawTile(t2X, 6, tw,
- "Scrap In/Min",
- math.floor(self.scrapInPerMin+0.5),
- self.minScrapPerMin, math.floor(self.smoothScrapPerMin+0.5), self.maxScrapPerMin,
- colors.orange, colors.yellow)
- self:fillRect(1,9,self.w,9, colors.black)
- -- Zeile C: ETA | Zoom-Info (y=10..12)
- local zoomLabel = (self.zoomMode==1) and "5s Kerzen [Touch=Zoom]" or "30s Kerzen [Touch=Zoom]"
- self:drawTile2(t1X, 10, tw,
- "ETA 10k UU", self:etaText(),
- "Avg UU: "..self:fmt(math.floor(self.smoothPerMin+0.5)).."/min",
- colors.lightBlue, colors.white, colors.lightGray)
- local spin = self.spinner[self.spinIndex]
- self:drawTile2(t2X, 10, tw,
- "Graph Zoom", zoomLabel,
- "Status: "..self.statusText.." "..spin,
- colors.cyan, colors.white, colors.gray)
- -- Progress Bar (y=13)
- self:fillRect(1,13,self.w,13, colors.black)
- self:drawProgress(13, progressRatio or 0)
- -- Graph (y=14 ... h-1)
- local graphTop = 14
- if self.h >= graphTop + 3 and drawFullGraph then
- local arr = (self.zoomMode==1) and self.historyShort or self.historyLong
- local smoothV = self.smoothPerMin
- self:drawGraph(1, graphTop, self.w, self.h-1, arr, smoothV)
- end
- -- Status Zeile (y=h)
- self:fillRect(1,self.h,self.w,self.h, colors.black)
- self:writeAt(2, self.h, "Status: "..self.statusText.." | "..pulseStr, colors.gray, colors.black)
- end
- -- ============================================================
- -- WARMUP: Graph sofort mit aktuellen Werten vorbelegen
- -- ============================================================
- function UUDashboard:warmup(graphCap)
- local warmVal = self.smoothPerMin > 0 and self.smoothPerMin or 0
- local warmScrap = self.smoothScrapPerMin > 0 and self.smoothScrapPerMin or 0
- -- Nur vorbelegen wenn der Verlauf leer ist (Erster Start, keine Save-Datei)
- if #self.historyShort == 0 then
- for i=1, math.min(graphCap, self.smoothPoints) do
- table.insert(self.historyShort, warmVal)
- end
- end
- if #self.historyLong == 0 then
- for i=1, math.min(graphCap, self.smoothPoints) do
- table.insert(self.historyLong, warmVal)
- end
- end
- if #self.scrapHistShort == 0 then
- for i=1, math.min(graphCap, self.smoothPoints) do
- table.insert(self.scrapHistShort, warmScrap)
- end
- end
- if #self.scrapHistLong == 0 then
- for i=1, math.min(graphCap, self.smoothPoints) do
- table.insert(self.scrapHistLong, warmScrap)
- end
- end
- end
- -- ============================================================
- -- MAIN LOOP
- -- ============================================================
- function UUDashboard:run()
- if not self.sen or not self.mon then
- print("Fehler: Sensor oder Monitor nicht gefunden!")
- return
- end
- pcall(function() self.mon.setTextScale(0.5) end)
- self.w, self.h = self.mon.getSize()
- self.mon.clear()
- -- Gespeicherten Stand laden
- self:load()
- redstone.setOutput(self.scrapOutputSide, false)
- self.currentStock, self.currentScrap = self:getInventories()
- local lastUU = self.currentStock
- local lastScrap = self.currentScrap
- local graphCap = self.w - 4
- if graphCap < 8 then graphCap = 8 end
- -- Graph sofort vorbelegen (Warmup)
- self:warmup(graphCap)
- -- Min/Max initialisieren wenn noch leer
- if self.minPerMin == 0 and self.smoothPerMin > 0 then
- self.minPerMin = self.smoothPerMin
- self.maxPerMin = self.smoothPerMin
- end
- if self.minScrapPerMin == 0 and self.smoothScrapPerMin > 0 then
- self.minScrapPerMin = self.smoothScrapPerMin
- self.maxScrapPerMin = self.smoothScrapPerMin
- end
- local pollsPerWindow = math.floor(self.sampleWindow / self.pollInterval)
- local saveCounter = 0
- self:render(0, true)
- while true do
- local uuProduced = 0
- local scrapProduced = 0
- for i = 1, pollsPerWindow do
- self:sleepWithTouch(self.pollInterval)
- local curUU, curScrap = self:getInventories()
- if curUU > lastUU then
- local d = curUU - lastUU
- uuProduced = uuProduced + d
- self.totalProduced = self.totalProduced + d
- end
- if curScrap > lastScrap then
- scrapProduced = scrapProduced + (curScrap - lastScrap)
- end
- lastUU = curUU
- lastScrap = curScrap
- self.currentStock = curUU
- self.currentScrap = curScrap
- self:updatePulseLogic()
- self.spinIndex = self.spinIndex + 1
- if self.spinIndex > #self.spinner then self.spinIndex = 1 end
- self:render(i / pollsPerWindow, false)
- end
- -- === 5s Kerze abschliessen ===
- -- UU
- self.currentPerMin = uuProduced * (60 / self.sampleWindow)
- self:pushTable(self.historyShort, self.currentPerMin, graphCap)
- self.smoothPerMin = self:avgTable(self.historyShort, self.smoothPoints)
- self.minPerMin = self:minTable(self.historyShort, self.smoothPoints)
- self.maxPerMin = self:maxTable(self.historyShort, self.smoothPoints)
- -- UU Long (30s = 6 x 5s)
- self.longAccum = self.longAccum + self.currentPerMin
- self.longCount = self.longCount + 1
- if self.longCount >= LONG_WINDOW then
- self:pushTable(self.historyLong, self.longAccum / LONG_WINDOW, graphCap)
- self.longAccum = 0
- self.longCount = 0
- end
- -- Scrap
- self.scrapInPerMin = scrapProduced * (60 / self.sampleWindow)
- self:pushTable(self.scrapHistShort, self.scrapInPerMin, graphCap)
- self.smoothScrapPerMin = self:avgTable(self.scrapHistShort, self.smoothPoints)
- self.minScrapPerMin = self:minTable(self.scrapHistShort, self.smoothPoints)
- self.maxScrapPerMin = self:maxTable(self.scrapHistShort, self.smoothPoints)
- -- Scrap Long
- self.scrapLongAccum = self.scrapLongAccum + self.scrapInPerMin
- self.scrapLongCount = self.scrapLongCount + 1
- if self.scrapLongCount >= LONG_WINDOW then
- self:pushTable(self.scrapHistLong, self.scrapLongAccum / LONG_WINDOW, graphCap)
- self.scrapLongAccum = 0
- self.scrapLongCount = 0
- end
- -- Alle 6 Kerzen (= 30s) speichern
- saveCounter = saveCounter + 1
- if saveCounter >= LONG_WINDOW then
- self:save()
- saveCounter = 0
- end
- self:render(1, true)
- end
- end
- -- ============================================================
- -- START
- -- ============================================================
- local app = UUDashboard:new("left", "bottom")
- app:run()
Advertisement