Skip to content

Commit f89018b

Browse files
dkulpclaude
andcommitted
FPP Connect: stop wiping FPP-owned universe output settings
Uploading UDP outputs regenerated the universes channel output from scratch and rescued only two keys (interface, pacingRate) out of the config already on the FPP, so FPP10's Sending mode was always lost and the global Pacing setting reverted to FPP's 90Mbps default whenever the rescue did not run - the pacing carry-forward was gated on the discovered FPP version, which is zero for an instance whose version probe failed. Invert it: rebuild the universes output on top of the existing one and overwrite only the keys xLights owns (type, enabled, timeout, startChannel, channelCount, universes). The source interface, sending mode, global and per-controller pacing, and anything a newer FPP adds now carry through untouched. The internal pacing-authoritative hint is also stripped when the existing config could not be read, instead of leaking into the upload. Fixes FalconChristmas/fpp#2845 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5758c55 commit f89018b

3 files changed

Lines changed: 69 additions & 62 deletions

File tree

README.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ XLIGHTS/NUTCRACKER RELEASE NOTES
1212
---------------------------------
1313
2026.16 August ??, 2026
1414

15+
-bug (dkulp) FPP Connect: uploading UDP outputs no longer resets FPP10's E1.31 Pacing
16+
and Sending mode - every setting on the universes output that xLights
17+
does not own is now carried forward unchanged
1518
-enh (derwin12) Windows: enhance Generate Custom Model for USB webcam to generate an .xmodel (#3791)
1619
-bug (dkulp) Windows/Linux: fix crash on startup when the machine falls back to the
1720
software OpenGL 1.1 rasterizer - the preview now draws nothing instead

plans/platform-parity/07-controllers-setup-upload.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ code is dark.
155155
| 63 | FPP Connect — HostName/IP column + sort | Header `FPPConnectDialog.cpp:260`; right-click "Sort by HostName" / "Sort by IP Address" `:1004-1012`, persisted as `FPPConnectLocationSort` `:277` | 🟡 | iPad shows host/IP in `FPPInstance.displayName`/`subtitle` (`FPPConnectSheet.swift:77-88`) but offers no sort control; grep `sort` in `FPPConnectSheet.swift` finds no user-facing sort. |
156156
| 64 | FPP Connect — Mode column | Header `FPPConnectDialog.cpp:262` || `FPPInstance.mode` `FPPConnectSheet.swift:35-107`, rendered in `subtitle` `:81-88`. |
157157
| 65 | FPP Connect — Media upload toggle | Header `FPPConnectDialog.cpp:263`; right-click Select All / Deselect All (`MediaPopupMenu` `:264`) | 🟡 | `FPPInstanceConfig.uploadMedia` `FPPConnectSheet.swift:122`, toggle `:1021`, media path resolved via `document.mediaPath(forXsq:)` `:522`. Gap: no bulk select-all/none for the column. |
158-
| 66 | FPP Connect — UDP Out mode | Header `FPPConnectDialog.cpp:265`; None / All / Proxied || `UDPOutMode` enum `FPPConnectSheet.swift:142-152` ("None"/"All Controllers"/"Proxied Only"), picker `:1047-1054`, applied via `document.applyConfig(toFPP:settings:progress:)` `:463-471`. |
158+
| 66 | FPP Connect — UDP Out mode | Header `FPPConnectDialog.cpp:265`; None / All / Proxied || `UDPOutMode` enum `FPPConnectSheet.swift:142-152` ("None"/"All Controllers"/"Proxied Only"), picker `:1047-1054`, applied via `document.applyConfig(toFPP:settings:progress:)` `:463-471`. **2026-08-22:** `FPP::UploadUDPOut` now rebuilds the universes channel output on top of the one already on the FPP, replacing only the keys xLights owns (type/enabled/timeout/startChannel/channelCount/universes) so FPP10's source interface, Sending mode and global + per-controller Pacing survive the upload; shared core, so both platforms get it automatically. |
159159
| 67 | FPP Connect — "Add Proxies" | Header `FPPConnectDialog.cpp:266`; `inst->UploadControllerProxies(_outputManager)` `:1276` || `uploadProxies` `FPPConnectSheet.swift:126`, toggle `:1029-1030`, in the config payload `:463-471`. |
160160
| 68 | FPP Connect — "Upload Outputs" / cape config | Header `FPPConnectDialog.cpp:267`; right-click Select All / Deselect All (`CapePopupMenu` `:433-440`, handler `:442-459`) | 🟡 | `uploadCape` `FPPConnectSheet.swift:125` — "Pixel Hat / Cape Outputs - <model>" when `inst.hasCape` `:1024-1027`, repurposed as "Upload Controller Config" for non-FPP devices `:1011-1016`. Gap: no Select All / Deselect All. |
161161
| 69 | FPP Connect — **FSEQ Type picker** | Header `FPPConnectDialog.cpp:269`; the per-instance widget depends on the device family — FPP gets V1 / V2 zstd / V2 Sparse/zstd / V2 Sparse/Uncompressed (`:657-660`) defaulting to **V2 zstd for a `master`-mode instance** and V2 Sparse/zstd otherwise (`:661`); Falcon V4/V5 + PowerDMX get V1 / V2 **zlib** / V2 Sparse/zlib / V2 Sparse/Uncompressed / V2 Uncompressed (`:671-675`, default `:676`); ESPixelStick and Genius are fixed at V2 Sparse/Uncompressed (`:680`) and everything else at V1 (`:682`). The chosen index is mapped to a codec id at upload (`:1382-1392` — FPP passes the index through, Falcon V4/V5 remaps 1→5 and 2→6, every other family is forced to 3), and persisted per-UUID as `FPPConnectUploadFSEQType_*` (`:1721-1726`, `:1771-1773`) | 🟡 | **The earlier missing-status evidence here overstated the impact and is corrected.** iPad discovery admits only `FPP_TYPE::FPP` and `FPP_TYPE::ESPIXELSTICK` — every other family is dropped by an explicit filter in the discovery path (`XLSequenceDocument.mm`, "iPad scope: FPP + ESPixelStick only"), so the claimed Falcon V4/V5 zstd-vs-zlib and Genius/PowerDMX type-2-vs-3 mis-sends were **unreachable**: those devices never enter `_fppInstances` and never receive an upload. Of the original claim only the master-mode case was real, and it is **fixed 2026-08-06**: a plain FPP now gets desktop's per-mode default (V2 zstd for a `master` instance, V2 Sparse/zstd otherwise, `FPPConnectDialog.cpp:661`) instead of always sparse; ESPixelStick keeps 3, which already matched. Remaining gap: no user-facing picker and no per-UUID persistence, so the desktop default is all you get. |

src-core/controllers/FPP.cpp

Lines changed: 65 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,72 +1518,76 @@ bool FPP::UploadUDPOut(const nlohmann::json &udp) {
15181518
nlohmann::json orig;
15191519
nlohmann::json newudp = udp;
15201520

1521-
if (GetURLAsJSON("/api/channel/output/universeOutputs", orig)) {
1522-
if (orig.contains("channelOutputs")) {
1523-
// FPP owns a few universe-output settings that xLights doesn't model
1524-
// (the network interface, and the packet-pacing/bandwidth caps used to
1525-
// throttle slower controllers). Carry those forward so regenerating the
1526-
// outputs file doesn't wipe them out. Pacing overrides are keyed by
1527-
// destination controller IP so they survive universe/start-channel
1528-
// renumbering; where a controller has several entries we keep the most
1529-
// conservative cap (FPP itself collapses to the lowest rate per IP).
1530-
// Per-output pacing only exists in FPP 10+.
1531-
bool const supportsPacing = IsVersionAtLeast(10, 0, 0);
1532-
std::map<std::string, int> pacingByAddress;
1533-
for (int x = 0; x < (int)orig["channelOutputs"].size(); x++) {
1534-
const auto& co = orig["channelOutputs"][x];
1535-
if (GetJSONStringValue(co, "type") != "universes") {
1536-
continue;
1537-
}
1538-
if (co.contains("interface")) {
1539-
newudp["channelOutputs"][0]["interface"] = GetJSONStringValue(co, "interface");
1540-
}
1541-
if (supportsPacing && co.contains("pacingRate")) {
1542-
// output-level global pacing default
1543-
newudp["channelOutputs"][0]["pacingRate"] = co["pacingRate"];
1544-
}
1545-
if (supportsPacing && co.contains("universes")) {
1546-
for (const auto& u : co["universes"]) {
1547-
if (!u.contains("pacingRate")) {
1548-
continue;
1549-
}
1550-
std::string addr = GetJSONStringValue(u, "address");
1551-
if (addr.empty()) {
1552-
continue; // pacing only applies to unicast destinations
1553-
}
1554-
int rate = GetJSONIntValue(u, "pacingRate", -1);
1555-
if (rate < 0) {
1556-
continue;
1557-
}
1558-
auto it = pacingByAddress.find(addr);
1559-
if (it == pacingByAddress.end()) {
1560-
pacingByAddress[addr] = rate;
1561-
} else if (rate > 0 && (it->second <= 0 || rate < it->second)) {
1562-
it->second = rate; // a real cap beats "unlimited" (0); lower Mbps wins
1563-
}
1564-
}
1521+
// Per-output pacing only exists in FPP 10+.
1522+
bool const supportsPacing = IsVersionAtLeast(10, 0, 0);
1523+
std::map<std::string, int> pacingByAddress;
1524+
1525+
if (GetURLAsJSON("/api/channel/output/universeOutputs", orig) && orig.contains("channelOutputs")) {
1526+
// xLights only owns the universe list itself. Everything else on the
1527+
// universes channel output belongs to FPP (the source interface, the
1528+
// sending/threading mode, the packet-pacing/bandwidth cap used to throttle
1529+
// slower controllers, plus anything a newer FPP adds), so carry those keys
1530+
// forward rather than dropping them when the outputs file is regenerated.
1531+
static const std::unordered_set<std::string> xlOwnedKeys = {
1532+
"type", "enabled", "timeout", "startChannel", "channelCount", "universes"
1533+
};
1534+
// Per-universe pacing overrides are keyed by destination controller IP so they
1535+
// survive universe/start-channel renumbering; where a controller has several
1536+
// entries we keep the most conservative cap (FPP itself collapses to the
1537+
// lowest rate per IP).
1538+
for (int x = 0; x < (int)orig["channelOutputs"].size(); x++) {
1539+
const auto& co = orig["channelOutputs"][x];
1540+
if (GetJSONStringValue(co, "type") != "universes") {
1541+
continue;
1542+
}
1543+
for (const auto& [key, value] : co.items()) {
1544+
if (xlOwnedKeys.find(key) == xlOwnedKeys.end()) {
1545+
newudp["channelOutputs"][0][key] = value;
15651546
}
15661547
}
1567-
if (supportsPacing && newudp.contains("channelOutputs")) {
1568-
for (auto& co : newudp["channelOutputs"]) {
1569-
if (!co.contains("universes")) {
1548+
if (supportsPacing && co.contains("universes")) {
1549+
for (const auto& u : co["universes"]) {
1550+
if (!u.contains("pacingRate")) {
15701551
continue;
15711552
}
1572-
for (auto& u : co["universes"]) {
1573-
// Entries flagged authoritative (controller under full xLights
1574-
// control) keep the xLights-set cap; others preserve whatever the
1575-
// FPP already had. Strip the internal hint either way.
1576-
bool const authoritative = u.contains("_xlPacingAuthoritative");
1577-
u.erase("_xlPacingAuthoritative");
1578-
if (authoritative) {
1579-
continue;
1580-
}
1581-
std::string addr = GetJSONStringValue(u, "address");
1582-
auto it = pacingByAddress.find(addr);
1583-
if (!addr.empty() && it != pacingByAddress.end()) {
1584-
u["pacingRate"] = it->second;
1585-
}
1553+
std::string addr = GetJSONStringValue(u, "address");
1554+
if (addr.empty()) {
1555+
continue; // pacing only applies to unicast destinations
1556+
}
1557+
int rate = GetJSONIntValue(u, "pacingRate", -1);
1558+
if (rate < 0) {
1559+
continue;
15861560
}
1561+
auto it = pacingByAddress.find(addr);
1562+
if (it == pacingByAddress.end()) {
1563+
pacingByAddress[addr] = rate;
1564+
} else if (rate > 0 && (it->second <= 0 || rate < it->second)) {
1565+
it->second = rate; // a real cap beats "unlimited" (0); lower Mbps wins
1566+
}
1567+
}
1568+
}
1569+
}
1570+
}
1571+
// The authoritative hint is internal to xLights and must be stripped whether or
1572+
// not the existing config could be read.
1573+
if (newudp.contains("channelOutputs")) {
1574+
for (auto& co : newudp["channelOutputs"]) {
1575+
if (!co.contains("universes")) {
1576+
continue;
1577+
}
1578+
for (auto& u : co["universes"]) {
1579+
// Entries flagged authoritative (controller under full xLights
1580+
// control) keep the xLights-set cap; others preserve whatever the
1581+
// FPP already had.
1582+
bool const authoritative = u.contains("_xlPacingAuthoritative");
1583+
u.erase("_xlPacingAuthoritative");
1584+
if (authoritative || !supportsPacing) {
1585+
continue;
1586+
}
1587+
std::string addr = GetJSONStringValue(u, "address");
1588+
auto it = pacingByAddress.find(addr);
1589+
if (!addr.empty() && it != pacingByAddress.end()) {
1590+
u["pacingRate"] = it->second;
15871591
}
15881592
}
15891593
}

0 commit comments

Comments
 (0)