Files
Inventory-Manager-CC/inventoryWebBridge.lua

382 lines
13 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
local BRIDGE_REPLY_CHANNEL = 4206 -- dedicated reply channel for bridge
-------------------------------------------------
-- Load config from file if present
-------------------------------------------------
local _baseDir = fs.getDir(shell.getRunningProgram())
local function _path(rel) return fs.combine(_baseDir, rel) end
-- Persistent config path: survives Opus package updates
local _PERSIST_DIR = "usr/config/inventory-manager"
local function _configPath(rel)
if fs.isDir(_PERSIST_DIR) or fs.isDir("packages/inventory-manager") then
if not fs.isDir(_PERSIST_DIR) then fs.makeDir(_PERSIST_DIR) end
return fs.combine(_PERSIST_DIR, rel)
end
return _path(rel)
end
local CONFIG_FILE = _configPath(".webbridge_config")
local API_KEY = nil -- optional API key for server auth
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
-------------------------------------------------
-- 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)
modem.open(BRIDGE_REPLY_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, result = pcall(function()
local response = http.post(url, data, headers)
if response then
local responseData = response.readAll()
response.close()
return responseData
end
end)
if ok then
return result
end
print(string.format("[ERR] HTTP POST %s: %s", path, tostring(result)))
return nil
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
print(string.format("[ERR] HTTP GET %s: %s", path, tostring(result)))
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, BRIDGE_REPLY_CHANNEL, {
type = "order",
commandId = cmd.commandId,
itemName = cmd.itemName,
amount = cmd.amount,
dropperName = cmd.dropperName,
})
elseif action == "scan" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "scan",
commandId = cmd.commandId,
})
elseif action == "toggle_pause" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "toggle_pause",
commandId = cmd.commandId,
})
elseif action == "toggle_recipe" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "toggle_recipe",
commandId = cmd.commandId,
recipe = cmd.recipe,
})
elseif action == "enable_all" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "enable_all",
commandId = cmd.commandId,
})
elseif action == "disable_all" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "disable_all",
commandId = cmd.commandId,
})
elseif action == "sort_barrel" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "sort_barrel",
commandId = cmd.commandId,
barrelName = cmd.barrelName,
})
elseif action == "craft" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "craft",
commandId = cmd.commandId,
recipeIdx = cmd.recipeIdx,
})
elseif action == "recursive_craft" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "recursive_craft",
commandId = cmd.commandId,
itemName = cmd.itemName,
count = cmd.count,
})
elseif action == "learn_crafting_recipe" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "learn_crafting_recipe",
commandId = cmd.commandId,
output = cmd.output,
count = cmd.count,
grid = cmd.grid,
})
elseif action == "learn_smelting_recipe" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "learn_smelting_recipe",
commandId = cmd.commandId,
input = cmd.input,
result = cmd.result,
furnaces = cmd.furnaces,
})
elseif action == "forget_recipe" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "forget_recipe",
commandId = cmd.commandId,
recipe = cmd.recipe,
})
elseif action == "sync_disabled_recipes" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "sync_disabled_recipes",
commandId = cmd.commandId,
disabledRecipes = cmd.disabledRecipes,
smeltingPaused = cmd.smeltingPaused,
})
elseif action == "reboot" then
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
type = "reboot",
commandId = cmd.commandId,
target = cmd.target or "all",
})
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
elseif channel == BRIDGE_REPLY_CHANNEL and type(message) == "table" then
-- Forward command results back to web server
local resultType = message.type
if resultType == "order_result" or resultType == "craft_result"
or resultType == "recursive_craft_result" or resultType == "find_item_result" then
local fwdOk, fwdErr = pcall(httpPost, "/api/bridge/result", {
action = resultType,
commandId = message.commandId,
success = message.success,
message = message.message,
error = message.error,
})
if not fwdOk then
print(string.format("[ERR] Forward result %s: %s", resultType, tostring(fwdErr)))
end
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
print(string.format("[ERR] State forward: %s", tostring(err)))
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
local cmdOk, cmdErr = pcall(processCommand, cmd)
if not cmdOk then
print(string.format("[ERR] Process cmd %s: %s", tostring(cmd.action), tostring(cmdErr)))
end
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)
if not ok then
print(string.format("[ERR] Command poll: %s", tostring(err)))
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()