Files
Inventory-Manager-CC/inventoryWebBridge.lua

301 lines
9.1 KiB
Lua

-- 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()