FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
vita49_serializer.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
8
9#include <algorithm>
10#include <bit>
11#include <cmath>
12#include <cstring>
13#include <limits>
14#include <nlohmann/json.hpp>
15#include <stdexcept>
16#include <string_view>
17
18namespace serial::vita49
19{
20 namespace
21 {
22 [[nodiscard]] std::int16_t scaleComponentToInt16(const RealType value, const RealType fullscale,
23 bool& clipped) noexcept
24 {
25 if (value > fullscale)
26 {
27 clipped = true;
28 return std::numeric_limits<std::int16_t>::max();
29 }
30 if (value < -fullscale)
31 {
32 clipped = true;
33 return std::numeric_limits<std::int16_t>::min();
34 }
35 if (value == fullscale)
36 {
37 return std::numeric_limits<std::int16_t>::max();
38 }
39 if (value == -fullscale)
40 {
41 return std::numeric_limits<std::int16_t>::min();
42 }
43
44 const RealType scaled =
45 std::round((value / fullscale) * static_cast<RealType>(std::numeric_limits<std::int16_t>::max()));
46 return static_cast<std::int16_t>(
47 std::clamp<RealType>(scaled, static_cast<RealType>(std::numeric_limits<std::int16_t>::min()),
48 static_cast<RealType>(std::numeric_limits<std::int16_t>::max())));
49 }
50
51 [[nodiscard]] std::uint16_t checkedWordCount(const std::size_t byte_count)
52 {
53 if (byte_count % sizeof(std::uint32_t) != 0u)
54 {
55 throw std::logic_error("VITA packet size must be 32-bit aligned");
56 }
57 const auto words = byte_count / sizeof(std::uint32_t);
58 if (words > std::numeric_limits<std::uint16_t>::max())
59 {
60 throw std::length_error("VITA packet exceeds 16-bit word count");
61 }
62 return static_cast<std::uint16_t>(words);
63 }
64
65 [[nodiscard]] nlohmann::json makeCwMetadataJson(const ContextPacket& packet)
66 {
67 const RealType carrier_frequency =
68 packet.cw.carrier_frequency != 0.0 ? packet.cw.carrier_frequency : packet.reference_frequency;
69 return {{"present", packet.cw.present},
70 {"waveform_id", packet.cw.waveform_id},
71 {"waveform_name", packet.cw.waveform_name},
72 {"carrier_hz", carrier_frequency},
73 {"power_w", packet.cw.power}};
74 }
75
76 [[nodiscard]] nlohmann::json makePulsedMetadataJson(const ContextPacket& packet)
77 {
78 const RealType carrier_frequency =
79 packet.pulsed.carrier_frequency != 0.0 ? packet.pulsed.carrier_frequency : packet.reference_frequency;
80 nlohmann::json result = {{"present", packet.pulsed.present},
81 {"waveform_id", packet.pulsed.waveform_id},
82 {"waveform_name", packet.pulsed.waveform_name},
83 {"carrier_hz", carrier_frequency},
84 {"power_w", packet.pulsed.power},
85 {"pulse_width_s", packet.pulsed.pulse_width},
86 {"native_sample_rate_hz", packet.pulsed.native_sample_rate},
87 {"native_sample_count", packet.pulsed.native_sample_count},
88 {"window_length_s", packet.pulsed.window_length},
89 {"window_prf_hz", packet.pulsed.window_prf},
90 {"window_skip_s", packet.pulsed.window_skip},
91 {"window_count", packet.pulsed.window_count}};
92 result["pri_s"] = packet.pulsed.window_prf > 0.0 ? nlohmann::json(1.0 / packet.pulsed.window_prf)
94 return result;
95 }
96
97 [[nodiscard]] nlohmann::json makeFmcwMetadataJson(const ContextPacket& packet)
98 {
99 return {{"present", packet.fmcw.present},
100 {"waveform_shape", packet.fmcw.waveform_shape},
101 {"chirp_bandwidth_hz", packet.fmcw.chirp_bandwidth},
102 {"chirp_duration_s", packet.fmcw.chirp_duration},
103 {"chirp_period_s", packet.fmcw.chirp_period},
104 {"chirp_rate_hz_per_s", packet.fmcw.chirp_rate},
105 {"chirp_rate_signed_hz_per_s", packet.fmcw.chirp_rate_signed},
106 {"sweep_direction", packet.fmcw.sweep_direction},
107 {"start_frequency_offset_hz", packet.fmcw.start_frequency_offset},
108 {"triangle_period_s", packet.fmcw.triangle_period},
109 {"chirp_count", packet.fmcw.chirp_count},
110 {"triangle_count", packet.fmcw.triangle_count},
111 {"dechirp_mode", packet.fmcw.dechirp_mode},
112 {"dechirp_reference_source", packet.fmcw.dechirp_reference_source},
113 {"dechirp_reference_transmitter_id", packet.fmcw.dechirp_reference_transmitter_id},
114 {"dechirp_reference_transmitter_name", packet.fmcw.dechirp_reference_transmitter_name},
115 {"dechirp_reference_waveform_id", packet.fmcw.dechirp_reference_waveform_id},
116 {"dechirp_reference_waveform_name", packet.fmcw.dechirp_reference_waveform_name}};
117 }
118
119 [[nodiscard]] nlohmann::json makeSfcwMetadataJson(const ContextPacket& packet)
120 {
121 nlohmann::json result = {{"present", packet.sfcw.present},
122 {"waveform_id", packet.sfcw.waveform_id},
123 {"waveform_name", packet.sfcw.waveform_name},
124 {"carrier_hz", packet.sfcw.carrier_frequency},
125 {"start_frequency_offset_hz", packet.sfcw.start_frequency_offset},
126 {"step_size_hz", packet.sfcw.step_size},
127 {"step_count", packet.sfcw.step_count},
128 {"dwell_time_s", packet.sfcw.dwell_time},
129 {"step_period_s", packet.sfcw.step_period},
130 {"sweep_period_s", packet.sfcw.sweep_period},
131 {"first_frequency_hz", packet.sfcw.first_frequency},
132 {"last_frequency_hz", packet.sfcw.last_frequency},
133 {"frequency_span_hz", packet.sfcw.frequency_span},
134 {"effective_bandwidth_hz", packet.sfcw.effective_bandwidth}};
135 result["sweep_count"] = packet.sfcw.sweep_count.has_value() ? nlohmann::json(*packet.sfcw.sweep_count)
137 return result;
138 }
139
140 [[nodiscard]] nlohmann::json makeWaveformMetadataJson(const ContextPacket& packet)
141 {
142 if (packet.receiver_mode == "fmcw")
143 {
144 return {{"kind", "fmcw"}, {"metadata_ref", "fmcw"}};
145 }
146 if (packet.receiver_mode == "sfcw")
147 {
148 return {{"kind", "sfcw"}, {"metadata_ref", "sfcw"}};
149 }
150 if (packet.receiver_mode == "pulsed")
151 {
152 return {{"kind", "pulsed"}, {"metadata_ref", "pulsed"}};
153 }
154 if (packet.receiver_mode == "cw")
155 {
156 return {{"kind", "cw"}, {"metadata_ref", "cw"}};
157 }
158 if (packet.fmcw.present)
159 {
160 return {{"kind", "fmcw"}, {"metadata_ref", "fmcw"}};
161 }
162 if (packet.sfcw.present)
163 {
164 return {{"kind", "sfcw"}, {"metadata_ref", "sfcw"}};
165 }
166 if (packet.pulsed.present)
167 {
168 return {{"kind", "pulsed"}, {"metadata_ref", "pulsed"}};
169 }
170 if (packet.cw.present)
171 {
172 return {{"kind", "cw"}, {"metadata_ref", "cw"}};
173 }
174 return {{"kind", packet.receiver_mode.empty() ? "unknown" : packet.receiver_mode}};
175 }
176
177 [[nodiscard]] bool hasKnownReceiverMode(const ContextPacket& packet) noexcept
178 {
179 return packet.receiver_mode == "fmcw" || packet.receiver_mode == "sfcw" ||
180 packet.receiver_mode == "pulsed" || packet.receiver_mode == "cw";
181 }
182
183 [[nodiscard]] nlohmann::json makeContextMetadataJson(const ContextPacket& packet)
184 {
185 nlohmann::json metadata{{"schema", "fers-vita49-context-v1"},
186 {"simulation_name", packet.simulation_name},
187 {"receiver",
188 {{"id", packet.receiver_id},
189 {"name", packet.receiver_name},
190 {"mode", packet.receiver_mode},
191 {"adc_bits", packet.adc_bits},
192 {"context_flags", packet.context_flags}}},
193 {"coordinate_frame",
194 {{"frame", packet.coordinate.frame},
195 {"origin",
196 {{"latitude", packet.coordinate.origin_latitude},
197 {"longitude", packet.coordinate.origin_longitude},
198 {"altitude", packet.coordinate.origin_altitude}}},
199 {"utm_zone", packet.coordinate.utm_zone},
200 {"utm_north_hemisphere", packet.coordinate.utm_north_hemisphere}}},
201 {"initial_platform_state",
202 {{"platform_id", packet.initial_platform_state.platform_id},
203 {"platform_name", packet.initial_platform_state.platform_name},
204 {"position_m",
205 {{"x", packet.initial_platform_state.position_x},
206 {"y", packet.initial_platform_state.position_y},
207 {"z", packet.initial_platform_state.position_z}}},
208 {"velocity_mps",
209 {{"x", packet.initial_platform_state.velocity_x},
210 {"y", packet.initial_platform_state.velocity_y},
211 {"z", packet.initial_platform_state.velocity_z}}},
212 {"rotation_rad",
213 {{"azimuth", packet.initial_platform_state.azimuth},
214 {"elevation", packet.initial_platform_state.elevation}}}}},
215 {"waveform", makeWaveformMetadataJson(packet)}};
217 if (packet.receiver_mode == "pulsed" || (!known_mode && packet.pulsed.present))
218 {
219 metadata["pulsed"] = makePulsedMetadataJson(packet);
220 }
221 if (packet.receiver_mode == "cw" || (!known_mode && packet.cw.present))
222 {
223 metadata["cw"] = makeCwMetadataJson(packet);
224 }
225 if (packet.receiver_mode == "fmcw" || (!known_mode && packet.fmcw.present))
226 {
227 metadata["fmcw"] = makeFmcwMetadataJson(packet);
228 }
229 if (packet.receiver_mode == "sfcw" || (!known_mode && packet.sfcw.present))
230 {
231 metadata["sfcw"] = makeSfcwMetadataJson(packet);
232 }
233 return metadata;
234 }
235 }
236
238 {
239 if (reserve_bytes > 0u)
240 {
241 _bytes.reserve(reserve_bytes);
242 }
243 }
244
245 void ByteWriter::writeU16(const std::uint16_t value)
246 {
247 _bytes.push_back(static_cast<std::uint8_t>((value >> 8u) & 0xFFu));
248 _bytes.push_back(static_cast<std::uint8_t>(value & 0xFFu));
249 }
250
251 void ByteWriter::writeI16(const std::int16_t value) { writeU16(static_cast<std::uint16_t>(value)); }
252
253 void ByteWriter::writeU32(const std::uint32_t value)
254 {
255 _bytes.push_back(static_cast<std::uint8_t>((value >> 24u) & 0xFFu));
256 _bytes.push_back(static_cast<std::uint8_t>((value >> 16u) & 0xFFu));
257 _bytes.push_back(static_cast<std::uint8_t>((value >> 8u) & 0xFFu));
258 _bytes.push_back(static_cast<std::uint8_t>(value & 0xFFu));
259 }
260
261 void ByteWriter::writeU64(const std::uint64_t value)
262 {
263 writeU32(static_cast<std::uint32_t>((value >> 32u) & 0xFFFFFFFFull));
264 writeU32(static_cast<std::uint32_t>(value & 0xFFFFFFFFull));
265 }
266
268 {
269 static_assert(sizeof(RealType) == sizeof(std::uint64_t));
270 static_assert(std::numeric_limits<RealType>::is_iec559,
271 "VITA F64 context serialization requires IEEE 754 binary64 RealType");
272 // VRT context doubles are serialized from their IEEE 754 bit pattern as a
273 // big-endian unsigned integer. This assumes the host uses a conventional
274 // IEEE representation for RealType.
275 const auto bits = std::bit_cast<std::uint64_t>(value);
276 writeU64(bits);
277 }
278
279 void ByteWriter::writeAsciiMetadata(const std::string_view value)
280 {
281 if (value.size() >= std::numeric_limits<std::uint32_t>::max())
282 {
283 throw std::length_error("VITA ASCII metadata field too large");
284 }
285 for (const auto ch : value)
286 {
287 if (static_cast<unsigned char>(ch) > 0x7Fu)
288 {
289 throw std::invalid_argument("VITA ASCII metadata must contain ASCII bytes only");
290 }
291 }
292 _bytes.insert(_bytes.end(), value.begin(), value.end());
293 _bytes.push_back(0);
294 while (_bytes.size() % sizeof(std::uint32_t) != 0u)
295 {
296 _bytes.push_back(0);
297 }
298 }
299
300 void ByteWriter::writeBytes(const std::span<const std::uint8_t> bytes)
301 {
302 _bytes.insert(_bytes.end(), bytes.begin(), bytes.end());
303 }
304
305 const std::vector<std::uint8_t>& ByteWriter::bytes() const noexcept { return _bytes; }
306
307 std::vector<std::uint8_t> ByteWriter::takeBytes() noexcept { return std::move(_bytes); }
308
310 {
311 if (packet.iq_interleaved.size() % 2u != 0u)
312 {
313 throw std::invalid_argument("VITA signal IQ payload must contain I/Q pairs");
314 }
315
316 const std::size_t byte_count = kSignalDataFixedBytes + packet.iq_interleaved.size() * sizeof(std::int16_t);
317 const auto packet_size_words = checkedWordCount(byte_count);
318
319 ByteWriter writer(byte_count);
323 writer.writeU32(packet.stream_id);
324 writer.writeU64(packet.class_id);
325 writer.writeU32(packet.timestamp.integer_seconds);
326 writer.writeU64(packet.timestamp.fractional_picoseconds);
327 for (const auto item : packet.iq_interleaved)
328 {
329 writer.writeI16(item);
330 }
331 writer.writeU32(packet.trailer);
332
333 if (writer.bytes().size() != byte_count)
334 {
335 throw std::logic_error("VITA signal packet byte count mismatch");
336 }
337 return writer.takeBytes();
338 }
339
342 {
343 if (!std::isfinite(packet.fullscale) || packet.fullscale <= 0.0)
344 {
345 throw std::invalid_argument("VITA signal full-scale must be positive and finite");
346 }
347
348 const std::size_t byte_count = kSignalDataFixedBytes + packet.samples.size() * sizeof(std::int16_t) * 2u;
349 const auto packet_size_words = checkedWordCount(byte_count);
350
351 ByteWriter writer(byte_count);
355 writer.writeU32(packet.stream_id);
356 writer.writeU64(packet.class_id);
357 writer.writeU32(packet.timestamp.integer_seconds);
358 writer.writeU64(packet.timestamp.fractional_picoseconds);
359
360 std::uint64_t clipped_sample_count = 0;
361 for (const auto& sample : packet.samples)
362 {
363 bool clipped = false;
364 writer.writeI16(scaleComponentToInt16(sample.real(), packet.fullscale, clipped));
365 writer.writeI16(scaleComponentToInt16(sample.imag(), packet.fullscale, clipped));
366 if (clipped)
367 {
368 ++clipped_sample_count;
369 }
370 }
371 writer.writeU32(makeTrailer(packet.valid_data, packet.calibrated_time, packet.reference_lock,
372 clipped_sample_count > 0, packet.sample_loss));
373
374 if (writer.bytes().size() != byte_count)
375 {
376 throw std::logic_error("VITA direct signal packet byte count mismatch");
377 }
378 return SignalDataSerializationResult{.bytes = writer.takeBytes(), .clipped_sample_count = clipped_sample_count};
379 }
380
382 {
383 if (packet.cif0 != kFersContextCif0)
384 {
385 throw std::invalid_argument(
386 "Unsupported VITA 49.2 context CIF0: FERS serializer only supports kFersContextCif0");
387 }
388
390 payload.writeU32(packet.cif0);
391 payload.writeU32(packet.state_indicators);
392 payload.writeU64(packet.payload_format);
393 payload.writeF64(packet.sample_rate);
394 payload.writeF64(packet.reference_frequency);
395 payload.writeF64(packet.if_offset);
396 payload.writeF64(packet.bandwidth);
397 payload.writeF64(packet.adc_fullscale);
398 payload.writeU64(packet.receiver_id);
399 const auto metadata = makeContextMetadataJson(packet).dump(-1, ' ', true);
400 payload.writeAsciiMetadata(metadata);
401
402 const std::size_t byte_count = 4u + 4u + 8u + 4u + 8u + payload.bytes().size();
403 const auto packet_size_words = checkedWordCount(byte_count);
404
405 ByteWriter writer(byte_count);
409 writer.writeU32(packet.stream_id);
410 writer.writeU64(packet.class_id);
411 writer.writeU32(packet.timestamp.integer_seconds);
412 writer.writeU64(packet.timestamp.fractional_picoseconds);
413 writer.writeBytes(payload.bytes());
414 return writer.takeBytes();
415 }
416}
std::vector< std::uint8_t > takeBytes() noexcept
void writeU32(std::uint32_t value)
void writeBytes(std::span< const std::uint8_t > bytes)
void writeAsciiMetadata(std::string_view value)
const std::vector< std::uint8_t > & bytes() const noexcept
void writeU16(std::uint16_t value)
void writeI16(std::int16_t value)
void writeU64(std::uint64_t value)
ByteWriter(std::size_t reserve_bytes=0)
static std::vector< std::uint8_t > serializeContext(const ContextPacket &packet)
static std::vector< std::uint8_t > serializeSignalData(const SignalDataPacket &packet)
static SignalDataSerializationResult serializeSignalDataFixedFullscale(const FixedFullscaleSignalDataPacket &packet)
double RealType
Type for real numbers.
Definition config.h:27
constexpr std::uint32_t kFersContextCif0
constexpr std::uint32_t kSignalDataFixedBytes
std::uint32_t makeTrailer(const bool valid_data, const bool calibrated_time, const bool reference_lock, const bool over_range, const bool sample_loss) noexcept
std::uint32_t makeHeader(const PacketType type, const bool class_id_present, const bool trailer_present, const IntegerTimestampMode tsi, const FractionalTimestampMode tsf, const std::uint8_t packet_count, const std::uint16_t packet_size_words) noexcept
math::Vec3 max