-
-
Notifications
You must be signed in to change notification settings - Fork 228
Architecture
Falcon Player (FPP) is a high-performance sequence player for LED lighting control, designed for Raspberry Pi and BeagleBone SBCs. It supports E1.31, DDP, DMX, ArtNet, KiNet, Pixelnet, and Renard protocols, with hardware cape support for direct GPIO driving.
Key Architectural Principles:
- Event-driven architecture using EPoll/kqueue for efficient I/O multiplexing
- Single-buffer data pipeline with in-place transformations
- Plugin-based extensibility for channel outputs and custom functionality
- Lock-free bridge data for external DMX controller input
- Real-time processing with 10ms-50ms timing loops
┌─────────────────────────────────────────────────────────────────────────────┐
│ fppd Daemon (PID 1) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Command │ │ Player │ │ Sequence │ │ Scheduler │ │
│ │ Manager │ │ Instance │ │ Instance │ │ Instance │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │ │
│ └──────────────────┼──────────────────┼──────────────────┘ │
│ │ │ │
│ ┌──────────────────┼──────────────────┼──────────────────┐ │
│ │ │ │ │ │
│ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │ Plugin │ │ MultiSync │ │ Channel │ │ E1.31 │ │
│ │ Manager │ │ │ │ Output │ │ Bridge │ │
│ │ │ │ │ │ Thread │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ EPoll/kqueue Event Loop │ │
│ │ (10ms when playing, 50ms idle) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Entry Point: src/fppd.cpp (1167 lines)
Responsibilities:
- Main event loop using EPoll (Linux) or kqueue (macOS)
- Daemonization (fork, setsid)
- Signal handling (SIGTERM, SIGINT, SIGSEGV, etc.)
- Subsystem coordination (Player, Scheduler, Sequence, MultiSync)
- Graceful shutdown and auto-restart
Startup Sequence:
1. setupExceptionHandlers() // Crash handlers
2. LoadSettings() // Load /media/settings
3. ParseArguments() // Command-line args
4. CreateDaemon() // Fork if configured
5. GPIO::Init() // Initialize GPIO
6. CommandManager::Init() // Web/API commands
7. MQTT::init() // MQTT client
8. new Scheduler() // Schedule manager
9. new Playlist() // Playlist system
10. new Sequence() // Sequence player
11. MultiSync::Init() // Remote sync
12. Player::Init() // Player state machine
13. PluginManager::init() // Plugin system
14. InitializeChannelOutputs() // Load channel outputs
15. MainLoop() // Start event loopMain Event Loop: MainLoop() (fppd.cpp:862)
while (runMainFPPDLoop) {
// Wait for events on file descriptors
EPollManager::INSTANCE.waitForEvents(sleepms); // 10ms playing, 50ms idle
// If events: push bridge data
if (epollresult == EPollManager::WaitResult::SOME_TRUE) {
pushBridgeData();
}
// Process Player state machine
if (Player::INSTANCE.IsPlaying()) {
Player::INSTANCE.Process();
}
// Check scheduler
scheduler->CheckIfShouldBePlayingNow();
// Force channel output if needed
forceChannelOutput();
// Periodic ping every 1 second
multiSync->PeriodicPing();
// Fire timers
EPollManager::INSTANCE.fireTimers();
// Process curl requests
curlManager->processPendingRequests();
}Threading Model:
| Thread | Purpose |
|---|---|
| Main | Event loop, subsystem coordination |
ChannelOutput |
Background thread for async output |
WarningHolder |
Notify thread for warnings |
PublishStats |
Detached thread for stats upload (every 10 days) |
Build Target: src/libfpp.so (or .dylib on macOS)
Purpose: Core FPP functionality shared by fppd and CLI tools
Key Components:
-
Buffer: 8MB
m_seqData(FPPD_MAX_CHANNELS = 2097152 channels × 3 bytes) -
Bridge Data:
m_bridgeDatafor E1.31/DDP/ArtNet input -
Frame Cache:
frameCachefor seek operations
Data Pipeline:
FSEQ File on Disk
│
▼
ReadFramesLoop() // Background thread
FSEQFile::getFrame() decompresses zstd/zlib
caches into frameCache
│
▼
ReadSequenceData() // Channel output thread
data->readFrame()
writes into m_seqData
│
▼
ProcessSequenceData()
1. Bridge/E1.31 merge memcpy non-expired m_bridgeData
2. PluginManager::modifySequenceData()
3. OverlayEffects() legacy effects
4. SDLOutput::ProcessVideoOverlay()
5. PixelOverlayManager::doOverlays()
6. ChannelTester::OverlayTestData()
7. PluginManager::modifyChannelData()
8. PrepareChannelData() // Output processors chain
│
▼
SendSequenceData()
Execute frame-triggered commands
SendChannelData(m_seqData)
│
▼
Channel Output Plugins
class PluginManager {
public:
void init(void);
void loadUserPlugins();
void Cleanup();
void mediaCallback(...) // Playlist media events
void playlistCallback(...) // Playlist state changes
void multiSyncData(...) // Remote sync data
void modifySequenceData(...) // Before overlays
void modifyChannelData(...) // After overlays
FPPPlugins::Plugin* findPlugin(...)
private:
std::vector<FPPPlugins::Plugin*> mPlugins;
std::vector<FPPPlugins::PlaylistEventPlugin*> mPlaylistPlugins;
std::vector<FPPPlugins::ChannelOutputPlugin*> mChannelOutputPlugins;
std::vector<FPPPlugins::ChannelDataPlugin*> mChannelDataPlugins;
std::vector<FPPPlugins::APIProviderPlugin*> mAPIProviderPlugins;
};Plugin Interfaces (Plugin.h):
| Plugin Type | Purpose | Callback Methods |
|---|---|---|
Plugin |
Base plugin |
multiSyncData(), settingChanged()
|
ChannelOutputPlugin |
Channel output drivers | createChannelOutput() |
PlaylistEventPlugin |
Playlist hooks |
eventCallback(), mediaCallback(), playlistCallback()
|
ChannelDataPlugin |
Channel data modification |
modifySequenceData(), modifyChannelData()
|
APIProviderPlugin |
Web API extension |
registerApis(), addControlCallbacks()
|
Base Classes:
// ChannelOutput.h
class ChannelOutput {
public:
virtual void Init(int startChannel, int channelCount) = 0;
virtual void SendData(const uint8_t* data) = 0;
virtual void PrepData(uint8_t* data) = 0;
virtual std::vector<std::pair<int, int>> GetRequiredChannelRanges() = 0;
};
// ThreadedChannelOutput.h
class ThreadedChannelOutput : public ChannelOutput {
protected:
uint8_t* writeBuffer; // Double-buffer for async send
uint8_t* sendBuffer;
std::thread sendThread;
std::mutex sendMutex;
std::condition_variable sendCv;
void SendLoop(); // Background thread
};Channel Output Plugins: 41+ plugins in src/channeloutput/
-
UDPOutput.cpp- UDP multicast/unicast -
E131.cpp- E1.31 sender -
ArtNet.cpp- ArtNet sender -
KiNet.cpp- KiNet sender -
DDP.cpp- DDP sender -
BBB48String.cpp- Bit-banged GPIO (BeagleBone) -
rpi_ws281x.cpp- WS2811/WS2812 driver (Raspberry Pi) -
RGBMatrix.cpp- SPI framebuffer (Raspberry Pi) -
MQTTOutput.cpp- MQTT remote control -
VirtualDisplay.cpp- Virtual display outputs
Output Processor Chain (processors/OutputProcessor.h):
class OutputProcessors {
public:
void addProcessor(OutputProcessor* processor);
void process(int channel, uint8_t* data);
private:
std::vector<OutputProcessor*> processors;
std::mutex processorsLock;
};Processor Types (executed in order during PrepareChannelData):
-
RemapOutputProcessor- Channel remapping -
SetValueOutputProcessor- Set all channels to value -
BrightnessOutputProcessor- Brightness + gamma correction -
ColorOrderOutputProcessor- RGB/BGR swap -
HoldValueOutputProcessor- Hold last value -
ThreeToFourOutputProcessor- R → RGB expansion -
OverrideZeroOutputProcessor- Override zero channels -
FoldOutputProcessor- Fold channel ranges -
ClampValueOutputProcessor- Clamp to range -
ScaleValueOutputProcessor- Scale values
Example: ThreeToFourOutputProcessor (processors/ThreeToFourOutputProcessor.cpp):
int ThreeToFourOutputProcessor::process(int value) {
if (inputChannel % 3 == 0) { // R only
redValue = value;
return 0;
} else if (inputChannel % 3 == 1) { // G
greenValue = value;
return 0;
} else { // B + return R
blueValue = value;
return redValue;
}
}Purpose: Receive E1.31, DDP, ArtNet UDP packets and merge into sequence data
Event-Driven Architecture:
// Socket initialization
bridgeSock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
bind(bridgeSock, &addr, sizeof(addr)); // Port 1935
// Join multicast groups
for (each universe) {
setsockopt(bridgeSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq);
}
// Register with EPollManager
std::function<bool(int)> f = [](int i) { return Bridge_ReceiveE131Data(); };
EPollManager::INSTANCE.addFileDescriptor(bridgeSock, f);Data Flow:
UDP Packets Arrive
│
▼
EPollManager detects readable FDs
│
▼
Bridge_ReceiveE131Data() // Uses recvmmsg() for batch receive
│
▼
Bridge_StoreData() // Parse E1.31 header
│ // Extract universe, sequence #, DMX data
▼
SetBridgeData() // Thread-safe write
│ // m_bridgeData[startChannel:start+len] = packetData
│ // timestamp = packetTime + 1000ms expiry
▼
sequence->SetBridgeData() // Mark as "dirty" for merge
Merge Process (Sequence::ProcessSequenceData step 1):
// memcpy non-expired bridge data into m_seqData
for (int i = start; i < start + length; i++) {
if (m_bridgeDataExpiry[i] > now) {
memcpy(&m_seqData[i], &m_bridgeData[i], 3); // 3 bytes per channel
}
}Purpose: Apply overlay effects to sequence data
Two Buffers Per Model:
-
Overlay Buffer (
/FPP-Model-Overlay-Buffer-{name}): Full RGB image, effects render here -
Channel Data Buffer (
/FPP-Model-Data-{name}): Sparse channel-mapped output
Effect Update Thread:
- Background thread ("FPP-OverlayME")
- Wakes at precise ms deadlines
- Calls
model->updateRunningEffects()→RunningEffect::update()
Return Values:
-
> 0: Reschedule in N milliseconds -
0(EFFECT_DONE): Effect finished, delete -
-1(EFFECT_AFTER_NEXT_OUTPUT): Defer to phase 5
doOverlays() Phases:
Phase 1: Flush externally-dirtied overlay buffers
for each active model: if overlayBufferIsDirty() -> flushOverlayBuffer()
Phase 2: Apply sub-models (type "Sub") to m_seqData
sub-models have X/Y offset within a parent
Phase 3: Apply non-sub models to m_seqData
copy channelData -> m_seqData based on overlay state:
Enabled(1)=opaque, Transparent(2)=non-zero only,
TransparentRGB(3)=all-RGB-non-zero only
Phase 4: Apply activeRanges (channel range overwrites)
Phase 5: Process AFTER_NEXT_OUTPUT effects (after lock release)
calls updateRunningEffects() for deferred modelsPurpose: Manage playlist playback state
Key Methods:
void Init();
int StartPlaylist(const std::string& name, ...);
int StopNow(int forceStop = 0);
int StopGracefully(int forceStop = 0, int afterCurrentLoop = 0);
int Process(); // State machine update
void ProcessMedia(); // Media player control
PlaylistStatus GetStatus(); // IDLE, PLAYING, PAUSED, STOPPED
int IsPlaying(); // BooleanState Machine:
IDLE ──StartPlaylist()──> PLAYING ──StopNow()──> IDLE
│ │ │
│ ▼ │
└──────────StopGracefully()───────────────────────┘
Purpose: Schedule playlist playback based on time/date
Key Methods:
void ScheduleProc();
void CheckIfShouldBePlayingNow();| Tool | Purpose | Build Target |
|---|---|---|
fppd |
Main daemon | fppd |
fpp |
CLI client | fpp |
fppmm |
Memory map utility | fppmm |
fppoled |
OLED display driver |
fppoled (Pi/BBB only) |
fppcapedetect |
Hardware cape detection |
fppcapedetect (Pi/BBB only) |
fpprtc |
Real-time clock utility |
fpprtc (Pi/BBB only) |
fppinit |
FPP initialization | fppinit |
Primary Makefile: src/Makefile
# Include platform fragments
include ${SRCDIR}/makefiles/platform/*.mk
# Include build targets
include ${SRCDIR}/makefiles/*.mkPlatform Fragments:
| Platform | File | Defines | Notes |
|---|---|---|---|
| Raspberry Pi | pi.mk |
PLATFORM_PI |
libgpiod, builds all external submodules |
| BeagleBone | bb.mk |
PLATFORM_BBB or PLATFORM_BB64
|
PRU support, NEON SIMD |
| macOS | osx.mk |
PLATFORM_OSX |
clang++, CoreAudio framework, .dylib
|
| Linux | linux.mk |
PLATFORM_DEBIAN/PLATFORM_UBUNTU
|
Docker detection skips OLED/cape/RTC |
Build Targets:
make # default optimized build (-O3, -g1)
make debug # debug build (-g -DDEBUG)
make asan # address sanitizer build
make tsan # thread sanitizer build
make clean # remove all build artifactsKey Build Artifacts:
-
libfpp.so- Core shared library -
fppd- Main daemon -
fpp- CLI tool -
fppoled,fppcapedetect,fpprtc- Platform-specific utilities - Channel output plugins:
libfpp-co-*.so
FSEQ File on Disk
│
▼
ReadFramesLoop() // Background thread
FSEQFile::getFrame() decompresses zstd/zlib
caches into frameCache
│
▼
ReadSequenceData() // Channel output thread
data->readFrame()
writes into m_seqData // 8MB buffer (2097152 channels × 3 bytes)
│
▼
ProcessSequenceData()
1. Bridge/E1.31 merge memcpy non-expired m_bridgeData
2. PluginManager::modifySequenceData()
3. OverlayEffects() legacy effects
4. SDLOutput::ProcessVideoOverlay()
5. PixelOverlayManager::doOverlays()
6. ChannelTester::OverlayTestData()
7. PluginManager::modifyChannelData()
8. PrepareChannelData() // Output processors chain
│
▼
SendSequenceData()
Execute frame-triggered commands
SendChannelData(m_seqData)
│
▼
Channel Output Plugins
-
In-Place Buffer Transformations: All transforms happen in-place on the single
m_seqDatabuffer - Bridge Data Priority: Bridge/E1.31 input is merged first, before any overlays
- Test Data Overlay: Test data overlays after pixel overlays but before output processors
- Output Processor Finalization: Output processors modify the buffer right before sending
| Lock | Protects | Location |
|---|---|---|
m_sequenceLock (recursive_mutex) |
FSEQ file operations, bridge data access | Sequence.h:108 |
frameCacheLock (mutex) |
Frame cache between read and output threads | Sequence.h:118 |
m_bridgeRangesLock (mutex) |
Bridge data ranges and expiry timestamps | Sequence.h:91 |
outputProcessors.processorsLock (mutex) |
Output processor chain during execution | ChannelOutputSetup.h:74 |
activeModelsLock (recursive_mutex) |
Active overlay model list during doOverlays | PixelOverlay.h |
| Lock | Protects |
|---|---|
effectLock per model (recursive_mutex) |
runningEffect pointer during update/replace |
threadLock (mutex) |
updates map, afterOverlayModels list |
| Type | Purpose | Callback Methods |
|---|---|---|
Plugin |
Base plugin |
multiSyncData(), settingChanged()
|
ChannelOutputPlugin |
Channel output drivers | createChannelOutput() |
PlaylistEventPlugin |
Playlist hooks |
eventCallback(), mediaCallback(), playlistCallback(), playlistInserted()
|
ChannelDataPlugin |
Channel data modification |
modifySequenceData(), modifyChannelData()
|
APIProviderPlugin |
Web API extension |
registerApis(), unregisterApis(), addControlCallbacks()
|
// PluginManager::loadUserPlugins()
for (DIR* dir = opendir("/home/fpp/media/plugins"); ...) {
if (isPluginDirectory(dir)) {
Plugin* plugin = loadUserPlugin(name);
addPlugin(plugin);
}
}Signals Handled:
-
SIGFPE- Floating-point exception -
SIGILL- Illegal instruction -
SIGBUS- Bus error -
SIGSEGV- Segmentation fault -
SIGABRT- Abort
Actions:
- Log crash info with sequence state
- Attempt gdb backtrace (if debugger present)
- Fallback to
backtrace()and write to/tmp/fppd_crash.log - Upload to crash server (if
ShareCrashData >= 1) - Set warning flag and exit
// command.cpp:55-65
static void exit_handler(int signum) {
LogInfo("Caught signal %d\n", signum)
if (mediaOutputStatus.status == MEDIAOUTPUTSTATUS_PLAYING) {
CloseMediaOutput()
}
ShutdownFPPD() // Triggers SHUTDOWN_HOOK
sleep(1)
CloseCommand()
}
signal(SIGINT, exit_handler)
signal(SIGTERM, exit_handler)Technology Stack:
- PHP 7.x - Server-side rendering
- Limonade - Micro-framework (v0.5.0) for routing
- Bootstrap 3.x - UI framework
- jQuery - DOM manipulation
- jQuery TableSorter - Table sorting/filtering
- ZeroClipboard - Clipboard functionality
- Zebra_Pin - Sticky positioning
Directory Structure:
www/
api/
controllers/ # 25 API controllers (REST-like)
lib/ # Limonade framework, custom libs
common/ # Shared PHP utilities
help/ # Help pages
images/ # Static assets
jquery/ # jQuery plugins
media/ # Dynamic content
phpqrcode/ # QR code generation
API Controllers (25 total):
| Controller | Purpose |
|---|---|
backups.php |
Backup/restore management |
cape.php |
Hardware cape configuration |
channel.php |
Channel output configuration |
configfile.php |
Config file management |
email.php |
Email settings |
effects.php |
Effect configuration |
events.php |
Event triggers |
files.php |
File operations |
git.php |
Git version control |
help.php |
Help content |
media.php |
Media library |
network.php |
Network configuration |
options.php |
System options |
playlist.php |
Playlist management |
plugin.php |
Plugin management |
proxies.php |
Proxy configuration |
schedule.php |
Schedule management |
scripts.php |
Script browser |
sequence.php |
Sequence management |
settings.php |
Settings management |
stats.php |
Statistics |
system.php |
System operations |
testmode.php |
Test patterns |
Key Files:
-
www/config.php- Configuration, media directory, settings -
www/common.php- Shared utilities ( ScrubFile, ReadSettingFromFile) -
www/index.php- Main dashboard page
Settings System:
// www/config.php
$settingsFile = $mediaDirectory . "/settings"; // /media/settings
// Key-value store loaded at runtime
$settings = array();
$settings['fppMode'] = "player";
// Helper function
function GetSettingValue($setting, $default = '', $prefix = '', $suffix = '') {
global $settings;
if (isset($settings[$setting]) && $settings[$setting] != '') {
return $prefix . $settings[$setting] . $suffix;
}
return $default;
}API Pattern:
// www/api/controllers/sequence.php
require_once '../common.php';
require_once '../../common/settings.php';
function sequence_GetInfo() {
global $sequence;
$info = array();
$info['filename'] = $sequence->m_seqFilename;
$info['elapsed'] = $sequence->m_seqMSElapsed;
return $info;
}UI Components:
- Dashboard with playlist, player controls, sync stats
- Settings pages (input, output, playback, system, email, logs, privacy)
- Configuration editors (channel outputs, models, command presets)
- File browser
- Script browser
- Help system
| File | Purpose |
|---|---|
config/channeloutputs.json |
Output type, startChannel, channelCount |
config/model-overlays.json |
Pixel grid definitions |
config/commandPresets.json |
Named command sequences |
/media/settings |
Key=value settings file |
www/settings.json |
Declarative settings metadata with UI types |
capes/ |
GPIO pin mappings, output channel definitions |
etc/asoundrc.* |
ALSA configurations |
ci-universes.json |
E1.31/DDP/ArtNet input configuration |
Current Issues:
- Large monolithic
fppd.cppfile (1167 lines) - Mixed concerns in
Sequence.cpp(file I/O, bridge data, overlays, processing) - Global singleton instances (
PluginManager::INSTANCE,Sequence::sequence, etc.)
Suggestions:
src/
daemon/
fppd.cpp # Main entry point
DaemonManager.cpp # Daemon lifecycle
SignalHandler.cpp # Signal handling
player/
Player.cpp # Playlist state machine
Scheduler.cpp # Time-based scheduling
sequence/
Sequence.cpp # FSEQ playback
FSEQFile.cpp # File I/O
bridge/
E131Bridge.cpp # E1.31/DDP/ArtNet
BridgeManager.cpp # Bridge lifecycle
plugins/
PluginManager.cpp # Plugin system
PluginRegistry.cpp # Plugin registration
channeloutput/
ChannelOutput.cpp # Base classes
ChannelOutputSetup.cpp # Setup
ChannelOutputThread.cpp # Output thread
[plugins/] # Individual plugins
overlays/
PixelOverlay.cpp # Overlay system
PixelOverlayEffects.cpp # Effects
config/
ConfigManager.cpp # Configuration loading
Settings.cpp # Settings management
Current Issues:
- Single main event loop blocks on EPoll
- Channel output thread is optional
- Detached statistics thread
Suggestions:
- Separate threads for:
- Event loop (EPoll)
- Sequence playback
- Channel output
- Bridge processing
- Overlay effects
Current Issues:
- Raw pointers throughout
- Manual memory management in plugins
- Shared memory for overlays
Suggestions:
- Use
std::unique_ptrwhere possible - Smart pointers for plugin management
- RAII for shared memory
Current Issues:
- Crash handler uploads to external server
- Limited error recovery
- Silent failures in some areas
Suggestions:
- Structured error types
- Error propagation instead of logging
- Recovery mechanisms for common failures
Current Issues:
- No visible unit tests
- Integration testing via CLI
- Manual testing of channel outputs
Suggestions:
- Unit tests for core algorithms
- Mock channel outputs for testing
- Automated integration tests
Current Issues:
- Minimal inline documentation
- No API documentation
- Architecture not well documented
Suggestions:
- Doxygen comments for public APIs
- Architecture decision records (ADRs)
- User-facing plugin documentation
Current Issues:
- JSON files loaded without validation
- No schema validation
- Runtime errors for bad config
Suggestions:
- Schema validation on load
- Config validation CLI tool
- Better error messages
Current Issues:
- Virtual functions in base classes
- Dynamic loading via dlopen
- No versioning mechanism
Suggestions:
- Versioned plugin interfaces
- Plugin capability negotiation
- Plugin sandboxing (future)
- Extract
Sequenceclass tosrc/sequence/ - Extract
E131Bridgetosrc/bridge/ - Extract
PluginManagertosrc/plugins/
- Separate event loop from sequence playback
- Add dedicated bridge processing thread
- Standardize thread lifecycle
- Replace raw pointers with smart pointers
- Add RAII wrappers for shared memory
- Add memory ownership documentation
- Add unit tests for core algorithms
- Create mock channel outputs
- Add integration test framework
- Add Doxygen comments
- Create architecture diagrams
- Write plugin developer guide
FPP is a well-engineered real-time system with:
- Event-driven architecture using EPoll/kqueue
- Plugin-based extensibility for channel outputs
- Efficient data pipeline with in-place transformations
- Robust error handling with crash reporting
Key strengths:
- Low-latency processing (10ms-50ms loops)
- Extensible plugin system
- Multi-platform support (Pi, BBB, Linux, macOS)
Key areas for improvement:
- Code organization (reduce monolithic files)
- Threading model (more parallelism)
- Memory management (smart pointers)
- Testing infrastructure
- Documentation
Generated: 2026-04-01