| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | /* | ||
| 3 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 4 | * 2013-2026 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 GPL v2 as described in the | ||
| 11 | * included LICENSE.GPL file. | ||
| 12 | */ | ||
| 13 | |||
| 14 | // Serial plumbing shared by the LOR and LOR Enhanced outputs. Each is its | ||
| 15 | // own plugin and compiles this in; it is not part of the plugin API. | ||
| 16 | |||
| 17 | #include <sys/ioctl.h> | ||
| 18 | #include <algorithm> | ||
| 19 | #include <chrono> | ||
| 20 | #include <condition_variable> | ||
| 21 | #include <mutex> | ||
| 22 | #include <thread> | ||
| 23 | #include <unistd.h> | ||
| 24 | |||
| 25 | #include "../common.h" | ||
| 26 | |||
| 27 | #include "SerialChannelOutput.h" | ||
| 28 | #include "serialutil.h" | ||
| 29 | |||
| 30 | #define LOR_HEARTBEAT_SIZE 5 | ||
| 31 | |||
| 32 | // Units go inactive after 2 seconds without a heartbeat, blank their | ||
| 33 | // outputs, and can need several heartbeats before they act on commands | ||
| 34 | // again, so the heartbeat runs for as long as the port is open, not only | ||
| 35 | // while frames are being sent. | ||
| 36 | #define LOR_HEARTBEAT_INTERVAL 300000 | ||
| 37 | |||
| 38 | // Marks a channel whose state on the unit is not known: never sent since | ||
| 39 | // the output opened, or its command was cut off by a short write. | ||
| 40 | #define LOR_UNKNOWN 0x100 | ||
| 41 | |||
| 42 | // A frame's commands go out in one write(), capped below what the tty can | ||
| 43 | // queue so the non-blocking fd does not drop the tail of a batch. | ||
| 44 | #define LOR_MAX_BATCH_SIZE 4096 | ||
| 45 | |||
| 46 | // Frame interval assumed for the first frame, and the range a measured | ||
| 47 | // interval is clamped to when sizing a frame's byte budget. | ||
| 48 | #define LOR_DEFAULT_FRAME_TIME 50000 | ||
| 49 | #define LOR_MIN_FRAME_TIME 10000 | ||
| 50 | #define LOR_MAX_FRAME_TIME 100000 | ||
| 51 | |||
| 52 | // Shortest time the background refresh takes to resend every channel. | ||
| 53 | #define LOR_REFRESH_PERIOD 1000000 | ||
| 54 | |||
| 55 | class LORSerialOutput : public SerialChannelOutput { | ||
| 56 | public: | ||
| 57 | 32 | virtual ~LORSerialOutput() { | |
| 58 | 32 | StopHeartbeat(); | |
| 59 | 32 | } | |
| 60 | |||
| 61 | 14 | void StartHeartbeat() { | |
| 62 | 14 | stopHeartbeat = false; | |
| 63 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
28 | heartbeatThread = std::thread([this]() { HeartbeatLoop(); }); |
| 64 | 14 | } | |
| 65 | 30 | void StopHeartbeat() { | |
| 66 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 16 times.
|
30 | if (heartbeatThread.joinable()) { |
| 67 | { | ||
| 68 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | std::unique_lock<std::mutex> lk(lock); |
| 69 | 14 | stopHeartbeat = true; | |
| 70 | 14 | } | |
| 71 | 14 | heartbeatCond.notify_all(); | |
| 72 | 14 | heartbeatThread.join(); | |
| 73 | } | ||
| 74 | 30 | } | |
| 75 | |||
| 76 | // Bytes the line can carry before the next frame, less whatever is | ||
| 77 | // still queued in the tty. Writing more only builds a backlog that | ||
| 78 | // delays the lights behind the audio, and once the tty is full the | ||
| 79 | // non-blocking write drops bytes. Call once per frame; it also records | ||
| 80 | // the frame interval in frameTime. | ||
| 81 | 329 | int FrameBudget() { | |
| 82 |
1/2✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
|
329 | long long now = GetMonotonicTimeMicros(); |
| 83 |
2/2✓ Branch 0 taken 315 times.
✓ Branch 1 taken 14 times.
|
329 | long long ft = lastFrame ? now - lastFrame : LOR_DEFAULT_FRAME_TIME; |
| 84 | 329 | lastFrame = now; | |
| 85 |
1/2✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
|
329 | frameTime = std::clamp(ft, (long long)LOR_MIN_FRAME_TIME, (long long)LOR_MAX_FRAME_TIME); |
| 86 | 329 | long long budget = (long long)speed / 10 * frameTime / 1000000; | |
| 87 | 329 | int queued = 0; | |
| 88 |
3/6✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 329 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 329 times.
|
329 | if (ioctl(getFD(), TIOCOUTQ, &queued) == 0 && queued > 0) { |
| 89 | ✗ | budget -= queued; | |
| 90 | } | ||
| 91 |
1/2✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
|
329 | return (int)std::clamp(budget, 0LL, (long long)LOR_MAX_BATCH_SIZE); |
| 92 | } | ||
| 93 | |||
| 94 | // Writes a frame's batch, serialised against the heartbeat so the | ||
| 95 | // heartbeat always lands between frames. Returns the bytes written, | ||
| 96 | // never negative; anything short of len did not go out. | ||
| 97 | 276 | int WriteBatch(const unsigned char* buf, int len) { | |
| 98 |
1/2✓ Branch 0 taken 276 times.
✗ Branch 1 not taken.
|
276 | std::unique_lock<std::mutex> lk(lock); |
| 99 |
1/2✓ Branch 0 taken 276 times.
✗ Branch 1 not taken.
|
276 | int written = SerialWrite(getFD(), buf, len); |
| 100 | 276 | return written < 0 ? 0 : written; | |
| 101 | 276 | } | |
| 102 | |||
| 103 | int speed = 19200; | ||
| 104 | long long frameTime = LOR_DEFAULT_FRAME_TIME; | ||
| 105 | |||
| 106 | private: | ||
| 107 | 14 | void HeartbeatLoop() { | |
| 108 | static const unsigned char heartbeat[LOR_HEARTBEAT_SIZE] = { 0x00, 0xFF, 0x81, 0x56, 0x00 }; | ||
| 109 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | std::unique_lock<std::mutex> lk(lock); |
| 110 |
2/2✓ Branch 0 taken 565 times.
✓ Branch 1 taken 14 times.
|
579 | while (!stopHeartbeat) { |
| 111 | // monotonic: on the wall clock a step back stopped the heartbeat | ||
| 112 | // for the size of the step, and the controllers dropped out | ||
| 113 |
1/2✓ Branch 0 taken 565 times.
✗ Branch 1 not taken.
|
565 | long long now = GetMonotonicTimeMicros(); |
| 114 |
2/2✓ Branch 0 taken 99 times.
✓ Branch 1 taken 466 times.
|
565 | if (now - lastHeartbeat >= LOR_HEARTBEAT_INTERVAL) { |
| 115 |
1/2✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
|
99 | SerialWrite(getFD(), heartbeat, LOR_HEARTBEAT_SIZE); |
| 116 | 99 | lastHeartbeat = now; | |
| 117 | } | ||
| 118 |
1/2✓ Branch 0 taken 565 times.
✗ Branch 1 not taken.
|
565 | heartbeatCond.wait_for(lk, std::chrono::milliseconds(50)); |
| 119 | } | ||
| 120 | 14 | } | |
| 121 | |||
| 122 | long long lastFrame = 0; | ||
| 123 | long long lastHeartbeat = 0; | ||
| 124 | std::mutex lock; | ||
| 125 | std::condition_variable heartbeatCond; | ||
| 126 | std::thread heartbeatThread; | ||
| 127 | bool stopHeartbeat = false; | ||
| 128 | }; | ||
| 129 |