| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2022 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the GPL v2 as described in the | ||
| 10 | * included LICENSE.GPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | #include <cstdint> | ||
| 18 | #include <fcntl.h> | ||
| 19 | #include <termios.h> | ||
| 20 | #include <unistd.h> | ||
| 21 | |||
| 22 | #include "../Warnings.h" | ||
| 23 | #include "../common.h" | ||
| 24 | #include "../log.h" | ||
| 25 | |||
| 26 | #include "LORSerial.h" | ||
| 27 | |||
| 28 | #include "LOR.h" | ||
| 29 | |||
| 30 | #define LOR_INTENSITY_SIZE 6 | ||
| 31 | #define LOR_MAX_CHANNELS 3840 | ||
| 32 | #define LOR_MAX_UNIT_ID 0xF0 | ||
| 33 | #define LOR_MAX_BATCH_CMDS (LOR_MAX_BATCH_SIZE / LOR_INTENSITY_SIZE) | ||
| 34 | |||
| 35 | ///////////////////////////////////////////////////////////////////////////// | ||
| 36 | |||
| 37 | class LOROutputData : public LORSerialOutput { | ||
| 38 | public: | ||
| 39 | 10 | LOROutputData() { | |
| 40 |
2/2✓ Branch 0 taken 38400 times.
✓ Branch 1 taken 10 times.
|
38410 | for (int i = 0; i < LOR_MAX_CHANNELS; i++) { |
| 41 | 38400 | lastValue[i] = LOR_UNKNOWN; | |
| 42 | } | ||
| 43 | 10 | } | |
| 44 | |||
| 45 | int controllerOffset = 0; | ||
| 46 | unsigned char intensityMap[256]; | ||
| 47 | |||
| 48 | // What each unit was last sent, or LOR_UNKNOWN. | ||
| 49 | uint16_t lastValue[LOR_MAX_CHANNELS]; | ||
| 50 | // Frame number a channel was last queued in, so the refresh pass does not | ||
| 51 | // send a channel twice in one frame. | ||
| 52 | uint32_t queuedFrame[LOR_MAX_CHANNELS] = {}; | ||
| 53 | uint32_t frameNumber = 0; | ||
| 54 | |||
| 55 | // Where the next frame starts looking for changes, so a frame with more | ||
| 56 | // changes than fit the budget does not starve the higher channels. | ||
| 57 | int scanStart = 0; | ||
| 58 | // Next channel the background refresh resends. | ||
| 59 | int refreshAt = 0; | ||
| 60 | |||
| 61 | unsigned char batchData[LOR_MAX_BATCH_SIZE]; | ||
| 62 | int batchChannel[LOR_MAX_BATCH_CMDS]; | ||
| 63 | int batchCount = 0; | ||
| 64 | }; | ||
| 65 | |||
| 66 | ///////////////////////////////////////////////////////////////////////////// | ||
| 67 | |||
| 68 | #include "Plugin.h" | ||
| 69 | class LORPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin { | ||
| 70 | public: | ||
| 71 | 9 | LORPlugin() : | |
| 72 |
2/4✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
|
27 | FPPPlugins::Plugin("LOR") { |
| 73 | 9 | } | |
| 74 | 10 | virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override { | |
| 75 |
2/6✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
20 | return new LOROutput(startChannel, channelCount); |
| 76 | } | ||
| 77 | }; | ||
| 78 | |||
| 79 | extern "C" { | ||
| 80 | 9 | FPPPlugins::Plugin* createPlugin() { | |
| 81 |
2/6✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
18 | return new LORPlugin(); |
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | 10 | LOROutput::LOROutput(unsigned int startChannel, unsigned int channelCount) : | |
| 86 | ChannelOutput(startChannel, channelCount), | ||
| 87 | 10 | data(nullptr) {} | |
| 88 | 40 | LOROutput::~LOROutput() { | |
| 89 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
20 | if (data) { |
| 90 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
20 | delete data; |
| 91 | } | ||
| 92 | 40 | } | |
| 93 | |||
| 94 | 16 | void LOROutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) { | |
| 95 | 16 | addRange(m_startChannel, m_startChannel + m_channelCount - 1); | |
| 96 | 16 | } | |
| 97 | |||
| 98 | // LOR intensity is a whole percent: 0xF0 is off, 0x01 is full, and 1-99% | ||
| 99 | // are 228 - 2 * percent. The Pixie manual's intensity table is per percent | ||
| 100 | // (101 levels), and this is the table LOR's own software was captured using. | ||
| 101 | // A linear map over 0xF0..0x01 lands low values at or past the off end (so | ||
| 102 | // the bottom of a fade goes dark) and reads 50% as about 54%. | ||
| 103 | 8 | static void LOR_SetupIntensityMap(LOROutputData* privData) { | |
| 104 |
2/2✓ Branch 0 taken 2048 times.
✓ Branch 1 taken 8 times.
|
2056 | for (int i = 0; i < 256; i++) { |
| 105 | 2048 | int percent = (i * 100 + 127) / 255; | |
| 106 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2032 times.
|
2048 | if (percent == 0) { |
| 107 | 16 | privData->intensityMap[i] = 0xF0; | |
| 108 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2016 times.
|
2032 | } else if (percent == 100) { |
| 109 | 16 | privData->intensityMap[i] = 0x01; | |
| 110 | } else { | ||
| 111 | 2016 | privData->intensityMap[i] = 228 - percent * 2; | |
| 112 | } | ||
| 113 | } | ||
| 114 | 8 | } | |
| 115 | |||
| 116 | 8 | int LOROutput::Close(void) { | |
| 117 | 8 | LogDebug(VB_CHANNELOUT, "LOROutput::Close()\n"); | |
| 118 | 8 | data->StopHeartbeat(); | |
| 119 | 8 | data->closeSerialPort(); | |
| 120 | 8 | return ChannelOutput::Close(); | |
| 121 | } | ||
| 122 | |||
| 123 | 8 | void LOROutput::DumpConfig(void) { | |
| 124 | 8 | ChannelOutput::DumpConfig(); | |
| 125 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | if (data) { |
| 126 | 8 | data->dumpSerialConfig(); | |
| 127 | 8 | LogDebug(VB_CHANNELOUT, " port speed : %d\n", data->speed); | |
| 128 | 8 | LogDebug(VB_CHANNELOUT, " controllerOffset: %d\n", data->controllerOffset); | |
| 129 | } | ||
| 130 | 8 | } | |
| 131 | 10 | int LOROutput::Init(Json::Value config) { | |
| 132 | 10 | LogDebug(VB_CHANNELOUT, "LOROutput::Init()\n"); | |
| 133 |
2/6✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
10 | data = new LOROutputData(); |
| 134 | |||
| 135 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | if (config.isMember("speed")) { |
| 136 | 10 | data->speed = config["speed"].asInt(); | |
| 137 | } | ||
| 138 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | if (config.isMember("firstControllerId")) { |
| 139 | 10 | data->controllerOffset = config["firstControllerId"].asInt(); | |
| 140 | // Fix for box off by one error reported in issue 820 | ||
| 141 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | if (data->controllerOffset > 0) { |
| 142 | 10 | data->controllerOffset -= 1; | |
| 143 | } | ||
| 144 | } | ||
| 145 | // SendData() refuses (and logged) every frame of an output larger than | ||
| 146 | // LOR can address: say so once, here, and don't start it. | ||
| 147 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
|
10 | if (m_channelCount > LOR_MAX_CHANNELS) { |
| 148 | 1 | LogErr(VB_CHANNELOUT, "LOR carries at most %d channels, %u configured\n", | |
| 149 | LOR_MAX_CHANNELS, m_channelCount); | ||
| 150 | 1 | return 0; | |
| 151 | } | ||
| 152 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
|
9 | if (!data->setupSerialPort(config, data->speed, "8N1")) { |
| 153 | 1 | return 0; | |
| 154 | } | ||
| 155 | 8 | int lastUnit = data->controllerOffset + ((std::min(m_channelCount, (unsigned int)LOR_MAX_CHANNELS) + 15) >> 4); | |
| 156 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
|
8 | if (lastUnit > LOR_MAX_UNIT_ID) { |
| 157 | 1 | LogWarn(VB_CHANNELOUT, "LOR: %d channels starting at unit %d run past unit ID %d; channels beyond it are not sent\n", | |
| 158 | m_channelCount, data->controllerOffset + 1, LOR_MAX_UNIT_ID); | ||
| 159 | } | ||
| 160 | |||
| 161 | 8 | LOR_SetupIntensityMap(data); | |
| 162 | 8 | data->StartHeartbeat(); | |
| 163 | |||
| 164 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | return ChannelOutput::Init(config); |
| 165 | } | ||
| 166 | |||
| 167 | 1828 | static void LOR_QueueChannel(LOROutputData* privData, int channel, unsigned char value) { | |
| 168 | 1828 | unsigned char* cmd = &privData->batchData[privData->batchCount * LOR_INTENSITY_SIZE]; | |
| 169 | 1828 | cmd[0] = 0x00; | |
| 170 | 1828 | cmd[1] = privData->controllerOffset + (channel >> 4) + 1; | |
| 171 | 1828 | cmd[2] = 0x03; | |
| 172 | 1828 | cmd[3] = privData->intensityMap[value]; | |
| 173 | 1828 | cmd[4] = 0x80 | (channel & 0x0F); | |
| 174 | 1828 | cmd[5] = 0x00; | |
| 175 | 1828 | privData->batchChannel[privData->batchCount++] = channel; | |
| 176 | 1828 | privData->lastValue[channel] = value; | |
| 177 | 1828 | privData->queuedFrame[channel] = privData->frameNumber; | |
| 178 | 1828 | } | |
| 179 | |||
| 180 | 179 | int LOROutput::SendData(unsigned char* channelData) { | |
| 181 |
1/2✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
|
179 | LogDebug(VB_CHANNELDATA, "LOROutput::SendData()\n"); |
| 182 | |||
| 183 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
|
179 | if (m_channelCount > LOR_MAX_CHANNELS) { |
| 184 | ✗ | LogErr(VB_CHANNELOUT, | |
| 185 | "LOR_SendData() tried to send %d bytes when max is %d at %d baud\n", | ||
| 186 | m_channelCount, LOR_MAX_CHANNELS, data->speed); | ||
| 187 | ✗ | return 0; | |
| 188 | } | ||
| 189 | |||
| 190 | // Channels past unit 0xF0 cannot be addressed (higher IDs are reserved, | ||
| 191 | // 0xFF is broadcast), so they are left out rather than wrapped. | ||
| 192 | 179 | int count = std::min((int)m_channelCount, (LOR_MAX_UNIT_ID - data->controllerOffset) * 16); | |
| 193 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
|
179 | if (count <= 0) { |
| 194 | ✗ | return m_channelCount; | |
| 195 | } | ||
| 196 | |||
| 197 | // Changes that do not fit the frame's budget stay pending (lastValue is | ||
| 198 | // untouched) and go out on a later frame. | ||
| 199 |
1/2✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
|
179 | int maxCmds = data->FrameBudget() / LOR_INTENSITY_SIZE; |
| 200 | |||
| 201 | 179 | data->frameNumber++; | |
| 202 | 179 | data->batchCount = 0; | |
| 203 | |||
| 204 | 179 | int i = data->scanStart; | |
| 205 |
4/4✓ Branch 0 taken 3806 times.
✓ Branch 1 taken 71 times.
✓ Branch 2 taken 3698 times.
✓ Branch 3 taken 108 times.
|
3877 | for (int checked = 0; checked < count && data->batchCount < maxCmds; checked++) { |
| 206 |
2/2✓ Branch 0 taken 1757 times.
✓ Branch 1 taken 1941 times.
|
3698 | if (data->lastValue[i] != channelData[i]) { |
| 207 | 1757 | LOR_QueueChannel(data, i, channelData[i]); | |
| 208 | } | ||
| 209 |
2/2✓ Branch 0 taken 75 times.
✓ Branch 1 taken 3623 times.
|
3698 | if (++i == count) { |
| 210 | 75 | i = 0; | |
| 211 | } | ||
| 212 | } | ||
| 213 | 179 | data->scanStart = i; | |
| 214 | |||
| 215 | // With room left over, resend a few channels that have not changed. A | ||
| 216 | // unit that missed a command (line noise, waking up, power cycled) would | ||
| 217 | // otherwise hold the wrong value until the channel next changed, which | ||
| 218 | // for a static channel is never. Capped at a quarter of the frame so | ||
| 219 | // refresh never crowds out real changes, and to one pass over the | ||
| 220 | // channels per LOR_REFRESH_PERIOD so a small setup is not resent | ||
| 221 | // constantly. | ||
| 222 | 179 | int refreshCmds = std::min(maxCmds - data->batchCount, std::max(1, maxCmds / 4)); | |
| 223 | 179 | refreshCmds = std::min(refreshCmds, (int)((count * data->frameTime + LOR_REFRESH_PERIOD - 1) / LOR_REFRESH_PERIOD)); | |
| 224 |
3/4✓ Branch 0 taken 250 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✓ Branch 3 taken 179 times.
|
250 | for (int checked = 0; checked < count && refreshCmds > 0; checked++) { |
| 225 | 71 | int ch = data->refreshAt; | |
| 226 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 69 times.
|
71 | if (++data->refreshAt >= count) { |
| 227 | 2 | data->refreshAt = 0; | |
| 228 | } | ||
| 229 |
2/4✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✗ Branch 3 not taken.
|
71 | if (data->queuedFrame[ch] != data->frameNumber && data->lastValue[ch] != LOR_UNKNOWN) { |
| 230 | 71 | LOR_QueueChannel(data, ch, channelData[ch]); | |
| 231 | 71 | refreshCmds--; | |
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 |
1/2✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
|
179 | if (data->batchCount) { |
| 236 | 179 | int len = data->batchCount * LOR_INTENSITY_SIZE; | |
| 237 |
1/2✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
|
179 | int written = data->WriteBatch(data->batchData, len); |
| 238 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
|
179 | if (written < len) { |
| 239 | // Every command starts with 0x00, which flushes the unit's | ||
| 240 | // buffer, so a cut-off command is discarded. Its channel and | ||
| 241 | // every one after it has to be resent. | ||
| 242 | ✗ | for (int c = written / LOR_INTENSITY_SIZE; c < data->batchCount; c++) { | |
| 243 | ✗ | data->lastValue[data->batchChannel[c]] = LOR_UNKNOWN; | |
| 244 | } | ||
| 245 | } | ||
| 246 | } | ||
| 247 | 179 | return m_channelCount; | |
| 248 | } | ||
| 249 |