-- websocket/ws.lua -- ComputerCraft / CC:Tweaked script -- Opens a WebSocket to a server and sends aggregated item lists whenever the inventory changes. -- Also listens for incoming messages from the server and prints them as: -- from YYYY-MM-DD HH:MM:SS message -- -- Configuration: local url = "ws://10.0.1.1:3000" -- websocket URL (ws:// or wss://) local inventorySide = "back" -- side to check first (e.g. "back", "left") local requesterSide = "left" -- side to check second (e.g. "left", "right") local pollInterval = 2 -- seconds between inventory scans local fromName = os.getComputerLabel() -- identity included in outgoing payload local computerID = os.getComputerID() -- identity included in outgoing payload local reconnectBase = 2 -- seconds, base reconnect delay local reconnectMax = 60 -- seconds, max reconnect delay -- Helper: format timestamp (accepts seconds or ms) local function format_ts(ts) if not ts then return os.date("%Y-%m-%d %H:%M:%S") end ts = tonumber(ts) if not ts then return os.date("%Y-%m-%d %H:%M:%S") end if ts > 1e11 then ts = ts / 1000 end -- treat large numbers as ms return os.date("%Y-%m-%d %H:%M:%S", ts) end -- JSON serialization helper (prefer textutils.serializeJSON if available) local function toJSON(tbl) if type(textutils) == "table" and type(textutils.serializeJSON) == "function" then local ok, s = pcall(textutils.serializeJSON, tbl) if ok and type(s) == "string" then return s end end -- Simple fallback (escapes minimal characters) local function esc(s) s = tostring(s or "") s = s:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n"):gsub("\r", "\\r") return s end local parts = { "{" } local first = true for k, v in pairs(tbl) do if not first then table.insert(parts, ",") end first = false local key = esc(k) if type(v) == "table" then table.insert(parts, string.format('"%s":%s', key, toJSON(v))) else if type(v) == "string" then table.insert(parts, string.format('"%s":"%s"', key, esc(v))) else table.insert(parts, string.format('"%s":%s', key, tostring(v))) end end end table.insert(parts, "}") return table.concat(parts) end -- Peripheral discovery: try wrapping specified side local ItemInventory = nil local peripheralName = nil if peripheral and peripheral.wrap then ItemInventory = peripheral.wrap(inventorySide) if ItemInventory then peripheralName = inventorySide end end if not ItemInventory then print("No inventory peripheral found on '" .. inventorySide .. "'") return end -- Peripheral discovery: try finding requester on specified side local Requester = nil if peripheral and peripheral.wrap then Requester = peripheral.wrap(requesterSide) end if not Requester then print("No requester peripheral found on '" .. requesterSide .. "'") return end -- robust inventory read supporting list() or size()+getItemDetail() local function readInventory() local items = {} -- try list() if type(ItemInventory.list) == "function" then local ok, res = pcall(ItemInventory.list, ItemInventory) if ok and type(res) == "table" then for slot, detail in pairs(res) do if type(detail) == "table" then local name = tostring(detail.name or detail.displayName or detail.id or "unknown") local count = tonumber(detail.count or detail.qty or detail.size) or 1 table.insert(items, { slot = tonumber(slot) or slot, name = name, count = count }) else local ok2, d2 = pcall(ItemInventory.getItemDetail, ItemInventory, slot) if ok2 and type(d2) == "table" then local name = tostring(d2.name or d2.displayName or d2.id or "unknown") local count = tonumber(d2.count) or 1 table.insert(items, { slot = tonumber(slot) or slot, name = name, count = count }) end end end return items end end -- fallback: size() + getItemDetail() if type(ItemInventory.size) == "function" and type(ItemInventory.getItemDetail) == "function" then local ok, sz = pcall(ItemInventory.size, ItemInventory) if ok and tonumber(sz) then for i = 1, tonumber(sz) do local ok2, d = pcall(ItemInventory.getItemDetail, ItemInventory, i) if ok2 and type(d) == "table" then local name = tostring(d.name or d.displayName or d.id or "unknown") local count = tonumber(d.count) or 1 table.insert(items, { slot = i, name = name, count = count }) end end return items end end return items end -- aggregate items by name (combine counts, DO NOT include slots) local function aggregateItems(items) local map = {} for _, it in ipairs(items) do local key = tostring(it.name) local cnt = tonumber(it.count) or 0 if not map[key] then map[key] = { name = key, count = cnt } else map[key].count = map[key].count + cnt end end local out = {} for _, v in pairs(map) do table.insert(out, v) end table.sort(out, function(a, b) return a.name < b.name end) return out end -- canonicalize aggregated list for easy change detection local function canonicalize(items) local agg = aggregateItems(items) local out = {} for _, it in ipairs(agg) do table.insert(out, string.format("%s:%d", it.name, tonumber(it.count) or 0)) end table.sort(out) return table.concat(out, ";") end -- Format and print incoming payloads as: from timestamp message local function handlePayload(payload) local parsed = nil local okJson, res = pcall(function() return textutils.unserializeJSON(payload) end) if okJson then parsed = res end print("Received payload:", textutils.serialize(parsed)) local from, text, ts, items, address, to if type(parsed) == "table" then if parsed.type == "request" then items = parsed.items address = tostring(parsed.address) to = parsed.to else from = tostring(parsed.from or parsed.sender or parsed.user or "server") text = tostring(parsed.text or parsed.message or parsed.msg or payload) ts = parsed.ts or parsed.time end else from = "server" text = payload ts = nil type = "message" end if parsed and parsed.type == "request" and to == computerID then print("Request: ", textutils.serialize(items), " from ", address, type(address)) print(Requester.request(items, address)) else local when = format_ts(ts) print(string.format("%s %s %s", from, when, text)) end end -- Send aggregated items via websocket (wsHandle is the websocket object) local function sendItemsWs(wsHandle, items) if not wsHandle then print("Cannot send items: websocket not connected") return false, "not-connected" end local agg = aggregateItems(items) local payload = { from = fromName, id = computerID, ts = os.epoch("utc"), items = agg } local s = toJSON(payload) local ok, err = pcall(function() wsHandle.send(s) end) if not ok then print("Failed to send items:", err) return false, err end print("Sent items (" .. tostring(#agg) .. " aggregated entries)") return true end -- Open websocket with retries (returns ws handle or nil) local function openWebsocket() local attempt = 0 while true do attempt = attempt + 1 local ok, wsOrErr = pcall(http.websocket, url) if ok and wsOrErr then print("Websocket opened to", url) return wsOrErr end local err = wsOrErr or "unknown error" print("Failed to open websocket:", err) -- exponential backoff local delay = math.min(reconnectMax, reconnectBase * (2 ^ (attempt - 1))) -- add jitter delay = delay + math.random() * math.min(3, delay) print("Retrying in " .. string.format("%.1f", delay) .. "s...") os.sleep(delay) end end -- Main orchestrator: keep websocket open, poll inventory and send on change, -- and handle incoming websocket events. Reconnect automatically. local function run() local prevKey = canonicalize(readInventory()) -- Outer loop to maintain connection while true do local ws = openWebsocket() if not ws then -- shouldn't happen as openWebsocket loops, but be safe: wait then continue os.sleep(reconnectBase) else -- After connecting, send an initial snapshot right away local initialItems = readInventory() prevKey = canonicalize(initialItems) pcall(sendItemsWs, ws, initialItems) -- set up timer for polling local timerId = os.startTimer(pollInterval) local connected = true -- Inner event loop: watch for timer ticks and websocket events while connected do local event, a, b = os.pullEvent() if event == "timer" and a == timerId then -- restart timer timerId = os.startTimer(pollInterval) local cur = readInventory() local curKey = canonicalize(cur) if curKey ~= prevKey then prevKey = curKey -- try sending; if send fails it's fine, the ws events will indicate closure pcall(sendItemsWs, ws, cur) end elseif event == "websocket_message" and a == url then -- b is payload pcall(handlePayload, b) elseif event == "websocket_request" and a == url then pcall(handlePayload, b) elseif event == "websocket_closed" and a == url then print("Websocket closed:", tostring(b)) connected = false elseif event == "websocket_failure" and a == url then print("Websocket failure:", tostring(b)) connected = false elseif event == "terminate" then -- user requested termination (Ctrl+T) print("Terminated by user. Closing websocket and exiting.") pcall(function() if ws and type(ws.close) == "function" then ws.close() end end) return end end -- If we break out of inner loop, ensure websocket is closed then reconnect pcall(function() if ws and type(ws.close) == "function" then ws.close() end end) -- small pause before reconnect loop continues (openWebsocket has its own backoff) os.sleep(0.5) end end end -- Start math.randomseed(os.time() % 2 ^ 31) print("Starting websocket inventory watcher") print("Watching inventory on '" .. tostring(peripheralName or inventorySide) .. "' and sending updates to: " .. url) run()