This repository has been archived by the owner on May 17, 2021. It is now read-only.
/
PckGenerator.java
669 lines (638 loc) · 24 KB
/
PckGenerator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lcn.common;
/**
* Helpers to generate LCN-PCK commands.
* <p>
* LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
*
* @author Tobias J�ttner
*/
public final class PckGenerator {
/** Terminates a PCK command. */
public static final String TERMINATION = "\n";
/**
* Generates a keep-alive.
* LCN-PCHK will close the connection if it does not receive any commands from
* an open {@link Connection} for a specific period (10 minutes by default).
*
* @param counter the current ping's id (optional, but "best practice"). Should start with 1
* @return the PCK command as text
*/
public static String ping(int counter) {
return String.format("^ping%d", counter);
}
/**
* Generates a PCK command that will set the LCN-PCHK connection's operation mode.
* This influences how output-port commands and status are interpreted and must be
* in sync with the LCN bus.
*
* @param dimMode see {@link LcnDefs.OutputPortDimMode}
* @param statusMode see {@link LcnDefs.OutputPortStatusMode}
* @return the PCK command as text
*/
public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.STEPS200 ? "1" : "0")
+ (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
}
/**
* Generates a PCK address header.
* Used for commands to LCN modules and groups.
*
* @param addr the target's address (module or group)
* @param localSegId the local segment id where the physical bus connection is located
* @param wantsAck true to claim an acknowledge / receipt from the target
* @return the PCK address header as text
*/
public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegId(localSegId),
addr.getId(), wantsAck ? "!" : ".");
}
/**
* Generates a scan-command for LCN segment-couplers.
* Used to detect the local segment (where the physical bus connection is located).
*
* @return the PCK command (without address header) as text
*/
public static String segmentCouplerScan() {
return "SK";
}
/**
* Generates a firmware/serial-number request.
*
* @return the PCK command (without address header) as text
*/
public static String requestSn() {
return "SN";
}
/**
* Generates an output-port status request.
*
* @param outputId 0..3
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String requestOutputStatus(int outputId) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
return String.format("SMA%d", outputId + 1);
}
/**
* Generates a dim command for a single output-port.
*
* @param outputId 0..3
* @param percent 0..100
* @param ramp use {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String dimOutput(int outputId, double percent, int ramp) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
int n = (int) Math.round(percent * 2);
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
return String.format("A%dDI%03d%03d", outputId + 1, n / 2, ramp);
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
return String.format("O%dDI%03d%03d", outputId + 1, n, ramp);
}
}
/**
* Generates a dim command for all output-ports.
*
* @param percent 0..100
* @param ramp use {@link LcnDefs#timeToRampValue(int)} (might be ignored in some cases)
* @param is1805 true if the target module's firmware is 180501 or newer
* @return the PCK command (without address header) as text
*/
public static String dimAllOutputs(double percent, int ramp, boolean is1805) {
int n = (int) Math.round(percent * 2);
if (is1805) {
return String.format("OY%03d%03d%03d%03d%03d", n, n, n, n, ramp); // Supported since LCN-PCHK 2.61
}
if (n == 0) { // All off
return String.format("AA%03d", ramp);
} else if (n == 200) { // All on
return String.format("AE%03d", ramp);
}
// This is our worst-case: No high-res, no ramp
return String.format("AH%03d", n / 2);
}
/**
* Generates a command to change the value of an output-port.
*
* @param outputId 0..3
* @param percent -100..100
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String relOutput(int outputId, double percent) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
int n = (int) Math.round(percent * 2);
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
}
}
/**
* Generates a command that toggles a single output-port (on->off, off->on).
*
* @param outputId 0..3
* @param ramp see {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String toggleOutput(int outputId, int ramp) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
return String.format("A%dTA%03d", outputId + 1, ramp);
}
/**
* Generates a command that toggles all output-ports (on->off, off->on).
*
* @param ramp see {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
*/
public static String toggleAllOutputs(int ramp) {
return String.format("AU%03d", ramp);
}
/**
* Generates a relays-status request.
*
* @return the PCK command (without address header) as text
*/
public static String requestRelaysStatus() {
return "SMR";
}
/**
* Generates a command to control relays.
*
* @param states the 8 modifiers for the relay states
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws IllegalArgumentException {
if (states.length != 8) {
throw new IllegalArgumentException();
}
String ret = "R8";
for (int i = 0; i < 8; ++i) {
switch (states[i]) {
case ON:
ret += "1";
break;
case OFF:
ret += "0";
break;
case TOGGLE:
ret += "U";
break;
case NOCHANGE:
ret += "-";
break;
default:
throw new Error();
}
}
return ret;
}
/**
* Generates a binary-sensors status request.
*
* @return the PCK command (without address header) as text
*/
public static String requestBinSensorsStatus() {
return "SMB";
}
/**
* Generates a command that sets a variable absolute.
*
* @param var the target variable to set
* @param value the absolute value to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException
*/
public static String varAbs(LcnDefs.Var var, int value) throws IllegalArgumentException {
int id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
// Set absolute (not in PCK yet)
int b1 = id << 6; // 01000000
b1 |= 0x20; // xx10xxxx (set absolute)
value -= 1000; // Offset
b1 |= (value >> 8) & 0x0f; // xxxx1111
int b2 = value & 0xff;
return String.format("X2%03d%03d%03d", 30, b1, b2);
}
// Setting variables and thresholds absolute not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command that send variable status updates.
* PCHK provides this variables by itself on selected segments
* is only possible with group 4
*
* @param var the target variable to set
* @param value the absolute value to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException
*/
public static String updateStatusVar(LcnDefs.Var var, int value) throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
// define variable to set, offset 0x01000000
int x2cmd = id | 0x40;
int b1 = (value >> 8) & 0xff;
int b2 = value & 0xff;
return String.format("X2%03d%03d%03d", x2cmd, b1, b2);
}
// Setting variables and thresholds absolute not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command that resets a variable to 0.
*
* @param var the target variable to set 0
* @param is2013 the target module's firmware version is 170101 or newer
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String varReset(LcnDefs.Var var, boolean is2013) throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
if (is2013) {
return String.format("Z-%03d%04d", id + 1, 4090);
} else {
if (id == 0) {
return "ZS30000";
} else {
throw new IllegalArgumentException();
}
}
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
// Set absolute = 0 (not in PCK yet)
int b1 = id << 6; // 01000000
b1 |= 0x20; // xx10xxxx (set absolute)
int b2 = 0;
return String.format("X2%03d%03d%03d", 30, b1, b2);
}
// Reset for threshold not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command to change the value of a variable.
*
* @param var the target variable to change
* @param type the reference-point
* @param value the native LCN value to add/subtract (can be negative)
* @param is2013 the target module's firmware version is 170101 or newer
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String varRel(LcnDefs.Var var, LcnDefs.RelVarRef type, int value, boolean is2013)
throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
if (id == 0) { // Old command for variable 1 / T-var (compatible with all modules)
return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
} else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", id + 1, Math.abs(value));
}
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
return String.format("RE%sS%s%s%d", id == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
value >= 0 ? "+" : "-", Math.abs(value));
}
int registerId = LcnDefs.Var.toThrsRegisterId(var);
id = LcnDefs.Var.toThrsId(var);
if (registerId != -1 && id != -1) {
if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
value >= 0 ? "A" : "S", registerId + 1, id + 1);
} else if (registerId == 0) { // Old command for register 1 (before 170206)
return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E",
Math.abs(value), value >= 0 ? "A" : "S", id == 0 ? "1" : "0", id == 1 ? "1" : "0",
id == 2 ? "1" : "0", id == 3 ? "1" : "0", id == 4 ? "1" : "0");
}
}
throw new IllegalArgumentException();
}
/**
* Generates a variable value request.
*
* @param var the variable to request
* @param swAge the target module's firmware version
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String requestVarStatus(LcnDefs.Var var, int swAge) throws IllegalArgumentException {
if (swAge >= 0x170206) {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
return String.format("MWT%03d", id + 1);
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
return String.format("MWS%03d", id + 1);
}
id = LcnDefs.Var.toThrsRegisterId(var);
if (id != -1) {
return String.format("SE%03d", id + 1); // Whole register
}
id = LcnDefs.Var.toS0Id(var);
if (id != -1) {
return String.format("MWC%03d", id + 1);
}
throw new IllegalArgumentException();
} else {
switch (var) {
case VAR1ORTVAR:
return "MWV";
case VAR2ORR1VAR:
return "MWTA";
case VAR3ORR2VAR:
return "MWTB";
case R1VARSETPOINT:
return "MWSA";
case R2VARSETPOINT:
return "MWSB";
case THRS1:
case THRS2:
case THRS3:
case THRS4:
case THRS5:
return "SL1"; // Whole register
default:
throw new IllegalArgumentException();
}
}
}
/**
* Generates a request for LED and logic-operations states.
*
* @return the PCK command (without address header) as text
*/
public static String requestLedsAndLogicOpsStatus() {
return "SMT";
}
/**
* Generates a command to the set the state of a single LED.
*
* @param ledId 0..11
* @param state the state to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String controlLed(int ledId, LcnDefs.LedStatus state) throws IllegalArgumentException {
if (ledId < 0 || ledId > 11) {
throw new IllegalArgumentException();
}
return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
: state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
}
/**
* Generates a command to send LCN keys.
*
* @param cmds the 4 concrete commands to send for the tables (A-D)
* @param keys the tables' 8 key-states (true means "send")
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws IllegalArgumentException {
if (cmds.length != 4 || keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = "TS";
for (int i = 0; i < 4; ++i) {
switch (cmds[i]) {
case HIT:
ret += "K";
break;
case MAKE:
ret += "L";
break;
case BREAK:
ret += "O";
break;
case DONTSEND:
// By skipping table D (if it is not used), we use the old command
// for table A-C which is compatible with older LCN modules
if (i < 3) {
ret += "-";
}
break;
default:
throw new Error();
}
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates a command to send LCN keys deferred / delayed.
*
* @param tableId 0(A)..3(D)
* @param time the delay time
* @param timeUnit the time unit
* @param keys the key-states (true means "send")
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
throws IllegalArgumentException {
if (tableId < 0 || tableId > 3 || keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = "TV";
switch (tableId) {
case 0:
ret += "A";
break;
case 1:
ret += "B";
break;
case 2:
ret += "C";
break;
case 3:
ret += "D";
break;
default:
throw new IllegalArgumentException();
}
ret += String.format("%03d", time);
switch (timeUnit) {
case SECONDS:
if (time < 1 || time > 60) {
throw new IllegalArgumentException();
}
ret += "S";
break;
case MINUTES:
if (time < 1 || time > 90) {
throw new IllegalArgumentException();
}
ret += "M";
break;
case HOURS:
if (time < 1 || time > 50) {
throw new IllegalArgumentException();
}
ret += "H";
break;
case DAYS:
if (time < 1 || time > 45) {
throw new IllegalArgumentException();
}
ret += "D";
break;
default:
throw new Error();
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates a request for key-lock states.
* Always requests table A-D. Supported since LCN-PCHK 2.8.
*
* @return the PCK command (without address header) as text
*/
public static String requestKeyLocksStatus() {
return "STX";
}
/**
* Generates a command to lock keys.
*
* @param tableId 0(A)..3(D)
* @param states the 8 key-lock modifiers
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws IllegalArgumentException {
if (tableId < 0 || tableId > 3 || states.length != 8) {
throw new IllegalArgumentException();
}
String ret = String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D");
for (int i = 0; i < 8; ++i) {
switch (states[i]) {
case ON:
ret += "1";
break;
case OFF:
ret += "0";
break;
case TOGGLE:
ret += "U";
break;
case NOCHANGE:
ret += "-";
break;
default:
throw new Error();
}
}
return ret;
}
/**
* Generates a command to lock keys for table A temporary.
* There is no hardware-support for locking tables B-D.
*
* @param time the lock time
* @param timeUnit the time unit
* @param keys the 8 key-lock states (true means lock)
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
throws IllegalArgumentException {
if (keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = String.format("TXZA%03d", time);
switch (timeUnit) {
case SECONDS:
if (time < 1 || time > 60) {
throw new IllegalArgumentException();
}
ret += "S";
break;
case MINUTES:
if (time < 1 || time > 90) {
throw new IllegalArgumentException();
}
ret += "M";
break;
case HOURS:
if (time < 1 || time > 50) {
throw new IllegalArgumentException();
}
ret += "H";
break;
case DAYS:
if (time < 1 || time > 45) {
throw new IllegalArgumentException();
}
ret += "D";
break;
default:
throw new Error();
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates the command header / start for sending dynamic texts.
* Used by LCN-GTxD periphery (supports 4 text rows).
* To complete the command, the text to send must be appended (UTF-8 encoding).
* Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
*
* @param row 0..3
* @param part 0..4
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String dynTextHeader(int row, int part) throws IllegalArgumentException {
if (row < 0 || row > 3 || part < 0 || part > 4) {
throw new IllegalArgumentException();
}
return String.format("GTDT%d%d", row + 1, part + 1);
}
/**
* Generates a command to lock a regulator.
*
* @param regId 0..1
* @param state the lock state
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockRegulator(int regId, boolean state) throws IllegalArgumentException {
if (regId < 0 || regId > 1) {
throw new IllegalArgumentException();
}
return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
}
}