119 lines
3.6 KiB
Lua
119 lines
3.6 KiB
Lua
--[[
|
|
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
|