crestron-mcp 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +298 -0
- package/LICENSE +21 -0
- package/PROTOCOL.md +650 -0
- package/README.md +81 -0
- package/dist/config.js +71 -0
- package/dist/connection.js +728 -0
- package/dist/index.js +285 -0
- package/mcpb/manifest.json +76 -0
- package/package.json +55 -0
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
# MCP-Crestron Protocol Specification v1.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Text-based protocol for AI control of Crestron systems via MCP (Model Context Protocol).
|
|
6
|
+
|
|
7
|
+
**Design Principles:**
|
|
8
|
+
- Human-readable for debugging
|
|
9
|
+
- Simple to parse in both C# and Python
|
|
10
|
+
- Extensible for future features
|
|
11
|
+
- Clear error handling
|
|
12
|
+
- Low latency
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Connection
|
|
17
|
+
|
|
18
|
+
**Protocol:** TCP/IP
|
|
19
|
+
**Port:** 50794 (configurable)
|
|
20
|
+
**Encoding:** UTF-8
|
|
21
|
+
**Line Termination:** `\n` (LF)
|
|
22
|
+
**Max Message Size:** 8KB per message
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Message Format
|
|
27
|
+
|
|
28
|
+
### Command Structure
|
|
29
|
+
```
|
|
30
|
+
COMMAND[:PARAM1[:PARAM2[:...]]]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Response Structure
|
|
34
|
+
```
|
|
35
|
+
OK[:data]
|
|
36
|
+
ERROR:code:message
|
|
37
|
+
DATA:json_string
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Rules:**
|
|
41
|
+
- Commands and parameters are case-insensitive
|
|
42
|
+
- Device IDs are case-sensitive
|
|
43
|
+
- The **value** is always the LAST field of a command/response, so it is parsed
|
|
44
|
+
positionally and **may contain colons** (e.g. `SET:dvd_s1:now playing 12:34`). Earlier
|
|
45
|
+
fields (command, device ID, room, category) must not contain colons; device IDs never do.
|
|
46
|
+
- `BATCH_SET` pairs are comma-separated and each split on its first colon, so a value in a
|
|
47
|
+
batch may contain colons but **not commas**.
|
|
48
|
+
- A value must not contain a raw newline (`\n` is the frame delimiter / line terminator).
|
|
49
|
+
- Empty parameters are represented by empty string between colons
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Core Commands
|
|
54
|
+
|
|
55
|
+
### 1. HELLO
|
|
56
|
+
**Purpose:** Initial handshake, get system information
|
|
57
|
+
**Format:** `HELLO`
|
|
58
|
+
|
|
59
|
+
**Response:**
|
|
60
|
+
```
|
|
61
|
+
OK:MCP-CRESTRON:1.0:PROCESSOR_ID
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Example:**
|
|
65
|
+
```
|
|
66
|
+
Client: HELLO
|
|
67
|
+
Server: OK:MCP-CRESTRON:1.0:PROC-CP4-01
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 1a. AUTH
|
|
73
|
+
**Purpose:** Authenticate the connection when the server is in a non-open auth mode
|
|
74
|
+
**Format:** `AUTH:credential` (mode 1) or bare `AUTH` then `AUTH:<digest>` (mode 2)
|
|
75
|
+
|
|
76
|
+
- **Mode 0 (open):** no `AUTH` needed; any client may issue commands. `AUTH` is still accepted (returns `OK:AUTH`).
|
|
77
|
+
- **Mode 1 (password):** the client must send `AUTH:<password>` after `HELLO`; until it succeeds, every command except `HELLO` and `AUTH` is rejected with `ERROR:1009`. The password is sent in plaintext, so this is for a trusted LAN only.
|
|
78
|
+
- **Mode 2 (secure key):** runs over **TLS** (the processor listens with `SecureTCPServer`), so the whole channel is encrypted, not just the credential. On top of TLS the client authenticates by HMAC-SHA256 challenge-response: it sends a bare `AUTH` (no argument); the server replies `CHALLENGE:<nonce>` (a one-time random value); the client returns `AUTH:<digest>` where `digest = lowercase-hex( HMAC-SHA256(key, nonce) )`. The server recomputes with its copy of the key and compares in constant time. The nonce is single-use per connection (a failed attempt needs a fresh bare `AUTH`). The same `ERROR:1009` gate applies until authenticated. The processor presents `SecureTCPServer`'s built-in self-signed certificate; on a trusted LAN the client encrypts without verifying it (no PKI). Modes 0 and 1 are plaintext TCP.
|
|
79
|
+
- Authentication is per-connection and lasts the life of the TCP connection. The server's mode is set program-side (the `MCP_Server_Config` SIMPL+ module). In mode 2 the processor mints and persists the key and surfaces it on the module's `Key$` output; copy it to the client once. The client supplies its credential via `CRESTRON_AUTH` (mode-1 password) or `CRESTRON_KEY` (mode-2 key) as env vars or in `config.json`; `CRESTRON_KEY` takes precedence and also turns on TLS (set `CRESTRON_TLS=1` to force TLS without a key).
|
|
80
|
+
|
|
81
|
+
**Response:**
|
|
82
|
+
```
|
|
83
|
+
CHALLENGE:<nonce> (mode 2, after a bare AUTH)
|
|
84
|
+
OK:AUTH (authenticated)
|
|
85
|
+
ERROR:1010:... (wrong credential)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Example (mode 1, password):**
|
|
89
|
+
```
|
|
90
|
+
Client: HELLO
|
|
91
|
+
Server: OK:MCP-CRESTRON:1.0:PROC-CP4-01
|
|
92
|
+
Client: AUTH:s3cret
|
|
93
|
+
Server: OK:AUTH
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Example (mode 2, secure key):**
|
|
97
|
+
```
|
|
98
|
+
Client: HELLO
|
|
99
|
+
Server: OK:MCP-CRESTRON:1.0:PROC-CP4-01
|
|
100
|
+
Client: AUTH
|
|
101
|
+
Server: CHALLENGE:3eaae71c1c862bf21863c0d36cf06558
|
|
102
|
+
Client: AUTH:40c3180c8311a87dbf15f9e4b5dc0d1bcf4cbfb4f92d866f5810126a684ce9e1
|
|
103
|
+
Server: OK:AUTH
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### 1b. ACTIVATE (licensing)
|
|
109
|
+
|
|
110
|
+
**Format:** `ACTIVATE:<license>` (alias: `LICENSE:<license>`)
|
|
111
|
+
|
|
112
|
+
Licensing is **per-processor and stored on the box**. A license is an offline-verifiable token
|
|
113
|
+
signed by the vendor's RSA-2048 private key and bound to the processor's MAC. The processor embeds
|
|
114
|
+
only the public key; it verifies the signature, checks the license is for its own MAC, and (if
|
|
115
|
+
valid) **persists** it to the data store, so the box stays licensed across reboots and for every
|
|
116
|
+
client. No network call, no per-connection license exchange.
|
|
117
|
+
|
|
118
|
+
- The license is **installed once** via `ACTIVATE`, typically through the MCP client's
|
|
119
|
+
`activate_crestron_license` tool: the user pastes the key in chat and the LLM activates the box.
|
|
120
|
+
- `ACTIVATE` is allowed before authentication (like `HELLO`/`AUTH`), so an unlicensed box can be
|
|
121
|
+
activated. The license blob is base64url (no colons); whitespace in a pasted key is tolerated.
|
|
122
|
+
- When **license enforcement is on**, every command except `HELLO`, `AUTH`, and `ACTIVATE` is
|
|
123
|
+
rejected with `ERROR:1011` until the processor is licensed. The error names the processor's MAC
|
|
124
|
+
(its activation code) so the client can tell the user what to get a key for.
|
|
125
|
+
- Enforcement is enabled only if the processor's boot **RSA self-test passes** (so a crypto issue
|
|
126
|
+
disables the gate rather than bricking the box). The license is orthogonal to auth: a command
|
|
127
|
+
must pass **both** the license gate and the auth gate.
|
|
128
|
+
|
|
129
|
+
**Responses:**
|
|
130
|
+
```
|
|
131
|
+
OK:ACTIVATED:<customer> (verified for this processor + persisted)
|
|
132
|
+
ERROR:1012:<reason> (bad signature, wrong MAC, malformed, or expired)
|
|
133
|
+
ERROR:1011:license required for this processor (MAC <mac>) (gated command while unlicensed)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
```
|
|
138
|
+
Client: HELLO
|
|
139
|
+
Server: OK:MCP-CRESTRON:1.0:CRESTRON-MCP
|
|
140
|
+
Client: LIST_ROOMS
|
|
141
|
+
Server: ERROR:1011:license required for this processor (MAC 00107ff0ab17)
|
|
142
|
+
Client: ACTIVATE:eyJ2Ijox...<signed license>
|
|
143
|
+
Server: OK:ACTIVATED:Acme Integrators
|
|
144
|
+
Client: LIST_ROOMS
|
|
145
|
+
Server: DATA:[...]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 1c. Trials (time-limited licenses)
|
|
149
|
+
|
|
150
|
+
A **trial** is just a normal signed license that carries an `expiry` (the vendor issues 7-day
|
|
151
|
+
trials, capped per MAC by the licensing server). It activates through the same `ACTIVATE` path and
|
|
152
|
+
persists the same way. Two differences from a perpetual license:
|
|
153
|
+
|
|
154
|
+
- The **signature is verified once** (at boot / on `ACTIVATE`); the **expiry is re-checked on every
|
|
155
|
+
command** (a cheap timestamp compare, no crypto). So a trial lapses on its own at the next command
|
|
156
|
+
with no timer, after which gated commands return `ERROR:1011` again.
|
|
157
|
+
- A lapsed trial only re-gates the MCP layer; the underlying control program is unaffected.
|
|
158
|
+
|
|
159
|
+
The cap (default 3 trials per processor) is enforced by the **licensing server**, not the box; the
|
|
160
|
+
box only knows "valid until `<expiry>`". The MCP client's `start_crestron_trial` tool fetches a
|
|
161
|
+
signed trial from the server and relays it via `ACTIVATE`.
|
|
162
|
+
|
|
163
|
+
### 1d. LICENSE_STATUS
|
|
164
|
+
|
|
165
|
+
**Format:** `LICENSE_STATUS`
|
|
166
|
+
|
|
167
|
+
Reports the processor's current license state. Allowed before the gates (like `HELLO`), so a client
|
|
168
|
+
can detect an unlicensed box and read its MAC. Lets the client surface a trial's remaining time.
|
|
169
|
+
|
|
170
|
+
**Response:** `DATA:<json>`
|
|
171
|
+
```json
|
|
172
|
+
{ "licensed": true, "enforced": true, "time_limited": true,
|
|
173
|
+
"remaining_ms": 518400000, "expiry_epoch_ms": 1782432000000,
|
|
174
|
+
"trial_seq": 2, "trial_max": 3,
|
|
175
|
+
"customer": "Trial", "mac": "00107ff0ab17" }
|
|
176
|
+
```
|
|
177
|
+
`time_limited` is true for a trial; `remaining_ms` / `expiry_epoch_ms` are present only then. For a
|
|
178
|
+
perpetual license `time_limited` is false and those fields are omitted. When unlicensed,
|
|
179
|
+
`licensed` is false and `mac` still reports the activation code. `trial_seq` / `trial_max` (which
|
|
180
|
+
trial this is, 1-based, and the total allowed) appear only on a server-minted trial that carries
|
|
181
|
+
them, so the LLM can say "trial 2 of 3".
|
|
182
|
+
|
|
183
|
+
Licenses are issued with the vendor `licgen` tool (`licgen sign --mac <mac> --customer "<name>"`),
|
|
184
|
+
which signs `{mac, customer, tier, issued, expiry?, trial_seq?, trial_max?}`. Perpetual licenses
|
|
185
|
+
omit `expiry` (and the trial fields); an `expiry` date, if present, is re-checked on each
|
|
186
|
+
boot/verify. The trial fields are appended after `expiry` and minted by the licensing server.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### 2. DISCOVER
|
|
191
|
+
**Purpose:** Get complete system capabilities
|
|
192
|
+
**Format:** `DISCOVER`
|
|
193
|
+
|
|
194
|
+
**Response:**
|
|
195
|
+
```
|
|
196
|
+
DATA:{"processor_id":"PROC-CP4-01","version":"1.0","rooms":[...],"categories":[...],"total_devices":47}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**JSON Structure:**
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"processor_id": "PROC-CP4-01",
|
|
203
|
+
"version": "1.0",
|
|
204
|
+
"capabilities": ["query", "control", "pulse", "ramp", "delay", "cancel", "batch", "state", "time"],
|
|
205
|
+
"rooms": [
|
|
206
|
+
{
|
|
207
|
+
"id": "conf_rm_a",
|
|
208
|
+
"name": "Conference Room A",
|
|
209
|
+
"device_count": 12
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"categories": ["Lighting", "AV", "HVAC", "Shades"],
|
|
213
|
+
"total_devices": 47
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 3. LIST_ROOMS
|
|
220
|
+
**Purpose:** Get list of all rooms
|
|
221
|
+
**Format:** `LIST_ROOMS`
|
|
222
|
+
|
|
223
|
+
**Response:**
|
|
224
|
+
```
|
|
225
|
+
DATA:[{"id":"conf_rm_a","name":"Conference Room A","device_count":12},...]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 4. LIST_DEVICES
|
|
231
|
+
**Purpose:** Get all devices, optionally filtered by room or category
|
|
232
|
+
**Format:**
|
|
233
|
+
- `LIST_DEVICES` - All devices
|
|
234
|
+
- `LIST_DEVICES:room_id` - Devices in specific room
|
|
235
|
+
- `LIST_DEVICES:room_id:category` - Devices in room and category
|
|
236
|
+
|
|
237
|
+
**Response:**
|
|
238
|
+
```
|
|
239
|
+
DATA:[{"id":"conf_rm_a_lights","type":"analog","name":"Lights",...},...]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**JSON Device Structure:**
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"id": "conf_rm_a_lights_level",
|
|
246
|
+
"name": "Conference Room A Lights Level",
|
|
247
|
+
"type": "analog",
|
|
248
|
+
"access": "readwrite",
|
|
249
|
+
"room": "Conference Room A",
|
|
250
|
+
"category": "Lighting",
|
|
251
|
+
"description": "Dimmer control for overhead lights",
|
|
252
|
+
"current_value": "32767",
|
|
253
|
+
"min_value": "0",
|
|
254
|
+
"max_value": "65535",
|
|
255
|
+
"unit": "level"
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Device Types:**
|
|
260
|
+
- `digital` - Boolean/binary (0 or 1)
|
|
261
|
+
- `analog` - Unsigned short (0-65535)
|
|
262
|
+
- `serial` - Text string
|
|
263
|
+
- `string` - Text string
|
|
264
|
+
|
|
265
|
+
**Access Modes:**
|
|
266
|
+
- `readonly` - Can only query
|
|
267
|
+
- `writeonly` - Can only set
|
|
268
|
+
- `readwrite` - Can query and set
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### 5. QUERY
|
|
273
|
+
**Purpose:** Get current state of one or more devices
|
|
274
|
+
**Format:**
|
|
275
|
+
- `QUERY:device_id` - Single device
|
|
276
|
+
- `QUERY:device_id1,device_id2,...` - Multiple devices (comma-separated)
|
|
277
|
+
|
|
278
|
+
**Response (single device):**
|
|
279
|
+
```
|
|
280
|
+
OK:device_id:value
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Response (multiple devices):**
|
|
284
|
+
```
|
|
285
|
+
DATA:[{"id":"device_id1","value":"1"},{"id":"device_id2","value":"32767"}]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Examples:**
|
|
289
|
+
```
|
|
290
|
+
Client: QUERY:conf_rm_a_lights_on
|
|
291
|
+
Server: OK:conf_rm_a_lights_on:1
|
|
292
|
+
|
|
293
|
+
Client: QUERY:conf_rm_a_lights_on,conf_rm_a_lights_level
|
|
294
|
+
Server: DATA:[{"id":"conf_rm_a_lights_on","value":"1"},{"id":"conf_rm_a_lights_level","value":"32767"}]
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### 5a. STATE
|
|
300
|
+
**Purpose:** Get the live state of one device (value + in-flight / pending activity)
|
|
301
|
+
**Format:** `STATE:device_id`
|
|
302
|
+
|
|
303
|
+
A richer single-device read than `QUERY`: the current value plus whether the device is **idle**,
|
|
304
|
+
**ramping** (analog: `target` + `completes_at`), or **pulsing** (digital: `releases_at`), and any
|
|
305
|
+
**pending** scheduled action (a delayed `SET_AFTER`/`PULSE`: `action`, `value`, `fires_at`). All
|
|
306
|
+
`*_at` fields are **epoch milliseconds** (see `TIME`); `remaining_ms` / `in_ms` give the time left
|
|
307
|
+
directly so a client can decide how long to wait without correlating a clock. `value` is omitted
|
|
308
|
+
for a write-only device; `pending` is omitted when nothing is scheduled.
|
|
309
|
+
|
|
310
|
+
**Response:**
|
|
311
|
+
```
|
|
312
|
+
DATA:{json}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Examples:**
|
|
316
|
+
```
|
|
317
|
+
Client: STATE:lounge_a3
|
|
318
|
+
Server: DATA:{"device_id":"lounge_a3","value":"13020","type":"analog","state":"ramping","target":"60000","completes_at":1718671410000,"remaining_ms":4200}
|
|
319
|
+
|
|
320
|
+
Client: STATE:lounge_d8
|
|
321
|
+
Server: DATA:{"device_id":"lounge_d8","value":"1","type":"digital","state":"pulsing","releases_at":1718671406380,"remaining_ms":380}
|
|
322
|
+
|
|
323
|
+
Client: STATE:porch_d1
|
|
324
|
+
Server: DATA:{"device_id":"porch_d1","value":"0","type":"digital","state":"idle","pending":{"action":"pulse","value":"500","fires_at":1718671413600,"in_ms":7600}}
|
|
325
|
+
|
|
326
|
+
Client: STATE:lounge_a3
|
|
327
|
+
Server: DATA:{"device_id":"lounge_a3","value":"13020","type":"analog","state":"idle"}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### 6. SET
|
|
333
|
+
**Purpose:** Set device value
|
|
334
|
+
**Format:** `SET:device_id:value`
|
|
335
|
+
|
|
336
|
+
**Value Types:**
|
|
337
|
+
- Digital: `0` or `1`
|
|
338
|
+
- Analog: `0` to `65535`
|
|
339
|
+
- Serial/String: any text; may contain colons (the value is the last field), but not a raw newline
|
|
340
|
+
|
|
341
|
+
**Response:**
|
|
342
|
+
```
|
|
343
|
+
OK:device_id:value_set
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Examples:**
|
|
347
|
+
```
|
|
348
|
+
Client: SET:conf_rm_a_lights_on:1
|
|
349
|
+
Server: OK:conf_rm_a_lights_on:1
|
|
350
|
+
|
|
351
|
+
Client: SET:conf_rm_a_lights_level:49151
|
|
352
|
+
Server: OK:conf_rm_a_lights_level:49151
|
|
353
|
+
|
|
354
|
+
Client: SET:conf_rm_a_display_source:HDMI 1
|
|
355
|
+
Server: OK:conf_rm_a_display_source:HDMI 1
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### 6b. SET_AFTER
|
|
361
|
+
**Purpose:** Set a device value after a delay (deferred set)
|
|
362
|
+
**Format:** `SET_AFTER:device_id:delay_ms:value`
|
|
363
|
+
|
|
364
|
+
- Works for any device type. `value` is the **last field** (may contain colons); `delay_ms` is numeric.
|
|
365
|
+
- The processor validates the device + access immediately and returns `OK` right away; the
|
|
366
|
+
actual set runs `delay_ms` milliseconds later (a value error at fire time is logged on the
|
|
367
|
+
processor console, not returned).
|
|
368
|
+
- `delay_ms` of `0` is equivalent to a plain `SET`.
|
|
369
|
+
|
|
370
|
+
**Response:**
|
|
371
|
+
```
|
|
372
|
+
OK:device_id:value
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Example:**
|
|
376
|
+
```
|
|
377
|
+
Client: SET_AFTER:porch_light_on:30000:1
|
|
378
|
+
Server: OK:porch_light_on:1
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### 6a. RAMP
|
|
384
|
+
**Purpose:** Smoothly ramp (fade) an analog device to a value over a duration
|
|
385
|
+
**Format:** `RAMP:device_id:value:duration_ms[:delay_ms]`
|
|
386
|
+
|
|
387
|
+
- **Analog devices only.** Digital/serial return an error.
|
|
388
|
+
- `value` is the target `0`-`65535`; `duration_ms` is the ramp time in milliseconds.
|
|
389
|
+
- Optional `delay_ms` (default 0): start the fade after that wait. A delayed ramp occupies
|
|
390
|
+
the device's pending-action slot (see Scheduling and supersede); the `OK` means it's scheduled.
|
|
391
|
+
- All fields are numeric, so the line is split positionally on every colon.
|
|
392
|
+
- The ramp runs on the processor (a timer steps the output from its last commanded
|
|
393
|
+
value to the target); the `OK` response means the ramp has started (or is scheduled).
|
|
394
|
+
- A subsequent `SET`/`RAMP`/`CANCEL` on the same device cancels an in-progress or pending ramp.
|
|
395
|
+
|
|
396
|
+
**Response:**
|
|
397
|
+
```
|
|
398
|
+
OK:device_id:target_value
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Examples:**
|
|
402
|
+
```
|
|
403
|
+
Client: RAMP:conf_rm_a_lights_level:32767:3000
|
|
404
|
+
Server: OK:conf_rm_a_lights_level:32767
|
|
405
|
+
|
|
406
|
+
Client: RAMP:conf_rm_a_lights_on:1:1000
|
|
407
|
+
Server: ERROR:1001:Device 'conf_rm_a_lights_on' is not analog; only analog devices ramp
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
### 6c. PULSE
|
|
413
|
+
**Purpose:** Momentarily pulse a digital device (a simulated button press)
|
|
414
|
+
**Format:** `PULSE:device_id:pulse_ms[:delay_ms]`
|
|
415
|
+
|
|
416
|
+
- **Digital devices only.** Analog/serial return an error.
|
|
417
|
+
- After an optional `delay_ms` (default 0) the processor drives the output **high**, holds it
|
|
418
|
+
for `pulse_ms` milliseconds, then drives it **low**. All fields are numeric.
|
|
419
|
+
- Runs on the processor (a timer drives the high/low writes); the `OK` response means the
|
|
420
|
+
pulse has been scheduled/started.
|
|
421
|
+
- A subsequent `SET` or `PULSE` on the same device cancels a pulse in progress.
|
|
422
|
+
|
|
423
|
+
**Response:**
|
|
424
|
+
```
|
|
425
|
+
OK:device_id:pulse_ms
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Examples:**
|
|
429
|
+
```
|
|
430
|
+
Client: PULSE:conf_rm_a_doorbell:500
|
|
431
|
+
Server: OK:conf_rm_a_doorbell:500
|
|
432
|
+
|
|
433
|
+
Client: PULSE:conf_rm_a_gate:1000:5000
|
|
434
|
+
Server: OK:conf_rm_a_gate:1000
|
|
435
|
+
|
|
436
|
+
Client: PULSE:conf_rm_a_lights_level:500
|
|
437
|
+
Server: ERROR:1001:Device 'conf_rm_a_lights_level' is not digital; only digital devices pulse
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### 6d. CANCEL
|
|
443
|
+
**Purpose:** Stop activity on a device and clear its pending scheduled action
|
|
444
|
+
**Format:** `CANCEL:device_id`
|
|
445
|
+
|
|
446
|
+
- Clears the device's pending scheduled action (a delayed `SET_AFTER` or delayed `PULSE`).
|
|
447
|
+
- **Digital:** if a pulse is in progress (line held high *by the pulse*), it is released to **off**.
|
|
448
|
+
A line that is high from a plain `SET` (no pulse running) is left untouched.
|
|
449
|
+
- **Analog:** a ramp in progress is **stopped where it is** (the level is not snapped anywhere).
|
|
450
|
+
- Otherwise it changes nothing; it is safe to call when nothing is pending/running.
|
|
451
|
+
|
|
452
|
+
**Response:**
|
|
453
|
+
```
|
|
454
|
+
OK:device_id:cancelled
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Example:**
|
|
458
|
+
```
|
|
459
|
+
Client: CANCEL:conf_rm_a_lights_level
|
|
460
|
+
Server: OK:conf_rm_a_lights_level:cancelled
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### Scheduling and supersede (SET_AFTER / PULSE / RAMP delay)
|
|
466
|
+
|
|
467
|
+
Each device has **one** pending scheduled-action slot (a delayed `SET_AFTER`, `PULSE`, or `RAMP`).
|
|
468
|
+
**Last command wins:** any new command on a device (an immediate `SET`/`PULSE`/`RAMP`/`BATCH_SET`, a
|
|
469
|
+
new delayed action, or a `CANCEL`) replaces/clears that device's pending action. Scheduling a future action does **not**
|
|
470
|
+
abort an action already executing right now (e.g. a pulse mid-press finishes, then a separately
|
|
471
|
+
scheduled action fires later); only immediate commands and `CANCEL` interrupt an in-flight action.
|
|
472
|
+
The slot is per **device id**, so the `_d` / `_a` / `_s` facets of one physical device are
|
|
473
|
+
independent.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
### 7. GET_INFO
|
|
478
|
+
**Purpose:** Get detailed information about a specific device
|
|
479
|
+
**Format:** `GET_INFO:device_id`
|
|
480
|
+
|
|
481
|
+
**Response:**
|
|
482
|
+
```
|
|
483
|
+
DATA:{"id":"conf_rm_a_lights_level","type":"analog","name":"Conference Room A Lights","room":"Conference Room A","category":"Lighting","access":"readwrite","current_value":"32767","min_value":"0","max_value":"65535"}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### 8. BATCH_SET
|
|
489
|
+
**Purpose:** Set multiple devices in one command
|
|
490
|
+
**Format:** `BATCH_SET:device_id1:value1,device_id2:value2,...`
|
|
491
|
+
|
|
492
|
+
**Response:**
|
|
493
|
+
```
|
|
494
|
+
DATA:[{"id":"device_id1","success":true},{"id":"device_id2","success":true}]
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Example:**
|
|
498
|
+
```
|
|
499
|
+
Client: BATCH_SET:conf_rm_a_lights_on:1,conf_rm_a_lights_level:49151
|
|
500
|
+
Server: DATA:[{"id":"conf_rm_a_lights_on","success":true,"value":"1"},{"id":"conf_rm_a_lights_level","success":true,"value":"49151"}]
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
### 9. PING
|
|
506
|
+
**Purpose:** Keep-alive / connectivity check
|
|
507
|
+
**Format:** `PING`
|
|
508
|
+
|
|
509
|
+
**Response:**
|
|
510
|
+
```
|
|
511
|
+
OK:PONG
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### 10. TIME
|
|
517
|
+
**Purpose:** Get the processor's current time in a machine-friendly form
|
|
518
|
+
**Format:** `TIME`
|
|
519
|
+
|
|
520
|
+
Reads the processor's own clock. `epoch_ms` is UTC epoch milliseconds (use it to correlate the
|
|
521
|
+
`*_at` fields from `STATE`); `iso` is local ISO 8601 with offset for human legibility.
|
|
522
|
+
|
|
523
|
+
**Response:**
|
|
524
|
+
```
|
|
525
|
+
DATA:{"epoch_ms":1718671406000,"iso":"2026-06-18T12:43:26+10:00"}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Error Responses
|
|
531
|
+
|
|
532
|
+
**Format:** `ERROR:code:message`
|
|
533
|
+
|
|
534
|
+
### Error Codes
|
|
535
|
+
|
|
536
|
+
| Code | Name | Description |
|
|
537
|
+
|------|------|-------------|
|
|
538
|
+
| 1000 | UNKNOWN_COMMAND | Command not recognized |
|
|
539
|
+
| 1001 | INVALID_PARAMETERS | Wrong number or format of parameters |
|
|
540
|
+
| 1002 | DEVICE_NOT_FOUND | Device ID does not exist |
|
|
541
|
+
| 1003 | ACCESS_DENIED | Device is read-only or write-only |
|
|
542
|
+
| 1004 | VALUE_OUT_OF_RANGE | Value exceeds device limits |
|
|
543
|
+
| 1005 | DEVICE_OFFLINE | Device is not responding |
|
|
544
|
+
| 1006 | INTERNAL_ERROR | Server-side error |
|
|
545
|
+
| 1007 | PARSE_ERROR | Could not parse command |
|
|
546
|
+
| 1008 | TIMEOUT | Command execution timeout |
|
|
547
|
+
| 1009 | AUTH_REQUIRED | Authentication required before this command |
|
|
548
|
+
| 1010 | AUTH_FAILED | Authentication credential rejected |
|
|
549
|
+
| 1011 | LICENSE_REQUIRED | Processor not licensed; activate it before this command |
|
|
550
|
+
| 1012 | LICENSE_INVALID | License rejected (bad signature, wrong MAC, malformed, or expired) |
|
|
551
|
+
|
|
552
|
+
**Examples:**
|
|
553
|
+
```
|
|
554
|
+
ERROR:1002:Device 'invalid_device' not found
|
|
555
|
+
ERROR:1003:Device 'temp_sensor' is read-only
|
|
556
|
+
ERROR:1004:Value 70000 out of range (0-65535) for device 'lights'
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Connection Lifecycle
|
|
562
|
+
|
|
563
|
+
### 1. Client Connects
|
|
564
|
+
```
|
|
565
|
+
[TCP connection established]
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### 2. Handshake
|
|
569
|
+
```
|
|
570
|
+
Client: HELLO
|
|
571
|
+
Server: OK:MCP-CRESTRON:1.0:PROC-CP4-01
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### 3. Discovery (optional)
|
|
575
|
+
```
|
|
576
|
+
Client: DISCOVER
|
|
577
|
+
Server: DATA:{...full system capabilities...}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 4. Normal Operations
|
|
581
|
+
```
|
|
582
|
+
Client: QUERY:conf_rm_a_lights_on
|
|
583
|
+
Server: OK:conf_rm_a_lights_on:1
|
|
584
|
+
|
|
585
|
+
Client: SET:conf_rm_a_lights_level:49151
|
|
586
|
+
Server: OK:conf_rm_a_lights_level:49151
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### 5. Disconnect
|
|
590
|
+
```
|
|
591
|
+
[TCP connection closed by either party]
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Best Practices
|
|
597
|
+
|
|
598
|
+
### For Crestron Implementation
|
|
599
|
+
- Validate all incoming commands before execution
|
|
600
|
+
- Return errors immediately for invalid requests
|
|
601
|
+
- Use thread-safe device registry
|
|
602
|
+
- Implement command timeouts (5 seconds default)
|
|
603
|
+
- Log all SET commands for audit trail
|
|
604
|
+
|
|
605
|
+
### For MCP Client Implementation
|
|
606
|
+
- Send HELLO immediately after connection
|
|
607
|
+
- Cache DISCOVER results to minimize queries
|
|
608
|
+
- Implement reconnection logic with exponential backoff
|
|
609
|
+
- Use PING for keep-alive (every 30 seconds)
|
|
610
|
+
- Handle ERROR responses gracefully
|
|
611
|
+
|
|
612
|
+
### Performance
|
|
613
|
+
- Batch queries when possible (QUERY with multiple devices)
|
|
614
|
+
- Batch sets for scene activation (BATCH_SET)
|
|
615
|
+
- Keep device IDs short but descriptive
|
|
616
|
+
- Limit LIST_DEVICES queries (cache results)
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Future Extensions (v2.0)
|
|
621
|
+
|
|
622
|
+
Potential additions for future versions:
|
|
623
|
+
- `SUBSCRIBE:device_id` - Real-time change notifications
|
|
624
|
+
- `UNSUBSCRIBE:device_id` - Stop notifications
|
|
625
|
+
- `EVENT:device_id:old_value:new_value` - Push notifications
|
|
626
|
+
- `AUTH:username:password` - Authentication
|
|
627
|
+
- `MACRO:macro_name` - Execute predefined macros
|
|
628
|
+
- `HISTORY:device_id:duration` - Get device value history
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Security Considerations
|
|
633
|
+
|
|
634
|
+
**v1.0 Security Model:**
|
|
635
|
+
- No built-in authentication (assumes secure local network)
|
|
636
|
+
- Access control enforced at device level (readonly/writeonly)
|
|
637
|
+
- Command validation prevents malformed requests
|
|
638
|
+
- Rate limiting recommended at firewall/network level
|
|
639
|
+
|
|
640
|
+
**Recommendations:**
|
|
641
|
+
- Run on isolated control network
|
|
642
|
+
- Use firewall to restrict access to MCP port
|
|
643
|
+
- Consider VPN for remote access
|
|
644
|
+
- Implement authentication in v2.0
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Version History
|
|
649
|
+
|
|
650
|
+
- **v1.0** - Initial specification
|