--[[ platform.protocol — Canonical message envelope for cc-platform-core Provides a standard message format that is backward-compatible with existing modem messages. All messages retain the `type` field as the primary discriminator. New metadata fields (_v, _src, _ts, _id) are additive — legacy consumers ignore them. Usage: local Protocol = require('platform.protocol') Protocol.setServiceId('inventory-manager') -- Create a well-formed message local msg = Protocol.message('state', { items = {...}, total = 42 }) -- => { type='state', items={...}, total=42, _v=1, _src='inventory-manager', _ts=... } -- Parse incoming message (works with both legacy and versioned) local parsed = Protocol.parse(msg) -- => { type='state', version=1, source='inventory-manager', timestamp=..., data=msg } -- Quick type check if Protocol.is(msg, 'status') then ... end ]] local os = _G.os local Protocol = { VERSION = 1, serviceId = nil, } --- Set the service identity for this process. -- All messages created after this call will include the service ID. -- @param id string Service identifier (e.g. 'remoteturtle', 'inventory-manager') function Protocol.setServiceId(id) Protocol.serviceId = id end --- Create a well-formed platform message. -- Merges data fields into a new table and adds protocol metadata. -- The `type` field is always set from the first argument. -- @param msgType string Message type discriminator -- @param data table|nil Service-specific payload fields (merged into message) -- @param opts table|nil Options: { src=string, id=string, noMeta=bool } -- @return table The constructed message function Protocol.message(msgType, data, opts) opts = opts or {} local msg = {} -- Merge data fields into the message (flat, not nested) if data then for k, v in pairs(data) do msg[k] = v end end -- Primary discriminator msg.type = msgType -- Protocol metadata (additive — legacy consumers ignore these) if not opts.noMeta then msg._v = Protocol.VERSION msg._src = opts.src or Protocol.serviceId msg._ts = os.epoch and os.epoch('utc') or math.floor(os.clock() * 1000) if opts.id then msg._id = opts.id end end return msg end --- Parse a received message, normalizing both legacy and versioned formats. -- Returns nil if the input is not a valid message table. -- @param msg table Raw message from modem or WebSocket -- @return table|nil Normalized message descriptor function Protocol.parse(msg) if type(msg) ~= 'table' then return nil end if not msg.type then return nil end return { type = msg.type, version = msg._v or 0, source = msg._src, timestamp = msg._ts, correlationId = msg._id, data = msg, -- full original message for backward-compat field access } end --- Check whether a message matches a given type. -- Works on both raw messages and parsed descriptors. -- @param msg table Message or parsed descriptor -- @param msgType string Expected type -- @return boolean function Protocol.is(msg, msgType) if type(msg) ~= 'table' then return false end return msg.type == msgType end --- Generate a correlation ID for request/response patterns. -- Uses a combination of computer ID, clock, and random to ensure uniqueness. -- @return string Unique correlation ID function Protocol.correlationId() local id = os.getComputerID and os.getComputerID() or 0 local clock = os.epoch and os.epoch('utc') or os.clock() local rand = math.random(0, 0xFFFF) return string.format('%d-%d-%04x', id, clock, rand) end return Protocol