fix: implement reliable modem command delivery with acknowledgment and retry mechanism
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user