| 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 |