diff --git a/README.md b/README.md index 6a2fc9f..203b6e6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,23 @@ install Der Installer ist momentan ein Bootstrap. Hardwaretreiber und konkrete Anlagenlogik werden ergaenzt, sobald die Peripherie- und Methodennamen der gebauten Anlagen feststehen. +## Einfache Create-Boiler-Anzeige + +Fuer die erste Kraftwerksanzeige werden ein Monitor, pro Boiler ein Create-Speedometer, +ein Create-Stressometer an der Hauptwelle und optional pro Boiler ein Block Reader aus +Advanced Peripherals ueber Wired Modems mit dem Computer verbunden. Installation und Start: + +```lua +wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_boiler_display.lua create_boiler_display.lua +create_boiler_display +``` + +Das Programm zeigt pro Boiler Online-Status, RPM und - mit Block Reader - den Heizmodus. +Zusaetzlich zeigt es Kapazitaet, Verbrauch und Auslastung der Hauptwelle. Die Zuordnung erfolgt +nach den internen Peripheral-Namen; Speedometer und Block Reader sollten deshalb jeweils in der +Reihenfolge Boiler 1 bis 3 an das Wired Network angeschlossen werden. Jeder Block Reader muss +auf den Controller-Tank des zugeordneten Boiler-Multiblocks zeigen. + ## Struktur ```text @@ -34,4 +51,3 @@ docs/ Architektur- und Protokolldokumentation Die Zentrale fordert nur einen Zielzustand an. Lokale Controller fuehren sichere Start-/Stoppsequenzen aus und bestaetigen den tatsaechlichen Zustand. - diff --git a/create_boiler_display.lua b/create_boiler_display.lua new file mode 100644 index 0000000..878d95c --- /dev/null +++ b/create_boiler_display.lua @@ -0,0 +1,189 @@ +-- Create boiler status board for CC:Tweaked. +-- Hardware: +-- * one monitor +-- * one Create Speedometer per boiler +-- * one Create Stressometer on the main shaft +-- * optionally one Advanced Peripherals Block Reader per boiler controller +-- Everything must be connected to the computer, preferably using Wired Modems. + +local EXPECTED_BOILERS = 3 +local REFRESH_SECONDS = 1 +local MONITOR_SCALE = 0.5 +local SPEEDOMETER_TYPE = "Create_Speedometer" +local STRESSOMETER_TYPE = "Create_Stressometer" +local BLOCK_READER_TYPE = "block_reader" + +local monitor = peripheral.find("monitor") +if not monitor then + error("Kein Monitor gefunden. Monitor direkt anschliessen oder per Wired Modem verbinden.", 0) +end + +monitor.setTextScale(MONITOR_SCALE) +monitor.setCursorBlink(false) + +local function center(y, text, foreground, background) + local width = monitor.getSize() + text = tostring(text) + if #text > width then text = text:sub(1, width) end + local x = math.max(1, math.floor((width - #text) / 2) + 1) + monitor.setBackgroundColor(background or colors.black) + monitor.setTextColor(foreground or colors.white) + monitor.setCursorPos(1, y) + monitor.write((" "):rep(width)) + monitor.setCursorPos(x, y) + monitor.write(text) +end + +local function getPeripheralsByType(wantedType) + local names = {} + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == wantedType then + names[#names + 1] = name + end + end + table.sort(names) + return names +end + +local function getValueIgnoringCase(data, wantedKeys) + if type(data) ~= "table" then return nil end + for key, value in pairs(data) do + local lowered = tostring(key):lower() + for _, wanted in ipairs(wantedKeys) do + if lowered == wanted:lower() then return value end + end + end + return nil +end + +local function findBoilerData(data, visited) + if type(data) ~= "table" then return nil end + visited = visited or {} + if visited[data] then return nil end + visited[data] = true + + local boiler = getValueIgnoringCase(data, { "Boiler" }) + if type(boiler) == "table" then return boiler end + + local heat = getValueIgnoringCase(data, { "Heat", "activeHeat", "actualHeat" }) + local passive = getValueIgnoringCase(data, { "PassiveHeat", "passive" }) + if heat ~= nil or passive ~= nil then return data end + + for _, value in pairs(data) do + local found = findBoilerData(value, visited) + if found then return found end + end + return nil +end + +local function readHeatMode(readerName) + if not readerName then return "UNKNOWN" end + local ok, data = pcall(peripheral.call, readerName, "getBlockData") + if not ok then return "ERROR" end + + local boiler = findBoilerData(data) + if not boiler then return "UNKNOWN" end + + local passive = getValueIgnoringCase(boiler, { "PassiveHeat", "passive" }) + local heat = tonumber(getValueIgnoringCase(boiler, + { "Heat", "activeHeat", "actualHeat", "boilerLevel", "level" })) + + if passive == true or passive == 1 then return "CAMPFIRE" end + if heat and heat >= 18 then return "SUPERHEATED" end + if heat and heat > 0 then return "HEATED" end + return "COLD" +end + +local function readBoiler(name, readerName) + if not name then + return "MISSING", 0, "UNKNOWN", colors.red + end + + local ok, speed = pcall(peripheral.call, name, "getSpeed") + if not ok or type(speed) ~= "number" then + return "ERROR", 0, "UNKNOWN", colors.red + end + + speed = math.abs(speed) + if speed < 0.5 then + return "STANDBY", 0, readHeatMode(readerName), colors.orange + end + return "ONLINE", speed, readHeatMode(readerName), colors.lime +end + +local function formatInteger(value) + value = math.floor(math.abs(tonumber(value) or 0) + 0.5) + local digits = tostring(value) + local parts = {} + while #digits > 3 do + table.insert(parts, 1, digits:sub(-3)) + digits = digits:sub(1, -4) + end + table.insert(parts, 1, digits) + return table.concat(parts, ".") +end + +local function readMainShaft(stressometerName) + if not stressometerName then return nil, nil, nil end + local okCapacity, capacity = pcall(peripheral.call, + stressometerName, "getNetworkCapacity") + local okUsage, usage = pcall(peripheral.call, + stressometerName, "getNetworkStress") + if not okCapacity or not okUsage then return nil, nil, nil end + capacity = tonumber(capacity) or 0 + usage = tonumber(usage) or 0 + local load = capacity > 0 and usage / capacity * 100 or 0 + return capacity, usage, load +end + +local function render() + local _, height = monitor.getSize() + local speedometers = getPeripheralsByType(SPEEDOMETER_TYPE) + local stressometers = getPeripheralsByType(STRESSOMETER_TYPE) + local readers = getPeripheralsByType(BLOCK_READER_TYPE) + + monitor.setBackgroundColor(colors.black) + monitor.clear() + center(1, "CREATE KRAFTWERK", colors.cyan) + local firstRow = 3 + for index = 1, EXPECTED_BOILERS do + local state, speed, heatMode, color = readBoiler(speedometers[index], readers[index]) + local row = firstRow + index - 1 + if row <= height then + center(row, ("BOILER %d %-7s %5.1f RPM %s") + :format(index, state, speed, heatMode), color) + end + end + + local capacity, usage, load = readMainShaft(stressometers[1]) + if height >= 7 then center(7, "MAIN SHAFT", colors.cyan) end + if capacity then + if height >= 8 then center(8, "Capacity: " .. formatInteger(capacity) .. " SU", colors.white) end + if height >= 9 then center(9, "Usage: " .. formatInteger(usage) .. " SU", colors.white) end + if height >= 10 then + local loadText = ("Load: %.1f %%"):format(load):gsub("%.", ",") + center(10, loadText, load >= 90 and colors.red or load >= 70 and colors.orange or colors.lime) + end + else + if height >= 8 then center(8, "Stressometer nicht gefunden", colors.red) end + end + + if height >= 12 then + center(height, "Nur Anzeige - keine Steuerung", colors.gray) + end + + monitor.setTextColor(colors.white) + monitor.setBackgroundColor(colors.black) + term.setCursorPos(1, 1) +end + +while true do + local ok, reason = pcall(render) + if not ok then + monitor.setBackgroundColor(colors.black) + monitor.clear() + center(1, "ANZEIGEFEHLER", colors.red) + center(3, tostring(reason), colors.orange) + end + sleep(REFRESH_SECONDS) +end diff --git a/manifest.lua b/manifest.lua index a7f0f9c..43477a7 100644 --- a/manifest.lua +++ b/manifest.lua @@ -4,6 +4,7 @@ return { channel = "main", files = { "startup.lua", + "create_boiler_display.lua", "core/constants.lua", "core/log.lua", "core/protocol.lua", @@ -26,4 +27,3 @@ return { "config/default.lua", }, } -