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 makeWaveformMetadataJson(const ContextPacket& packet)
120 {
121 if (packet.receiver_mode == "fmcw")
122 {
123 return {{"kind", "fmcw"}, {"metadata_ref", "fmcw"}};
124 }
125 if (packet.receiver_mode == "pulsed")
126 {
127 return {{"kind", "pulsed"}, {"metadata_ref", "pulsed"}};
128 }
129 if (packet.receiver_mode == "cw")
130 {
131 return {{"kind", "cw"}, {"metadata_ref", "cw"}};
132 }
133 if (packet.fmcw.present)
134 {
135 return {{"kind", "fmcw"}, {"metadata_ref", "fmcw"}};
136 }
137 if (packet.pulsed.present)
138 {
139 return {{"kind", "pulsed"}, {"metadata_ref", "pulsed"}};
140 }
141 if (packet.cw.present)
142 {
143 return {{"kind", "cw"}, {"metadata_ref", "cw"}};
144 }
145 return {{"kind", packet.receiver_mode.empty() ? "unknown" : packet.receiver_mode}};
146 }
147
148 [[nodiscard]] bool hasKnownReceiverMode(const ContextPacket& packet) noexcept
149 {
150 return packet.receiver_mode == "fmcw" || packet.receiver_mode == "pulsed" || packet.receiver_mode == "cw";
151 }
152
153 [[nodiscard]] nlohmann::json makeContextMetadataJson(const ContextPacket& packet)
154 {
155 nlohmann::json metadata{{"schema", "fers-vita49-context-v1"},
156 {"simulation_name", packet.simulation_name},
157 {"receiver",
158 {{"id", packet.receiver_id},
159 {"name", packet.receiver_name},
160 {"mode", packet.receiver_mode},
161 {"adc_bits", packet.adc_bits},
162 {"context_flags", packet.context_flags}}},
163 {"coordinate_frame",
164 {{"frame", packet.coordinate.frame},
165 {"origin",
166 {{"latitude", packet.coordinate.origin_latitude},
167 {"longitude", packet.coordinate.origin_longitude},
168 {"altitude", packet.coordinate.origin_altitude}}},
169 {"utm_zone", packet.coordinate.utm_zone},
170 {"utm_north_hemisphere", packet.coordinate.utm_north_hemisphere}}},
171 {"initial_platform_state",
172 {{"platform_id", packet.initial_platform_state.platform_id},
173 {"platform_name", packet.initial_platform_state.platform_name},
174 {"position_m",
175 {{"x", packet.initial_platform_state.position_x},
176 {"y", packet.initial_platform_state.position_y},
177 {"z", packet.initial_platform_state.position_z}}},
178 {"velocity_mps",
179 {{"x", packet.initial_platform_state.velocity_x},
180 {"y", packet.initial_platform_state.velocity_y},
181 {"z", packet.initial_platform_state.velocity_z}}},
182 {"rotation_rad",
183 {{"azimuth", packet.initial_platform_state.azimuth},
184 {"elevation", packet.initial_platform_state.elevation}}}}},
185 {"waveform", makeWaveformMetadataJson(packet)}};
187 if (packet.receiver_mode == "pulsed" || (!known_mode && packet.pulsed.present))
188 {
189 metadata["pulsed"] = makePulsedMetadataJson(packet);
190 }
191 if (packet.receiver_mode == "cw" || (!known_mode && packet.cw.present))
192 {
193 metadata["cw"] = makeCwMetadataJson(packet);
194 }
195 if (packet.receiver_mode == "fmcw" || (!known_mode && packet.fmcw.present))
196 {
197 metadata["fmcw"] = makeFmcwMetadataJson(packet);
198 }
199 return metadata;
200 }
201 }
202
204 {
205 if (reserve_bytes > 0u)
206 {
207 _bytes.reserve(reserve_bytes);
208 }
209 }
210
211 void ByteWriter::writeU16(const std::uint16_t value)
212 {
213 _bytes.push_back(static_cast<std::uint8_t>((value >> 8u) & 0xFFu));
214 _bytes.push_back(static_cast<std::uint8_t>(value & 0xFFu));
215 }
216
217 void ByteWriter::writeI16(const std::int16_t value) { writeU16(static_cast<std::uint16_t>(value)); }
218
219 void ByteWriter::writeU32(const std::uint32_t value)
220 {
221 _bytes.push_back(static_cast<std::uint8_t>((value >> 24u) & 0xFFu));
222 _bytes.push_back(static_cast<std::uint8_t>((value >> 16u) & 0xFFu));
223 _bytes.push_back(static_cast<std::uint8_t>((value >> 8u) & 0xFFu));
224 _bytes.push_back(static_cast<std::uint8_t>(value & 0xFFu));
225 }
226
227 void ByteWriter::writeU64(const std::uint64_t value)
228 {
229 writeU32(static_cast<std::uint32_t>((value >> 32u) & 0xFFFFFFFFull));
230 writeU32(static_cast<std::uint32_t>(value & 0xFFFFFFFFull));
231 }
232
234 {
235 static_assert(sizeof(RealType) == sizeof(std::uint64_t));
236 static_assert(std::numeric_limits<RealType>::is_iec559,
237 "VITA F64 context serialization requires IEEE 754 binary64 RealType");
238 // VRT context doubles are serialized from their IEEE 754 bit pattern as a
239 // big-endian unsigned integer. This assumes the host uses a conventional
240 // IEEE representation for RealType.
241 const auto bits = std::bit_cast<std::uint64_t>(value);
242 writeU64(bits);
243 }
244
245 void ByteWriter::writeAsciiMetadata(const std::string_view value)
246 {
247 if (value.size() >= std::numeric_limits<std::uint32_t>::max())
248 {
249 throw std::length_error("VITA ASCII metadata field too large");
250 }
251 for (const auto ch : value)
252 {
253 if (static_cast<unsigned char>(ch) > 0x7Fu)
254 {
255 throw std::invalid_argument("VITA ASCII metadata must contain ASCII bytes only");
256 }
257 }
258 _bytes.insert(_bytes.end(), value.begin(), value.end());
259 _bytes.push_back(0);
260 while (_bytes.size() % sizeof(std::uint32_t) != 0u)
261 {
262 _bytes.push_back(0);
263 }
264 }
265
266 void ByteWriter::writeBytes(const std::span<const std::uint8_t> bytes)
267 {
268 _bytes.insert(_bytes.end(), bytes.begin(), bytes.end());
269 }
270
271 const std::vector<std::uint8_t>& ByteWriter::bytes() const noexcept { return _bytes; }
272
273 std::vector<std::uint8_t> ByteWriter::takeBytes() noexcept { return std::move(_bytes); }
274
276 {
277 if (packet.iq_interleaved.size() % 2u != 0u)
278 {
279 throw std::invalid_argument("VITA signal IQ payload must contain I/Q pairs");
280 }
281
282 const std::size_t byte_count = kSignalDataFixedBytes + packet.iq_interleaved.size() * sizeof(std::int16_t);
283 const auto packet_size_words = checkedWordCount(byte_count);
284
285 ByteWriter writer(byte_count);
289 writer.writeU32(packet.stream_id);
290 writer.writeU64(packet.class_id);
291 writer.writeU32(packet.timestamp.integer_seconds);
292 writer.writeU64(packet.timestamp.fractional_picoseconds);
293 for (const auto item : packet.iq_interleaved)
294 {
295 writer.writeI16(item);
296 }
297 writer.writeU32(packet.trailer);
298
299 if (writer.bytes().size() != byte_count)
300 {
301 throw std::logic_error("VITA signal packet byte count mismatch");
302 }
303 return writer.takeBytes();
304 }
305
308 {
309 if (!std::isfinite(packet.fullscale) || packet.fullscale <= 0.0)
310 {
311 throw std::invalid_argument("VITA signal full-scale must be positive and finite");
312 }
313
314 const std::size_t byte_count = kSignalDataFixedBytes + packet.samples.size() * sizeof(std::int16_t) * 2u;
315 const auto packet_size_words = checkedWordCount(byte_count);
316
317 ByteWriter writer(byte_count);
321 writer.writeU32(packet.stream_id);
322 writer.writeU64(packet.class_id);
323 writer.writeU32(packet.timestamp.integer_seconds);
324 writer.writeU64(packet.timestamp.fractional_picoseconds);
325
326 std::uint64_t clipped_sample_count = 0;
327 for (const auto& sample : packet.samples)
328 {
329 bool clipped = false;
330 writer.writeI16(scaleComponentToInt16(sample.real(), packet.fullscale, clipped));
331 writer.writeI16(scaleComponentToInt16(sample.imag(), packet.fullscale, clipped));
332 if (clipped)
333 {
334 ++clipped_sample_count;
335 }
336 }
337 writer.writeU32(makeTrailer(packet.valid_data, packet.calibrated_time, packet.reference_lock,
338 clipped_sample_count > 0, packet.sample_loss));
339
340 if (writer.bytes().size() != byte_count)
341 {
342 throw std::logic_error("VITA direct signal packet byte count mismatch");
343 }
344 return SignalDataSerializationResult{.bytes = writer.takeBytes(), .clipped_sample_count = clipped_sample_count};
345 }
346
348 {
349 if (packet.cif0 != kFersContextCif0)
350 {
351 throw std::invalid_argument(
352 "Unsupported VITA 49.2 context CIF0: FERS serializer only supports kFersContextCif0");
353 }
354
356 payload.writeU32(packet.cif0);
357 payload.writeU32(packet.state_indicators);
358 payload.writeU64(packet.payload_format);
359 payload.writeF64(packet.sample_rate);
360 payload.writeF64(packet.reference_frequency);
361 payload.writeF64(packet.if_offset);
362 payload.writeF64(packet.bandwidth);
363 payload.writeF64(packet.adc_fullscale);
364 payload.writeU64(packet.receiver_id);
365 const auto metadata = makeContextMetadataJson(packet).dump(-1, ' ', true);
366 payload.writeAsciiMetadata(metadata);
367
368 const std::size_t byte_count = 4u + 4u + 8u + 4u + 8u + payload.bytes().size();
369 const auto packet_size_words = checkedWordCount(byte_count);
370
371 ByteWriter writer(byte_count);
375 writer.writeU32(packet.stream_id);
376 writer.writeU64(packet.class_id);
377 writer.writeU32(packet.timestamp.integer_seconds);
378 writer.writeU64(packet.timestamp.fractional_picoseconds);
379 writer.writeBytes(payload.bytes());
380 return writer.takeBytes();
381 }
382}
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