diff --git a/inventoryWebBridge.lua b/inventoryWebBridge.lua new file mode 100644 index 0000000..3671270 --- /dev/null +++ b/inventoryWebBridge.lua @@ -0,0 +1,272 @@ +-- Inventory Manager Web Bridge +-- Runs on a CC:Tweaked computer with a modem and HTTP access. +-- Listens to the inventory master's broadcasts via modem and +-- forwards state to the web server via HTTP. +-- Also polls the web server for commands and sends them to the master. + +------------------------------------------------- +-- Configuration +------------------------------------------------- + +-- Web server URL (change to your Docker host IP/hostname) +local SERVER_URL = "http://localhost:4200" +local POLL_INTERVAL = 0.5 -- seconds between command polls +local STATE_INTERVAL = 1 -- seconds between state forwards + +-- Modem channels (must match inventoryManager.lua) +local BROADCAST_CHANNEL = 4200 +local ORDER_CHANNEL = 4201 + +------------------------------------------------- +-- Load config from file if present +------------------------------------------------- + +local CONFIG_FILE = ".webbridge_config" + +local function loadConfig() + if fs.exists(CONFIG_FILE) then + local f = fs.open(CONFIG_FILE, "r") + local data = f.readAll() + f.close() + local ok, cfg = pcall(textutils.unserialiseJSON, data) + if ok and cfg then + if cfg.serverUrl then SERVER_URL = cfg.serverUrl end + if cfg.pollInterval then POLL_INTERVAL = cfg.pollInterval end + if cfg.stateInterval then STATE_INTERVAL = cfg.stateInterval end + print("[CONFIG] Loaded from " .. CONFIG_FILE) + end + end +end + +------------------------------------------------- +-- State +------------------------------------------------- + +local latestState = nil -- last broadcast from master +local modem = nil +local modemName = nil +local running = true + +------------------------------------------------- +-- Find modem +------------------------------------------------- + +local function findModem() + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == "modem" then + modem = peripheral.wrap(name) + modemName = name + modem.open(BROADCAST_CHANNEL) + return true + end + end + return false +end + +------------------------------------------------- +-- HTTP helpers +------------------------------------------------- + +local function httpPost(path, body) + local url = SERVER_URL .. path + local data = textutils.serialiseJSON(body) + local headers = { ["Content-Type"] = "application/json" } + + local ok, err = pcall(function() + local response = http.post(url, data, headers) + if response then + local responseData = response.readAll() + response.close() + return responseData + end + end) + + if not ok then + -- Silent fail, will retry + return nil + end +end + +local function httpGet(path) + local url = SERVER_URL .. path + local ok, result = pcall(function() + local response = http.get(url) + if response then + local data = response.readAll() + response.close() + return textutils.unserialiseJSON(data) + end + end) + + if ok then + return result + end + return nil +end + +------------------------------------------------- +-- Forward state to web server +------------------------------------------------- + +local function forwardState() + if not latestState then return end + httpPost("/api/bridge/state", latestState) +end + +------------------------------------------------- +-- Process commands from web server +------------------------------------------------- + +local function processCommand(cmd) + if not modem then return end + + local action = cmd.action + if not action then return end + + print(string.format("[CMD] %s", action)) + + if action == "order" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "order", + itemName = cmd.itemName, + amount = cmd.amount, + dropperName = cmd.dropperName, + }) + elseif action == "scan" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "scan", + }) + elseif action == "toggle_pause" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "toggle_pause", + }) + elseif action == "toggle_recipe" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "toggle_recipe", + recipe = cmd.recipe, + }) + elseif action == "enable_all" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "enable_all", + }) + elseif action == "disable_all" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "disable_all", + }) + elseif action == "sort_barrel" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "sort_barrel", + barrelName = cmd.barrelName, + }) + elseif action == "craft" then + modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { + type = "craft", + recipeIdx = cmd.recipeIdx, + }) + else + print("[CMD] Unknown action: " .. tostring(action)) + end +end + +------------------------------------------------- +-- Tasks +------------------------------------------------- + +-- Task 1: Listen for modem broadcasts from master +local function modemListener() + while running do + local event, side, channel, replyChannel, message = os.pullEvent("modem_message") + if channel == BROADCAST_CHANNEL and type(message) == "table" then + if message.type == "state" then + latestState = message + end + end + end +end + +-- Task 2: Forward state to web server periodically +local function stateForwarder() + while running do + local ok, err = pcall(forwardState) + if not ok then + -- Connection error, will retry + end + sleep(STATE_INTERVAL) + end +end + +-- Task 3: Poll web server for commands +local function commandPoller() + while running do + local ok, err = pcall(function() + local result = httpGet("/api/bridge/commands") + if result and result.commands and #result.commands > 0 then + -- Process each command + for _, cmd in ipairs(result.commands) do + pcall(processCommand, cmd) + end + -- Acknowledge + httpPost("/api/bridge/commands/ack", {}) + end + end) + sleep(POLL_INTERVAL) + end +end + +-- Task 4: Heartbeat / connection status display +local function heartbeat() + local connected = false + while running do + local ok, result = pcall(function() + return httpGet("/api/health") + end) + local nowConnected = ok and result ~= nil + if nowConnected ~= connected then + connected = nowConnected + if connected then + print("[WEB] Connected to web server") + else + print("[WEB] Disconnected from web server") + end + end + sleep(5) + end +end + +------------------------------------------------- +-- Main +------------------------------------------------- + +local function main() + print("===================================") + print(" Inventory Manager - Web Bridge") + print("===================================") + print("") + + loadConfig() + + if findModem() then + print("[OK] Modem: " .. modemName) + else + print("[WARN] No modem found! Bridge needs a modem.") + print(" Attach a modem and restart.") + return + end + + print("[OK] Server URL: " .. SERVER_URL) + print("[OK] Poll interval: " .. POLL_INTERVAL .. "s") + print("[OK] State interval: " .. STATE_INTERVAL .. "s") + print("") + print("Bridge is running. Press Ctrl+T to stop.") + print("Listening for master broadcasts on ch " .. BROADCAST_CHANNEL) + print("") + + parallel.waitForAny( + modemListener, + stateForwarder, + commandPoller, + heartbeat + ) +end + +main()