-- 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" 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 if cfg.apiKey then API_KEY = cfg.apiKey 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 local API_KEY = nil -- optional API key for server auth ------------------------------------------------- -- 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" } if API_KEY then headers["Authorization"] = "Bearer " .. API_KEY end 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 headers = nil if API_KEY then headers = { ["Authorization"] = "Bearer " .. API_KEY } end local ok, result = pcall(function() local response = http.get(url, headers) 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", commandId = cmd.commandId, itemName = cmd.itemName, amount = cmd.amount, dropperName = cmd.dropperName, }) elseif action == "scan" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "scan", commandId = cmd.commandId, }) elseif action == "toggle_pause" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "toggle_pause", commandId = cmd.commandId, }) elseif action == "toggle_recipe" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "toggle_recipe", commandId = cmd.commandId, recipe = cmd.recipe, }) elseif action == "enable_all" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "enable_all", commandId = cmd.commandId, }) elseif action == "disable_all" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "disable_all", commandId = cmd.commandId, }) elseif action == "sort_barrel" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "sort_barrel", commandId = cmd.commandId, barrelName = cmd.barrelName, }) elseif action == "craft" then modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, { type = "craft", commandId = cmd.commandId, 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 lastProcessedId = 0 -- track highest processed command ID for dedup 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 local maxId = lastProcessedId -- Process each command, skipping already-processed ones for _, cmd in ipairs(result.commands) do local cmdId = cmd.id or 0 if cmdId > lastProcessedId then pcall(processCommand, cmd) if cmdId > maxId then maxId = cmdId end end end -- Acknowledge up to the highest processed ID if maxId > lastProcessedId then lastProcessedId = maxId httpPost("/api/bridge/commands/ack", { lastProcessedId = lastProcessedId }) end 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") if API_KEY then print("[OK] API key configured") else print("[WARN] No API key set (open access)") end 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()