333 lines
11 KiB
Lua
333 lines
11 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.
|
|
--
|
|
-- Uses cc-platform-core for shared infrastructure (config, HTTP, modem).
|
|
-- Service-specific logic (command dispatch, state forwarding) remains here.
|
|
|
|
local WebBridge = require('platform.webbridge')
|
|
local Channels = require('platform.channels')
|
|
|
|
-------------------------------------------------
|
|
-- Configuration (via platform)
|
|
-------------------------------------------------
|
|
|
|
local _baseDir = fs.getDir(shell.getRunningProgram())
|
|
|
|
local config, configSource = WebBridge.loadConfig({
|
|
serverUrl = "http://localhost",
|
|
pollInterval = 0.5,
|
|
stateInterval = 1,
|
|
apiKey = nil,
|
|
}, {
|
|
"usr/config/inventory-manager/.webbridge_config",
|
|
fs.combine(_baseDir, ".webbridge_config"),
|
|
})
|
|
|
|
local SERVER_URL = config.serverUrl
|
|
local POLL_INTERVAL = config.pollInterval
|
|
local STATE_INTERVAL = config.stateInterval
|
|
local API_KEY = config.apiKey
|
|
|
|
if configSource then
|
|
print("[CONFIG] Loaded from " .. configSource)
|
|
end
|
|
|
|
-- Channels from platform registry (matches inventoryManager.lua)
|
|
local BROADCAST_CHANNEL = Channels.get('inventory.broadcast')
|
|
local ORDER_CHANNEL = Channels.get('inventory.order')
|
|
local BRIDGE_REPLY_CHANNEL = Channels.get('inventory.bridge')
|
|
|
|
-------------------------------------------------
|
|
-- State
|
|
-------------------------------------------------
|
|
|
|
local latestState = nil -- last broadcast from master
|
|
local modem = nil
|
|
local modemName = nil
|
|
local running = true
|
|
|
|
-------------------------------------------------
|
|
-- HTTP helpers (thin wrappers around platform)
|
|
-------------------------------------------------
|
|
|
|
local function httpPost(path, body)
|
|
local result, err = WebBridge.httpPost(SERVER_URL .. path, body,
|
|
WebBridge.authHeaders(API_KEY))
|
|
if not result and err then
|
|
print(string.format("[ERR] HTTP POST %s: %s", path, tostring(err)))
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function httpGet(path)
|
|
local rawBody, err = WebBridge.httpGet(SERVER_URL .. path,
|
|
WebBridge.authHeaders(API_KEY))
|
|
if not rawBody then
|
|
if err then
|
|
print(string.format("[ERR] HTTP GET %s: %s", path, tostring(err)))
|
|
end
|
|
return nil
|
|
end
|
|
return textutils.unserialiseJSON(rawBody)
|
|
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("")
|
|
|
|
-- Config already loaded at require-time via WebBridge.loadConfig above
|
|
|
|
modem, modemName = WebBridge.findModem()
|
|
if modem then
|
|
WebBridge.openChannels(modem,
|
|
{ 'inventory.broadcast', 'inventory.bridge' })
|
|
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()
|