fix: implement reliable modem command delivery with acknowledgment and retry mechanism

This commit is contained in:
MayaTheShy
2026-03-29 01:11:24 -04:00
parent bb15c78ca9
commit 5a83d89509
2 changed files with 136 additions and 70 deletions

View File

@@ -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