GCC Code Coverage Report


Directory: ./
File: channeloutput/GenericUDPOutput.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 128 138 92.8%
Functions: 18 18 100.0%
Branches: 128 230 55.7%

Line Branch Exec Source
1 #include "fpp-pch.h"
2
3 #include "fpp-json.h"
4
5 #include "../Warnings.h"
6 #include "../log.h"
7
8 #include "GenericUDPOutput.h"
9 #include "UDPOutput.h"
10 #include "ChannelOutputSetup.h"
11
12 #include "Plugin.h"
13 class GenericUDPPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
14 public:
15 49 GenericUDPPlugin() :
16
2/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
147 FPPPlugins::Plugin("GenericUDP") {
17 49 }
18 56 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
19
2/6
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 56 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
112 return new GenericUDPOutput(startChannel, channelCount);
20 }
21 };
22
23 extern "C" {
24 49 FPPPlugins::Plugin* createPlugin() {
25
2/6
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 49 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
98 return new GenericUDPPlugin();
26 }
27 }
28
29 static const std::string GENERIC_UDP_TYPE = "GenericUDP";
30
31 class GenericUDPOutputData : public UDPOutputData {
32 public:
33 52 GenericUDPOutputData(const Json::Value& config) :
34 UDPOutputData(config),
35 52 count(0),
36 52 isMulticast(false),
37 52 isBroadcast(false) {
38
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
52 tokens = config["tokens"].asString();
39
3/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✓ Branch 3 taken 2 times.
52 port = config["port"].asInt();
40
41 // s_addr is left unset when the output is inactive, so it has to start
42 // zeroed: the address classification below reads it either way.
43 50 memset(&udpAddress, 0, sizeof(udpAddress));
44 50 udpAddress.sin_family = AF_INET;
45 50 udpAddress.sin_port = htons(port);
46
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 if (active) {
47
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 udpAddress.sin_addr.s_addr = toInetAddr(ipAddress, valid);
48
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
50 if (!valid) {
49 ✗ AddOutputConfigWarning(14, "Could not resolve host name " + ipAddress + " - disabling output");
50 ✗ active = false;
51 }
52 }
53
54 // s_addr is network byte order, so the octets have to be read out of the
55 // host-order value: on a little-endian host the top byte of the raw word
56 // is the LAST octet, not the first.
57 50 uint32_t hostAddr = ntohl(udpAddress.sin_addr.s_addr);
58 50 int firstOctet = (hostAddr >> 24) & 0xFF;
59 50 int lastOctet = hostAddr & 0xFF;
60
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
50 if (firstOctet >= 224 && firstOctet <= 239) {
61 2 isMulticast = true;
62
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 } else if (lastOctet == 255) {
63 // Directed (x.y.z.255) and limited (255.255.255.255) broadcast. This
64 // is matched deliberately - the old byte-swapped math happened to
65 // catch the same addresses, so dropping it silently would take
66 // working broadcast configs with it.
67 ✗ isBroadcast = true;
68 }
69
70
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 std::string nt = tokens;
71
72 50 std::string token;
73 50 std::vector<uint8_t> bytes;
74
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 50 times.
100 while (!nt.empty()) {
75 50 std::string::size_type pos = nt.find_first_of(' ');
76
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 42 times.
50 if (pos < nt.size()) {
77
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 token = nt.substr(0, pos);
78
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 nt = nt.substr(pos + 1);
79 } else {
80
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 token = nt;
81
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 nt = "";
82 }
83
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 if (token.size() > 2) {
84
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 2 times.
50 if (token[0] == '{') {
85
3/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✓ Branch 3 taken 6 times.
48 if (token == "{data}") {
86
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 41 times.
42 if (bytes.size() > 0) {
87
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 udpBuffers.push_back(bytes);
88 }
89
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 channelIovecs.push_back(udpBuffers.size());
90 42 bytes.clear();
91
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 udpBuffers.push_back(bytes);
92
11/18
✓ 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 5 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 5 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 5 times.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 5 times.
6 } else if (token == "{P}" || token == "{p}" || token == "{P1}" || token == "{p1}") {
93
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 bytes.push_back(channelCount / 3);
94
11/18
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 4 times.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 4 times.
5 } else if (token == "{S}" || token == "{s}" || token == "{S1}" || token == "{s1}") {
95
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 bytes.push_back(channelCount);
96
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 } else if (token == "{S2}") {
97 // Upper case is low byte first and lower case high
98 // byte first, as these have always gone on the wire;
99 // the helpers they called were named the other way
100 // round.
101
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addLittleEndian(bytes, channelCount);
102
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 } else if (token == "{s2}") {
103
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addBigEndian(bytes, channelCount);
104
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 } else if (token == "{P2}") {
105
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addLittleEndian(bytes, channelCount / 3);
106
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 } else if (token == "{p2}") {
107
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 addBigEndian(bytes, channelCount / 3);
108 } else {
109 ✗ LogWarn(VB_CHANNELOUT, "Unknown token -%s-\n", token.c_str());
110 ✗ for (int x = 0; x < token.size(); x++) {
111 ✗ bytes.push_back((uint8_t)token[x]);
112 }
113 }
114
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
2 } else if (token[0] == '0' && token[1] == 'x') {
115 1 int i = std::strtoul(token.c_str(), 0, 16);
116
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 bytes.push_back(i);
117
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } else if (token[0] == '#') {
118
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 int i = std::strtoul(token.substr(1).c_str(), 0, 16);
119
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 bytes.push_back(i);
120 }
121 } else {
122 ✗ for (int x = 0; x < token.size(); x++) {
123 ✗ bytes.push_back((uint8_t)token[x]);
124 }
125 }
126 }
127
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 49 times.
50 if (!bytes.empty()) {
128
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 udpBuffers.push_back(bytes);
129 }
130
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 udpIovecs.resize(udpBuffers.size());
131
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 50 times.
94 for (int x = 0; x < udpBuffers.size(); x++) {
132 // data(), not &[0]: the {data} placeholder is an empty vector
133 44 udpIovecs[x].iov_base = udpBuffers[x].data();
134 44 udpIovecs[x].iov_len = udpBuffers[x].size();
135 }
136
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 50 times.
92 for (auto idx : channelIovecs) {
137 42 udpIovecs[idx].iov_len = channelCount;
138 }
139 60 }
140 200 virtual ~GenericUDPOutputData() {
141 200 }
142
143 48 virtual const std::string& GetOutputTypeString() const override {
144 48 return GENERIC_UDP_TYPE;
145 }
146
147 2 void addLittleEndian(std::vector<uint8_t>& bytes, int v) {
148
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bytes.push_back(v & 0xFF);
149
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bytes.push_back((v >> 8) & 0xFF);
150 2 }
151 2 void addBigEndian(std::vector<uint8_t>& bytes, int v) {
152
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bytes.push_back((v >> 8) & 0xFF);
153
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bytes.push_back(v & 0xFF);
154 2 }
155 50 virtual bool IsPingable() override {
156
3/4
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
50 return !isMulticast && !isBroadcast;
157 }
158
159 6171 virtual void PrepareData(unsigned char* channelData,
160 UDPOutputMessages& msgs) override {
161
4/8
✓ Branch 0 taken 6171 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6171 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6171 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6171 times.
✗ Branch 7 not taken.
6171 if (valid && active && NeedToOutputFrame(channelData, startChannel - 1, 0, channelCount)) {
162 struct mmsghdr msg;
163 6171 memset(&msg, 0, sizeof(msg));
164
165 6171 msg.msg_hdr.msg_name = &udpAddress;
166 6171 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
167 6171 msg.msg_hdr.msg_iov = &udpIovecs[0];
168 6171 msg.msg_hdr.msg_iovlen = udpIovecs.size();
169 6171 msg.msg_len = 0;
170
2/2
✓ Branch 0 taken 5984 times.
✓ Branch 1 taken 6171 times.
12155 for (int x = 0; x < udpIovecs.size(); x++) {
171 5984 msg.msg_len += udpIovecs[x].iov_len;
172 }
173
174 6171 count++;
175 6171 int start = startChannel - 1;
176
2/2
✓ Branch 0 taken 5842 times.
✓ Branch 1 taken 6171 times.
12013 for (auto idx : channelIovecs) {
177 5842 udpIovecs[idx].iov_base = (void*)(channelData + start);
178 }
179
180
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6171 times.
6171 if (isBroadcast) {
181 ✗ msgs[BROADCAST_MESSAGES_KEY].push_back(msg);
182
2/2
✓ Branch 0 taken 869 times.
✓ Branch 1 taken 5302 times.
6171 } else if (isMulticast) {
183 // With the other multicast: that socket has IP_MULTICAST_IF on
184 // the output interface. One keyed by the group's address
185 // doesn't, and its packets followed the default route instead.
186
2/4
✓ Branch 0 taken 869 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 869 times.
✗ Branch 3 not taken.
869 msgs[MULTICAST_MESSAGES_KEY].push_back(msg);
187 } else {
188
2/4
✓ Branch 0 taken 5302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5302 times.
✗ Branch 3 not taken.
5302 msgs[udpAddress.sin_addr.s_addr].push_back(msg);
189 }
190
1/2
✓ Branch 0 taken 6171 times.
✗ Branch 1 not taken.
6171 SaveFrame(&channelData[startChannel - 1], channelCount);
191 6171 skippedFrames = 0;
192 } else {
193 ✗ skippedFrames++;
194 }
195 6171 }
196
197 5 virtual void DumpConfig() override {
198 5 }
199
200 int port;
201 int count;
202
203 bool isMulticast;
204 bool isBroadcast;
205
206 std::string tokens;
207
208 sockaddr_in udpAddress;
209 std::vector<int> channelIovecs;
210 std::vector<struct iovec> udpIovecs;
211 std::vector<std::vector<uint8_t>> udpBuffers;
212 };
213
214 56 GenericUDPOutput::GenericUDPOutput(unsigned int startChannel, unsigned int channelCount) :
215 56 ChannelOutput(startChannel, channelCount) {
216 56 }
217 224 GenericUDPOutput::~GenericUDPOutput() {
218 // deleted without Close() when Init() fails; a no-op otherwise
219 112 UDPOutput::UnregisterExternalOutput(this);
220 224 }
221
222 56 int GenericUDPOutput::Init(Json::Value config) {
223
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 45 times.
56 if (!UDPOutput::INSTANCE) {
224 11 LogErr(VB_CHANNELOUT, "GenericUDPOutput Requires the UDP outputs to be enabled\n");
225 }
226 56 config.removeMember("type");
227 // Registered by owner, not handed over as data: the UDP output is rebuilt
228 // whenever the E1.31/DDP config is saved and has to recreate this, and
229 // Close() has to be able to take it back out.
230
3/6
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 56 times.
✗ Branch 5 not taken.
56 UDPOutput::RegisterExternalOutput(this, [config]() -> UDPOutputData* {
231
4/6
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 50 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
102 return new GenericUDPOutputData(config);
232 });
233
2/4
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56 times.
✗ Branch 3 not taken.
56 return ChannelOutput::Init(config);
234 }
235 56 int GenericUDPOutput::Close(void) {
236 56 UDPOutput::UnregisterExternalOutput(this);
237 56 return ChannelOutput::Close();
238 }
239
240 119 void GenericUDPOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
241 119 addRange(m_startChannel, m_startChannel + m_channelCount - 1);
242 119 }
243
244 6138 int GenericUDPOutput::SendData(unsigned char* channelData) {
245 6138 return m_channelCount;
246 }
247