--[[ platform.service — Service lifecycle helpers for cc-platform-core Provides a lightweight service context for Opus-based CC services. Handles modem discovery, channel management, config loading, and message sending using platform conventions. Usage: local Service = require('platform.service') local ctx = Service.create({ name = 'inventory-manager', defaults = { scanInterval = 120 }, }) Service.openChannels(ctx) -- Send on a named channel Service.send(ctx, 'inventory.broadcast', 'state', { items = {...} }) -- Broadcast on both legacy and target channels during migration Service.broadcast(ctx, 'inventory.broadcast', 'state', { items = {...} }) ]] local Config = require('opus.config') local Protocol = require('platform.protocol') local Channels = require('platform.channels') local peripheral = _G.peripheral local Service = {} --- Create a new service context. -- Initializes the protocol identity, loads config, discovers modem. -- @param opts table Options: -- name string (required) Service identifier -- defaults table Default config values -- modem boolean Set to false to skip modem discovery -- channelPrefix string Override for channel name prefix (defaults to name) -- @return table Service context function Service.create(opts) assert(opts and opts.name, 'Service name required') Protocol.setServiceId(opts.name) local prefix = opts.channelPrefix or opts.name local ctx = { name = opts.name, config = Config.load(opts.name, opts.defaults or {}), prefix = prefix, channels = Channels.forService(prefix), modem = nil, } -- Discover modem if opts.modem ~= false then -- Prefer Opus device registry if _G.device and _G.device.wireless_modem then ctx.modem = _G.device.wireless_modem else -- Fallback: scan peripherals (wired or wireless) for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == 'modem' then ctx.modem = peripheral.wrap(name) break end end end end return ctx end --- Open all registered channels for this service on its modem. -- Respects dual-mode: opens both current and target channels when appropriate. -- @param ctx table Service context from Service.create() function Service.openChannels(ctx) if not ctx.modem then return end local channelList = Channels.openListForService(ctx.prefix) for _, ch in ipairs(channelList) do ctx.modem.open(ch) end end --- Close all registered channels for this service. -- @param ctx table Service context function Service.closeChannels(ctx) if not ctx.modem then return end local channelList = Channels.openListForService(ctx.prefix) for _, ch in ipairs(channelList) do ctx.modem.close(ch) end end --- Send a message on a single named channel. -- Uses Protocol.message() to construct a well-formed envelope. -- @param ctx table Service context -- @param channelName string Logical channel name (e.g. 'inventory.broadcast') -- @param msgType string Message type discriminator -- @param data table|nil Payload fields -- @param opts table|nil Protocol.message options (id, src, noMeta) -- @return boolean true if sent successfully function Service.send(ctx, channelName, msgType, data, opts) if not ctx.modem then return false end local ch = Channels.get(channelName) local msg = Protocol.message(msgType, data, opts) ctx.modem.transmit(ch, ch, msg) return true end --- Broadcast a message, sending on both channels during dual-mode migration. -- In current or target mode, behaves identically to send(). -- @param ctx table Service context -- @param channelName string Logical channel name -- @param msgType string Message type discriminator -- @param data table|nil Payload fields -- @param opts table|nil Protocol.message options -- @return boolean true if sent on at least one channel function Service.broadcast(ctx, channelName, msgType, data, opts) if not ctx.modem then return false end local msg = Protocol.message(msgType, data, opts) if Channels.mode == 'dual' then local sent = false for _, ch in ipairs(Channels.getBoth(channelName)) do ctx.modem.transmit(ch, ch, msg) sent = true end return sent else local ch = Channels.get(channelName) ctx.modem.transmit(ch, ch, msg) return true end end --- Save the service config back to persistent storage. -- @param ctx table Service context function Service.saveConfig(ctx) Config.update(ctx.name, ctx.config) end --- Reload the service config from persistent storage. -- @param ctx table Service context -- @param defaults table|nil Default values function Service.reloadConfig(ctx, defaults) ctx.config = Config.load(ctx.name, defaults or ctx.config) end return Service