ReduxLib C++ 2024.2.0
Loading...
Searching...
No Matches
Frame.h
1// Copyright (c) Redux Robotics and other contributors.
2// This is open source and can be modified and shared under the 3-clause BSD license.
3
4#pragma once
5#include <units/time.h>
6#include <optional>
7#include <mutex>
8#include <condition_variable>
9#include <tuple>
10#include <unordered_map>
11#include <set>
12#include <chrono>
13#include <algorithm>
14#include <functional>
15namespace redux::frames {
16
17/**
18 * Immutable container class for timestamped values.
19*/
20template <typename T>
21class FrameData {
22 public:
23 FrameData() = default;
24 /**
25 * Constructs a new FrameData object.
26 *
27 * @param value The value to hold.
28 * @param timestamp The timestamp at which the value was received in seconds.
29 */
30 FrameData(T value, units::second_t timestamp): value{value}, ts{timestamp} {};
31 /**
32 * Returns the value of the data frame.
33 * @return the value the data frame holds.
34 */
35 inline T GetValue() { return *value; }
36
37 /**
38 * Gets the timestamp in seconds of when this value was updated.
39 * The time base is relative to the FPGA timestamp.
40 * @return the timestamp in seconds.
41 */
42 inline units::second_t GetTimestamp() { return ts; }
43 private:
44 std::optional<T> value; // value
45 units::second_t ts; // timestamp
46};
47
48/**
49 * Internal class.
50*/
51template<typename T>
53 public:
54 /**
55 * Instantiates a FrameListener, used internally for WaitForFrames
56 * @param cv the condition variable to flag when data is received
57 * @param dataLock the associated data lock for the frame listener
58 */
59 FrameListener(std::condition_variable& cv, std::mutex& dataLock): cv{cv}, dataLock{dataLock}{};
60 /**
61 * updates the framelistener with the value/timestamp pair, notifying its associated condition variable
62 * @param value value to update with
63 * @param timestamp timestamp to update with
64 */
65 void UpdateValue(T value, units::second_t timestamp) {
66 std::unique_lock<std::mutex> lock(dataLock);
67 this->data = std::optional<FrameData<T>>{FrameData{value, timestamp}};
68 this->cv.notify_all();
69 };
70 /** condition variable reference */
71 std::condition_variable& cv;
72 /** data lock reference */
73 std::mutex& dataLock;
74 /** payload to hold received data -- check if nullopt for populated-ness */
75 std::optional<FrameData<T>> data{std::nullopt};
76};
77
78/**
79 * Class representing periodic timestamped data received from CAN or other sources.
80 *
81 * <p>
82 * For applications like latency compensation, we often need both sensor/device data and a timestamp of when the data was received.
83 * Frames provide this by holding the most recently received raw data and the timestamps they were received at and allowing retrieval of both data and timestamps
84 * in one FrameData object via Frame.GetFrameData(), avoiding race conditions involving reading data and timestamp separately.
85 * Additionally, they allow for synchronous reads through WaitForFrames by notifying when new data has been received.
86 * </p>
87 */
88template <typename T>
89class Frame {
90 public:
91 /**
92 * Constructs a new Frame object.
93 *
94 * @param value The initial value to hold.
95 * @param timestamp The initial timestamp at which the value was received in seconds.
96 */
97 Frame(T value, units::second_t timestamp): value{value}, ts{timestamp} {};
98 /**
99 * Updates the Frame's value, notifying any listeners of new data.
100 *
101 * @param value the new value
102 * @param timestamp the new timestamp of the received data
103 */
104 void Update(T value, units::second_t timestamp) {
105 std::unique_lock<std::mutex> lock(frameLock);
106 this->value = value;
107 this->ts = timestamp;
108 for (FrameListener<T>* fl : listeners) {
109 fl->UpdateValue(value, ts);
110 }
111 for (const auto& [key, cb] : callbacks) {
112 cb(FrameData<T>{value, ts});
113 }
114 };
115 /**
116 * Fetches an immutable FrameData snapshot of the currently stored values
117 * @return FrameData of value/timestamp pair
118 */
120 std::unique_lock<std::mutex> lock(frameLock);
121 return FrameData<T>{value, ts};
122 }
123 /**
124 * Returns the current frame's value.
125 * @return the value the data frame holds.
126 */
127 inline T GetValue() {
128 std::unique_lock<std::mutex> lock(frameLock);
129 return value;
130 }
131
132 /**
133 * Gets the timestamp in seconds of when this frame was last updated.
134 * @return the timestamp in seconds.
135 */
136 inline units::second_t GetTimestamp() {
137 std::unique_lock<std::mutex> lock(frameLock);
138 return ts;
139 }
140
141 /**
142 * Add a callback that will be run whenever this Frame gets updated.
143 * Example application (may not be applicable)
144 * ```cpp
145 * // Log Canandmag position FrameData.
146 * std::vector<FrameData<units::turn_t>> position_packets;
147 * redux::sensors::canandmag::Canandmag enc0{0};
148 *
149 * enc0.GetPositionFrame().AddCallback([&](FrameData<units::turn_t> frameData) {
150 * position_packets.push_back(frameData);
151 * });
152 * // Timestamped data is now appended to the Vector.
153 *
154 * ```
155 * @param callback the callback
156 * @return an handle key that can be used to unregister the callback later
157 */
158 inline uint32_t AddCallback(std::function<void(FrameData<T>)> callback) {
159 callbacks[key++] = callback;
160 return key;
161 }
162
163 /**
164 * Unregister a callback run whenever this Frame gets updated.
165 * @param key the key returned by AddCallback
166 * @return true on unregister, false if the callback didn't exist
167 */
168 inline bool RemoveCallback(uint32_t key) {
169 return callbacks.erase(key);
170 }
171
172 /**
173 * Internal use function (for WaitForFrames)
174 * @param listener listener pointer to add
175 */
176 inline void AddListener(FrameListener<T>* listener) {
177 std::unique_lock<std::mutex> lock(frameLock);
178 listeners.insert(listener);
179 }
180
181 /**
182 * Internal use function (for WaitForFrames)
183 * @param listener listener pointer to remove.
184 * you must remove your listener before the end of its life or you will cause memory corruption
185 */
186 inline void RemoveListener(FrameListener<T>* listener) {
187 std::unique_lock<std::mutex> lock(frameLock);
188 listeners.erase(listener);
189 }
190
191
192 private:
193 T value; // value
194 units::second_t ts; // timestamp
195 std::mutex frameLock;
196 std::set<FrameListener<T>*> listeners;
197 std::unordered_map<uint32_t, std::function<void(FrameData<T>)>> callbacks;
198 uint32_t key{0};
199};
200
201/**
202 * Waits for all Frames to have transmitted a value.
203 * Either returns an std::tuple of FrameData<T>; representing the data from corresponding frames passed in (in the order they are passed in) or std::nullopt if timeout or interrupt is hit.
204 *
205 * Code example:
206 * ```cpp
207 * // Keep in mind this code sample will likely cause timing overruns if on the main thread of your robot code.
208 * // Device definitions:
209 * redux::sensors::canandmag::Canandmag enc0{0};
210 * redux::sensors::canandmag::Canandmag enc1{1};
211 *
212 * // wait up to 40 ms for position and velocity data to come in from two Canandmags
213 * auto data = redux::frames::WaitForFrames(40_ms, enc0.GetPositionFrame(), enc0.GetVelocityFrame(), enc1.GetPositionFrame());
214 * if (!data.has_value()) {
215 * fmt::print("WaitForFrames timed out before receiving all data\n");
216 * } else {
217 * redux::frames::FrameData<units::turn_t> posFrame;
218 * redux::frames::FrameData<units::turn_t> posFram1;
219 * redux::frames::FrameData<units::turns_per_second_t> velFrame;
220 *
221 * // populates the above FrameData variables with the received data (unpacks the tuple)
222 * std::tie(posFrame, velFrame, posFram1) = *data;
223 *
224 * // fetches the maximum timestamp across all received timestamps (the "latest" value)
225 * units::second_t maxTs = redux::frames::MaxTimestamp(*data);
226 *
227 * // prints the received frame value and how far behind the latest received CAN timestamp it was
228 * fmt::print("posFrame: {}, {}\n", posFrame.GetValue(),
229 * (units::millisecond_t) (maxTs - posFrame.GetTimestamp()));
230 * fmt::print("velFrame: {}, {}\n", velFrame.GetValue(),
231 * (units::millisecond_t) (maxTs - velFrame.GetTimestamp()));
232 * fmt::print("posFram1: {}, {}\n", posFram1.GetValue(),
233 * (units::millisecond_t) (maxTs - posFram1.GetTimestamp()));
234 *
235 * }
236 * ```
237 *
238 * @param timeout maximum seconds to wait for before giving up
239 * @param frames references to Frames to wait on. Position in argument list corresponds to position in the returned FrameData tuple.
240 * @return a tuple of FrameData<T> representing the data from corresponding frames passed in or null if timeout or interrupt is hit.
241 */
242template<typename...T>
243std::optional<std::tuple<FrameData<T>...>> WaitForFrames(units::second_t timeout, Frame<T>&... frames) {
244 constexpr auto sec = std::chrono::seconds(1);
245 std::condition_variable cv;
246 std::mutex dataLock;
247
248 auto listeners = std::make_tuple(std::make_pair(FrameListener<T>(cv, dataLock), &frames)...);
249 {
250 std::unique_lock<std::mutex> lock(dataLock);
251 std::apply([](std::pair<FrameListener<T>, Frame<T>*>&... i) {(i.second->AddListener(&i.first), ...);}, listeners);
252
253 if (!cv.wait_for(lock, timeout.to<double>() * sec, [&]{
254 return std::apply([](std::pair<FrameListener<T>, Frame<T>*>&... i) { return ((i.first.data != std::nullopt) && ...); }, listeners);
255 })) {
256 // timeout
257 // perform cleanup and return std::nullopt
258 std::apply([](std::pair<FrameListener<T>, Frame<T>*>&... i) {(i.second->RemoveListener(&i.first), ...);}, listeners);
259 return std::nullopt;
260 }
261 std::apply([](std::pair<FrameListener<T>, Frame<T>*>&... i) {(i.second->RemoveListener(&i.first), ...);}, listeners);
262 }
263 return std::apply([](std::pair<FrameListener<T>, Frame<T>*>&... i) { return std::make_tuple(*(i.first.data)...); }, listeners);
264}
265
266
267/**
268 * Returns the max timestamp from a tuple of FrameData objects.
269 * Most useful for getting the "latest" CAN timestamp from a result of WaitForFrames.
270 * @param frameData value from WaitForFrames
271 * @return the maximum timestamp
272*/
273template<typename...T>
274units::second_t MaxTimestamp(std::tuple<FrameData<T>...> frameData) {
275 return std::apply([](FrameData<T>&... i) {
276 // we don't have std::max( std::initalizer_list<T> ilist) for some reason so we have to do this
277 auto ilist = {(i.GetTimestamp())...};
278 return *std::max_element(ilist.begin(), ilist.end());
279 }, frameData);
280}
281
282}
Definition: Frame.h:21
units::second_t GetTimestamp()
Definition: Frame.h:42
FrameData(T value, units::second_t timestamp)
Definition: Frame.h:30
T GetValue()
Definition: Frame.h:35
Definition: Frame.h:52
FrameListener(std::condition_variable &cv, std::mutex &dataLock)
Definition: Frame.h:59
std::mutex & dataLock
Definition: Frame.h:73
std::optional< FrameData< T > > data
Definition: Frame.h:75
std::condition_variable & cv
Definition: Frame.h:71
void UpdateValue(T value, units::second_t timestamp)
Definition: Frame.h:65
Definition: Frame.h:89
void Update(T value, units::second_t timestamp)
Definition: Frame.h:104
void RemoveListener(FrameListener< T > *listener)
Definition: Frame.h:186
uint32_t AddCallback(std::function< void(FrameData< T >)> callback)
Definition: Frame.h:158
FrameData< T > GetFrameData()
Definition: Frame.h:119
T GetValue()
Definition: Frame.h:127
units::second_t GetTimestamp()
Definition: Frame.h:136
Frame(T value, units::second_t timestamp)
Definition: Frame.h:97
void AddListener(FrameListener< T > *listener)
Definition: Frame.h:176
bool RemoveCallback(uint32_t key)
Definition: Frame.h:168
Definition: Frame.h:15
std::optional< std::tuple< FrameData< T >... > > WaitForFrames(units::second_t timeout, Frame< T > &... frames)
Definition: Frame.h:243
units::second_t MaxTimestamp(std::tuple< FrameData< T >... > frameData)
Definition: Frame.h:274