GCC Code Coverage Report


Directory: ./
File: channeloutput/ArtNet.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 113 123 91.9%
Functions: 9 10 90.0%
Branches: 61 99 61.6%

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 <arpa/inet.h>
18 #include <net/if.h>
19 #include <netinet/in.h>
20 #include <sys/ioctl.h>
21 #include <sys/socket.h>
22 #include <sys/time.h>
23 #include <ctype.h>
24 #include <errno.h>
25 #include <math.h>
26 #include <netdb.h>
27 #include <signal.h>
28 #include <string.h>
29 #include <strings.h>
30 #include <unistd.h>
31
32 #include "../Warnings.h"
33 #include "../log.h"
34
35 #include "ArtNet.h"
36 #include "ChannelOutputSetup.h"
37 #include "channeloutputthread.h"
38 #include "e131bridge.h"
39 #include "fpp.h"
40
41 #define ARTNET_HEADER_LENGTH 18
42 #define ARTNET_SYNC_PACKET_LENGTH 14
43
44 #define ARTNET_DEST_PORT 6454
45
46 #define ARTNET_SEQUENCE_INDEX 12
47 #define ARTNET_UNIVERSE_INDEX 14
48 #define ARTNET_LENGTH_INDEX 16
49
50 #define ARTNET_TYPE_BROADCAST 2
51 #define ARTNET_TYPE_UNICAST_ARTNETPORT 3
52 #define ARTNET_TYPE_UNICAST 9
53
54 const char ArtNetHeader[] = {
55 'A', 'r', 't', '-', 'N', 'e', 't', 0x00, // 8-byte ID
56 0x00, // Opcode Low
57 0x50, // Opcode High
58 0x00, // Protocol Version High
59 0x0E, // Protocol Version Low
60 0x00, // Sequence #
61 0x00, // Physical #
62 0x00, // Universe Number Low
63 0x00, // Universe Number High (7 bits)
64 0x00, // Length High (length always an even number)
65 0x00 // Length Low
66 };
67
68 const char ArtNetSyncPacket[] = {
69 'A', 'r', 't', '-', 'N', 'e', 't', 0x00, // 8-byte ID
70 0x00, // Opcode Low
71 0x52, // Opcode High
72 0x00, // Protocol Version High
73 0x0E, // Protocol Version Low
74 0x00, // Aux1
75 0x00 // Aux2
76 };
77
78 static struct iovec ArtNetSyncIovecs = { (void*)ArtNetSyncPacket, ARTNET_SYNC_PACKET_LENGTH };
79 static struct sockaddr_in ArtNetSyncAddress;
80
81 static const std::string ARTNETTYPE = "ArtNet";
82
83 ✗ const std::string& ArtNetOutputData::GetOutputTypeString() const {
84 ✗ return ARTNETTYPE;
85 }
86
87 26 ArtNetOutputData::ArtNetOutputData(const Json::Value& config) :
88 UDPOutputData(config),
89 26 sequenceNumber(1),
90 26 universeCount(1) {
91 26 memset((char*)&anAddress, 0, sizeof(sockaddr_in));
92 26 anAddress.sin_family = AF_INET;
93 26 anAddress.sin_port = htons(ARTNET_DEST_PORT);
94
95 26 memset((char*)&ArtNetSyncAddress, 0, sizeof(sockaddr_in));
96 26 ArtNetSyncAddress.sin_family = AF_INET;
97 26 ArtNetSyncAddress.sin_port = htons(ARTNET_DEST_PORT);
98 26 ArtNetSyncAddress.sin_addr.s_addr = inet_addr("255.255.255.255");
99
100
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 universe = config["id"].asInt();
101
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 priority = config["priority"].asInt();
102
3/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 4 times.
26 if (config.isMember("universeCount")) {
103
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 universeCount = config["universeCount"].asInt();
104 }
105
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (universeCount < 1) {
106 ✗ universeCount = 1;
107 }
108
109
1/3
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
26 switch (type) {
110 ✗ case ARTNET_TYPE_BROADCAST: // Multicast
111 ✗ ipAddress = "";
112 ✗ break;
113 26 case ARTNET_TYPE_UNICAST_ARTNETPORT: // UnicastAddress
114 case ARTNET_TYPE_UNICAST: // UnicastAddress
115
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 ipAddress = config["address"].asString();
116 26 break;
117 }
118
119
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (type == ARTNET_TYPE_BROADCAST) {
120 ✗ anAddress.sin_addr.s_addr = inet_addr("255.255.255.255");
121
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 } else if (active) {
122
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 anAddress.sin_addr.s_addr = toInetAddr(ipAddress, valid);
123
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (!valid) {
124 ✗ AddOutputConfigWarning(14, "Could not resolve host name " + ipAddress + " - disabling output");
125 ✗ active = false;
126 }
127 }
128
129
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 anIovecs.resize(universeCount * 2);
130
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 anHeaders.resize(universeCount);
131
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 26 times.
53 for (int x = 0; x < universeCount; x++) {
132 27 unsigned char* anBuffer = (unsigned char*)malloc(ARTNET_HEADER_LENGTH);
133 27 anHeaders[x] = anBuffer;
134
135 27 memcpy(anBuffer, ArtNetHeader, ARTNET_HEADER_LENGTH);
136
137 27 int uni = universe + x;
138
139 27 anBuffer[ARTNET_UNIVERSE_INDEX] = (char)(uni % 256);
140 27 anBuffer[ARTNET_UNIVERSE_INDEX + 1] = (char)(uni / 256);
141
142 27 anBuffer[ARTNET_LENGTH_INDEX] = (char)(channelCount / 256);
143 27 anBuffer[ARTNET_LENGTH_INDEX + 1] = (char)(channelCount % 256);
144
145 // use scatter/gather for the packet. One IOV will contain
146 // the header, the second will point into the raw channel data
147 // and will be set at output time. This avoids any memcpy.
148 27 anIovecs[x * 2].iov_base = anBuffer;
149 27 anIovecs[x * 2].iov_len = ARTNET_HEADER_LENGTH;
150 27 anIovecs[x * 2 + 1].iov_base = nullptr;
151 27 anIovecs[x * 2 + 1].iov_len = channelCount;
152 }
153 26 }
154
155 104 ArtNetOutputData::~ArtNetOutputData() {
156
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 26 times.
106 for (auto a : anHeaders) {
157 54 free(a);
158 }
159 104 }
160
161 52 bool ArtNetOutputData::IsPingable() {
162
3/4
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 46 times.
✗ Branch 3 not taken.
52 return type == ARTNET_TYPE_UNICAST || type == ARTNET_TYPE_UNICAST_ARTNETPORT;
163 }
164
165 // The shared ArtNet socket for `key`, or -1 while it can't be opened (the
166 // port is taken; CreateArtNetSocket() warns about that once and retries now
167 // and then). Never hand the sender -1: every frame it would fail on it.
168 8601 static int ArtNetSocketFor(UDPOutputMessages& messages, unsigned int key) {
169 8601 int sock = messages.GetSocket(key);
170
2/2
✓ Branch 0 taken 922 times.
✓ Branch 1 taken 7679 times.
8601 if (sock == -1) {
171 // we MAY be bridging ArtNet so we need to use that same socket
172 922 sock = CreateArtNetSocket();
173
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 914 times.
922 if (sock != -1) {
174 8 messages.ForceSocket(key, sock, true);
175 }
176 }
177 8601 return sock;
178 }
179
180 4495 void ArtNetOutputData::PrepareData(unsigned char* channelData, UDPOutputMessages& messages) {
181 4495 sequencePending = false;
182
3/6
✓ Branch 0 taken 4495 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4495 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4495 times.
✗ Branch 5 not taken.
4495 if (valid && active) {
183 4495 ArtNetSocketFor(messages, ARTNET_SYNC_KEY);
184
185 4495 int key = ARTNET_MESSAGES_KEY;
186
2/2
✓ Branch 0 taken 389 times.
✓ Branch 1 taken 4106 times.
4495 if (type == ARTNET_TYPE_UNICAST) {
187 389 key = anAddress.sin_addr.s_addr;
188 } else {
189 // ALL ArtNet messages must go out on the same socket
190 // and the socket MUST have the source port of ARTNET_DEST_PORT
191 // as per the ArtNet protocol
192 // Use a reserved key that won't collide with IP addresses
193
2/2
✓ Branch 0 taken 355 times.
✓ Branch 1 taken 3751 times.
4106 if (ArtNetSocketFor(messages, ARTNET_MESSAGES_KEY) == -1) {
194 355 return;
195 }
196 }
197 4140 unsigned char* cur = channelData + startChannel - 1;
198 4140 int start = 0;
199 4140 bool anySkipped = false;
200 4140 bool allSkipped = true;
201
202 4140 std::vector<struct mmsghdr>& msgs = messages[key];
203
2/2
✓ Branch 0 taken 4209 times.
✓ Branch 1 taken 4140 times.
8349 for (int x = 0; x < universeCount; x++) {
204
2/2
✓ Branch 0 taken 4109 times.
✓ Branch 1 taken 100 times.
4209 if (NeedToOutputFrame(channelData, startChannel - 1, start, channelCount)) {
205 struct mmsghdr msg;
206 4109 memset(&msg, 0, sizeof(msg));
207 4109 msg.msg_hdr.msg_name = &anAddress;
208 4109 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
209 4109 msg.msg_hdr.msg_iov = &anIovecs[x * 2];
210 4109 msg.msg_hdr.msg_iovlen = 2;
211 4109 msg.msg_len = channelCount + ARTNET_HEADER_LENGTH;
212
1/2
✓ Branch 0 taken 4109 times.
✗ Branch 1 not taken.
4109 msgs.push_back(msg);
213
214 4109 anHeaders[x][ARTNET_SEQUENCE_INDEX] = sequenceNumber;
215 4109 anIovecs[x * 2 + 1].iov_base = (void*)cur;
216 4109 allSkipped = false;
217 } else {
218 100 anySkipped = true;
219 }
220 4209 cur += channelCount;
221 4209 start += channelCount;
222 }
223 4140 pendingSequenceNumber = sequenceNumber + 1;
224
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4140 times.
4140 if (pendingSequenceNumber == 0) {
225 ✗ pendingSequenceNumber++;
226 }
227 4140 sequencePending = true;
228 // the counter forces a full resend once 10 frames in a row skipped
229 // something; without the reset, after the first 10 skips dedup was
230 // off for the rest of the run
231
2/2
✓ Branch 0 taken 100 times.
✓ Branch 1 taken 4040 times.
4140 if (anySkipped) {
232 100 skippedFrames++;
233 } else {
234 4040 skippedFrames = 0;
235 }
236
2/2
✓ Branch 0 taken 4040 times.
✓ Branch 1 taken 100 times.
4140 if (!allSkipped) {
237 4040 SaveFrame(&channelData[startChannel - 1], start);
238 }
239 }
240 }
241 4495 void ArtNetOutputData::PostPrepareData(unsigned char* channelData, UDPOutputMessages& msgs) {
242
6/8
✓ Branch 0 taken 4495 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4495 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3936 times.
✓ Branch 5 taken 559 times.
✓ Branch 6 taken 3936 times.
✓ Branch 7 taken 559 times.
4495 if (valid && active && msgs.GetSocket(ARTNET_SYNC_KEY) != -1) {
243
3/4
✓ Branch 0 taken 3936 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3558 times.
✓ Branch 3 taken 378 times.
3936 for (auto msg : msgs[ARTNET_SYNC_KEY]) {
244
1/2
✓ Branch 0 taken 3558 times.
✗ Branch 1 not taken.
3558 if (msg.msg_hdr.msg_iov == &ArtNetSyncIovecs) {
245 // already added, skip
246 3558 return;
247 }
248 }
249
250 struct mmsghdr msg;
251 378 memset(&msg, 0, sizeof(msg));
252
253 378 msg.msg_hdr.msg_name = &ArtNetSyncAddress;
254 378 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
255 378 msg.msg_hdr.msg_iov = &ArtNetSyncIovecs;
256 378 msg.msg_hdr.msg_iovlen = 1;
257 378 msg.msg_len = ARTNET_SYNC_PACKET_LENGTH;
258
2/4
✓ Branch 0 taken 378 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 378 times.
✗ Branch 3 not taken.
378 msgs[ARTNET_SYNC_KEY].push_back(msg);
259 }
260 }
261
262 4290 void ArtNetOutputData::FrameSent() {
263
2/2
✓ Branch 0 taken 4088 times.
✓ Branch 1 taken 202 times.
4290 if (sequencePending) {
264 4088 sequenceNumber = pendingSequenceNumber;
265 4088 sequencePending = false;
266 }
267 4290 }
268
269 52 void ArtNetOutputData::GetRequiredChannelRange(int& min, int& max) {
270 52 min = startChannel - 1;
271 52 max = startChannel + (channelCount * universeCount) - 1;
272 52 }
273
274 26 void ArtNetOutputData::DumpConfig() {
275 26 LogDebug(VB_CHANNELOUT, "ArtNet Universe: %s %d:%d:%d:%d:%d:%d %s\n",
276 description.c_str(),
277 active,
278 universe,
279 universeCount,
280 startChannel,
281 channelCount,
282 type,
283 ipAddress.c_str());
284 26 }
285