| 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 LGPL v2.1 as described in the | ||
| 10 | * included LICENSE.LGPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | // not in fpp-pch.h; libc++ pulls them in transitively but libstdc++ does not | ||
| 18 | #include <cmath> | ||
| 19 | #include <memory> | ||
| 20 | #include <tuple> | ||
| 21 | |||
| 22 | #include "../Sequence.h" | ||
| 23 | #include "../log.h" | ||
| 24 | |||
| 25 | #include "PixelString.h" | ||
| 26 | #include "ChannelOutputSetup.h" | ||
| 27 | #include "../OutputMonitor.h" | ||
| 28 | #include "../Warnings.h" | ||
| 29 | #include "overlays/PixelOverlay.h" | ||
| 30 | |||
| 31 | ///////////////////////////////////////////////////////////////////////////// | ||
| 32 | |||
| 33 | #define MAX_PIXEL_STRING_LENGTH 1600 | ||
| 34 | |||
| 35 | constexpr uint32_t SMART_RECEIVER_LEADIN = 6 * 3; | ||
| 36 | constexpr uint32_t SMART_RECEIVER_LEN = 6 * 3; | ||
| 37 | constexpr uint32_t SMART_RECEIVER_LEADOUT = 6 * 3; | ||
| 38 | |||
| 39 | constexpr uint32_t SMART_RECEIVER_V2_GAP = 10; | ||
| 40 | |||
| 41 | #define CHECKPS_SETTING(SETTING) \ | ||
| 42 | if (SETTING) { \ | ||
| 43 | LogErr(VB_CHANNELOUT, "Invalid PixelString Config %s\n", #SETTING); \ | ||
| 44 | AddOutputConfigWarning(26, "Invalid PixelString Config: " + std::string(#SETTING)); \ | ||
| 45 | return 0; \ | ||
| 46 | } | ||
| 47 | |||
| 48 | // one config key per receiver; the hasChannels array in Init() is sized to | ||
| 49 | // match, so nothing may index either of them past this | ||
| 50 | constexpr int MAX_SMART_RECEIVERS = 6; | ||
| 51 | |||
| 52 | const char* SMART_RECEIVER_LABELS[MAX_SMART_RECEIVERS] = { | ||
| 53 | "virtualStrings", | ||
| 54 | "virtualStringsB", | ||
| 55 | "virtualStringsC", | ||
| 56 | "virtualStringsD", | ||
| 57 | "virtualStringsE", | ||
| 58 | "virtualStringsF", | ||
| 59 | }; | ||
| 60 | |||
| 61 | 578 | VirtualString::VirtualString() : | |
| 62 | 578 | receiverNum(-1), | |
| 63 | 578 | startChannel(0), | |
| 64 | 578 | pixelCount(0), | |
| 65 | 578 | groupCount(0), | |
| 66 | 578 | reverse(0), | |
| 67 | 578 | colorOrder(FPPColorOrder::kColorOrderRGB), | |
| 68 | 578 | startNulls(0), | |
| 69 | 578 | endNulls(0), | |
| 70 | 578 | zigZag(0), | |
| 71 | 578 | brightness(100), | |
| 72 | 578 | gamma(1.0), | |
| 73 | 578 | leadInCount(0), | |
| 74 | 578 | toggleCount(0), | |
| 75 | 578 | leadOutCount(0) { | |
| 76 | // never leave this dangling - ReadVirtualString normally replaces it, but | ||
| 77 | // a default constructed string is used directly for empty ports | ||
| 78 |
1/2✓ Branch 0 taken 578 times.
✗ Branch 1 not taken.
|
578 | brightnessMap = BrightnessLUTCache::get(100, 1.0f, false); |
| 79 | 578 | } | |
| 80 | |||
| 81 | ✗ | VirtualString::VirtualString(int rt, int rn) : | |
| 82 | ✗ | receiverNum(rn), | |
| 83 | ✗ | startChannel(0), | |
| 84 | ✗ | pixelCount(0), | |
| 85 | ✗ | groupCount(0), | |
| 86 | ✗ | reverse(0), | |
| 87 | ✗ | colorOrder(FPPColorOrder::kColorOrderRGB), | |
| 88 | ✗ | startNulls(0), | |
| 89 | ✗ | endNulls(0), | |
| 90 | ✗ | zigZag(0), | |
| 91 | ✗ | brightness(100), | |
| 92 | ✗ | gamma(1.0), | |
| 93 | ✗ | leadInCount(0), | |
| 94 | ✗ | toggleCount(0), | |
| 95 | ✗ | leadOutCount(0) { | |
| 96 | ✗ | switch (rt) { | |
| 97 | ✗ | case 0: | |
| 98 | ✗ | leadInCount = 0; | |
| 99 | ✗ | toggleCount = SMART_RECEIVER_LEN; | |
| 100 | ✗ | leadOutCount = SMART_RECEIVER_LEADOUT; | |
| 101 | ✗ | break; | |
| 102 | ✗ | case 1: | |
| 103 | ✗ | leadInCount = SMART_RECEIVER_LEADIN; | |
| 104 | ✗ | toggleCount = SMART_RECEIVER_LEN * 2; | |
| 105 | ✗ | leadOutCount = SMART_RECEIVER_LEADOUT; | |
| 106 | ✗ | break; | |
| 107 | ✗ | case 2: | |
| 108 | ✗ | leadInCount = SMART_RECEIVER_LEADIN; | |
| 109 | ✗ | toggleCount = SMART_RECEIVER_LEN * 3; | |
| 110 | ✗ | leadOutCount = SMART_RECEIVER_LEADOUT; | |
| 111 | ✗ | break; | |
| 112 | ✗ | case 3: | |
| 113 | ✗ | leadInCount = SMART_RECEIVER_V2_GAP; | |
| 114 | ✗ | toggleCount = 0; | |
| 115 | ✗ | leadOutCount = 0; | |
| 116 | ✗ | break; | |
| 117 | ✗ | case 4: // Falcon v5 | |
| 118 | ✗ | leadInCount = 0; | |
| 119 | ✗ | toggleCount = 0; | |
| 120 | ✗ | leadOutCount = 0; | |
| 121 | ✗ | break; | |
| 122 | } | ||
| 123 | ✗ | pixelCount = leadInCount + toggleCount + leadOutCount; | |
| 124 | // identity: full brightness, no gamma | ||
| 125 | ✗ | brightnessMap = BrightnessLUTCache::get(100, 1.0f, false); | |
| 126 | ✗ | } | |
| 127 | namespace { | ||
| 128 | struct LUTKey { | ||
| 129 | int brightness; | ||
| 130 | int gammaScaled; | ||
| 131 | bool inverted; | ||
| 132 | 2120 | bool operator<(const LUTKey& o) const { | |
| 133 |
1/2✓ Branch 0 taken 2120 times.
✗ Branch 1 not taken.
|
4010 | return std::tie(brightness, gammaScaled, inverted) < |
| 134 | 4240 | std::tie(o.brightness, o.gammaScaled, o.inverted); | |
| 135 | } | ||
| 136 | }; | ||
| 137 | std::map<LUTKey, std::unique_ptr<std::array<uint8_t, 256>>> g_luts; | ||
| 138 | std::map<LUTKey, std::unique_ptr<std::array<uint16_t, 256>>> g_wideLuts; | ||
| 139 | std::mutex g_lutLock; | ||
| 140 | |||
| 141 | // gamma is quantised so it can key the cache; build from the quantised value | ||
| 142 | // so two strings sharing a key get byte-identical tables whichever is built | ||
| 143 | // first. 1/10000 is past any configurable gamma - 2.2f round trips exactly. | ||
| 144 | 1149 | int scaleGamma(float gamma) { | |
| 145 | 1149 | return (int)std::lround(gamma * 10000.0f); | |
| 146 | } | ||
| 147 | } // namespace | ||
| 148 | |||
| 149 | 1149 | const uint8_t* BrightnessLUTCache::get(int brightness, float gamma, bool inverted) { | |
| 150 | // Quantise gamma so it can key the cache, then build from the quantised | ||
| 151 | // value rather than the caller's: two strings that share a key must get | ||
| 152 | // byte-identical tables whichever of them is built first. 1/10000 is fine | ||
| 153 | // past any gamma anyone configures - 2.2f round trips exactly. | ||
| 154 | 1149 | int gammaScaled = scaleGamma(gamma); | |
| 155 | 1149 | LUTKey key{ brightness, gammaScaled, inverted }; | |
| 156 | |||
| 157 |
1/2✓ Branch 0 taken 1149 times.
✗ Branch 1 not taken.
|
1149 | std::lock_guard<std::mutex> lock(g_lutLock); |
| 158 |
1/2✓ Branch 0 taken 1149 times.
✗ Branch 1 not taken.
|
1149 | auto it = g_luts.find(key); |
| 159 |
2/2✓ Branch 0 taken 1056 times.
✓ Branch 1 taken 93 times.
|
1149 | if (it != g_luts.end()) { |
| 160 | 2112 | return it->second->data(); | |
| 161 | } | ||
| 162 | |||
| 163 |
1/2✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
|
93 | auto table = std::make_unique<std::array<uint8_t, 256>>(); |
| 164 | // identical to the original per-string computation, so the tables are | ||
| 165 | // byte for byte what they always were (at 100/1.0 that is the identity | ||
| 166 | // map, which is what the smart receiver markers want) | ||
| 167 | 93 | float maxB = brightness * 2.55f; | |
| 168 | 93 | float g = gammaScaled / 10000.0f; | |
| 169 |
2/2✓ Branch 0 taken 23808 times.
✓ Branch 1 taken 93 times.
|
23901 | for (int x = 0; x < 256; x++) { |
| 170 | 23808 | float f = maxB * pow((float)x / 255.0f, g); | |
| 171 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 23808 times.
|
23808 | if (f > 255.0) { |
| 172 | ✗ | f = 255.0; | |
| 173 | } | ||
| 174 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 23808 times.
|
23808 | if (f < 0.0) { |
| 175 | ✗ | f = 0.0; | |
| 176 | } | ||
| 177 | 23808 | uint8_t v = (uint8_t)round(f); | |
| 178 |
2/2✓ Branch 0 taken 512 times.
✓ Branch 1 taken 23296 times.
|
23808 | (*table)[x] = inverted ? (uint8_t)~v : v; |
| 179 | } | ||
| 180 | 93 | const uint8_t* p = table->data(); | |
| 181 |
1/2✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
|
93 | g_luts[key] = std::move(table); |
| 182 | 93 | return p; | |
| 183 | 1149 | } | |
| 184 | |||
| 185 | ✗ | const uint16_t* BrightnessLUTCache::getWide(int brightness, float gamma, bool inverted) { | |
| 186 | ✗ | int gammaScaled = scaleGamma(gamma); | |
| 187 | ✗ | LUTKey key{ brightness, gammaScaled, inverted }; | |
| 188 | |||
| 189 | ✗ | std::lock_guard<std::mutex> lock(g_lutLock); | |
| 190 | ✗ | auto it = g_wideLuts.find(key); | |
| 191 | ✗ | if (it != g_wideLuts.end()) { | |
| 192 | ✗ | return it->second->data(); | |
| 193 | } | ||
| 194 | |||
| 195 | ✗ | auto table = std::make_unique<std::array<uint16_t, 256>>(); | |
| 196 | // the same curve as the 8 bit table, evaluated to full 16 bit range | ||
| 197 | ✗ | float maxB = brightness * 655.35f; | |
| 198 | ✗ | float g = gammaScaled / 10000.0f; | |
| 199 | ✗ | for (int x = 0; x < 256; x++) { | |
| 200 | ✗ | float f = maxB * pow((float)x / 255.0f, g); | |
| 201 | ✗ | if (f > 65535.0) { | |
| 202 | ✗ | f = 65535.0; | |
| 203 | } | ||
| 204 | ✗ | if (f < 0.0) { | |
| 205 | ✗ | f = 0.0; | |
| 206 | } | ||
| 207 | ✗ | uint16_t v = (uint16_t)llround(f); | |
| 208 | ✗ | (*table)[x] = inverted ? (uint16_t)~v : v; | |
| 209 | } | ||
| 210 | ✗ | const uint16_t* p = table->data(); | |
| 211 | ✗ | g_wideLuts[key] = std::move(table); | |
| 212 | ✗ | return p; | |
| 213 | ✗ | } | |
| 214 | |||
| 215 | ✗ | size_t BrightnessLUTCache::tableCount() { | |
| 216 | ✗ | std::lock_guard<std::mutex> lock(g_lutLock); | |
| 217 | ✗ | return g_luts.size() + g_wideLuts.size(); | |
| 218 | ✗ | } | |
| 219 | |||
| 220 | 4 | bool VirtualString::isSmartReceiver() const { | |
| 221 | 4 | return receiverNum >= 0; | |
| 222 | } | ||
| 223 | 109 | bool VirtualString::isFalconV5SmartReceiver() const { | |
| 224 |
1/8✗ Branch 0 not taken.
✓ Branch 1 taken 109 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
109 | return receiverNum >= 0 && leadInCount == 0 && toggleCount == 0 && leadOutCount == 0; |
| 225 | } | ||
| 226 | |||
| 227 | 159195 | int VirtualString::channelsPerNode() const { | |
| 228 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 159195 times.
|
159195 | if (receiverNum >= 0) { |
| 229 | ✗ | return 1; | |
| 230 | } | ||
| 231 | 159195 | return colorOrder.channelCount(); | |
| 232 | } | ||
| 233 | |||
| 234 | /* | ||
| 235 | * | ||
| 236 | */ | ||
| 237 | 570 | PixelString::PixelString(bool supportSmart) : | |
| 238 | 570 | m_portNumber(0), | |
| 239 | 570 | m_channelOffset(0), | |
| 240 | 570 | m_outputBytes(0), | |
| 241 | 570 | m_isSmartReceiver(supportSmart), | |
| 242 | 570 | m_brightnessMaps(nullptr), | |
| 243 |
1/2✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
|
570 | m_outputBuffer(nullptr) { |
| 244 | 570 | } | |
| 245 | |||
| 246 | /* | ||
| 247 | * | ||
| 248 | */ | ||
| 249 | 570 | PixelString::~PixelString() { | |
| 250 |
2/2✓ Branch 0 taken 506 times.
✓ Branch 1 taken 64 times.
|
570 | if (!m_pinConfig.empty()) { |
| 251 | 506 | OutputMonitor::INSTANCE.RemovePortConfiguration(m_portNumber, m_pinConfig); | |
| 252 | } | ||
| 253 |
2/2✓ Branch 0 taken 568 times.
✓ Branch 1 taken 2 times.
|
570 | if (m_brightnessMaps) { |
| 254 | 568 | free(m_brightnessMaps); | |
| 255 | } | ||
| 256 |
2/2✓ Branch 0 taken 568 times.
✓ Branch 1 taken 2 times.
|
570 | if (m_outputBuffer) { |
| 257 | 568 | free(m_outputBuffer); | |
| 258 | } | ||
| 259 | 570 | } | |
| 260 | |||
| 261 | /* | ||
| 262 | * | ||
| 263 | */ | ||
| 264 | 570 | int PixelString::Init(Json::Value config, Json::Value* pinConfig) { | |
| 265 |
2/4✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
|
570 | m_portNumber = config["portNumber"].asInt(); |
| 266 | // Drivers index per-port arrays (smart receiver packets, GPIO commands, | ||
| 267 | // current monitoring) by this, unchecked. | ||
| 268 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 569 times.
|
570 | if (m_portNumber < 0) { |
| 269 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogErr(VB_CHANNELOUT, "Pixel string has an invalid port number %d\n", m_portNumber); |
| 270 | 1 | return 0; | |
| 271 | } | ||
| 272 | 569 | m_outputBytes = 0; | |
| 273 | // needed before the virtual strings are read so their lookup tables are | ||
| 274 | // built at the right width | ||
| 275 |
3/6✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 569 times.
✗ Branch 5 not taken.
|
569 | m_bytesPerChannel = protocolBytesPerChannel(config["protocol"].asString()); |
| 276 | |||
| 277 | 569 | int receiverCount = 1; | |
| 278 |
7/8✓ Branch 0 taken 510 times.
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 510 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 472 times.
✓ Branch 5 taken 38 times.
✓ Branch 6 taken 472 times.
✓ Branch 7 taken 97 times.
|
569 | if (m_isSmartReceiver && config.isMember("differentialType")) { |
| 279 | 472 | smartReceiverType = ReceiverType::Standard; | |
| 280 |
2/4✓ Branch 0 taken 472 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 472 times.
✗ Branch 3 not taken.
|
472 | int rt = config["differentialType"].asInt(); |
| 281 |
2/2✓ Branch 0 taken 34 times.
✓ Branch 1 taken 438 times.
|
472 | if (rt) { |
| 282 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
|
34 | if (rt <= 3) { |
| 283 | // v1 smart receivers | ||
| 284 | ✗ | receiverCount = rt; | |
| 285 | ✗ | smartReceiverType = ReceiverType::v1; | |
| 286 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 26 times.
|
34 | } else if (rt > 15) { |
| 287 | // Falcon V4 smart receivers (send-only config packet) | ||
| 288 | 8 | receiverCount = rt - 15; | |
| 289 | 8 | smartReceiverType = ReceiverType::FalconV4; | |
| 290 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 4 times.
|
26 | } else if (rt > 9) { |
| 291 | // Falcon V5 smart receivers (bidirectional) | ||
| 292 | 22 | receiverCount = rt - 9; | |
| 293 | 22 | smartReceiverType = ReceiverType::FalconV5; | |
| 294 | } else { | ||
| 295 | // v2 smart receivers | ||
| 296 | 4 | receiverCount = rt - 3; | |
| 297 | 4 | smartReceiverType = ReceiverType::v2; | |
| 298 | } | ||
| 299 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
|
34 | if (receiverCount > MAX_SMART_RECEIVERS) { |
| 300 | // only the Falcon v4 range is open ended (rt - 15), so a hand | ||
| 301 | // edited differentialType is the only way to get here | ||
| 302 | ✗ | LogWarn(VB_CHANNELOUT, "Invalid smart receiver count %d on port %d; using %d\n", | |
| 303 | receiverCount, m_portNumber + 1, MAX_SMART_RECEIVERS); | ||
| 304 | ✗ | AddOutputConfigWarning(UNKNOWN_WARNING_ID, "PixelString: invalid smart receiver count on port " + | |
| 305 | ✗ | std::to_string(m_portNumber + 1) + ", using " + | |
| 306 | ✗ | std::to_string(MAX_SMART_RECEIVERS)); | |
| 307 | ✗ | receiverCount = MAX_SMART_RECEIVERS; | |
| 308 | } | ||
| 309 | } | ||
| 310 | 472 | m_isSmartReceiver = smartReceiverType != ReceiverType::Standard; | |
| 311 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 472 times.
|
472 | if (m_bytesPerChannel != 1 && |
| 312 | ✗ | (smartReceiverType == ReceiverType::v1 || smartReceiverType == ReceiverType::v2)) { | |
| 313 | // v1/v2 receivers interleave fixed width marker patterns with the | ||
| 314 | // pixel data, so a wide part would need two widths in one stream. | ||
| 315 | // Falcon v4/v5 markers are zero length, so those are fine. | ||
| 316 | ✗ | LogWarn(VB_CHANNELOUT, "16 bit protocols are not supported with v1/v2 smart receivers on port %d; using 8 bit\n", m_portNumber + 1); | |
| 317 | ✗ | AddOutputConfigWarning(UNKNOWN_WARNING_ID, "PixelString: 16 bit protocol not supported with v1/v2 smart receivers on port " + std::to_string(m_portNumber + 1)); | |
| 318 | ✗ | m_bytesPerChannel = 1; | |
| 319 | } | ||
| 320 | } else { | ||
| 321 | 97 | smartReceiverType = ReceiverType::None; | |
| 322 | 97 | m_isSmartReceiver = false; | |
| 323 | } | ||
| 324 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
|
569 | if (smartReceiverType == ReceiverType::v1) { |
| 325 | // v1 needs a lead in | ||
| 326 | ✗ | AddVirtualString(VirtualString(0, 0)); | |
| 327 | } | ||
| 328 | 569 | int startMaxChan = m_outputBytes; | |
| 329 |
4/6✓ Branch 0 taken 1138 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1138 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 570 times.
✓ Branch 5 taken 568 times.
|
1138 | for (int i = 0; i < config["virtualStrings"].size(); i++) { |
| 330 |
3/6✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 570 times.
✗ Branch 5 not taken.
|
570 | Json::Value vsc = config["virtualStrings"][i]; |
| 331 |
1/2✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
|
570 | VirtualString vs; |
| 332 |
3/4✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 569 times.
|
570 | if (!ReadVirtualString(vsc, vs)) { |
| 333 | 1 | return 0; | |
| 334 | } | ||
| 335 |
1/2✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
|
569 | AddVirtualString(vs); |
| 336 |
4/4✓ Branch 0 taken 569 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 569 times.
✓ Branch 3 taken 1 times.
|
571 | } |
| 337 | std::array<bool, MAX_SMART_RECEIVERS> hasChannels; | ||
| 338 | 568 | hasChannels[0] = startMaxChan != m_outputBytes; | |
| 339 |
5/8✓ Branch 0 taken 8 times.
✓ Branch 1 taken 560 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 568 times.
|
568 | if (!hasChannels[0] && (smartReceiverType == ReceiverType::v1 || smartReceiverType == ReceiverType::v2)) { |
| 340 | // we need to output at least 1 pixel | ||
| 341 | ✗ | AddNullPixelString(); | |
| 342 | } | ||
| 343 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 568 times.
|
568 | for (int p = 1; p < receiverCount; p++) { |
| 344 | ✗ | int sz = m_virtualStrings.size(); | |
| 345 | ✗ | int mc = m_outputBytes; | |
| 346 | |||
| 347 | ✗ | if (smartReceiverType == ReceiverType::v1) { | |
| 348 | // v1 smart receiver | ||
| 349 | ✗ | AddVirtualString(VirtualString(p, p)); | |
| 350 | ✗ | } else if (smartReceiverType == ReceiverType::v2) { | |
| 351 | // v2 smart receiver, .15ms gap | ||
| 352 | ✗ | AddVirtualString(VirtualString(3, p)); | |
| 353 | ✗ | } else if (smartReceiverType == ReceiverType::FalconV5 || smartReceiverType == ReceiverType::FalconV4) { | |
| 354 | // Falcon v4/v5, just marker to determine config packets later | ||
| 355 | ✗ | AddVirtualString(VirtualString(4, p)); | |
| 356 | } | ||
| 357 | ✗ | startMaxChan = m_outputBytes; | |
| 358 | ✗ | for (int i = 0; i < config[SMART_RECEIVER_LABELS[p]].size(); i++) { | |
| 359 | ✗ | Json::Value vsc = config[SMART_RECEIVER_LABELS[p]][i]; | |
| 360 | ✗ | VirtualString vs; | |
| 361 | ✗ | if (!ReadVirtualString(vsc, vs)) { | |
| 362 | ✗ | return 0; | |
| 363 | } | ||
| 364 | ✗ | AddVirtualString(vs); | |
| 365 | ✗ | } | |
| 366 | ✗ | hasChannels[p] = startMaxChan != m_outputBytes; | |
| 367 | ✗ | hasChannels[0] |= hasChannels[p]; | |
| 368 | ✗ | if (!hasChannels[p]) { | |
| 369 | ✗ | if (p != (receiverCount - 1)) { | |
| 370 | // we need to output at least 1 pixel | ||
| 371 | ✗ | AddNullPixelString(); | |
| 372 | } else { | ||
| 373 | // in this case, we can revert and just send one less receivers of data | ||
| 374 | ✗ | m_outputBytes = mc; | |
| 375 | ✗ | while (sz != m_virtualStrings.size()) { | |
| 376 | ✗ | m_virtualStrings.pop_back(); | |
| 377 | } | ||
| 378 | } | ||
| 379 | } | ||
| 380 | } | ||
| 381 | |||
| 382 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 560 times.
|
568 | if (!hasChannels[0]) { |
| 383 | // empty, no strings | ||
| 384 | 8 | m_outputBytes = 0; | |
| 385 | 8 | m_virtualStrings.clear(); | |
| 386 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | AddVirtualString(VirtualString()); |
| 387 | } | ||
| 388 |
2/2✓ Branch 0 taken 508 times.
✓ Branch 1 taken 60 times.
|
568 | if (pinConfig) { |
| 389 |
1/2✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
|
508 | m_pinConfig = *pinConfig; |
| 390 |
1/2✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
|
508 | OutputMonitor::INSTANCE.AddPortConfiguration(m_portNumber, m_pinConfig, m_outputBytes > 0); |
| 391 | } | ||
| 392 | |||
| 393 |
1/2✓ Branch 0 taken 568 times.
✗ Branch 1 not taken.
|
568 | m_outputMap.resize(m_outputBytes); |
| 394 | |||
| 395 | // Initialize all maps to an unused location which should be zero. | ||
| 396 | // We need this so that null nodes in the middle of a string are sent | ||
| 397 | // all zeroes to keep them dark. | ||
| 398 |
2/2✓ Branch 0 taken 44336 times.
✓ Branch 1 taken 568 times.
|
44904 | for (int i = 0; i < m_outputBytes; i++) { |
| 399 | 44336 | m_outputMap[i] = FPPD_OFF_CHANNEL; | |
| 400 | } | ||
| 401 | |||
| 402 | 568 | int offset = 0; | |
| 403 | 568 | int mapIndex = 0; | |
| 404 | |||
| 405 | 568 | m_brightnessMaps = (const uint8_t**)calloc(1, sizeof(const uint8_t*) * m_outputBytes); | |
| 406 | |||
| 407 |
2/2✓ Branch 0 taken 569 times.
✓ Branch 1 taken 568 times.
|
1137 | for (int i = 0; i < m_virtualStrings.size(); i++) { |
| 408 | 569 | m_virtualStrings[i].chMap = &m_outputMap[offset]; | |
| 409 | 569 | int start = offset; | |
| 410 |
1/2✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
|
569 | offset += m_virtualStrings[i].startNulls * m_virtualStrings[i].channelsPerNode(); |
| 411 |
1/2✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
|
569 | SetupMap(offset, m_virtualStrings[i]); |
| 412 |
1/2✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
|
569 | offset += m_virtualStrings[i].pixelCount * m_virtualStrings[i].channelsPerNode(); |
| 413 |
1/2✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
|
569 | offset += m_virtualStrings[i].endNulls * m_virtualStrings[i].channelsPerNode(); |
| 414 | 569 | m_virtualStrings[i].chMapCount = offset - start; | |
| 415 |
5/8✓ Branch 0 taken 44905 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44905 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44905 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 44336 times.
✓ Branch 7 taken 569 times.
|
44905 | for (int j = 0; j < ((m_virtualStrings[i].startNulls * m_virtualStrings[i].channelsPerNode()) + (m_virtualStrings[i].pixelCount * m_virtualStrings[i].channelsPerNode()) + (m_virtualStrings[i].endNulls * m_virtualStrings[i].channelsPerNode())); j++) |
| 416 | 44336 | m_brightnessMaps[mapIndex++] = m_virtualStrings[i].brightnessMap; | |
| 417 | } | ||
| 418 | // A 16 bit part clocks two bytes per colour channel. The channel maps | ||
| 419 | // above are per channel and stay as they are - only the wire length grows. | ||
| 420 | // Marker strings from a smart receiver are raw wire bytes and do not | ||
| 421 | // widen, but the only receivers allowed here (Falcon v4/v5) have zero | ||
| 422 | // length markers, so this is a straight doubling. | ||
| 423 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 568 times.
|
568 | if (m_bytesPerChannel != 1) { |
| 424 | ✗ | m_outputBytes *= m_bytesPerChannel; | |
| 425 | } | ||
| 426 | |||
| 427 | // Protocol preamble, if any. It has to land after the channel maps are | ||
| 428 | // built (those are sized for the pixel bytes only) and before the gpio off | ||
| 429 | // command below, which needs the full wire length. | ||
| 430 |
11/16✓ Branch 0 taken 560 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 560 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 560 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 560 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 558 times.
✓ Branch 10 taken 560 times.
✓ Branch 11 taken 8 times.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 566 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
|
568 | if (m_outputBytes && protocolNeedsTM1814Preamble(config["protocol"].asString())) { |
| 431 | // per channel drive current; the part powers up at its 6.5mA floor, so | ||
| 432 | // without this a TM1814 string just looks dim | ||
| 433 |
2/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
2 | float mA = config.isMember("tm1814MilliAmps") ? config["tm1814MilliAmps"].asFloat() : 38.0f; |
| 434 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | m_preamble = buildTM1814Preamble(mA); |
| 435 | 2 | m_outputBytes += m_preamble.size(); | |
| 436 | } | ||
| 437 | |||
| 438 | // turn gpio off after all channels on this port are done | ||
| 439 |
2/2✓ Branch 0 taken 560 times.
✓ Branch 1 taken 8 times.
|
568 | if (m_outputBytes) { |
| 440 |
1/2✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
|
560 | m_gpioCommands.push_back(GPIOCommand(m_portNumber, m_outputBytes)); |
| 441 | } | ||
| 442 | |||
| 443 | // certain testing capabilities (like pixel counting) may require a | ||
| 444 | // buffer larger than the number of pixels configured | ||
| 445 | 568 | int obs = std::max(2400, m_outputBytes); | |
| 446 | 568 | m_outputBuffer = (uint8_t*)calloc(obs, 1); | |
| 447 | |||
| 448 | // Cape declared inversion corrects a wiring defect; protocol inversion is | ||
| 449 | // the pixel type asking for it. Both flip the same line, so they compose | ||
| 450 | // by xor and cancel when a TM18xx string lands on a backwards-wired port. | ||
| 451 |
3/6✓ Branch 0 taken 568 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 568 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 568 times.
✗ Branch 5 not taken.
|
568 | m_protocolInverted = isInvertedProtocol(config["protocol"].asString()); |
| 452 | // isMember first: pinConfig points into the cape config and a bare | ||
| 453 | // operator[] would insert a null member into it | ||
| 454 |
4/12✓ Branch 0 taken 508 times.
✓ Branch 1 taken 60 times.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 508 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
568 | bool wireInverted = pinConfig && pinConfig->isMember("inverted") && (*pinConfig)["inverted"].asBool(); |
| 455 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 566 times.
|
568 | if (m_protocolInverted != wireInverted) { |
| 456 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | invertOutput(); |
| 457 | } | ||
| 458 | |||
| 459 | // after any inversion, so the bytes copied in are the wire form | ||
| 460 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 566 times.
|
568 | if (!m_preamble.empty()) { |
| 461 | 2 | memcpy(m_outputBuffer, m_preamble.data(), m_preamble.size()); | |
| 462 | } | ||
| 463 | |||
| 464 | 568 | return 1; | |
| 465 | } | ||
| 466 | |||
| 467 | // TM1814 frames are "C1 C2 D1..Dn Reset". C1 carries four 6 bit constant | ||
| 468 | // current levels in the chip's physical W,R,G,B pin order with the top two | ||
| 469 | // bits of each byte fixed at 0; C2 is the bitwise complement of C1 and the | ||
| 470 | // chip will not decode the frame without it. Every chip in the chain reads | ||
| 471 | // and forwards both, so this is once per frame, not once per pixel. | ||
| 472 | 2 | std::vector<uint8_t> PixelString::buildTM1814Preamble(float milliAmps) { | |
| 473 | // 64 levels spanning 6.5mA (all zero) to 38mA (all ones) | ||
| 474 | 2 | constexpr float MIN_MA = 6.5f; | |
| 475 | 2 | constexpr float MAX_MA = 38.0f; | |
| 476 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | milliAmps = std::clamp(milliAmps, MIN_MA, MAX_MA); |
| 477 | 2 | uint8_t level = (uint8_t)std::lround((milliAmps - MIN_MA) * 63.0f / (MAX_MA - MIN_MA)) & 0x3F; | |
| 478 | |||
| 479 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | std::vector<uint8_t> p(8); |
| 480 | // C1: W, R, G, B - all four channels get the same level. This is the | ||
| 481 | // chip's pin order and is independent of the string's color order, which | ||
| 482 | // only decides how channel data lands in the W/R/G/B slots of Dn. | ||
| 483 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
|
10 | for (int i = 0; i < 4; i++) { |
| 484 | 8 | p[i] = level; | |
| 485 | 8 | p[i + 4] = ~level; | |
| 486 | } | ||
| 487 | 2 | return p; | |
| 488 | } | ||
| 489 | |||
| 490 | // Per protocol bit cell. Anything not listed is the 800kHz ws281x family, | ||
| 491 | // which is the overwhelming majority and what the timing has always been. | ||
| 492 | // | ||
| 493 | // The listed ones are genuinely different parts, not aliases: driving them | ||
| 494 | // with the ws281x cell does not merely shift an edge, it puts the pulse | ||
| 495 | // outside the window the part decodes. That is why they were kept out of the | ||
| 496 | // protocol dropdown until the firmware could take its timing at runtime. | ||
| 497 | 304 | PixelString::Timing PixelString::protocolTiming(const std::string& protocol) { | |
| 498 | // the ws281x family - see the alias list in co-pixelStrings.php | ||
| 499 | static const Timing ws281x{ 320, 750, 1120 }; | ||
| 500 | |||
| 501 | // 400kHz parts: a 2.5us cell, twice the ws281x period | ||
| 502 |
6/8✓ Branch 0 taken 286 times.
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 286 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
✓ Branch 6 taken 18 times.
✓ Branch 7 taken 286 times.
|
304 | if (protocol == "ucs1903" || protocol == "ws2811slow" || protocol == "ws2811 slow") { |
| 503 | 18 | return { 500, 2000, 2500 }; | |
| 504 | } | ||
| 505 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
|
286 | if (protocol == "tm1803") { |
| 506 | ✗ | return { 750, 1875, 2625 }; | |
| 507 | } | ||
| 508 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
|
286 | if (protocol == "gw6205") { |
| 509 | ✗ | return { 750, 1625, 2375 }; | |
| 510 | } | ||
| 511 | // longer cell parts, between the two | ||
| 512 |
3/6✓ Branch 0 taken 286 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 286 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
|
286 | if (protocol == "tm1804" || protocol == "tm1809slow") { |
| 513 | ✗ | return { 500, 1000, 1500 }; | |
| 514 | } | ||
| 515 |
3/6✓ Branch 0 taken 286 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 286 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
|
286 | if (protocol == "sk6822" || protocol == "pl9823") { |
| 516 | ✗ | return { 375, 1375, 1750 }; | |
| 517 | } | ||
| 518 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
|
286 | if (protocol == "ucs1912") { |
| 519 | ✗ | return { 250, 1250, 1625 }; | |
| 520 | } | ||
| 521 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
|
286 | if (protocol == "ge8822") { |
| 522 | ✗ | return { 375, 1000, 1375 }; | |
| 523 | } | ||
| 524 | 286 | return ws281x; | |
| 525 | } | ||
| 526 | |||
| 527 | 569 | int PixelString::protocolBytesPerChannel(const std::string& protocol) { | |
| 528 | // single wire 16 bit parts; bit depths follow the pixel table in xLights | ||
| 529 | // (src-core/models/Pixels.cpp). The bare name is the 16 bit part itself, | ||
| 530 | // and the explicit "16 bit"/"(16)" spellings some controllers use mean the | ||
| 531 | // same thing here. | ||
| 532 |
2/4✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
|
1138 | if (protocol == "ucs8903" || protocol == "ucs8904" || protocol == "ucs7604" || |
| 533 |
4/8✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 569 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 569 times.
|
1707 | protocol == "ucs8903 16 bit" || protocol == "ucs8904 16 bit" || |
| 534 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
|
569 | protocol == "ucs7604 16 bit") { |
| 535 | ✗ | return 2; | |
| 536 | } | ||
| 537 | 569 | return 1; | |
| 538 | } | ||
| 539 | |||
| 540 | 560 | bool PixelString::protocolNeedsTM1814Preamble(const std::string& protocol) { | |
| 541 |
3/4✓ Branch 0 taken 558 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 558 times.
|
560 | return protocol == "tm1814" || protocol == "tm1814a"; |
| 542 | } | ||
| 543 | |||
| 544 | 426 | void PixelString::setPixelDataChannels(int channels) { | |
| 545 | // a port clamped to nothing sends nothing, preamble included - the same | ||
| 546 | // rule Init uses when deciding to build one at all | ||
| 547 |
2/2✓ Branch 0 taken 396 times.
✓ Branch 1 taken 30 times.
|
426 | m_outputBytes = channels ? channels * m_bytesPerChannel + (int)m_preamble.size() : 0; |
| 548 | 426 | } | |
| 549 | |||
| 550 | ✗ | void PixelString::dropPreamble() { | |
| 551 | ✗ | if (m_preamble.empty()) { | |
| 552 | ✗ | return; | |
| 553 | } | ||
| 554 | ✗ | int n = (int)m_preamble.size(); | |
| 555 | ✗ | m_outputBytes -= n; | |
| 556 | // the preamble shifted the whole stream, so every command past it moves back | ||
| 557 | ✗ | for (auto& c : m_gpioCommands) { | |
| 558 | ✗ | if (c.channelOffset >= n) { | |
| 559 | ✗ | c.channelOffset -= n; | |
| 560 | } | ||
| 561 | } | ||
| 562 | ✗ | m_preamble.clear(); | |
| 563 | ✗ | memset(m_outputBuffer, 0, n); | |
| 564 | } | ||
| 565 | |||
| 566 | 568 | bool PixelString::isInvertedProtocol(const std::string& protocol) { | |
| 567 | // Only the protocols FPP can actually drive today. The TM18xx bit cell | ||
| 568 | // is close enough to ws2811 that the shared timing covers it; what makes | ||
| 569 | // them different is the idle level. | ||
| 570 |
3/4✓ Branch 0 taken 566 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 566 times.
|
568 | return protocol == "tm1814" || protocol == "tm1814a"; |
| 571 | } | ||
| 572 | |||
| 573 | 577 | void PixelString::AddVirtualString(const VirtualString& vs) { | |
| 574 | 577 | m_outputBytes += vs.startNulls * vs.channelsPerNode(); | |
| 575 | 577 | m_outputBytes += vs.pixelCount * vs.channelsPerNode(); | |
| 576 | 577 | m_outputBytes += vs.endNulls * vs.channelsPerNode(); | |
| 577 | |||
| 578 | 577 | m_virtualStrings.push_back(vs); | |
| 579 | 577 | } | |
| 580 | |||
| 581 | 570 | int PixelString::ReadVirtualString(Json::Value& vsc, VirtualString& vs) const { | |
| 582 | 570 | vs.startChannel = vsc["startChannel"].asInt(); | |
| 583 | 570 | vs.pixelCount = vsc["pixelCount"].asInt(); | |
| 584 | 570 | vs.groupCount = vsc["groupCount"].asInt(); | |
| 585 | 570 | vs.reverse = vsc["reverse"].asInt(); | |
| 586 | 570 | vs.startNulls = vsc["nullNodes"].asInt(); | |
| 587 |
1/2✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
|
570 | if (vsc.isMember("description")) { |
| 588 |
2/4✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
|
570 | vs.description = vsc["description"].asString(); |
| 589 | } | ||
| 590 |
1/2✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
|
570 | if (vsc.isMember("endNulls")) { |
| 591 | 570 | vs.endNulls = vsc["endNulls"].asInt(); | |
| 592 | } else { | ||
| 593 | ✗ | vs.endNulls = 0; | |
| 594 | } | ||
| 595 | 570 | vs.zigZag = vsc["zigZag"].asInt(); | |
| 596 | 570 | vs.brightness = vsc["brightness"].asInt(); | |
| 597 |
2/4✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
|
570 | vs.gamma = atof(vsc["gamma"].asString().c_str()); |
| 598 | |||
| 599 |
2/4✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 570 times.
|
570 | if (vs.brightness > 100 || vs.brightness < 0) { |
| 600 | ✗ | vs.brightness = 100; | |
| 601 | } | ||
| 602 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 568 times.
|
570 | if (!std::isfinite(vs.gamma)) { |
| 603 | // "nan" or "inf" in a hand-edited config: not a gamma anyone chose, | ||
| 604 | // so use the default rather than clamping it to an end of the range | ||
| 605 | // (NaN fails both clamps below and landed on 0.01, a curve that | ||
| 606 | // drives every lit channel near full brightness) | ||
| 607 | 2 | vs.gamma = 1.0f; | |
| 608 | } | ||
| 609 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
|
570 | if (vs.gamma < 0.01f) { |
| 610 | ✗ | vs.gamma = 0.01f; | |
| 611 | } | ||
| 612 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
|
570 | if (vs.gamma > 50.0f) { |
| 613 | ✗ | vs.gamma = 50.0f; | |
| 614 | } | ||
| 615 | |||
| 616 |
1/8✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
570 | CHECKPS_SETTING(vs.pixelCount < 0); |
| 617 |
1/8✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
570 | CHECKPS_SETTING(vs.pixelCount > MAX_PIXEL_STRING_LENGTH); |
| 618 |
2/2✓ Branch 0 taken 562 times.
✓ Branch 1 taken 8 times.
|
570 | if (vs.pixelCount) { |
| 619 | // only validate settings and such if there are pixels on this string | ||
| 620 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
563 | CHECKPS_SETTING(vs.startChannel < 0); |
| 621 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.startChannel > FPPD_MAX_CHANNELS); |
| 622 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.startNulls < 0); |
| 623 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.startNulls > MAX_PIXEL_STRING_LENGTH); |
| 624 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.endNulls < 0); |
| 625 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.endNulls > MAX_PIXEL_STRING_LENGTH); |
| 626 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING((vs.startNulls + vs.pixelCount + vs.endNulls) > MAX_PIXEL_STRING_LENGTH); |
| 627 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.reverse < 0); |
| 628 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.reverse > 1); |
| 629 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.groupCount < 0); |
| 630 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.groupCount > vs.pixelCount); |
| 631 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.zigZag > vs.pixelCount); |
| 632 |
1/10✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
562 | CHECKPS_SETTING(vs.zigZag < 0); |
| 633 | |||
| 634 |
2/4✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 562 times.
✗ Branch 3 not taken.
|
562 | std::string colorOrder = vsc["colorOrder"].asString(); |
| 635 |
1/2✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
|
562 | vs.colorOrder = ColorOrderFromString(colorOrder); |
| 636 | |||
| 637 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
|
562 | if (vs.groupCount == 1) |
| 638 | ✗ | vs.groupCount = 0; | |
| 639 | |||
| 640 | // The start is checked above, but not where the string ends: | ||
| 641 | // prepareOutput() reads up to the last node's channels, and the drivers | ||
| 642 | // drop channels at or past FPPD_MAX_CHANNELS from the range they | ||
| 643 | // report, so the loader's range check never saw the overrun. | ||
| 644 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
|
562 | int64_t vsNodes = vs.groupCount ? (vs.pixelCount + vs.groupCount - 1) / vs.groupCount : vs.pixelCount; |
| 645 |
1/2✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
|
562 | int64_t vsEnd = (int64_t)vs.startChannel + vsNodes * vs.channelsPerNode(); |
| 646 |
6/10✓ Branch 0 taken 1 times.
✓ Branch 1 taken 561 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
|
564 | CHECKPS_SETTING(vsEnd > FPPD_MAX_CHANNELS); |
| 647 | |||
| 648 |
2/4✓ Branch 0 taken 561 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 561 times.
|
561 | if ((vs.zigZag == vs.pixelCount) || (vs.zigZag == 1)) |
| 649 | ✗ | vs.zigZag = 0; | |
| 650 | |||
| 651 |
1/2✓ Branch 0 taken 561 times.
✗ Branch 1 not taken.
|
561 | vs.brightnessMap = BrightnessLUTCache::get(vs.brightness, vs.gamma, false); |
| 652 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 561 times.
|
561 | if (m_bytesPerChannel == 2) { |
| 653 | ✗ | vs.brightnessMap16 = BrightnessLUTCache::getWide(vs.brightness, vs.gamma, false); | |
| 654 | } | ||
| 655 |
2/2✓ Branch 0 taken 561 times.
✓ Branch 1 taken 1 times.
|
562 | } else { |
| 656 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
8 | vs.colorOrder = ColorOrderFromString("RGB"); |
| 657 | 8 | vs.groupCount = 1; | |
| 658 | 8 | vs.zigZag = 0; | |
| 659 | 8 | vs.reverse = 0; | |
| 660 | 8 | vs.startNulls = 0; | |
| 661 | 8 | vs.endNulls = 0; | |
| 662 | 8 | vs.startChannel = 0; | |
| 663 | 8 | vs.brightnessMap = BrightnessLUTCache::get(100, 1.0f, false); | |
| 664 | } | ||
| 665 | |||
| 666 | 569 | return 1; | |
| 667 | } | ||
| 668 | ✗ | void PixelString::AddNullPixelString() { | |
| 669 | ✗ | VirtualString vs; | |
| 670 | ✗ | vs.pixelCount = 1; | |
| 671 | ✗ | vs.startChannel = FPPD_OFF_CHANNEL; | |
| 672 | ✗ | AddVirtualString(vs); | |
| 673 | ✗ | } | |
| 674 | |||
| 675 | 569 | void PixelString::SetupMap(int vsOffset, const VirtualString& vs) { | |
| 676 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
|
569 | if (vs.receiverNum >= 0) { |
| 677 | // smart receiever, add codes to outputs | ||
| 678 | // drop low to trigger receiver to start trying to figure out which data is next | ||
| 679 | ✗ | if (vs.leadInCount > 0) { | |
| 680 | ✗ | m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset)); | |
| 681 | ✗ | for (int x = 0; x < vs.channelsPerNode() * vs.leadInCount; x++) { | |
| 682 | ✗ | m_outputMap[vsOffset++] = FPPD_OFF_CHANNEL; | |
| 683 | } | ||
| 684 | // Turn back on | ||
| 685 | ✗ | m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset, 1)); | |
| 686 | } | ||
| 687 | ✗ | if (vs.toggleCount) { | |
| 688 | // Toggle a bunch back and forth | ||
| 689 | ✗ | for (int x = 0; x < vs.channelsPerNode() * vs.toggleCount; x++) { | |
| 690 | // using white allows an even up/down on the GPIO | ||
| 691 | ✗ | m_outputMap[vsOffset++] = FPPD_WHITE_CHANNEL; | |
| 692 | } | ||
| 693 | } | ||
| 694 | ✗ | if (vs.leadOutCount) { | |
| 695 | // drop low again and turn off so receiver can enable/disable whichever outputs are needed | ||
| 696 | ✗ | m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset)); | |
| 697 | ✗ | for (int x = 0; x < vs.channelsPerNode() * vs.leadOutCount; x++) { | |
| 698 | ✗ | m_outputMap[vsOffset++] = FPPD_OFF_CHANNEL; | |
| 699 | } | ||
| 700 | // turn gpio back on | ||
| 701 | ✗ | m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset, 1)); | |
| 702 | } | ||
| 703 | ✗ | return; | |
| 704 | } | ||
| 705 | |||
| 706 | 569 | int offset = vsOffset; | |
| 707 | 569 | int group = 0; | |
| 708 | 569 | int maxGroups = 0; | |
| 709 | 569 | int itemsInGroup = 0; | |
| 710 | 569 | int ch = 0; | |
| 711 | 569 | int pStart = 0; | |
| 712 | 569 | int ch1 = 0; | |
| 713 | 569 | int ch2 = 0; | |
| 714 | 569 | int ch3 = 0; | |
| 715 | |||
| 716 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
|
569 | if (vs.groupCount) { |
| 717 | ✗ | maxGroups = vs.pixelCount / vs.groupCount; | |
| 718 | } | ||
| 719 | |||
| 720 | 569 | int redOffset = vs.colorOrder.redOffset(); | |
| 721 | 569 | int greenOffset = vs.colorOrder.greenOffset(); | |
| 722 | 569 | int blueOffset = vs.colorOrder.blueOffset(); | |
| 723 | 569 | int whiteOffset = vs.colorOrder.whiteOffset(); | |
| 724 | |||
| 725 |
2/2✓ Branch 0 taken 14771 times.
✓ Branch 1 taken 569 times.
|
15340 | for (int p = pStart; p < vs.pixelCount; p++) { |
| 726 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 14771 times.
|
14771 | if (vs.groupCount) { |
| 727 | ✗ | if (itemsInGroup >= vs.groupCount) { | |
| 728 | ✗ | group++; | |
| 729 | ✗ | itemsInGroup = 0; | |
| 730 | } | ||
| 731 | |||
| 732 | ✗ | ch = vs.startChannel + (group * vs.channelsPerNode()); | |
| 733 | |||
| 734 | ✗ | itemsInGroup++; | |
| 735 | } else { | ||
| 736 | 14771 | ch = vs.startChannel + (p * vs.channelsPerNode()); | |
| 737 | } | ||
| 738 | |||
| 739 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 14761 times.
|
14771 | if (vs.colorOrder == FPPColorOrder::kColorOrderONE) { |
| 740 | 10 | m_outputMap[offset++] = ch; | |
| 741 | } else { | ||
| 742 | // incoming data is always RGB or RGBW, map to whatever order is | ||
| 743 | // needed. The offsets are positions within the output node with | ||
| 744 | // W counted, so for WRGB red is at 1, not 0. | ||
| 745 | 14761 | m_outputMap[offset + redOffset] = ch; // red is always data[0] | |
| 746 | 14761 | m_outputMap[offset + greenOffset] = ch + 1; // green is always data[1] | |
| 747 | 14761 | m_outputMap[offset + blueOffset] = ch + 2; // blue is always data[2] | |
| 748 |
2/2✓ Branch 0 taken 19 times.
✓ Branch 1 taken 14742 times.
|
14761 | if (whiteOffset >= 0) { |
| 749 | 19 | m_outputMap[offset + whiteOffset] = ch + 3; // W is always data[3] | |
| 750 | 19 | offset += 4; | |
| 751 | } else { | ||
| 752 | 14742 | offset += 3; | |
| 753 | } | ||
| 754 | } | ||
| 755 | } | ||
| 756 | |||
| 757 | 569 | DumpMap("BEFORE ZIGZAG"); | |
| 758 | |||
| 759 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 567 times.
|
569 | if (vs.zigZag) { |
| 760 | 2 | int chanCount = vs.channelsPerNode(); | |
| 761 | 2 | int zigChannelCount = vs.zigZag * chanCount; | |
| 762 | // the folds belong to this virtual string, so they are counted from | ||
| 763 | // its first pixel and stop at its last. Anchoring at the port instead | ||
| 764 | // re-flips segments owned by the other strings sharing the port, and | ||
| 765 | // any lead in or null nodes ahead of this one shift every boundary. | ||
| 766 | 2 | int vsEnd = vsOffset + (vs.pixelCount * chanCount); | |
| 767 | |||
| 768 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (int i = vsOffset; i < vsEnd; i += zigChannelCount) { |
| 769 | 6 | int segment = (i - vsOffset) / zigChannelCount; | |
| 770 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
|
6 | if (segment % 2) { |
| 771 | 2 | int offset1 = i; | |
| 772 | 2 | int offset2 = i + zigChannelCount - chanCount; | |
| 773 | |||
| 774 | // the whole of the segment's last node has to be there - it is | ||
| 775 | // chanCount channels wide, which is 4 for RGBW, not 3 | ||
| 776 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if ((offset2 + chanCount) <= vsEnd) |
| 777 | 2 | FlipPixels(offset1, offset2, chanCount); | |
| 778 | } | ||
| 779 | } | ||
| 780 | |||
| 781 | 2 | DumpMap("AFTER ZIGZAG"); | |
| 782 | } | ||
| 783 | |||
| 784 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 567 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
569 | if (vs.reverse && (vs.pixelCount > 1)) { |
| 785 | 2 | FlipPixels(vsOffset, vsOffset + (vs.pixelCount * vs.channelsPerNode()) - vs.channelsPerNode(), vs.channelsPerNode()); | |
| 786 | |||
| 787 | 2 | DumpMap("AFTER REVERSE"); | |
| 788 | } | ||
| 789 | } | ||
| 790 | |||
| 791 | /* | ||
| 792 | * | ||
| 793 | */ | ||
| 794 | 573 | void PixelString::DumpMap(const char* msg) { | |
| 795 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 573 times.
|
573 | if (WillLog(LOG_EXCESSIVE, VB_CHANNELOUT)) { |
| 796 | ✗ | LogExcess(VB_CHANNELOUT, "OutputMap: %s\n", msg); | |
| 797 | ✗ | for (int i = 0; i < m_outputBytes; i++) { | |
| 798 | ✗ | LogExcess(VB_CHANNELOUT, "map[%d] = %d\n", i, m_outputMap[i]); | |
| 799 | } | ||
| 800 | } | ||
| 801 | 573 | } | |
| 802 | |||
| 803 | /* | ||
| 804 | * | ||
| 805 | */ | ||
| 806 | 4 | void PixelString::FlipPixels(int offset1, int offset2, int chanCount) { | |
| 807 | 4 | int flipPixels = (offset2 - offset1 + chanCount) / chanCount / 2; | |
| 808 | |||
| 809 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4 times.
|
15 | for (int i = 0; i < flipPixels; i++) { |
| 810 | // a node is exactly chanCount channels wide - 1 for a single colour | ||
| 811 | // string, 3 for RGB, 4 for RGBW - so swapping a fixed three either | ||
| 812 | // steps outside the node or leaves part of it behind | ||
| 813 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 11 times.
|
34 | for (int c = 0; c < chanCount; c++) { |
| 814 | 23 | int ch = m_outputMap[offset1 + c]; | |
| 815 | 23 | m_outputMap[offset1 + c] = m_outputMap[offset2 + c]; | |
| 816 | 23 | m_outputMap[offset2 + c] = ch; | |
| 817 | } | ||
| 818 | |||
| 819 | 11 | offset1 += chanCount; | |
| 820 | 11 | offset2 -= chanCount; | |
| 821 | } | ||
| 822 | 4 | } | |
| 823 | |||
| 824 | /* | ||
| 825 | * | ||
| 826 | */ | ||
| 827 | 603 | void PixelString::DumpConfig(void) { | |
| 828 | 603 | LogDebug(VB_CHANNELOUT, " port number : %d\n", m_portNumber); | |
| 829 | |||
| 830 | 603 | int count = 0; | |
| 831 |
2/2✓ Branch 0 taken 605 times.
✓ Branch 1 taken 603 times.
|
1208 | for (auto& vs : m_virtualStrings) { |
| 832 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | if (vs.receiverNum == -1) { |
| 833 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " --- Virtual String #%d ---\n", ++count); |
| 834 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " description : %s\n", vs.description.c_str()); |
| 835 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " start channel : %d\n", vs.startChannel + m_channelOffset); |
| 836 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " pixel count : %d\n", vs.pixelCount); |
| 837 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " group count : %d\n", vs.groupCount); |
| 838 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " reverse : %d\n", vs.reverse); |
| 839 |
2/4✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 605 times.
✗ Branch 3 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " color order : %s\n", ColorOrderToString(vs.colorOrder).c_str()); |
| 840 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " start nulls : %d\n", vs.startNulls); |
| 841 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " end nulls : %d\n", vs.endNulls); |
| 842 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " zig zag : %d\n", vs.zigZag); |
| 843 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " brightness : %d\n", vs.brightness); |
| 844 |
1/2✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
|
605 | LogDebug(VB_CHANNELOUT, " gamma : %.3f\n", vs.gamma); |
| 845 | } | ||
| 846 | } | ||
| 847 | 603 | } | |
| 848 | |||
| 849 | // Everything downstream of here holds wire form, not logical form: on an | ||
| 850 | // inverted port the line idles high and the data phase drives high for a | ||
| 851 | // logical zero, so every byte that goes out has to be complemented. The | ||
| 852 | // brightness maps carry that for pixel data; the preamble needs it too. | ||
| 853 | 2 | void PixelString::invertOutput() { | |
| 854 | 2 | m_isInverted = !m_isInverted; | |
| 855 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | for (auto& vs : m_virtualStrings) { |
| 856 | // the tables are shared, so swap to the complemented one rather than | ||
| 857 | // flipping this string's in place and corrupting every other user | ||
| 858 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | int b = vs.isSmartReceiver() ? 100 : vs.brightness; |
| 859 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | float g = vs.isSmartReceiver() ? 1.0f : vs.gamma; |
| 860 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | vs.brightnessMap = BrightnessLUTCache::get(b, g, m_isInverted); |
| 861 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (vs.brightnessMap16) { |
| 862 | ✗ | vs.brightnessMap16 = BrightnessLUTCache::getWide(b, g, m_isInverted); | |
| 863 | } | ||
| 864 | } | ||
| 865 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
|
18 | for (auto& b : m_preamble) { |
| 866 | 16 | b = ~b; | |
| 867 | } | ||
| 868 | 2 | } | |
| 869 | |||
| 870 | 84 | void PixelString::AutoCreateOverlayModels(const std::vector<PixelString*>& strings, std::list<std::string>& autoModelNames) { | |
| 871 |
2/2✓ Branch 0 taken 83 times.
✓ Branch 1 taken 1 times.
|
84 | if (PixelOverlayManager::INSTANCE.isAutoCreatePixelOverlayModels()) { |
| 872 | 83 | std::map<std::string, std::vector<VirtualString*>> vstrings; | |
| 873 |
2/2✓ Branch 0 taken 393 times.
✓ Branch 1 taken 83 times.
|
476 | for (int s = 0; s < strings.size(); s++) { |
| 874 | 393 | char vsnum = 'A'; | |
| 875 | 393 | int vsidx = 0; | |
| 876 |
2/2✓ Branch 0 taken 393 times.
✓ Branch 1 taken 393 times.
|
786 | for (int vs = 0; vs < strings[s]->m_virtualStrings.size(); vs++) { |
| 877 |
1/2✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
|
393 | std::string desc = strings[s]->m_virtualStrings[vs].description; |
| 878 |
3/4✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 387 times.
|
393 | if (desc == "") { |
| 879 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | desc = "String-" + std::to_string(s + 1); |
| 880 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (strings[s]->m_virtualStrings.size() > 1) { |
| 881 | ✗ | desc.append(1, vsnum); | |
| 882 | ✗ | desc += std::string("-"); | |
| 883 | ✗ | desc += std::to_string(vsidx + 1); | |
| 884 | ✗ | if (strings[s]->m_virtualStrings[vs].receiverNum == -1) { | |
| 885 | ✗ | vsidx++; | |
| 886 | ✗ | } else if (vs != 0) { | |
| 887 | ✗ | vsidx = 0; | |
| 888 | ✗ | vsnum++; | |
| 889 | } | ||
| 890 | } | ||
| 891 | } | ||
| 892 | // xLights will name the individual strings of a matrix/prop/model with a -str-# postfix | ||
| 893 | // so we will try and detect this and recreate the original model. | ||
| 894 | // Anything that is not a modest non-negative number (free-form | ||
| 895 | // user names, corrupt descriptions) is treated as a plain name | ||
| 896 | // below instead of throwing (stoi) or resizing gigabytes (huge | ||
| 897 | // index) and terminating fppd at model creation. | ||
| 898 | 393 | size_t found = desc.find("-str-"); | |
| 899 |
2/2✓ Branch 0 taken 391 times.
✓ Branch 1 taken 2 times.
|
393 | if (found == std::string::npos) { |
| 900 |
2/4✓ Branch 0 taken 391 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 391 times.
✗ Branch 3 not taken.
|
391 | vstrings[desc].push_back(&strings[s]->m_virtualStrings[vs]); |
| 901 | } else { | ||
| 902 | 2 | int idx = -1; | |
| 903 | try { | ||
| 904 | 2 | size_t len = 0; | |
| 905 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | long long v = std::stoll(desc.substr(found + 5), &len); |
| 906 | // Entire tail must be the number (no "12x" partial | ||
| 907 | // parses) and it must fit a realistic string count. | ||
| 908 |
4/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
2 | if (len == desc.size() - (found + 5) && v >= 0 && v <= 100000) { |
| 909 | 2 | idx = (int)v; | |
| 910 | } | ||
| 911 | ✗ | } catch (...) { | |
| 912 | ✗ | } | |
| 913 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (idx < 0) { |
| 914 | ✗ | vstrings[desc].push_back(&strings[s]->m_virtualStrings[vs]); | |
| 915 | } else { | ||
| 916 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (idx > 0) |
| 917 | 2 | --idx; | |
| 918 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | desc = desc.substr(0, found); |
| 919 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | if (vstrings[desc].size() <= (size_t)idx) { |
| 920 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | vstrings[desc].resize(idx + 1); |
| 921 | } | ||
| 922 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | vstrings[desc][idx] = &strings[s]->m_virtualStrings[vs]; |
| 923 | } | ||
| 924 | } | ||
| 925 | 393 | } | |
| 926 | } | ||
| 927 |
2/2✓ Branch 0 taken 392 times.
✓ Branch 1 taken 83 times.
|
475 | for (auto& m : vstrings) { |
| 928 |
2/4✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 392 times.
|
392 | if (PixelOverlayManager::INSTANCE.getModel(m.first) != nullptr) { |
| 929 | ✗ | continue; | |
| 930 | } | ||
| 931 |
1/2✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
|
392 | std::string name = m.first; |
| 932 | 392 | auto& vs = m.second; | |
| 933 | |||
| 934 | 392 | uint32_t startChannel = FPPD_MAX_CHANNELS; | |
| 935 | 392 | uint32_t channelsPerNode = 0; | |
| 936 |
1/2✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
|
784 | std::string orientation = "H"; |
| 937 |
1/2✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
|
392 | std::string startLocation = "BL"; |
| 938 | 392 | uint32_t strings = vs.size(); | |
| 939 | 392 | uint32_t strands = 1; | |
| 940 | 392 | uint32_t maxChan = 0; | |
| 941 | 392 | uint32_t minChannelsPerNode = UINT32_MAX; | |
| 942 | 392 | int8_t rn = -1; | |
| 943 |
2/2✓ Branch 0 taken 393 times.
✓ Branch 1 taken 392 times.
|
785 | for (auto& a : vs) { |
| 944 |
1/2✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
|
393 | if (a) { |
| 945 | 393 | startChannel = std::min(startChannel, (uint32_t)a->startChannel); | |
| 946 |
2/4✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 393 times.
|
393 | maxChan = std::max(maxChan, (uint32_t)a->startChannel + (a->pixelCount * a->channelsPerNode() / (a->groupCount ? a->groupCount : 1))); |
| 947 |
1/2✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
|
393 | channelsPerNode = std::max(channelsPerNode, (uint32_t)a->channelsPerNode()); |
| 948 |
1/2✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
|
393 | minChannelsPerNode = std::min(minChannelsPerNode, (uint32_t)a->channelsPerNode()); |
| 949 | 393 | rn = std::max(rn, a->receiverNum); | |
| 950 | } else { | ||
| 951 | ✗ | --strings; | |
| 952 | } | ||
| 953 | } | ||
| 954 |
2/4✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 392 times.
|
392 | if (strings && minChannelsPerNode != channelsPerNode) { |
| 955 | // One model cannot describe two node widths. Taking the wider of | ||
| 956 | // them mis-addresses every pixel on the narrower strings, and the | ||
| 957 | // channel order reported for the model is wrong for them too, so | ||
| 958 | // say so rather than creating a silently broken model. | ||
| 959 | ✗ | LogWarn(VB_CHANNELOUT, "Overlay model %s spans strings of %u and %u channels per node; using %u - pixels on the narrower strings will not line up\n", | |
| 960 | name.c_str(), minChannelsPerNode, channelsPerNode, channelsPerNode); | ||
| 961 | } | ||
| 962 | // The model's channel order is derived from channelsPerNode by | ||
| 963 | // PixelOverlayModel - the virtual string's own colour order describes | ||
| 964 | // the wire, which is applied later by prepareOutput(), and must not be | ||
| 965 | // copied onto the model or it would be applied twice. | ||
| 966 | 392 | int32_t channelCount = maxChan - startChannel; | |
| 967 | |||
| 968 |
6/12✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 392 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 392 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 392 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 392 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 392 times.
|
392 | if (name.find("Tree") != std::string::npos || name.find("TREE") != std::string::npos || name.find("tree") != std::string::npos || name.find("Vert") != std::string::npos || name.find("vert") != std::string::npos) { |
| 969 | // some common names that are usually vertical oriented | ||
| 970 | ✗ | orientation = std::string("V"); | |
| 971 | } | ||
| 972 | |||
| 973 |
3/4✓ Branch 0 taken 386 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 386 times.
✗ Branch 3 not taken.
|
392 | if ((channelCount > 0) && (rn == -1)) { |
| 974 |
1/2✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
|
386 | autoModelNames.push_back(name); |
| 975 |
2/4✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 386 times.
✗ Branch 3 not taken.
|
1158 | PixelOverlayManager::INSTANCE.addAutoOverlayModel(name, startChannel, channelCount, channelsPerNode, orientation, |
| 976 | startLocation, strings, strands); | ||
| 977 | } | ||
| 978 | 392 | } | |
| 979 | 83 | } | |
| 980 | 84 | } | |
| 981 | |||
| 982 | 1651 | uint8_t* PixelString::prepareOutput(uint8_t* channelData) { | |
| 983 | // any protocol preamble is already sitting at the head of the buffer | ||
| 984 | 1651 | int idx = m_preamble.size(); | |
| 985 |
2/2✓ Branch 0 taken 1651 times.
✓ Branch 1 taken 1651 times.
|
3302 | for (auto& vs : m_virtualStrings) { |
| 986 | 1651 | int* map = vs.chMap; | |
| 987 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1651 times.
|
1651 | if (vs.brightnessMap16) { |
| 988 | // 16 bit parts take each channel most significant byte first | ||
| 989 | ✗ | const uint16_t* brightness = vs.brightnessMap16; | |
| 990 | ✗ | for (int ch = 0; ch < vs.chMapCount; ch++) { | |
| 991 | ✗ | uint16_t v = brightness[channelData[map[ch]]]; | |
| 992 | ✗ | m_outputBuffer[idx++] = (uint8_t)(v >> 8); | |
| 993 | ✗ | m_outputBuffer[idx++] = (uint8_t)v; | |
| 994 | } | ||
| 995 | } else { | ||
| 996 | 1651 | const uint8_t* brightness = vs.brightnessMap; | |
| 997 |
2/2✓ Branch 0 taken 116373 times.
✓ Branch 1 taken 1651 times.
|
118024 | for (int ch = 0; ch < vs.chMapCount; ch++) { |
| 998 | 116373 | m_outputBuffer[idx++] = brightness[channelData[map[ch]]]; | |
| 999 | } | ||
| 1000 | } | ||
| 1001 | } | ||
| 1002 | 1651 | return m_outputBuffer; | |
| 1003 | } | ||
| 1004 |