GCC Code Coverage Report


Directory: ./
File: MultiSync.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 4 4 100.0%
Functions: 3 3 100.0%
Branches: 0 0 -%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2022 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 // PLUGIN ABI: native plugins compile against this header and are loaded with
15 // dlopen(), so parts of it are baked into binaries FPP did not build.
16 // - MultiSyncPlugin is subclassed by plugins and FPP calls its virtuals:
17 // inserting, removing, reordering or even appending a virtual breaks
18 // them (appending reads past the end of an older plugin's vtable).
19 // - MultiSync's data layout: plugins call the inline isMultiSyncEnabled().
20 // - The MultiSyncSystemType enum values.
21 // Any such change must bump FPP_PLUGIN_API_VERSION in Plugin.h. Without the
22 // bump, a plugin built against the previous header still loads and silently
23 // uses the old layout - FPP 10.2 shipped exactly that and crash-looped fppd.
24 // See .claude/PLUGIN-ABI.md for which plugins use what.
25
26 #include <netinet/in.h>
27 #include "fpp-json-fwd.h"
28 #include <sys/types.h>
29 #include <atomic>
30 #include <deque>
31 #include <functional>
32 #include <mutex>
33 #include <pthread.h>
34 #include <map>
35 #include <set>
36 #include <string>
37 #include <vector>
38
39 #include "SysSocket.h"
40 #include "settings.h"
41
42 #define FPP_CTRL_PORT 32320
43
44 #define CTRL_PKT_CMD 0 // deprecated in favor of FPP Commands
45 #define CTRL_PKT_SYNC 1
46 #define CTRL_PKT_EVENT 2 // deprecated in favor of FPP Commands
47 #define CTRL_PKT_BLANK 3
48 #define CTRL_PKT_PING 4
49 #define CTRL_PKT_PLUGIN 5
50 #define CTRL_PKT_FPPCOMMAND 6
51
52 typedef struct __attribute__((packed)) {
53 char fppd[4]; // 'FPPD'
54 uint8_t pktType; // Control Packet Type
55 uint16_t extraDataLen; // Length of extra data following Control Packet
56 } ControlPkt;
57
58 typedef struct {
59 char command[1]; // Null-terminated Command to process
60 } CommandPkt;
61
62 #define SYNC_PKT_START 0
63 #define SYNC_PKT_STOP 1
64 #define SYNC_PKT_SYNC 2
65 #define SYNC_PKT_OPEN 3
66
67 #define SYNC_FILE_SEQ 0
68 #define SYNC_FILE_MEDIA 1
69
70 typedef struct __attribute__((packed)) {
71 uint8_t pktType; // Sync Packet Type
72 uint8_t fileType; // File Type being synced
73 uint32_t frameNumber; // Sequence frames displayed on Master
74 float secondsElapsed; // Seconds elapsed in file on Master
75 char filename[1]; // Null-terminated File Name to play on Slave
76 // (data may continue past this header)
77 } SyncPkt;
78
79 typedef enum systemType {
80 kSysTypeUnknown = 0x00,
81 kSysTypeFPP = 0x01,
82 kSysTypeFPPRaspberryPiA,
83 kSysTypeFPPRaspberryPiB,
84 kSysTypeFPPRaspberryPiAPlus,
85 kSysTypeFPPRaspberryPiBPlus,
86 kSysTypeFPPRaspberryPi2B,
87 kSysTypeFPPRaspberryPi2BNew,
88 kSysTypeFPPRaspberryPi3B,
89 kSysTypeFPPRaspberryPi3BPlus,
90 kSysTypeFPPRaspberryPiZero,
91 kSysTypeFPPRaspberryPiZeroW,
92 kSysTypeFPPRaspberryPi3APlus,
93 kSysTypeFPPRaspberryPi4,
94 kSysTypeFPPRaspberryPi5,
95 kSysTypeFPPRaspberryPiZero2W,
96 kSysTypeFPPBeagleBoneBlack = 0x41,
97 kSysTypeFPPBeagleBoneBlackWireless = 0x42,
98 kSysTypeFPPBeagleBoneGreen = 0x43,
99 kSysTypeFPPBeagleBoneGreenWireless = 0x44,
100 kSysTypeFPPPocketBeagle = 0x45,
101 kSysTypeFPPSanCloudBeagleBoneEnhanced = 0x46,
102 kSysTypeFPPPocketBeagle2 = 0x47,
103 kSysTypeFPPArmbian = 0x60,
104 kSysTypeMacOS = 0x70,
105 // Values under 0x80 are "FPP based" and run full FPP
106 kSysTypeFalconController = 0x80,
107 kSysTypeFalconF16v2 = 0x81,
108 kSysTypeFalconF4v2_64Mb = 0x82,
109 kSysTypeFalconF16v2R = 0x83,
110 kSysTypeFalconF4v2 = 0x84,
111 kSysTypeFalconF16v3 = 0x85,
112 kSysTypeFalconF4v3 = 0x86,
113 kSysTypeFalconF48 = 0x87,
114 kSysTypeFalconF16v4 = 0x88,
115 kSysTypeFalconF48v4 = 0x89,
116 kSysTypeFalconF16v5 = 0x8a,
117 kSysTypeFalconF48v5 = 0x8b,
118 kSysTypeFalconF32v5 = 0x8c,
119 kSysTypeExperienceGP16 = 0xA0,
120 kSysTypeExperienceGP8 = 0xA1,
121 kSysTypeExperienceGLR = 0xA2,
122 kSysTypeExperienceG16Pro = 0xA3,
123 kSysTypeExperienceG32Pro = 0xA4,
124 kSysTypeExperienceAURORACORE16 = 0xA5,
125 kSysTypeExperienceVIVID8 = 0xA6,
126 kSysTypeExperiencePIXELLINK4 = 0xA7,
127 kSysTypeExperienceGenius = 0xAF,
128 kSysTypeOtherSystem = 0xC0,
129 kSysTypexSchedule = 0xC1,
130 kSysTypeESPixelStick = 0xC2,
131 kSysTypeESPixelStickESP32 = 0xC3,
132 kSysTypeBaldrick = 0xC4,
133 kSysTypeNonMultiSyncCapable = 0xF0,
134 kSysTypeTwinkly = 0xFA,
135 kSysTypeWLED = 0xFB,
136 kSysTypeDIYLEDExpress = 0xFC,
137 kSysTypeHinksPix = 0xFD,
138 kSysTypeAlphaPix = 0xFE,
139 kSysTypeSanDevices = 0xFF
140 } MultiSyncSystemType;
141
142 // The two records below hold the slow-changing detail the multisync UI needs
143 // about every discovered FPP remote -- OS version, host description, background
144 // color, git branch/commit, whether channel inputs/outputs are enabled, and what
145 // cape is installed. None of it fits in (or belongs in) the UDP ping packet, so
146 // fppd collects it over HTTP instead; see MultiSync::CheckSystemInfoRefreshes().
147 //
148 // Both deliberately hold plain members rather than a Json::Value: MultiSync.h
149 // only forward-declares Json::Value (fpp-json-fwd.h), and spelling the fields
150 // out documents exactly what is relayed to peers.
151 class MultiSyncSystemInfo {
152 public:
153 bool valid = false;
154
155 std::string platform;
156 std::string variant;
157 std::string subPlatform;
158 std::string osVersion;
159 std::string osRelease;
160 std::string kernel;
161 std::string hostDescription;
162 std::string backgroundColor;
163 std::string branch;
164 std::string localGitVersion;
165 std::string remoteGitVersion;
166 std::string upgradeSource;
167 std::vector<std::string> ips;
168 // Tri-state: -1 when the remote is too old to report the field at all, in
169 // which case the UI still has to go look for itself.
170 int channelInputsEnabled = -1;
171 int channelOutputsEnabled = -1;
172
173 Json::Value toJSON() const;
174 };
175
176 class MultiSyncCapeInfo {
177 public:
178 bool valid = false;
179 // False when the remote answered but has no cape installed (/api/cape
180 // returns 404). That is a real answer and worth caching, as distinct from
181 // valid == false meaning "we have not managed to ask yet".
182 bool present = false;
183
184 std::string id;
185 std::string name;
186 std::string description;
187 std::string version;
188 std::string designer;
189 // Deliberately NOT carrying the cape's serialNumber: it is a per-board
190 // hardware identifier with no use in the UI, and this record is handed to
191 // every peer that asks for the systems list.
192 std::string vendorName;
193 std::string vendorURL;
194 std::string vendorEmail;
195 std::string vendorImage;
196 // The cape's own opt-out of hardware detail in the usage statistics. Only
197 // a signed cape can carry it -- CapeUtils strips it from an unsigned one --
198 // and it is relayed so that a peer-side collector can honour the flag of
199 // the cape it actually describes, rather than the reporting host's.
200 // Default true: absent means the cape never asked to be excluded.
201 bool sendStats = true;
202
203 Json::Value toJSON() const;
204 // Build from a /api/cape style object (a remote's reply, or the local
205 // media/tmp/cape-info.json). Result is valid+present.
206 static MultiSyncCapeInfo fromCapeJSON(const Json::Value& v);
207 };
208
209 // Identity synthesised from a device's MAC when it reports no UUID of its own.
210 // The prefix marks it as a stand-in so a real UUID arriving later can replace
211 // it -- see MultiSyncSystem::update().
212 #define MAC_UUID_PREFIX "MAC:"
213
214 class MultiSyncSystem {
215 public:
216 6728 MultiSyncSystem() {
217 6728 }
218 unsigned long lastSeen = 0;
219 std::string lastSeenStr;
220 bool multiSync = false;
221 MultiSyncSystemType type = kSysTypeUnknown;
222 unsigned int majorVersion = 0;
223 unsigned int minorVersion = 0;
224 FPPMode fppMode = FPPMode::PLAYER_MODE;
225 bool sendingMultiSync = false;
226 // True if this is a full FPP instance running in Remote mode and is therefore
227 // a valid target for "Send MultiSync to ALL KNOWN remotes via Unicast". This
228 // excludes non-FPP devices (WLED, ESPixelStick, Falcon controllers, etc.) and
229 // FPP instances not in Remote mode. Computed in update().
230 bool supportsUnicast = false;
231 std::string address;
232 std::string hostname;
233 std::string version;
234 std::string model;
235 std::string ranges;
236 std::string uuid;
237
238 // Fetched by MultiSync::CheckSystemInfoRefreshes(), relayed by toJSON().
239 // All six are guarded by MultiSync::m_systemsLock.
240 MultiSyncSystemInfo systemInfo; // subset of the remote's /api/system/info
241 MultiSyncCapeInfo capeInfo; // subset of the remote's /api/cape
242 std::string infoFetchedVersion; // 'version' the cached info was fetched for
243 // Earliest wall clock at which this remote may be fetched again. 0 means
244 // "due, but not yet scheduled": the sweep turns that into a jittered time
245 // rather than fetching immediately, so a fleet-wide event doesn't converge
246 // on one target. See CheckSystemInfoRefreshes().
247 time_t infoNextFetch = 0;
248 bool infoFetchPending = false; // a fetch is in flight right now
249
250 unsigned char ipa = 0;
251 unsigned char ipb = 0;
252 unsigned char ipc = 0;
253 unsigned char ipd = 0;
254
255 void update(MultiSyncSystemType type,
256 unsigned int majorVersion, unsigned int minorVersion,
257 FPPMode fppMode,
258 const std::string& address,
259 const std::string& hostname,
260 const std::string& version,
261 const std::string& model,
262 const std::string& ranges,
263 const std::string& uuid,
264 const bool multiSync,
265 const bool sendingMultiSync);
266 Json::Value toJSON(bool local, bool timestamps);
267 };
268
269 class MultiSyncStats {
270 public:
271 MultiSyncStats(std::string ip, std::string host);
272
273 Json::Value toJSON();
274
275 std::string sourceIP;
276 std::string hostname;
277 time_t lastReceiveTime;
278 uint32_t pktCommand;
279 uint32_t pktSyncSeqOpen;
280 uint32_t pktSyncSeqStart;
281 uint32_t pktSyncSeqStop;
282 uint32_t pktSyncSeqSync;
283 uint32_t pktSyncMedOpen;
284 uint32_t pktSyncMedStart;
285 uint32_t pktSyncMedStop;
286 uint32_t pktSyncMedSync;
287 uint32_t pktBlank;
288 uint32_t pktPing;
289 uint32_t pktPlugin;
290 uint32_t pktFPPCommand;
291 uint32_t pktError;
292 };
293
294 class MultiSyncPlugin {
295 public:
296 MultiSyncPlugin() {}
297 virtual ~MultiSyncPlugin() {}
298
299 virtual void SendSeqOpenPacket(const std::string& filename) {}
300 virtual void SendSeqSyncStartPacket(const std::string& filename) {}
301 virtual void SendSeqSyncStopPacket(const std::string& filename) {}
302 virtual void SendSeqSyncPacket(const std::string& filename, int frames, float seconds) {}
303 virtual void ShutdownSync(void) {}
304
305 virtual void SendMediaOpenPacket(const std::string& filename) {}
306 virtual void SendMediaSyncStartPacket(const std::string& filename) {}
307 virtual void SendMediaSyncStopPacket(const std::string& filename) {}
308 virtual void SendMediaSyncPacket(const std::string& filename, float seconds) {}
309
310 virtual void SendBlankingDataPacket(void) {}
311
312 virtual void SendPluginData(const std::string& name, const uint8_t* data, int len) {}
313 virtual void SendFPPCommandPacket(const std::string& host, const std::string& cmd, const std::vector<std::string>& args) {}
314
315 virtual void ReceivedSeqOpenPacket(const std::string& filename) {}
316 virtual void ReceivedSeqSyncStartPacket(const std::string& filename) {}
317 virtual void ReceivedSeqSyncStopPacket(const std::string& filename) {}
318 virtual void ReceivedSeqSyncPacket(const std::string& filename, int frames, float seconds) {}
319
320 virtual void ReceivedMediaOpenPacket(const std::string& filename) {}
321 virtual void ReceivedMediaSyncStartPacket(const std::string& filename) {}
322 virtual void ReceivedMediaSyncStopPacket(const std::string& filename) {}
323 virtual void ReceivedMediaSyncPacket(const std::string& filename, float seconds) {}
324
325 virtual void ReceivedBlankingDataPacket(void) {}
326 virtual void ReceivedPluginData(const std::string& name, const uint8_t* data, int len) {}
327 virtual void ReceivedFPPCommandPacket(const std::string& cmd, const std::vector<std::string>& args) {}
328 };
329
330 class NetInterfaceInfo {
331 public:
332 NetInterfaceInfo();
333 ~NetInterfaceInfo();
334
335 std::string interfaceName;
336 std::string interfaceAddress;
337 uint32_t address;
338 uint32_t broadcastAddress;
339 int multicastSocket;
340 };
341
342 class MultiSync {
343 public:
344 static MultiSync INSTANCE;
345
346 MultiSync();
347 ~MultiSync();
348
349 int Init(void);
350
351 668677 bool isMultiSyncEnabled() { return m_multiSyncEnabled; }
352
353 1886 int GetControlSocket(void) { return m_receiveSock; }
354 void ProcessControlPacket(bool pingOnly = false);
355
356 void UpdateSystem(MultiSyncSystemType type,
357 unsigned int majorVersion,
358 unsigned int minorVersion,
359 FPPMode fppMode,
360 const std::string& address,
361 const std::string& hostname,
362 const std::string& version,
363 const std::string& model,
364 const std::string& range,
365 const std::string& uuid,
366 const bool multiSync,
367 const bool sendingMultiSync);
368
369 Json::Value GetSystems(bool localOnly = false, bool timestamps = true);
370 Json::Value GetSyncStats();
371 void ResetSyncStats();
372
373 void WriteRuntimeInfoFile();
374
375 void Ping(int discover = 0, bool broadcast = true);
376 void PingSingleRemote(const char* address, int discover = 0);
377 void QueueDiscoverReply(const std::string& address, const std::string& srcIp);
378 // Detect/refresh a single non-multisync remote (e.g. a WLED node found
379 // via mDNS) by fetching its HTTP root and running controller detection.
380 void PingSingleRemoteViaHTTP(const std::string& address);
381 void Discover(void);
382 void PeriodicPing();
383
384 void SendSeqOpenPacket(const std::string& filename);
385 void SendSeqSyncStartPacket(const std::string& filename);
386 void SendSeqSyncStopPacket(const std::string& filename);
387 void SendSeqSyncPacket(const std::string& filename, int frames, float seconds);
388 void ShutdownSync(void);
389
390 void SendMediaOpenPacket(const std::string& filename);
391 void SendMediaSyncStartPacket(const std::string& filename);
392 void SendMediaSyncStopPacket(const std::string& filename);
393 void SendMediaSyncPacket(const std::string& filename, float seconds);
394
395 void SendFPPCommandPacket(const std::string& host, const std::string& cmd, const std::vector<std::string>& args);
396 void SendBlankingDataPacket(void);
397
398 void SendPluginData(const std::string& name, const uint8_t* data, int len);
399
400 void addMultiSyncPlugin(MultiSyncPlugin* p);
401 void removeMultiSyncPlugin(MultiSyncPlugin* p);
402
403 void OpenSyncedSequence(const std::string& filename);
404 void StartSyncedSequence(const std::string& filename);
405 void StopSyncedSequence(const std::string& filename);
406 void SyncSyncedSequence(const std::string& filename, int frameNumber, float secondsElapsed);
407
408 void OpenSyncedMedia(const std::string& filename);
409 void StartSyncedMedia(const std::string& filename, float secondsElapsed = 0.0f);
410 void StopSyncedMedia(const std::string& filename);
411 void SyncSyncedMedia(const std::string& filename, int frameNumber, float secondsElapsed);
412 void CheckSyncedMediaIdleTimeout();
413
414 void SyncPlaylistToMS(uint64_t ms, const std::string& pl = "", bool sendSyncPackets = false);
415 void SyncPlaylistToMS(uint64_t ms, int pos, const std::string& pl = "", bool sendSyncPackets = false);
416 void SyncStopAll();
417
418 int OpenControlSockets();
419
420 static std::string GetTypeString(MultiSyncSystemType type, bool local = false);
421 static MultiSyncSystemType ModelStringToType(std::string model);
422
423 // Was the curl write callback for the HTTP discovery scan, which now
424 // receives whole responses from CurlManager instead. Retained as a no-op
425 // only because it is public and an external plugin could reference it.
426 void StoreHTTPResponse(std::string* ipp, uint8_t* data, int sz) {}
427
428 [[nodiscard]] std::vector<MultiSyncSystem> const& GetLocalSystems() { return m_localSystems; }
429 [[nodiscard]] std::vector<MultiSyncSystem> const& GetRemoteSystems() { return m_remoteSystems; }
430
431 private:
432 bool isSupportedForMultisync(const char* address, const char* intface);
433
434 void setupMulticastReceive(bool cycle);
435 void PingSingleRemote(MultiSyncSystem& sys, int discover = 0);
436 int CreatePingPacket(MultiSyncSystem& sys, char* outBuf, int discover);
437
438 bool FillLocalSystemInfo(void);
439 std::string GetHardwareModel(void);
440
441 // Rebuild m_configuredOutputRanges from co-universes.json. Called once at
442 // startup and again whenever the file changes (via a FileMonitor watch).
443 void ReloadConfiguredOutputRanges();
444
445 int OpenBroadcastSocket(void);
446 void SendBroadcastPacket(void* outBuf, int len);
447 void SendControlPacket(void* outBuf, int len);
448 void SendControlPacketViaMsgs(std::vector<struct mmsghdr>& msgs, struct iovec& iovec, void* outBuf, int len);
449 void SendMulticastPacket(void* outBuf, int len);
450 void SendUnicastPacket(const std::string& address, void* outBuf, int len);
451 // Rebuild the cached "all known remotes" unicast destination list from the
452 // currently-known remote systems that support unicast. Only used when the
453 // MultiSyncUnicast ("send to ALL known remotes") setting is enabled.
454 // Neither form may be called with m_systemsLock held: they take
455 // m_unicastUpdateLock, and that lock is only ever acquired ahead of
456 // m_systemsLock, never behind it. The no-arg form snapshots the addresses
457 // under m_systemsLock and then releases it before doing the rebuild; a
458 // caller that already holds m_systemsLock takes its own snapshot while it
459 // has the lock and passes it to the overload after releasing it.
460 void UpdateUnicastDestinations();
461 void UpdateUnicastDestinations(const std::vector<std::string>& addrsToResolve);
462 // Re-read the sync send methods (MultiSyncBroadcast/Multicast/Unicast) and
463 // the statically-configured unicast remote list (MultiSyncRemotes plus
464 // MultiSyncExtraRemotes) from the settings and swap the resulting
465 // destination list into place. Called once from OpenControlSockets() and
466 // again from a settings listener whenever any of those settings change, so
467 // editing the remote list on the MultiSync page takes effect immediately
468 // instead of requiring an fppd restart.
469 void ReloadSyncDestinations();
470 bool FillInInterfaces();
471 bool RemoveInterface(const std::string& interface);
472
473 void InitControlPacket(ControlPkt* pkt);
474
475 int OpenReceiveSocket(void);
476
477 void PerformHTTPDiscovery(void);
478 void DiscoverViaHTTP(const std::set<std::string>& ips, const std::set<std::string>& exacts);
479
480 // Twinkly lights answer neither mDNS nor SSDP, and serve a 404 at "/", so
481 // they are invisible to the mDNS listener and are dropped by the HTTP subnet
482 // scan before any detector sees them. They do answer a short UDP broadcast
483 // on port 5555, which is the only way to find one that has not been
484 // configured as a channel output. See PerformTwinklyDiscovery().
485 bool OpenTwinklyDiscoverySocket();
486 void PerformTwinklyDiscovery();
487 bool ProcessTwinklyDiscoveryReplies();
488 void DiscoverIPViaHTTP(const std::string& ip, const std::string& html, bool allowUnknown = false);
489
490 // Both HTTP probe paths are queue-and-pump rather than fire-everything:
491 // each hands its addresses to a queue and then starts as many requests as
492 // the in-flight cap allows, and each completion starts the next one. See
493 // the cap defines in MultiSync.cpp for why an unbounded fan-out is the
494 // wrong thing on a show network.
495 void PumpHTTPPings();
496 void StartHTTPPing(const std::string& address);
497 void FinishHTTPPing();
498 void PumpHTTPDiscovery();
499 void FinishHTTPDiscovery();
500
501 void ProcessSyncPacket(ControlPkt* pkt, int len, MultiSyncStats* stats);
502 void ProcessCommandPacket(ControlPkt* pkt, int len, MultiSyncStats* stats);
503 void ProcessPingPacket(ControlPkt* pkt, int len, const std::string& src, MultiSyncStats* stats, const std::string& incomingIp = "");
504 void ProcessPluginPacket(ControlPkt* pkt, int len, MultiSyncStats* stats);
505 void ProcessFPPCommandPacket(ControlPkt* pkt, int len, MultiSyncStats* stats);
506
507 std::recursive_mutex m_systemsLock;
508 std::vector<MultiSyncSystem> m_localSystems;
509 std::vector<MultiSyncSystem> m_remoteSystems;
510 // Identities learned second-hand: some vendors report a stable id for each
511 // neighbour they can see but none for themselves, so a device's identity
512 // arrives while a DIFFERENT address is being probed. Kept rather than
513 // applied and discarded, because the neighbour is often identified before
514 // the device it describes has been discovered at all. Guarded by
515 // m_systemsLock. Keyed by address.
516 std::map<std::string, std::string> m_peerUUIDHints;
517
518 // Channel-output ranges parsed from co-universes.json, keyed by each output
519 // target's configured address and its resolved IP. Used to backfill the
520 // advertised channel range of non-FPP remotes that don't report their own
521 // range via MultiSync. Rebuilt only when co-universes.json changes (see
522 // ReloadConfiguredOutputRanges()) so the frequently-polled GetSystems() path
523 // is just a map lookup with no file IO or DNS resolution. Guarded by
524 // m_systemsLock.
525 std::map<std::string, std::string> m_configuredOutputRanges;
526
527 std::map<std::string, NetInterfaceInfo> m_interfaces;
528 // Which send methods are active. Read on the output thread from
529 // SendControlPacket() without any lock and rewritten by
530 // ReloadSyncDestinations() on the settings-reload thread, hence atomic.
531 std::atomic<bool> m_sendMulticast;
532 std::atomic<bool> m_sendBroadcast;
533 std::atomic<bool> m_sendUnicast = false;
534
535 int m_broadcastSock;
536 int m_controlSock;
537 int m_receiveSock;
538 // Sends the Twinkly discovery broadcast and receives the replies. They come
539 // back to the port the request went out from, so this stays on an ephemeral
540 // port and never binds 5555. Registered with EPollManager, so replies are
541 // handled on the main loop.
542 int m_twinklyDiscoverySock = -1;
543
544 // Guards the one-shot listener teardown in ShutdownSync(); see the comment
545 // there for why the second call has to be a no-op.
546 std::atomic<bool> m_settingsListenersRemoved{ false };
547
548 std::string m_hostname;
549 struct sockaddr_in m_srcAddr;
550 struct sockaddr_in m_receiveSrcAddr;
551 std::mutex m_socketLock;
552
553 // Serializes UpdateUnicastDestinations() calls against each other (DNS
554 // resolution happens without any other lock held, so overlapping calls are
555 // otherwise possible). Lock order: this lock is never acquired while
556 // m_systemsLock is held -- ReloadSyncDestinations() takes it first and then
557 // reaches m_systemsLock only indirectly, so the reverse order would be an
558 // ABBA deadlock against the discovery paths. m_socketLock is taken under
559 // it, for the brief swap step only.
560 std::mutex m_unicastUpdateLock;
561
562 // Async HTTP enrichment of discovered FPP remotes; see the comment above
563 // the definition of CheckSystemInfoRefreshes() in MultiSync.cpp.
564 void CheckSystemInfoRefreshes();
565 // Marks a remote's cached detail stale so a later sweep re-fetches it (at a
566 // jittered time, not immediately -- see CheckSystemInfoRefreshes()). Called
567 // when the remote announces itself with a discover ping, i.e. its fppd just
568 // started. Public because ProcessPingPacket reaches it through the global
569 // multiSync pointer.
570 public:
571 void InvalidateSystemInfo(const std::string& address);
572
573 private:
574 // Makes every entry for one device agree on its identity; see the comment
575 // above the definition. Caller must hold m_systemsLock.
576 void ReconcileDeviceIdentity(const std::string& hostname, FPPMode fppMode);
577 void ApplyPeerUUIDs(const std::map<std::string, std::string>& peerUUIDs);
578 // Fills in a system's identity from m_peerUUIDHints. Caller holds
579 // m_systemsLock.
580 void ApplyUUIDHint(MultiSyncSystem& sys);
581 void FetchSystemInfo(const std::string& address);
582 void FetchCapeInfo(const std::string& address);
583 // Runs `apply` on every remote entry describing the same device as
584 // `address` -- the same address, or the same non-empty uuid. A device with
585 // more than one NIC has one entry per address (the UI merges them into a
586 // single row by uuid), and one fetch should update all of them. Takes
587 // m_systemsLock.
588 void ForEachRemoteMatching(const std::string& address,
589 const std::function<void(MultiSyncSystem&)>& apply);
590
591 // Wall clock of the next CheckSystemInfoRefreshes() sweep, and how many of
592 // its HTTP fetches are outstanding. Both are touched only on the main loop
593 // -- the sweep and the CurlManager completions run on the same thread. The
594 // counter would be stranded high (and infoFetchPending stuck set) if a
595 // request were ever cancelled without running its callback, but
596 // CurlManager::cancelRequests() only ever cancels a named owner and these
597 // are issued with FPP's own empty owner, so that cannot happen.
598 time_t m_nextInfoScan = 0;
599 std::atomic<int> m_infoFetchesInFlight{ 0 };
600
601 unsigned long m_lastPingTime;
602 unsigned long m_lastCheckTime;
603 int m_lastMediaHalfSecond;
604 int m_lastFrame;
605 int m_lastFrameSent;
606
607 // Wall-clock ms of the last media sync packet we acted on as a remote.
608 // 0 when no synced media is open. Read/written from the sync-receive path
609 // and the main loop's idle check, hence atomic. See
610 // CheckSyncedMediaIdleTimeout().
611 std::atomic<uint64_t> m_lastSyncedMediaPacketMS{ 0 };
612
613 float m_remoteOffset;
614
615 // Statically-configured unicast remotes (MultiSyncRemotes plus
616 // MultiSyncExtraRemotes). Rebuilt by ReloadSyncDestinations() whenever
617 // those settings change. Guarded by m_socketLock (same as the send path).
618 struct iovec m_destIovec;
619 std::vector<struct mmsghdr> m_destMsgs;
620 std::vector<struct sockaddr_in> m_destAddr;
621
622 // Dynamic destination list for the "send to ALL KNOWN remotes via Unicast"
623 // mode. Rebuilt by UpdateUnicastDestinations() as remotes are discovered,
624 // change mode, or time out. Guarded by m_socketLock (same as the send path).
625 struct iovec m_unicastDestIovec;
626 std::vector<struct mmsghdr> m_unicastDestMsgs;
627 std::vector<struct sockaddr_in> m_unicastDestAddr;
628
629 // Guards m_plugins against add/removeMultiSyncPlugin() (which can run on any
630 // thread that owns a plugin) racing the many Send*/Received*/Process* sites
631 // that iterate it, some of which run on the output thread or the main loop.
632 // Iteration sites take a snapshot under this lock via getPluginsCopy() and
633 // iterate the copy so callbacks never run with the lock held.
634 std::mutex m_pluginsLock;
635 std::vector<MultiSyncPlugin*> m_plugins;
636 std::vector<MultiSyncPlugin*> getPluginsCopy();
637
638 #define MAX_MS_RCV_MSG 12
639 #define MAX_MS_RCV_BUFSIZE 1500
640 struct mmsghdr rcvMsgs[MAX_MS_RCV_MSG];
641 struct iovec rcvIovecs[MAX_MS_RCV_MSG];
642 unsigned char rcvBuffers[MAX_MS_RCV_MSG][MAX_MS_RCV_BUFSIZE + 1];
643 unsigned char rcvCmbuf[MAX_MS_RCV_MSG][0x100];
644 struct sockaddr_storage rcvSrcAddr[MAX_MS_RCV_MSG];
645
646 // Pending work for the two HTTP probe paths, plus how many of each are
647 // outstanding. A lock rather than bare atomics because a CurlManager
648 // completion does not reliably land on the main loop: processCurls() is
649 // also called from UDPOutput's init and teardown, so a callback can run on
650 // whichever thread is reloading channel outputs.
651 std::mutex m_httpProbeLock;
652 std::deque<std::string> m_httpPingQueue;
653 int m_httpPingsInFlight = 0;
654 std::deque<std::pair<std::string, bool>> m_httpDiscoveryQueue; // address, isExact
655 int m_httpDiscoveriesInFlight = 0;
656 // Addresses already handed to an HTTP probe by the current Twinkly sweep.
657 // The lights answer each broadcast more than once. Guarded by
658 // m_httpProbeLock, like the queues above.
659 std::set<std::string> m_twinklyDiscoverySeen;
660
661 std::recursive_mutex m_statsLock;
662 std::map<std::string, MultiSyncStats*> m_syncStats;
663 std::string m_syncMaster;
664 bool m_multiSyncEnabled = false;
665 };
666
667 extern MultiSync* multiSync;
668