14#include <nlohmann/json.hpp>
25 if (value > fullscale)
28 return std::numeric_limits<std::int16_t>::max();
30 if (value < -fullscale)
33 return std::numeric_limits<std::int16_t>::min();
35 if (value == fullscale)
37 return std::numeric_limits<std::int16_t>::max();
39 if (value == -fullscale)
41 return std::numeric_limits<std::int16_t>::min();
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())));
53 if (byte_count %
sizeof(std::uint32_t) != 0
u)
55 throw std::logic_error(
"VITA packet size must be 32-bit aligned");
57 const auto words = byte_count /
sizeof(std::uint32_t);
58 if (
words > std::numeric_limits<std::uint16_t>::max())
60 throw std::length_error(
"VITA packet exceeds 16-bit word count");
62 return static_cast<std::uint16_t
>(
words);
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}};
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)
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}};
121 if (
packet.receiver_mode ==
"fmcw")
123 return {{
"kind",
"fmcw"}, {
"metadata_ref",
"fmcw"}};
125 if (
packet.receiver_mode ==
"pulsed")
127 return {{
"kind",
"pulsed"}, {
"metadata_ref",
"pulsed"}};
129 if (
packet.receiver_mode ==
"cw")
131 return {{
"kind",
"cw"}, {
"metadata_ref",
"cw"}};
135 return {{
"kind",
"fmcw"}, {
"metadata_ref",
"fmcw"}};
137 if (
packet.pulsed.present)
139 return {{
"kind",
"pulsed"}, {
"metadata_ref",
"pulsed"}};
143 return {{
"kind",
"cw"}, {
"metadata_ref",
"cw"}};
145 return {{
"kind",
packet.receiver_mode.empty() ?
"unknown" :
packet.receiver_mode}};
150 return packet.receiver_mode ==
"fmcw" ||
packet.receiver_mode ==
"pulsed" ||
packet.receiver_mode ==
"cw";
155 nlohmann::json metadata{{
"schema",
"fers-vita49-context-v1"},
156 {
"simulation_name",
packet.simulation_name},
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}}},
164 {{
"frame",
packet.coordinate.frame},
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},
175 {{
"x",
packet.initial_platform_state.position_x},
176 {
"y",
packet.initial_platform_state.position_y},
177 {
"z",
packet.initial_platform_state.position_z}}},
179 {{
"x",
packet.initial_platform_state.velocity_x},
180 {
"y",
packet.initial_platform_state.velocity_y},
181 {
"z",
packet.initial_platform_state.velocity_z}}},
183 {{
"azimuth",
packet.initial_platform_state.azimuth},
184 {
"elevation",
packet.initial_platform_state.elevation}}}}},
213 _bytes.push_back(
static_cast<std::uint8_t
>((value >> 8u) & 0xFFu));
214 _bytes.push_back(
static_cast<std::uint8_t
>(value & 0xFFu));
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));
229 writeU32(
static_cast<std::uint32_t
>((value >> 32u) & 0xFFFFFFFFull));
230 writeU32(
static_cast<std::uint32_t
>(value & 0xFFFFFFFFull));
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");
241 const auto bits = std::bit_cast<std::uint64_t>(value);
247 if (value.size() >= std::numeric_limits<std::uint32_t>::max())
249 throw std::length_error(
"VITA ASCII metadata field too large");
251 for (
const auto ch : value)
253 if (
static_cast<unsigned char>(
ch) > 0x7Fu)
255 throw std::invalid_argument(
"VITA ASCII metadata must contain ASCII bytes only");
258 _bytes.insert(_bytes.end(), value.begin(), value.end());
260 while (_bytes.size() %
sizeof(std::uint32_t) != 0
u)
268 _bytes.insert(_bytes.end(),
bytes.begin(),
bytes.end());
277 if (
packet.iq_interleaved.size() % 2u != 0
u)
279 throw std::invalid_argument(
"VITA signal IQ payload must contain I/Q pairs");
292 writer.writeU64(
packet.timestamp.fractional_picoseconds);
299 if (
writer.bytes().size() != byte_count)
301 throw std::logic_error(
"VITA signal packet byte count mismatch");
303 return writer.takeBytes();
309 if (!std::isfinite(
packet.fullscale) ||
packet.fullscale <= 0.0)
311 throw std::invalid_argument(
"VITA signal full-scale must be positive and finite");
324 writer.writeU64(
packet.timestamp.fractional_picoseconds);
326 std::uint64_t clipped_sample_count = 0;
327 for (
const auto& sample :
packet.samples)
334 ++clipped_sample_count;
338 clipped_sample_count > 0,
packet.sample_loss));
340 if (
writer.bytes().size() != byte_count)
342 throw std::logic_error(
"VITA direct signal packet byte count mismatch");
351 throw std::invalid_argument(
352 "Unsupported VITA 49.2 context CIF0: FERS serializer only supports kFersContextCif0");
366 payload.writeAsciiMetadata(metadata);
368 const std::size_t byte_count = 4u + 4u + 8u + 4u + 8u +
payload.bytes().size();
378 writer.writeU64(
packet.timestamp.fractional_picoseconds);
380 return writer.takeBytes();
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 writeF64(RealType 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.
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
std::vector< std::uint8_t > bytes