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.needsRedraw = true
|
||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "toggle_pause" then
|
||||||
state.smeltingPaused = not state.smeltingPaused
|
state.smeltingPaused = not state.smeltingPaused
|
||||||
@@ -717,6 +724,13 @@ local function main()
|
|||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
state.needsRedraw = true
|
state.needsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "toggle_recipe" and message.recipe then
|
||||||
if state.disabledRecipes[message.recipe] then
|
if state.disabledRecipes[message.recipe] then
|
||||||
@@ -730,6 +744,13 @@ local function main()
|
|||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "enable_all" then
|
||||||
state.disabledRecipes = {}
|
state.disabledRecipes = {}
|
||||||
@@ -739,6 +760,13 @@ local function main()
|
|||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "disable_all" then
|
||||||
for inputName in pairs(cfg.SMELTABLE) do
|
for inputName in pairs(cfg.SMELTABLE) do
|
||||||
@@ -750,11 +778,25 @@ local function main()
|
|||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "sort_barrel" and message.barrelName then
|
||||||
log.info("NET", "Sort barrel: %s", message.barrelName)
|
log.info("NET", "Sort barrel: %s", message.barrelName)
|
||||||
pcall(ops.sortBarrel, message.barrelName)
|
pcall(ops.sortBarrel, message.barrelName)
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "register_droppers" and message.clientId and message.droppers then
|
||||||
local cid = tostring(message.clientId)
|
local cid = tostring(message.clientId)
|
||||||
@@ -847,6 +889,13 @@ local function main()
|
|||||||
state.smelterNeedsRedraw = true
|
state.smelterNeedsRedraw = true
|
||||||
state.needsRedraw = true
|
state.needsRedraw = true
|
||||||
pcall(broadcastState)
|
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
|
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)
|
cfg.recipeBook.learnCraftingRecipe(message.output, message.count, message.grid)
|
||||||
@@ -856,6 +905,13 @@ local function main()
|
|||||||
state.configDirty = true
|
state.configDirty = true
|
||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "learn_smelting_recipe" and message.input and message.result then
|
||||||
cfg.recipeBook.learnSmeltingRecipe(message.input, message.result, message.furnaces)
|
cfg.recipeBook.learnSmeltingRecipe(message.input, message.result, message.furnaces)
|
||||||
@@ -865,6 +921,13 @@ local function main()
|
|||||||
state.configDirty = true
|
state.configDirty = true
|
||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "forget_recipe" and message.recipe then
|
||||||
local forgot = cfg.recipeBook.forgetCraftingRecipe(message.recipe) or
|
local forgot = cfg.recipeBook.forgetCraftingRecipe(message.recipe) or
|
||||||
@@ -877,6 +940,13 @@ local function main()
|
|||||||
state.bumpStateVersion()
|
state.bumpStateVersion()
|
||||||
end
|
end
|
||||||
pcall(broadcastState)
|
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
|
elseif message.type == "find_item" and message.items then
|
||||||
-- Return chest+slot locations for the first matching item
|
-- 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 ws = nil -- active WebSocket handle (nil when not connected)
|
||||||
local wsConnected = false -- gates WS vs HTTP transport selection
|
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)
|
-- HTTP helpers (thin wrappers around platform)
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
@@ -134,96 +142,55 @@ local function processCommand(cmd)
|
|||||||
|
|
||||||
print(string.format("[CMD] %s", action))
|
print(string.format("[CMD] %s", action))
|
||||||
|
|
||||||
|
-- Build the modem payload from the server command
|
||||||
|
local payload
|
||||||
if action == "order" then
|
if action == "order" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = {
|
||||||
type = "order",
|
type = "order",
|
||||||
commandId = cmd.commandId,
|
commandId = cmd.commandId,
|
||||||
itemName = cmd.itemName,
|
itemName = cmd.itemName,
|
||||||
amount = cmd.amount,
|
amount = cmd.amount,
|
||||||
dropperName = cmd.dropperName,
|
dropperName = cmd.dropperName,
|
||||||
})
|
}
|
||||||
elseif action == "scan" then
|
elseif action == "scan" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "scan", commandId = cmd.commandId }
|
||||||
type = "scan",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
})
|
|
||||||
elseif action == "toggle_pause" then
|
elseif action == "toggle_pause" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "toggle_pause", commandId = cmd.commandId }
|
||||||
type = "toggle_pause",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
})
|
|
||||||
elseif action == "toggle_recipe" then
|
elseif action == "toggle_recipe" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "toggle_recipe", commandId = cmd.commandId, recipe = cmd.recipe }
|
||||||
type = "toggle_recipe",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
recipe = cmd.recipe,
|
|
||||||
})
|
|
||||||
elseif action == "enable_all" then
|
elseif action == "enable_all" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "enable_all", commandId = cmd.commandId }
|
||||||
type = "enable_all",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
})
|
|
||||||
elseif action == "disable_all" then
|
elseif action == "disable_all" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "disable_all", commandId = cmd.commandId }
|
||||||
type = "disable_all",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
})
|
|
||||||
elseif action == "sort_barrel" then
|
elseif action == "sort_barrel" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "sort_barrel", commandId = cmd.commandId, barrelName = cmd.barrelName }
|
||||||
type = "sort_barrel",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
barrelName = cmd.barrelName,
|
|
||||||
})
|
|
||||||
elseif action == "craft" then
|
elseif action == "craft" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "craft", commandId = cmd.commandId, recipeIdx = cmd.recipeIdx }
|
||||||
type = "craft",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
recipeIdx = cmd.recipeIdx,
|
|
||||||
})
|
|
||||||
elseif action == "recursive_craft" then
|
elseif action == "recursive_craft" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "recursive_craft", commandId = cmd.commandId, itemName = cmd.itemName, count = cmd.count }
|
||||||
type = "recursive_craft",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
itemName = cmd.itemName,
|
|
||||||
count = cmd.count,
|
|
||||||
})
|
|
||||||
elseif action == "learn_crafting_recipe" then
|
elseif action == "learn_crafting_recipe" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "learn_crafting_recipe", commandId = cmd.commandId, output = cmd.output, count = cmd.count, grid = cmd.grid }
|
||||||
type = "learn_crafting_recipe",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
output = cmd.output,
|
|
||||||
count = cmd.count,
|
|
||||||
grid = cmd.grid,
|
|
||||||
})
|
|
||||||
elseif action == "learn_smelting_recipe" then
|
elseif action == "learn_smelting_recipe" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "learn_smelting_recipe", commandId = cmd.commandId, input = cmd.input, result = cmd.result, furnaces = cmd.furnaces }
|
||||||
type = "learn_smelting_recipe",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
input = cmd.input,
|
|
||||||
result = cmd.result,
|
|
||||||
furnaces = cmd.furnaces,
|
|
||||||
})
|
|
||||||
elseif action == "forget_recipe" then
|
elseif action == "forget_recipe" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "forget_recipe", commandId = cmd.commandId, recipe = cmd.recipe }
|
||||||
type = "forget_recipe",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
recipe = cmd.recipe,
|
|
||||||
})
|
|
||||||
elseif action == "sync_disabled_recipes" then
|
elseif action == "sync_disabled_recipes" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "sync_disabled_recipes", commandId = cmd.commandId, disabledRecipes = cmd.disabledRecipes, smeltingPaused = cmd.smeltingPaused }
|
||||||
type = "sync_disabled_recipes",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
disabledRecipes = cmd.disabledRecipes,
|
|
||||||
smeltingPaused = cmd.smeltingPaused,
|
|
||||||
})
|
|
||||||
elseif action == "reboot" then
|
elseif action == "reboot" then
|
||||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
payload = { type = "reboot", commandId = cmd.commandId, target = cmd.target or "all" }
|
||||||
type = "reboot",
|
|
||||||
commandId = cmd.commandId,
|
|
||||||
target = cmd.target or "all",
|
|
||||||
})
|
|
||||||
else
|
else
|
||||||
print("[CMD] Unknown action: " .. tostring(action))
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -240,6 +207,10 @@ local function modemListener()
|
|||||||
latestState = message
|
latestState = message
|
||||||
end
|
end
|
||||||
elseif channel == BRIDGE_REPLY_CHANNEL and type(message) == "table" then
|
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
|
-- Forward command results back to web server
|
||||||
local resultType = message.type
|
local resultType = message.type
|
||||||
if resultType == "order_result" or resultType == "craft_result"
|
if resultType == "order_result" or resultType == "craft_result"
|
||||||
@@ -395,6 +366,30 @@ local function wsConnector()
|
|||||||
})
|
})
|
||||||
end
|
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
|
-- Main
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
@@ -437,7 +432,8 @@ local function main()
|
|||||||
stateForwarder,
|
stateForwarder,
|
||||||
commandPoller,
|
commandPoller,
|
||||||
heartbeat,
|
heartbeat,
|
||||||
wsConnector
|
wsConnector,
|
||||||
|
modemRetry
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user