GCC Code Coverage Report


Directory: ./
File: channeloutput/PCA9685.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 282 464 60.8%
Functions: 24 28 85.7%
Branches: 215 545 39.4%

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 <cinttypes>
18 #include <cmath>
19 #include <thread>
20
21 #include "PCA9685.h"
22 #include "Plugin.h"
23 #include "../Sequence.h"
24 #include "../common.h"
25 #include "../log.h"
26
27 #include "non-gpl/CapeUtils/CapeUtils.h"
28
29 #include "util/GPIOUtils.h"
30 #include "commands/Commands.h"
31
32 class PCA9685Plugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
33 public:
34 5 PCA9685Plugin() :
35
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
15 FPPPlugins::Plugin("PCA9685") {
36 5 }
37 6 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
38
2/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
12 return new PCA9685Output(startChannel, channelCount);
39 }
40 };
41
42 extern "C" {
43 5 FPPPlugins::Plugin* createPlugin() {
44
2/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
10 return new PCA9685Plugin();
45 }
46 }
47
48 /*
49 *
50 */
51 // MODE1 when its real value can't be read: ALLCALL and auto-increment, as the
52 // driver programs it. EXTCLK is sticky in the chip, so writing it as 0 leaves
53 // an external clock that was turned on alone.
54 static constexpr int MODE1_SAFE = 0x21;
55
56 6 PCA9685Output::PCA9685Output(unsigned int startChannel, unsigned int channelCount) :
57 ChannelOutput(startChannel, channelCount),
58 6 i2c(nullptr),
59
3/6
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
102 m_servoCommand(nullptr) {
60
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogDebug(VB_CHANNELOUT, "PCA9685Output::PCA9685Output(%u, %u)\n",
61 startChannel, channelCount);
62
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
6 }
63
64 /*
65 *
66 */
67
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
216 PCA9685Output::~PCA9685Output() {
68 12 LogDebug(VB_CHANNELOUT, "PCA9685Output::~PCA9685Output()\n");
69
70
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 if (m_servoCommand) {
71 // By pointer: an output reload registers the new output's command
72 // before this runs, and removing by name unregistered that one.
73 12 CommandManager::INSTANCE.removeCommand(m_servoCommand);
74
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 delete m_servoCommand;
75 12 m_servoCommand = nullptr;
76 }
77
78
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 if (i2c) {
79
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 delete i2c;
80 }
81
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 6 times.
240 }
82
83 24 static PCA9685Output::ZeroType decodeZTE(int i) {
84
1/5
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
24 switch (i) {
85 24 case 0:
86 24 return PCA9685Output::ZeroType::HOLD;
87 ✗ case 1:
88 ✗ return PCA9685Output::ZeroType::NORMAL;
89 ✗ case 2:
90 ✗ return PCA9685Output::ZeroType::CENTER;
91 ✗ case 3:
92 ✗ return PCA9685Output::ZeroType::OFF;
93 }
94 ✗ return PCA9685Output::ZeroType::HOLD;
95 }
96
97 2 static PCA9685Output::ZeroType decodeZTE(const std::string& i) {
98
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (i == "Hold") {
99 2 return PCA9685Output::ZeroType::HOLD;
100 }
101 ✗ if (i == "Min") {
102 ✗ return PCA9685Output::ZeroType::MIN;
103 }
104 ✗ if (i == "Max") {
105 ✗ return PCA9685Output::ZeroType::MAX;
106 }
107 ✗ if (i == "Normal") {
108 ✗ return PCA9685Output::ZeroType::NORMAL;
109 }
110 ✗ if (i == "Center") {
111 ✗ return PCA9685Output::ZeroType::CENTER;
112 }
113 ✗ return PCA9685Output::ZeroType::OFF;
114 }
115 2 static PCA9685Output::PWMType decodePWMType(const std::string& t) {
116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (t == "LED") {
117 ✗ return PCA9685Output::PWMType::LED;
118 }
119 2 return PCA9685Output::PWMType::SERVO;
120 }
121 2 static PCA9685Output::DataType decodeDataType(const std::string& i) {
122
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (i == "Scaled") {
123 ✗ return PCA9685Output::DataType::SCALED;
124 }
125
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (i == "2x Absolute") {
126 1 return PCA9685Output::DataType::ABSOLUTE_DOUBLE;
127 }
128
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (i == "1/2 Absolute") {
129 ✗ return PCA9685Output::DataType::ABSOLUTE_HALF;
130 }
131 1 return PCA9685Output::DataType::ABSOLUTE;
132 }
133 1 static int decodeFrequency(const std::string& fr) {
134 1 int it = atoi(fr.c_str());
135
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (it > 0) {
136 1 return it;
137 }
138 ✗ return 50;
139 }
140
141 ✗ unsigned short PCA9685Output::PCA9685Port::mapLEDValue(unsigned short val) {
142 // LED
143 ✗ float f = val;
144 ✗ if (m_reverse) {
145 ✗ if (is16Bit) {
146 ✗ f = 65535 - f;
147 } else {
148 ✗ f = 255 - f;
149 }
150 }
151 ✗ float bf = brightness;
152 ✗ float maxB = bf * 40.95f;
153 ✗ if (is16Bit) {
154 ✗ f = maxB * pow(f / 65535.0f, gamma);
155 } else {
156 ✗ f = maxB * pow(f / 255.0f, gamma);
157 }
158 ✗ val = std::round(f);
159 ✗ return val;
160 }
161
162 240 unsigned short PCA9685Output::PCA9685Port::readValue(unsigned char* channelData, float frequency, int port) {
163 240 uint32_t val = channelData[startChannel];
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 240 times.
240 if (is16Bit) {
165 ✗ uint32_t t = channelData[startChannel + 1];
166 ✗ val = val << 8;
167 ✗ val += t;
168 }
169
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 if (type == PWMType::SERVO) {
170
2/2
✓ Branch 0 taken 227 times.
✓ Branch 1 taken 13 times.
240 if (val == 0) {
171
1/5
✓ Branch 0 taken 227 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
227 switch (m_zeroBehavior) {
172 227 case PCA9685Output::ZeroType::HOLD:
173 227 val = m_lastValue;
174 227 break;
175 ✗ case PCA9685Output::ZeroType::CENTER:
176 ✗ val = m_center;
177 ✗ break;
178 ✗ case PCA9685Output::ZeroType::MIN:
179 ✗ if (m_reverse) {
180 ✗ val = m_max;
181 } else {
182 ✗ val = m_min;
183 }
184 ✗ break;
185 ✗ case PCA9685Output::ZeroType::MAX:
186 ✗ if (m_reverse) {
187 ✗ val = m_min;
188 } else {
189 ✗ val = m_max;
190 }
191 ✗ break;
192 }
193 } else {
194 13 float z = val;
195 13 DataType dt = m_dataType;
196
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
13 if (dt == PCA9685Output::DataType::ABSOLUTE_DOUBLE) {
197 3 z /= 2.0f;
198 3 dt = PCA9685Output::DataType::ABSOLUTE;
199
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 } else if (dt == PCA9685Output::DataType::ABSOLUTE) {
200 3 z *= 2.0f;
201 3 dt = PCA9685Output::DataType::ABSOLUTE;
202 }
203
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 7 times.
13 if (dt == PCA9685Output::DataType::ABSOLUTE) {
204 6 z *= frequency;
205 6 z *= 4096.0f;
206 6 z /= 1000000.0f;
207 6 val = std::round(z);
208
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (!is16Bit) {
209 6 val += m_min;
210 }
211
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 } else if (m_dataType == PCA9685Output::DataType::SCALED) {
212
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (is16Bit) {
213 ✗ if (val >= 32768) {
214 ✗ float scale = m_max - m_center + 1;
215 ✗ val -= 32768.0f;
216 ✗ scale *= val;
217 ✗ scale /= 32768.0f;
218 ✗ val = std::round(scale);
219 ✗ val += m_center;
220 } else {
221 ✗ float scale = m_center - m_min + 1;
222 ✗ scale *= val;
223 ✗ scale /= 32767.0f;
224 ✗ val = std::round(scale);
225 ✗ val += m_min;
226 }
227 } else {
228
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (val >= 128) {
229 7 float scale = m_max - m_center + 1;
230 7 val -= 128.0f;
231 7 scale *= val;
232 7 scale /= 128.0f;
233 7 val = std::round(scale);
234 7 val += m_center;
235 } else {
236 ✗ float scale = m_center - m_min + 1;
237 ✗ scale *= val;
238 ✗ scale /= 128.0f;
239 ✗ val = std::round(scale);
240 ✗ val += m_min;
241 }
242 }
243 }
244
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (val > m_max) {
245 ✗ val = m_max;
246 }
247
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (val < m_min) {
248 ✗ val = m_min;
249 }
250
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (m_reverse) {
251 ✗ int offset = val - m_min;
252 ✗ val = m_max - offset;
253 }
254 }
255 } else {
256 ✗ val = mapLEDValue(val);
257 }
258 240 m_nextValue = val;
259 240 return val;
260 }
261
262 /*
263 *
264 */
265 6 int PCA9685Output::Init(Json::Value config) {
266
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogDebug(VB_CHANNELOUT, "PCA9685Output::Init(JSON)\n");
267
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 origConfig = config;
268
269
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 Json::Value portConfigs;
270
271 6 uint32_t clock = 25000000;
272 6 bool externalClock = false;
273
274
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
6 if (config.isMember("deviceID")) {
275 // config from "Other" tab
276
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 if (config["deviceID"].isString()) {
277 ✗ m_deviceID = std::atoi(config["deviceID"].asString().c_str());
278 } else {
279
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 m_deviceID = config["deviceID"].asInt();
280 }
281
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 m_i2cDevice = config["device"].asString();
282
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (m_i2cDevice == "") {
283 ✗ m_i2cDevice = "i2c-1";
284 }
285
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 m_frequency = config["frequency"].asInt();
286
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 portConfigs = config["ports"];
287 } else {
288
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string subType = config["subType"].asString();
289
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value capeConfig;
290
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (!CapeUtils::INSTANCE.getPWMConfig(subType, capeConfig)) {
291 ✗ LogErr(VB_CHANNELOUT, "Could not get cape config for %s\n", subType.c_str());
292 ✗ return 0;
293 }
294
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_i2cDevice = capeConfig["bus"].asString();
295
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_deviceID = capeConfig["address"].asInt();
296
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (capeConfig.isMember("clock")) {
297 ✗ clock = capeConfig["clock"].asInt();
298 ✗ externalClock = true;
299 }
300
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 portConfigs = config["outputs"];
301
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_frequency = decodeFrequency(config["frequency"].asString());
302
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 }
303
304
3/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
6 i2c = new I2CUtils(m_i2cDevice.c_str(), m_deviceID);
305
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!i2c->isOk()) {
306 ✗ LogErr(VB_CHANNELOUT, "Error opening I2C device for PCA9685 output\n");
307 ✗ return 0;
308 }
309
310 6 float fs = clock;
311 6 float ofs = fs;
312 6 fs /= 4096.f;
313 6 fs /= m_frequency;
314
315 6 int fsi = (int)(fs + 0.5f);
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (fsi < 3) {
317 ✗ fsi = 3;
318 }
319
320 6 m_actualFrequency = ofs / (fsi * 4096.0f);
321
322 // printf("%0.3f fsi: %d of: %0.2f\n", fs, fsi, m_actualFrequency);
323
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogDebug(VB_CHANNELOUT, "PCA9685 using actual frequency of: %0.2f\n", m_actualFrequency);
324
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 restoreOriginalConfig();
325
326 // Initialize
327
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 int oldmode = i2c->readByteData(0x00);
328
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 if (oldmode < 0) {
329 // a failed read is -1, which as a register value would set every
330 // bit - EXTCLK included, which only a power cycle clears
331 1 oldmode = MODE1_SAFE;
332 }
333 6 int m0 = (oldmode & 0x7f) | 0x10;
334 // set sleep bit
335
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0x00, m0);
336
337
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0x01, 0x04); // ON STOP
338 // i2c->writeByteData(0x01, 0x0C); // ON ACK
339
340 // turn on the external clock
341
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (externalClock) {
342 ✗ oldmode |= 0x40;
343 ✗ m0 |= 0x40;
344 ✗ i2c->writeByteData(0x00, m0);
345 }
346
347 // set the pre-scaler
348
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0xFE, fsi);
349
350 // auto increment bit
351 6 oldmode |= 0x20;
352
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0x00, oldmode);
353
354 // clear sleep bit if set to restart oscillator
355 6 oldmode &= 0x6F;
356
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0x00, oldmode);
357
358 // sleep for at least 500uS for the oscillator to stabilize
359
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::this_thread::sleep_for(std::chrono::milliseconds(1));
360
361 // Check if any servo ports are enabled and register command
362 6 bool hasServoPort = false;
363
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 for (uint32_t x = 0; x < numPorts; x++) {
364
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (m_ports[x].type == PWMType::SERVO) {
365 6 hasServoPort = true;
366 6 break;
367 }
368 }
369
370
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (!m_servoCommand) {
371
3/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
6 m_servoCommand = new ServoPositionCommand(this);
372
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 CommandManager::INSTANCE.addCategorizedCommand(m_servoCommand, "Outputs", 1);
373
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogDebug(VB_CHANNELOUT, "PCA9685: Servo Position command registered\n");
374 }
375
376
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 return ChannelOutput::Init(config);
377 6 }
378
379 6 void PCA9685Output::restoreOriginalConfig() {
380
6/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
6 Json::Value portConfigs = origConfig.isMember("deviceID") ? origConfig["ports"] : origConfig["outputs"];
381
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 loadPortConfig(portConfigs);
382 6 }
383 6 void PCA9685Output::loadPortConfig(const Json::Value& portConfigs) {
384 6 uint32_t sc = m_startChannel;
385 6 bool asUsec = true;
386
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
6 if (origConfig.isMember("deviceID")) {
387
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (origConfig.isMember("asUsec")) {
388 5 asUsec = origConfig["asUsec"].asInt();
389 }
390 }
391 // the chip has 16 ports; m_ports and data[] are sized for exactly that
392
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 numPorts = std::min<int>(portConfigs.size(), 16);
393
394
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 6 times.
32 for (int x = 0; x < numPorts; x++) {
395
3/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
26 m_ports[x].description = portConfigs[x]["description"].asString();
396 26 m_ports[x].m_min = portConfigs[x]["min"].asInt();
397 26 m_ports[x].m_max = portConfigs[x]["max"].asInt();
398
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
26 if (portConfigs[x].isMember("center")) {
399 24 m_ports[x].m_center = portConfigs[x]["center"].asInt();
400 } else {
401 2 m_ports[x].m_center = (m_ports[x].m_min + m_ports[x].m_max) / 2;
402 }
403
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
26 if (portConfigs[x].isMember("type")) {
404
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 m_ports[x].type = decodePWMType(portConfigs[x]["type"].asString());
405 } else {
406 24 m_ports[x].type = PCA9685Output::PWMType::SERVO;
407 }
408
409
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 if (asUsec) {
410 26 float z = m_ports[x].m_min;
411 26 z *= m_actualFrequency;
412 26 z *= 4096;
413 26 z /= 1000000;
414 26 m_ports[x].m_min = std::round(z);
415
416 26 z = m_ports[x].m_max;
417 26 z *= m_actualFrequency;
418 26 z *= 4096;
419 26 z /= 1000000;
420 26 m_ports[x].m_max = std::round(z);
421
422 26 z = m_ports[x].m_center;
423 26 z *= m_actualFrequency;
424 26 z *= 4096;
425 26 z /= 1000000;
426 26 m_ports[x].m_center = std::round(z);
427 26 LogDebug(VB_CHANNELOUT, "PCA9685 pulse ranges for output %d: %d %d %d Steps: %d\n", x, m_ports[x].m_min, m_ports[x].m_center, m_ports[x].m_max, m_ports[x].m_max - m_ports[x].m_min);
428 // printf("PCA9685 pulse ranges for output %d: %d %d %d Steps: %d\n", x, m_ports[x].m_min, m_ports[x].m_center, m_ports[x].m_max, m_ports[x].m_max - m_ports[x].m_min);
429 }
430
431
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 if (m_ports[x].type == PCA9685Output::PWMType::SERVO) {
432
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
26 if (portConfigs[x].isMember("zeroBehavior")) {
433 // co-other config
434 24 m_ports[x].m_zeroBehavior = decodeZTE(portConfigs[x]["zeroBehavior"].asInt());
435 24 int dt = portConfigs[x]["dataType"].asInt();
436
1/7
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
24 switch (dt) {
437 24 case 0:
438 24 m_ports[x].m_dataType = PCA9685Output::DataType::SCALED;
439 24 m_ports[x].is16Bit = false;
440 24 m_ports[x].m_reverse = false;
441 24 break;
442 ✗ case 1:
443 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::SCALED;
444 ✗ m_ports[x].is16Bit = false;
445 ✗ m_ports[x].m_reverse = true;
446 ✗ break;
447 ✗ case 2:
448 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::SCALED;
449 ✗ m_ports[x].is16Bit = true;
450 ✗ m_ports[x].m_reverse = false;
451 ✗ break;
452 ✗ case 3:
453 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::SCALED;
454 ✗ m_ports[x].is16Bit = true;
455 ✗ m_ports[x].m_reverse = true;
456 ✗ break;
457 ✗ case 4:
458 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::ABSOLUTE;
459 ✗ m_ports[x].is16Bit = false;
460 ✗ m_ports[x].m_reverse = false;
461 ✗ break;
462 ✗ case 5:
463 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::ABSOLUTE;
464 ✗ m_ports[x].is16Bit = true;
465 ✗ m_ports[x].m_reverse = false;
466 ✗ break;
467 }
468
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (m_ports[x].m_zeroBehavior == PCA9685Output::ZeroType::NORMAL) {
469 ✗ if (m_ports[x].m_dataType == PCA9685Output::DataType::ABSOLUTE) {
470 ✗ m_ports[x].m_zeroBehavior = PCA9685Output::ZeroType::OFF;
471 } else {
472 ✗ m_ports[x].m_zeroBehavior = PCA9685Output::ZeroType::MIN;
473 }
474 }
475 } else {
476 // co-pwm config
477
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 m_ports[x].m_zeroBehavior = decodeZTE(portConfigs[x]["zero"].asString());
478
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 m_ports[x].m_dataType = decodeDataType(portConfigs[x]["dataType"].asString());
479 2 m_ports[x].m_reverse = portConfigs[x]["reverse"].asInt();
480 2 m_ports[x].is16Bit = portConfigs[x]["is16bit"].asInt();
481 }
482 } else {
483 ✗ m_ports[x].gamma = portConfigs[x]["gamma"].asFloat();
484 ✗ m_ports[x].brightness = portConfigs[x]["brightness"].asInt();
485 ✗ m_ports[x].is16Bit = portConfigs[x]["is16bit"].asInt();
486 ✗ m_ports[x].m_reverse = portConfigs[x]["reverse"].asInt();
487 ✗ if (m_ports[x].brightness > 100 || m_ports[x].brightness < 0) {
488 ✗ m_ports[x].brightness = 100;
489 }
490 ✗ if (m_ports[x].gamma < 0.01) {
491 ✗ m_ports[x].gamma = 0.01;
492 }
493 ✗ if (m_ports[x].gamma > 50.0f) {
494 ✗ m_ports[x].gamma = 50.0f;
495 }
496 }
497
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
26 if (portConfigs[x].isMember("startChannel")) {
498 2 m_ports[x].startChannel = portConfigs[x]["startChannel"].asInt();
499 } else {
500 24 m_ports[x].startChannel = sc;
501
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 sc += m_ports[x].is16Bit ? 2 : 1;
502 }
503 // PrepData reads and the output test writes the sequence buffer at
504 // this channel. A per-port start only comes from a hand-edited config
505 // or the output test's config over the API; a negative one arrives
506 // here as a huge unsigned value.
507
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
26 if (m_ports[x].startChannel + (m_ports[x].is16Bit ? 1 : 0) >= FPPD_MAX_CHANNELS) {
508 ✗ LogErr(VB_CHANNELOUT, "PCA9685 port %d start channel %u is out of range, ignoring it and the ports after it\n",
509 x + 1, m_ports[x].startChannel);
510 ✗ numPorts = x;
511 ✗ break;
512 }
513 }
514 6 }
515
516 /*
517 *
518 */
519 6 int PCA9685Output::Close(void) {
520 6 LogDebug(VB_CHANNELOUT, "PCA9685Output::Close()\n");
521
522 6 return ChannelOutput::Close();
523 }
524
525 6 PCA9685Output::ServoPositionCommand::ServoPositionCommand(PCA9685Output* output) :
526 Command("Servo Position", "Set servo position by percentage"),
527
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
24 m_output(output) {
528
5/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
36 args.push_back(CommandArg("ServoIndex", "int", "Servo index (1-16)").setRange(1, 16));
529
5/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
36 args.push_back(CommandArg("Position", "int", "Position percentage (0-100)").setRange(0, 100));
530 6 }
531
532 24 PCA9685Output::ServoPositionCommand::~ServoPositionCommand() {
533 12 LogDebug(VB_CHANNELOUT, "ServoPositionCommand destroyed\n");
534 24 }
535
536 6 std::unique_ptr<Command::Result> PCA9685Output::ServoPositionCommand::run(const std::vector<std::string>& args) {
537
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (args.size() != 2) {
538 ✗ return std::make_unique<Command::ErrorResult>("Servo Position requires 2 arguments: ServoIndex (1-16) and Position (0-100)");
539 }
540
541 int servo_index;
542 int position_percent;
543 try {
544
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 servo_index = std::stoi(args[0]);
545
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 position_percent = std::stoi(args[1]);
546 ✗ } catch (const std::exception& e) {
547 ✗ return std::make_unique<Command::ErrorResult>(std::string("Parse error: ") + e.what());
548 ✗ }
549
550
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (servo_index < 1 || servo_index > 16) {
551 ✗ return std::make_unique<Command::ErrorResult>("ServoIndex must be between 1 and 16");
552 }
553
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (position_percent < 0 || position_percent > 100) {
554 ✗ return std::make_unique<Command::ErrorResult>("Position must be between 0 and 100");
555 }
556
557
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!m_output) {
558 ✗ return std::make_unique<Command::ErrorResult>("PCA9685 output is not available");
559 }
560
561
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::lock_guard<std::recursive_mutex> lk(m_output->m_portsLock);
562 6 int internal_index = servo_index - 1;
563
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (internal_index < 0 || (uint32_t)internal_index >= m_output->numPorts) {
564 ✗ return std::make_unique<Command::ErrorResult>("Servo index is out of configured ports range");
565 }
566
567 6 PCA9685Output::PCA9685Port& port = m_output->m_ports[internal_index];
568
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (port.type != PCA9685Output::PWMType::SERVO) {
569 ✗ return std::make_unique<Command::ErrorResult>("Selected port is not configured as SERVO");
570 }
571
572 6 uint32_t targetValue = 0;
573
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (port.m_dataType == PCA9685Output::DataType::SCALED) {
574
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (port.is16Bit) {
575 ✗ if (position_percent <= 50) {
576 ✗ targetValue = static_cast<uint32_t>(std::round(port.m_min + (port.m_center - port.m_min) * (position_percent / 50.0f)));
577 } else {
578 ✗ targetValue = static_cast<uint32_t>(std::round(port.m_center + (port.m_max - port.m_center) * ((position_percent - 50) / 50.0f)));
579 }
580 } else {
581 // 8-bit range storage still in pulse ticks after config conversion
582
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 if (position_percent <= 50) {
583 3 targetValue = static_cast<uint32_t>(std::round(port.m_min + (port.m_center - port.m_min) * (position_percent / 50.0f)));
584 } else {
585 3 targetValue = static_cast<uint32_t>(std::round(port.m_center + (port.m_max - port.m_center) * ((position_percent - 50) / 50.0f)));
586 }
587 }
588 } else {
589 ✗ if (position_percent <= 50) {
590 ✗ targetValue = static_cast<uint32_t>(std::round(port.m_min + (port.m_center - port.m_min) * (position_percent / 50.0f)));
591 } else {
592 ✗ targetValue = static_cast<uint32_t>(std::round(port.m_center + (port.m_max - port.m_center) * ((position_percent - 50) / 50.0f)));
593 }
594 }
595
596
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (port.m_reverse) {
597 ✗ if (targetValue >= port.m_min && targetValue <= port.m_max) {
598 ✗ targetValue = port.m_max - (targetValue - port.m_min);
599 }
600 }
601
602
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (targetValue < port.m_min) targetValue = port.m_min;
603
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (targetValue > port.m_max) targetValue = port.m_max;
604
605 6 port.m_nextValue = targetValue;
606 6 port.m_lastValue = targetValue;
607
608
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 m_output->SendData(nullptr);
609
610
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogInfo(VB_CHANNELOUT, "ServoPosition: set servo %d to %d%% -> raw %u\n", servo_index, position_percent, targetValue);
611
612
7/14
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
6 return std::make_unique<Command::Result>("Servo " + std::to_string(servo_index) + " set to " + std::to_string(position_percent) + "% (raw " + std::to_string(targetValue) + ")");
613 6 }
614
615 6 void PCA9685Output::StartingOutput() {
616 6 int b = i2c->readByteData(0x00);
617
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 if (b < 0) {
618 1 b = MODE1_SAFE;
619 }
620 6 i2c->writeByteData(0x00, b & 0x6F);
621
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::this_thread::sleep_for(std::chrono::milliseconds(1));
622 6 }
623 6 void PCA9685Output::StoppingOutput() {
624 6 uint8_t allOff[4] = { 0x00, 0x00, 0x00, 0x10 };
625
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeRawI2CBlockData(0xFA, allOff, 4);
626
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::this_thread::sleep_for(std::chrono::milliseconds(3));
627
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 int b = i2c->readByteData(0x00);
628
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 if (b < 0) {
629 1 b = MODE1_SAFE;
630 }
631
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 i2c->writeByteData(0x00, b | 0x10);
632 6 }
633
634 50 void PCA9685Output::PrepData(unsigned char* channelData) {
635
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 std::lock_guard<std::recursive_mutex> lk(m_portsLock);
636
2/2
✓ Branch 0 taken 240 times.
✓ Branch 1 taken 50 times.
290 for (int x = 0; x < numPorts; x++) {
637
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 m_ports[x].readValue(channelData, m_actualFrequency, x);
638 }
639 50 }
640
641 /*
642 *
643 */
644 44 int PCA9685Output::SendData(unsigned char* channelData) {
645
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 LogExcess(VB_CHANNELOUT, "PCA9685Output::SendData(%p)\n", channelData);
646
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 std::lock_guard<std::recursive_mutex> lk(m_portsLock);
647
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 lastFrameTime = GetTimeMicros();
648
649
2/2
✓ Branch 0 taken 200 times.
✓ Branch 1 taken 44 times.
244 for (int x = 0; x < numPorts; x++) {
650 200 data[x * 2] = 0;
651 200 data[x * 2 + 1] = m_ports[x].m_nextValue;
652 200 m_ports[x].m_lastValue = m_ports[x].m_nextValue;
653 }
654
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 i2c->writeRawI2CBlockData(0x06, (uint8_t*)&(data[0]), numPorts * 4);
655 44 return m_channelCount;
656 44 }
657
658 13 void PCA9685Output::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
659
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 13 times.
67 for (int x = 0; x < numPorts; x++) {
660
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 54 times.
54 addRange(m_ports[x].startChannel, m_ports[x].startChannel + (m_ports[x].is16Bit ? 1 : 0));
661 }
662 13 }
663 ✗ static inline void writeChannelValue(unsigned char* channelData, uint32_t sc, bool is16bit, uint32_t value) {
664 ✗ if (is16bit) {
665 ✗ channelData[sc] = (value >> 8) & 0xFF;
666 ✗ channelData[sc + 1] = value & 0xFF;
667 } else {
668 ✗ channelData[sc] = value;
669 }
670 ✗ }
671
672 ✗ void PCA9685Output::OverlayTestData(unsigned char* channelData, int cycleNum, float percentOfCycle, int testType, const Json::Value& config) {
673 ✗ std::lock_guard<std::recursive_mutex> lk(m_portsLock);
674 ✗ if (!origConfig.isMember("deviceID")) {
675 ✗ if (testType != lastTestType) {
676 ✗ restoreOriginalConfig();
677 }
678 ✗ lastTestType = testType;
679 ✗ if (testType == 1) {
680 ✗ loadPortConfig(config["outputs"]);
681 ✗ int testingPort = config.isMember("manipulation") ? config["manipulation"]["port"].asInt() : -1;
682 ✗ uint32_t tvalue = config["manipulation"]["value"].asInt();
683 ✗ bool isMin = config["manipulation"]["isMin"].asBool();
684 ✗ for (int x = 0; x < numPorts; x++) {
685 ✗ if (m_ports[x].type == PCA9685Output::PWMType::SERVO) {
686 ✗ uint32_t value = m_ports[x].is16Bit ? 32768 : 128;
687 ✗ if (x == testingPort) {
688 ✗ if (m_ports[x].m_reverse) {
689 ✗ isMin = !isMin;
690 }
691 ✗ value = isMin ? 1 : (m_ports[x].is16Bit ? 65535 : 255);
692 ✗ } else if (m_ports[x].m_zeroBehavior == PCA9685Output::ZeroType::MIN) {
693 ✗ value = 0;
694 ✗ } else if (m_ports[x].m_zeroBehavior == PCA9685Output::ZeroType::MAX) {
695 ✗ value = m_ports[x].is16Bit ? 65535 : 255;
696 }
697 ✗ m_ports[x].m_dataType = PCA9685Output::DataType::SCALED;
698 ✗ writeChannelValue(channelData, m_ports[x].startChannel, m_ports[x].is16Bit, value);
699 } else {
700 // LED
701 ✗ uint32_t max = m_ports[x].is16Bit ? 65535 : 255;
702 ✗ uint32_t value = max;
703 ✗ value *= 100;
704 ✗ value *= percentOfCycle;
705 ✗ if (cycleNum % 2 == 0) {
706 ✗ value = max - value;
707 }
708 ✗ value = m_ports[x].mapLEDValue(value);
709 ✗ writeChannelValue(channelData, m_ports[x].startChannel, m_ports[x].is16Bit, value);
710 }
711 }
712 }
713 }
714 ✗ }
715 ✗ bool PCA9685Output::SupportsTesting() const {
716 ✗ return !origConfig.isMember("deviceID");
717 }
718
719 /*
720 *
721 */
722 6 void PCA9685Output::DumpConfig(void) {
723 6 LogDebug(VB_CHANNELOUT, "PCA9685Outputs::DumpConfig()\n");
724
725 6 LogDebug(VB_CHANNELOUT, " deviceID: %X\n", m_deviceID);
726 6 LogDebug(VB_CHANNELOUT, " Frequency: %d\n", m_frequency);
727
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 6 times.
32 for (int x = 0; x < numPorts; x++) {
728 26 LogDebug(VB_CHANNELOUT, " Port %d: %s\n", x, m_ports[x].description.c_str());
729 26 LogDebug(VB_CHANNELOUT, " Min: %d\n", m_ports[x].m_min);
730 26 LogDebug(VB_CHANNELOUT, " Center: %d\n", m_ports[x].m_center);
731 26 LogDebug(VB_CHANNELOUT, " Max: %d\n", m_ports[x].m_max);
732 26 LogDebug(VB_CHANNELOUT, " DataType: %d\n", m_ports[x].m_dataType);
733 26 LogDebug(VB_CHANNELOUT, " ZeroType: %d\n", m_ports[x].m_zeroBehavior);
734 }
735
736 6 ChannelOutput::DumpConfig();
737 6 }
738