2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lcn.internal.common;
15 import org.eclipse.jdt.annotation.NonNullByDefault;
16 import org.openhab.binding.lcn.internal.LcnBindingConstants;
17 import org.slf4j.Logger;
18 import org.slf4j.LoggerFactory;
21 * Helpers to generate LCN-PCK commands.
23 * LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
25 * @author Tobias Jüttner - Initial Contribution
26 * @author Fabian Wolter - Migration to OH2
29 public final class PckGenerator {
30 private static final Logger LOGGER = LoggerFactory.getLogger(PckGenerator.class);
31 /** Termination character after a PCK message */
32 public static final String TERMINATION = "\n";
35 * Generates a keep-alive.
36 * LCN-PCHK will close the connection if it does not receive any commands from
37 * an open {@link org.openhab.binding.lcn.internal.connection.Connection} for a specific period
38 * (10 minutes by default).
40 * @param counter the current ping's id (optional, but "best practice"). Should start with 1
41 * @return the PCK command as text
43 public static String ping(int counter) {
44 return String.format("^ping%d", counter);
48 * Generates a PCK command that will set the LCN-PCHK connection's operation mode.
49 * This influences how output-port commands and status are interpreted and must be
50 * in sync with the LCN bus.
52 * @param dimMode see {@link LcnDefs.OutputPortDimMode}
53 * @param statusMode see {@link LcnDefs.OutputPortStatusMode}
54 * @return the PCK command as text
56 public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
57 return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.NATIVE200 ? "1" : "0")
58 + (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
62 * Generates a PCK address header.
63 * Used for commands to LCN modules and groups.
65 * @param addr the target's address (module or group)
66 * @param localSegId the local segment id where the physical bus connection is located
67 * @param wantsAck true to claim an acknowledge / receipt from the target
68 * @return the PCK address header as text
70 public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
71 return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegmentId(localSegId),
72 addr.getId(), wantsAck ? "!" : ".");
76 * Generates a scan-command for LCN segment-couplers.
77 * Used to detect the local segment (where the physical bus connection is located).
79 * @return the PCK command (without address header) as text
81 public static String segmentCouplerScan() {
86 * Generates a firmware/serial-number request.
88 * @return the PCK command (without address header) as text
90 public static String requestSn() {
95 * Generates a command to request a part of a name of a module.
97 * @param partNumber 0..1
98 * @return the PCK command (without address header) as text
100 public static String requestModuleName(int partNumber) {
101 return "NMN" + (partNumber + 1);
105 * Generates an output-port status request.
107 * @param outputId 0..3
108 * @return the PCK command (without address header) as text
109 * @throws LcnException if out of range
111 public static String requestOutputStatus(int outputId) throws LcnException {
112 if (outputId < 0 || outputId > 3) {
113 throw new LcnException();
115 return String.format("SMA%d", outputId + 1);
119 * Generates a dim command for a single output-port.
121 * @param outputId 0..3
122 * @param percent 0..100
123 * @param rampMs ramp in milliseconds
124 * @return the PCK command (without address header) as text
125 * @throws LcnException if out of range
127 public static String dimOutput(int outputId, double percent, int rampMs) throws LcnException {
128 if (outputId < 0 || outputId > 3) {
129 throw new LcnException();
131 int rampNative = PckGenerator.timeToRampValue(rampMs);
132 int n = (int) Math.round(percent * 2);
133 if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
134 return String.format("A%dDI%03d%03d", outputId + 1, n / 2, rampNative);
135 } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
136 return String.format("O%dDI%03d%03d", outputId + 1, n, rampNative);
141 * Generates a command for setting the tunable white mode.
144 * @return the PCK command (without address header) as text
145 * @throws LcnException if out of range
147 public static String setTunableWhiteMode(int mode) throws LcnException {
148 if (mode < 0 || mode > 2) {
149 throw new LcnException();
152 return String.format("AW%d", mode);
156 * Generates a dim command for all output-ports.
158 * Attention: This command is supported since module firmware version 180501 AND LCN-PCHK 2.61
160 * @param firstPercent dimmer value of the first output 0..100
161 * @param secondPercent dimmer value of the first output 0..100
162 * @param thirdPercent dimmer value of the first output 0..100
163 * @param fourthPercent dimmer value of the first output 0..100
164 * @param rampMs ramp in milliseconds
165 * @return the PCK command (without address header) as text
167 public static String dimAllOutputs(double firstPercent, double secondPercent, double thirdPercent,
168 double fourthPercent, int rampMs) {
169 long n1 = Math.round(firstPercent * 2);
170 long n2 = Math.round(secondPercent * 2);
171 long n3 = Math.round(thirdPercent * 2);
172 long n4 = Math.round(fourthPercent * 2);
174 return String.format("OY%03d%03d%03d%03d%03d", n1, n2, n3, n4, timeToRampValue(rampMs));
178 * Generates a control command for switching all outputs ON or OFF with a fixed ramp of 0.5s.
180 * @param percent 0..100
181 * @return the PCK command (without address header) as text
183 public static String controlAllOutputs(double percent) {
184 return String.format("AH%03d", Math.round(percent));
188 * Generates a control command for switching dimmer output 1 and 2 both ON or OFF with a fixed ramp of 0.5s or
191 * @param on true, if outputs shall be switched on
192 * @param ramp true, if the ramp shall be 0.5s, else 0s
193 * @return the PCK command (without address header) as text
195 public static String controlOutputs12(boolean on, boolean ramp) {
198 commandByte = ramp ? 0xC8 : 0xFD;
200 commandByte = ramp ? 0x00 : 0xFC;
202 return String.format("X2%03d%03d%03d", 1, commandByte, commandByte);
206 * Generates a dim command for setting the brightness of dimmer output 1 and 2 with a fixed ramp of 0.5s.
208 * @param percent brightness of both outputs 0..100
209 * @return the PCK command (without address header) as text
211 public static String dimOutputs12(double percent) {
212 long localPercent = Math.round(percent);
213 return String.format("AY%03d%03d", localPercent, localPercent);
217 * Let an output flicker.
219 * @param outputId output id 0..3
220 * @param depth flicker depth, the higher the deeper 0..2
221 * @param ramp the flicker speed 0..2
222 * @param count number of flashes 1..15
223 * @return the PCK command (without address header) as text
224 * @throws LcnException when the input values are out of range
226 public static String flickerOutput(int outputId, int depth, int ramp, int count) throws LcnException {
227 if (outputId < 0 || outputId > 3) {
228 throw new LcnException("Output number out of range");
230 if (count < 1 || count > 15) {
231 throw new LcnException("Number of flashes out of range");
245 throw new LcnException("Depth out of range");
259 throw new LcnException("Ramp out of range");
261 return String.format("A%dFL%s%s%02d", outputId + 1, depthString, rampString, count);
265 * Generates a command to change the value of an output-port.
267 * @param outputId 0..3
268 * @param percent -100..100
269 * @return the PCK command (without address header) as text
270 * @throws LcnException if out of range
272 public static String relOutput(int outputId, double percent) throws LcnException {
273 if (outputId < 0 || outputId > 3) {
274 throw new LcnException();
276 int n = (int) Math.round(percent * 2);
277 if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
278 return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
279 } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
280 return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
285 * Generates a command that toggles a single output-port (on->off, off->on).
287 * @param outputId 0..3
288 * @param ramp see {@link PckGenerator#timeToRampValue(int)}
289 * @return the PCK command (without address header) as text
290 * @throws LcnException if out of range
292 public static String toggleOutput(int outputId, int ramp) throws LcnException {
293 if (outputId < 0 || outputId > 3) {
294 throw new LcnException();
296 return String.format("A%dTA%03d", outputId + 1, ramp);
300 * Generates a command that toggles all output-ports (on->off, off->on).
302 * @param ramp see {@link PckGenerator#timeToRampValue(int)}
303 * @return the PCK command (without address header) as text
305 public static String toggleAllOutputs(int ramp) {
306 return String.format("AU%03d", ramp);
310 * Generates a relays-status request.
312 * @return the PCK command (without address header) as text
314 public static String requestRelaysStatus() {
319 * Generates a command to control relays.
321 * @param states the 8 modifiers for the relay states
322 * @return the PCK command (without address header) as text
323 * @throws LcnException if out of range
325 public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws LcnException {
326 if (states.length != 8) {
327 throw new LcnException();
329 StringBuilder ret = new StringBuilder("R8");
330 for (int i = 0; i < 8; ++i) {
345 throw new LcnException();
348 return ret.toString();
352 * Generates a command to control the position of roller shutters on relays.
354 * @param motorNumber of the roller shutter (0-based)
355 * @param percent of the entire roller shutter height
356 * @return the PCK command (without address header) as text
357 * @throws LcnException if out of range
359 public static String controlShutterPosition(int motorNumber, int percent) throws LcnException {
360 return controlShutter(motorNumber, percent, "JH");
364 * Generates a command to control the slat angle of roller shutters on relays.
366 * @param motorNumber of the roller shutter (0-based)
367 * @param percent of the slat angle
368 * @return the PCK command (without address header) as text
369 * @throws LcnException if out of range
371 public static String controlShutterSlatAngle(int motorNumber, int percent) throws LcnException {
372 return controlShutter(motorNumber, percent, "JW");
375 private static String controlShutter(int motorNumber, int percent, String command) throws LcnException {
376 if (motorNumber < 0 || motorNumber >= 4) {
377 throw new LcnException("Roller shutter (relay) motor number out of range: " + motorNumber);
380 if (percent < 0 || percent > 100) {
381 throw new LcnException("Roller shutter (relay) position/angle out of range (percent): " + percent);
384 return String.format("%s%03d%03d", command, percent, 1 << motorNumber);
388 * Generates a binary-sensors status request.
390 * @return the PCK command (without address header) as text
392 public static String requestBinSensorsStatus() {
397 * Generates a command that sets a variable absolute.
399 * @param number regulator number 0..1
400 * @param value the absolute value to set
401 * @return the PCK command (without address header) as text
403 public static String setSetpointAbsolute(int number, int value) {
404 int internalValue = value;
405 // Set absolute (not in PCK yet)
406 int b1 = number << 6; // 01000000
407 b1 |= 0x20; // xx10xxxx (set absolute)
409 internalValue = 1000 - internalValue;
412 internalValue -= 1000;
414 b1 |= (internalValue >> 8) & 0x0f; // xxxx1111
415 int b2 = internalValue & 0xff;
416 return String.format("X2%03d%03d%03d", 30, b1, b2);
420 * Generates a command to change the regulator mode.
422 * @param number regulator number 0..1
423 * @param cooling true=cooling, false=heating
424 * @return the PCK command (without address header) as text
425 * @throws LcnException
427 public static String setRVarMode(int number, boolean cooling) throws LcnException {
437 throw new LcnException();
440 return "RE" + regulator + "T" + (cooling ? "C" : "H");
444 * Generates a command to change the value of a variable.
446 * @param variable the target variable to change
447 * @param type the reference-point
448 * @param value the native LCN value to add/subtract (can be negative)
449 * @return the PCK command (without address header) as text
451 public static String setVariableRelative(Variable variable, LcnDefs.RelVarRef type, int value) {
452 if (variable.getNumber() == 0) {
453 // Old command for variable 1 / T-var (compatible with all modules)
454 return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
455 } else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
456 return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", variable.getNumber() + 1, Math.abs(value));
461 * Generates a command the change the value of a regulator setpoint relative.
464 * @param type relative to the current or to the programmed value
465 * @param value the relative value -4000..+4000
466 * @return the PCK command (without address header) as text
468 public static String setSetpointRelative(int number, LcnDefs.RelVarRef type, int value) {
469 return String.format("RE%sS%s%s%d", number == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
470 value >= 0 ? "+" : "-", Math.abs(value));
474 * Generates a command the change the value of a threshold relative.
476 * @param variable the threshold to change
477 * @param type relative to the current or to the programmed value
478 * @param value the relative value -4000..+4000
479 * @param is2013 true, if the LCN module's firmware is equal to or newer than 2013
480 * @return the PCK command (without address header) as text
482 public static String setThresholdRelative(Variable variable, LcnDefs.RelVarRef type, int value, boolean is2013)
483 throws LcnException {
484 if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
485 return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
486 value >= 0 ? "A" : "S", variable.getNumber() + 1, variable.getThresholdNumber().get() + 1);
487 } else if (variable.getNumber() == 0) { // Old command for register 1 (before 170206)
488 return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
489 value >= 0 ? "A" : "S", variable.getThresholdNumber().get() == 0 ? "1" : "0",
490 variable.getThresholdNumber().get() == 1 ? "1" : "0",
491 variable.getThresholdNumber().get() == 2 ? "1" : "0",
492 variable.getThresholdNumber().get() == 3 ? "1" : "0",
493 variable.getThresholdNumber().get() == 4 ? "1" : "0");
495 throw new LcnException(
496 "Module does not have threshold register " + (variable.getThresholdNumber().get() + 1));
501 * Generates a variable value request.
503 * @param variable the variable to request
504 * @param firmwareVersion the target module's firmware version
505 * @return the PCK command (without address header) as text
506 * @throws LcnException if command is not supported
508 public static String requestVarStatus(Variable variable, int firmwareVersion) throws LcnException {
509 if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013) {
510 int id = variable.getNumber();
511 switch (variable.getType()) {
513 throw new LcnException("Variable unknown");
515 return "MWT" + (id + 1);
517 return "MWS" + (id + 1);
519 return "SE" + (id + 1); // Whole register
521 return "MWC" + (id + 1);
523 throw new LcnException("Unsupported variable type: " + variable);
536 case THRESHOLDREGISTER11:
537 case THRESHOLDREGISTER12:
538 case THRESHOLDREGISTER13:
539 case THRESHOLDREGISTER14:
540 case THRESHOLDREGISTER15:
541 return "SL1"; // Whole register
543 throw new LcnException("Unsupported variable type: " + variable);
549 * Generates a request for LED and logic-operations states.
551 * @return the PCK command (without address header) as text
553 public static String requestLedsAndLogicOpsStatus() {
558 * Generates a command to the set the state of a single LED.
561 * @param state the state to set
562 * @return the PCK command (without address header) as text
563 * @throws LcnException if out of range
565 public static String controlLed(int ledId, LcnDefs.LedStatus state) throws LcnException {
566 if (ledId < 0 || ledId > 11) {
567 throw new LcnException();
569 return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
570 : state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
574 * Generates a command to send LCN keys.
576 * @param cmds the 4 concrete commands to send for the tables (A-D)
577 * @param keys the tables' 8 key-states (true means "send")
578 * @return the PCK command (without address header) as text
579 * @throws LcnException if out of range
581 public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws LcnException {
582 if (cmds.length != 4 || keys.length != 8) {
583 throw new LcnException();
585 StringBuilder ret = new StringBuilder("TS");
586 for (int i = 0; i < 4; ++i) {
598 // By skipping table D (if it is not used), we use the old command
599 // for table A-C which is compatible with older LCN modules
605 throw new LcnException();
608 for (int i = 0; i < 8; ++i) {
609 ret.append(keys[i] ? "1" : "0");
611 return ret.toString();
615 * Generates a command to send LCN keys deferred / delayed.
617 * @param tableId 0(A)..3(D)
618 * @param time the delay time
619 * @param timeUnit the time unit
620 * @param keys the key-states (true means "send")
621 * @return the PCK command (without address header) as text
622 * @throws LcnException if out of range
624 public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
625 throws LcnException {
626 if (tableId < 0 || tableId > 3 || keys.length != 8) {
627 throw new LcnException();
629 StringBuilder ret = new StringBuilder("TV");
644 throw new LcnException();
646 ret.append(String.format("%03d", time));
649 if (time < 1 || time > 60) {
650 throw new LcnException();
655 if (time < 1 || time > 90) {
656 throw new LcnException();
661 if (time < 1 || time > 50) {
662 throw new LcnException();
667 if (time < 1 || time > 45) {
668 throw new LcnException();
673 throw new LcnException();
675 for (int i = 0; i < 8; ++i) {
676 ret.append(keys[i] ? "1" : "0");
678 return ret.toString();
682 * Generates a request for key-lock states.
683 * Always requests table A-D. Supported since LCN-PCHK 2.8.
685 * @return the PCK command (without address header) as text
687 public static String requestKeyLocksStatus() {
692 * Generates a command to lock keys.
694 * @param tableId 0(A)..3(D)
695 * @param states the 8 key-lock modifiers
696 * @return the PCK command (without address header) as text
697 * @throws LcnException if out of range
699 public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws LcnException {
700 if (tableId < 0 || tableId > 3 || states.length != 8) {
701 throw new LcnException();
703 StringBuilder ret = new StringBuilder(
704 String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D"));
705 for (int i = 0; i < 8; ++i) {
720 throw new LcnException();
723 return ret.toString();
727 * Generates a command to lock keys for table A temporary.
728 * There is no hardware-support for locking tables B-D.
730 * @param time the lock time
731 * @param timeUnit the time unit
732 * @param keys the 8 key-lock states (true means lock)
733 * @return the PCK command (without address header) as text
734 * @throws LcnException if out of range
736 public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) throws LcnException {
737 if (keys.length != 8) {
738 throw new LcnException();
740 StringBuilder ret = new StringBuilder(String.format("TXZA%03d", time));
743 if (time < 1 || time > 60) {
744 throw new LcnException();
749 if (time < 1 || time > 90) {
750 throw new LcnException();
755 if (time < 1 || time > 50) {
756 throw new LcnException();
761 if (time < 1 || time > 45) {
762 throw new LcnException();
767 throw new LcnException();
769 for (int i = 0; i < 8; ++i) {
770 ret.append(keys[i] ? "1" : "0");
772 return ret.toString();
776 * Generates the command header / start for sending dynamic texts.
777 * Used by LCN-GTxD periphery (supports 4 text rows).
778 * To complete the command, the text to send must be appended (UTF-8 encoding).
779 * Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
783 * @return the PCK command (without address header) as text
784 * @throws LcnException if out of range
786 public static String dynTextHeader(int row, int part) throws LcnException {
787 if (row < 0 || row > 3 || part < 0 || part > 4) {
788 throw new LcnException("Row number is out of range: " + (row + 1));
790 return String.format("GTDT%d%d", row + 1, part + 1);
794 * Generates a command to lock a regulator.
797 * @param state the lock state
798 * @return the PCK command (without address header) as text
799 * @throws LcnException if out of range
801 public static String lockRegulator(int regId, boolean state) throws LcnException {
802 if (regId < 0 || regId > 1) {
803 throw new LcnException();
805 return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
809 * Generates a command to start a relay timer
811 * @param relayNumber number of relay (1..8)
812 * @param duration duration in milliseconds
813 * @return the PCK command (without address header) as text
814 * @throws LcnException if out of range
816 public static String startRelayTimer(int relayNumber, double duration) throws LcnException {
817 if (relayNumber < 1 || relayNumber > 8 || duration < 30 || duration > 240960) {
818 throw new LcnException();
820 StringBuilder command = new StringBuilder("R8T");
821 command.append(String.format("%03d", convertMsecToLCNTimer(duration)));
822 StringBuilder data = new StringBuilder("--------");
823 data.setCharAt(relayNumber - 1, '1');
824 command.append(data);
825 return command.toString();
829 * Generates a command to set the beeping sound volume.
831 * @param volume the sound volume
832 * @return the PCK command (without address header) as text
833 * @throws LcnException if out of range
835 public static String setBeepVolume(double volume) throws LcnException {
836 if (volume < 0 || volume > 100) {
837 throw new LcnException();
840 return String.format("PIV%03d", Math.round(volume));
844 * Generates a command to let the beeper connected to the LCN module beep.
846 * @param tonality N=normal, S=special, 1-7 tonalities 1-7.
847 * @param count count number of beeps.
848 * @return the PCK command (without address header) as text
849 * @throws LcnException if out of range
851 public static String beep(String tonality, int count) throws LcnException {
852 LcnBindingConstants.ALLOWED_BEEP_TONALITIES.stream() //
853 .filter(t -> t.equals(tonality)) //
855 .orElseThrow(LcnException::new);
858 throw new LcnException();
861 return String.format("PI%s%d", tonality, Math.min(count, 50));
865 * Generates a null command, used for broadcast messages.
867 * @return the PCK command (without address header) as text
869 public static String nullCommand() {
874 * Converts the given time into an LCN ramp value.
876 * @param timeMSec the time in milliseconds
877 * @return the (LCN-internal) ramp value (0..250)
879 private static int timeToRampValue(int timeMSec) {
881 if (timeMSec < 250) {
883 } else if (timeMSec < 500) {
885 } else if (timeMSec < 660) {
887 } else if (timeMSec < 1000) {
889 } else if (timeMSec < 1400) {
891 } else if (timeMSec < 2000) {
893 } else if (timeMSec < 3000) {
895 } else if (timeMSec < 4000) {
897 } else if (timeMSec < 5000) {
899 } else if (timeMSec < 6000) {
902 ret = (timeMSec / 1000 - 6) / 2 + 10;
905 LOGGER.warn("Ramp value is too high. Limiting value to 486s.");
912 * Converts duration in milliseconds to lcntimer value
913 * Source: https://www.symcon.de/forum/threads/38603-LCN-Relais-Kurzzeit-Timer-umrechnen
915 * @param ms time in milliseconds
916 * @return lcn timer value
918 private static int convertMsecToLCNTimer(double ms) {
919 Integer lcntimer = -1;
920 if (ms >= 0 && ms <= 240960) {
921 double a = ms / 1000 / 0.03;
922 double b = (a / 32.0) + 1.0;
923 double c = Math.log(b) / Math.log(2);
924 double mod = Math.floor(c);
925 double faktor = 32 * (Math.pow(2, mod) - 1);
926 double offset = (a - faktor) / Math.pow(2, mod);
927 lcntimer = (int) (offset + mod * 32);
929 LOGGER.warn("Timer not in [0,240960] ms");