From 5a83d89509ac97aa61237892231f0b755cc30788 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 29 Mar 2026 01:11:24 -0400 Subject: [PATCH] fix: implement reliable modem command delivery with acknowledgment and retry mechanism --- inventoryManager.lua | 70 +++++++++++++++++++++ inventoryWebBridge.lua | 136 ++++++++++++++++++++--------------------- 2 files changed, 136 insertions(+), 70 deletions(-) diff --git a/inventoryManager.lua b/inventoryManager.lua index adbf68e..5946818 100644 --- a/inventoryManager.lua +++ b/inventoryManager.lua @@ -708,6 +708,13 @@ local function main() state.needsRedraw = true state.smelterNeedsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "toggle_pause" then state.smeltingPaused = not state.smeltingPaused @@ -717,6 +724,13 @@ local function main() state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "toggle_recipe" and message.recipe then if state.disabledRecipes[message.recipe] then @@ -730,6 +744,13 @@ local function main() state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "enable_all" then state.disabledRecipes = {} @@ -739,6 +760,13 @@ local function main() state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "disable_all" then for inputName in pairs(cfg.SMELTABLE) do @@ -750,11 +778,25 @@ local function main() state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "sort_barrel" and message.barrelName then log.info("NET", "Sort barrel: %s", message.barrelName) pcall(ops.sortBarrel, message.barrelName) pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "register_droppers" and message.clientId and message.droppers then local cid = tostring(message.clientId) @@ -847,6 +889,13 @@ local function main() state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "learn_crafting_recipe" and message.output and message.count and message.grid then cfg.recipeBook.learnCraftingRecipe(message.output, message.count, message.grid) @@ -856,6 +905,13 @@ local function main() state.configDirty = true state.bumpStateVersion() pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "learn_smelting_recipe" and message.input and message.result then cfg.recipeBook.learnSmeltingRecipe(message.input, message.result, message.furnaces) @@ -865,6 +921,13 @@ local function main() state.configDirty = true state.bumpStateVersion() pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "forget_recipe" and message.recipe then local forgot = cfg.recipeBook.forgetCraftingRecipe(message.recipe) or @@ -877,6 +940,13 @@ local function main() state.bumpStateVersion() end pcall(broadcastState) + pcall(function() + ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { + type = "command_ack", + commandId = message.commandId, + success = true, + }) + end) elseif message.type == "find_item" and message.items then -- Return chest+slot locations for the first matching item diff --git a/inventoryWebBridge.lua b/inventoryWebBridge.lua index 33fdfe7..49b8ccf 100644 --- a/inventoryWebBridge.lua +++ b/inventoryWebBridge.lua @@ -62,6 +62,14 @@ local running = true local ws = nil -- active WebSocket handle (nil when not connected) local wsConnected = false -- gates WS vs HTTP transport selection +-- Reliable modem delivery: pending commands awaiting manager acknowledgment. +-- processCommand() inserts here; modemListener removes on result receipt. +-- A retry task periodically re-transmits unacknowledged commands. +local pendingModem = {} -- commandId -> { payload, channel, replyChannel, sent, retries } +local MODEM_RETRY_INTERVAL = 2 -- seconds between retry sweeps +local MODEM_RETRY_MAX = 5 -- max retransmissions before giving up +local MODEM_RETRY_DELAY = 2 -- seconds before first retry (per command) + ------------------------------------------------- -- HTTP helpers (thin wrappers around platform) ------------------------------------------------- @@ -134,96 +142,55 @@ local function processCommand(cmd) print(string.format("[CMD] %s", action)) + -- Build the modem payload from the server command + local payload if action == "order" then - modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, { + payload = { 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, - }) + payload = { type = "scan", commandId = cmd.commandId } elseif action == "toggle_pause" then - modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, { - type = "toggle_pause", - commandId = cmd.commandId, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { type = "enable_all", commandId = cmd.commandId } elseif action == "disable_all" then - modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, { - type = "disable_all", - commandId = cmd.commandId, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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, - }) + payload = { 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", - }) + payload = { type = "reboot", commandId = cmd.commandId, target = cmd.target or "all" } else print("[CMD] Unknown action: " .. tostring(action)) + return + end + + -- Transmit and track for reliable delivery + modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, payload) + if payload.commandId then + pendingModem[payload.commandId] = { + payload = payload, + sent = os.clock(), + retries = 0, + } end end @@ -240,6 +207,10 @@ local function modemListener() latestState = message end elseif channel == BRIDGE_REPLY_CHANNEL and type(message) == "table" then + -- Clear pending retry on any response matching a commandId + if message.commandId and pendingModem[message.commandId] then + pendingModem[message.commandId] = nil + end -- Forward command results back to web server local resultType = message.type if resultType == "order_result" or resultType == "craft_result" @@ -395,6 +366,30 @@ local function wsConnector() }) end +-- Task 6: Retry unacknowledged modem commands +-- The manager deduplicates by commandId, so retransmits are safe. +local function modemRetry() + while running do + local now = os.clock() + for cmdId, entry in pairs(pendingModem) do + if now - entry.sent >= MODEM_RETRY_DELAY then + if entry.retries >= MODEM_RETRY_MAX then + print(string.format("[RETRY] Giving up on %s after %d retries", + tostring(cmdId), entry.retries)) + pendingModem[cmdId] = nil + else + entry.retries = entry.retries + 1 + entry.sent = now + print(string.format("[RETRY] Re-transmit %s (attempt %d/%d)", + tostring(cmdId), entry.retries, MODEM_RETRY_MAX)) + pcall(modem.transmit, ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, entry.payload) + end + end + end + sleep(MODEM_RETRY_INTERVAL) + end +end + ------------------------------------------------- -- Main ------------------------------------------------- @@ -437,7 +432,8 @@ local function main() stateForwarder, commandPoller, heartbeat, - wsConnector + wsConnector, + modemRetry ) end