[feat](trx-rs): parse VDES message headers
Add a first VDES payload parser on top of the decoded bitstream so the server surfaces message labels, source and destination IDs, session IDs, ASM IDs, ack fields, geographic hints, and payload previews. Update the VDES frontend pane to render those parsed fields in the history and live bar. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -126,7 +126,13 @@ impl VdesDecoder {
|
||||
if decoded_bits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let raw_bytes = pack_bits_msb(&decoded_bits);
|
||||
let parsed = parse_vdes_payload(&decoded_bits);
|
||||
let payload_bits = if parsed.payload_bits.is_empty() {
|
||||
decoded_bits.as_slice()
|
||||
} else {
|
||||
parsed.payload_bits.as_slice()
|
||||
};
|
||||
let raw_bytes = pack_bits_msb(payload_bits);
|
||||
let rms = burst_rms(&samples);
|
||||
let mode = classify_vdes_burst(framed.symbols.len());
|
||||
let link_text = link_id
|
||||
@@ -141,30 +147,46 @@ impl VdesDecoder {
|
||||
tail_zero_bits,
|
||||
TER_MCS1_100_FEC_TAIL_BITS
|
||||
);
|
||||
|
||||
Some(VdesMessage {
|
||||
ts_ms: None,
|
||||
channel: channel.to_string(),
|
||||
message_type: mode.message_type,
|
||||
repeat: 0,
|
||||
mmsi: 0,
|
||||
crc_ok: false,
|
||||
bit_len: decoded_bits.len(),
|
||||
raw_bytes,
|
||||
lat: None,
|
||||
lon: None,
|
||||
sog_knots: None,
|
||||
cog_deg: None,
|
||||
heading_deg: None,
|
||||
nav_status: None,
|
||||
vessel_name: Some(format!("VDES Frame {} sym", framed.symbols.len())),
|
||||
callsign: Some(format!("{} {} @{}", mode.label, link_text, framed.start_offset)),
|
||||
destination: Some(format!(
|
||||
let destination = parsed.summary.clone().or_else(|| {
|
||||
Some(format!(
|
||||
"TER-MCS-1.100 RMS {:.2} sync {:.0}% rot {}",
|
||||
rms,
|
||||
framed.sync_score * 100.0,
|
||||
framed.phase_rotation
|
||||
))
|
||||
});
|
||||
|
||||
Some(VdesMessage {
|
||||
ts_ms: None,
|
||||
channel: channel.to_string(),
|
||||
message_type: parsed.message_id.unwrap_or(mode.message_type),
|
||||
repeat: parsed.repeat,
|
||||
mmsi: parsed.source_id.unwrap_or(0),
|
||||
crc_ok: false,
|
||||
bit_len: payload_bits.len(),
|
||||
raw_bytes,
|
||||
lat: parsed.lat,
|
||||
lon: parsed.lon,
|
||||
sog_knots: None,
|
||||
cog_deg: None,
|
||||
heading_deg: None,
|
||||
nav_status: None,
|
||||
vessel_name: Some(format!(
|
||||
"{} {} sym",
|
||||
parsed.message_label.unwrap_or("VDES Frame"),
|
||||
framed.symbols.len()
|
||||
)),
|
||||
callsign: Some(format!("{} {} @{}", mode.label, link_text, framed.start_offset)),
|
||||
destination,
|
||||
message_label: parsed.message_label.map(str::to_string),
|
||||
session_id: parsed.session_id,
|
||||
source_id: parsed.source_id,
|
||||
destination_id: parsed.destination_id,
|
||||
data_count: parsed.data_count,
|
||||
asm_identifier: parsed.asm_identifier,
|
||||
ack_nack_mask: parsed.ack_nack_mask,
|
||||
channel_quality: parsed.channel_quality,
|
||||
payload_preview: parsed.payload_preview,
|
||||
link_id,
|
||||
sync_score: Some(framed.sync_score),
|
||||
sync_errors: Some(framed.sync_errors),
|
||||
@@ -208,6 +230,25 @@ struct BurstMode<'a> {
|
||||
message_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ParsedPayload {
|
||||
message_id: Option<u8>,
|
||||
message_label: Option<&'static str>,
|
||||
repeat: u8,
|
||||
session_id: Option<u8>,
|
||||
source_id: Option<u32>,
|
||||
destination_id: Option<u32>,
|
||||
data_count: Option<u16>,
|
||||
asm_identifier: Option<u16>,
|
||||
ack_nack_mask: Option<u16>,
|
||||
channel_quality: Option<u8>,
|
||||
payload_bits: Vec<u8>,
|
||||
payload_preview: Option<String>,
|
||||
summary: Option<String>,
|
||||
lat: Option<f64>,
|
||||
lon: Option<f64>,
|
||||
}
|
||||
|
||||
struct FrameSlice {
|
||||
start_offset: usize,
|
||||
sync_score: f32,
|
||||
@@ -329,6 +370,172 @@ fn split_fec_frame(symbols: &[u8]) -> (&[u8], &[u8]) {
|
||||
(&symbols[..input_end], &symbols[input_end..tail_end])
|
||||
}
|
||||
|
||||
fn parse_vdes_payload(bits: &[u8]) -> ParsedPayload {
|
||||
let Some(message_id) = read_bits_u8(bits, 0, 4) else {
|
||||
return ParsedPayload::default();
|
||||
};
|
||||
let repeat = read_bits_u8(bits, 5, 2).unwrap_or(0);
|
||||
let session_id = read_bits_u8(bits, 7, 6);
|
||||
let source_id = read_bits_u32(bits, 13, 32);
|
||||
let common = ParsedPayload {
|
||||
message_id: Some(message_id),
|
||||
repeat,
|
||||
session_id,
|
||||
source_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match message_id {
|
||||
0 => parse_msg_0(bits, common),
|
||||
1 => parse_msg_1(bits, common),
|
||||
2 => parse_msg_2(bits, common),
|
||||
3 => parse_msg_3(bits, common),
|
||||
4 => parse_msg_4(bits, common),
|
||||
5 => parse_msg_5(bits, common),
|
||||
6 => parse_msg_6(bits, common),
|
||||
_ => parse_unknown_msg(bits, common),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_msg_0(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Broadcast");
|
||||
parsed.data_count = read_bits_u16(bits, 45, 11);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 56, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"Broadcast from {} · {} data bits",
|
||||
parsed.source_id.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_1(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Scheduled");
|
||||
parsed.data_count = read_bits_u16(bits, 45, 11);
|
||||
parsed.asm_identifier = read_bits_u16(bits, 56, 16);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 72, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"Scheduled ASM {} · {} data bits",
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_2(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Scheduled");
|
||||
parsed.data_count = read_bits_u16(bits, 45, 11);
|
||||
parsed.asm_identifier = read_bits_u16(bits, 56, 16);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 72, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"Scheduled ITDMA ASM {} · {} data bits",
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_3(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Addressed");
|
||||
parsed.destination_id = read_bits_u32(bits, 45, 32);
|
||||
parsed.data_count = read_bits_u16(bits, 77, 11);
|
||||
parsed.asm_identifier = read_bits_u16(bits, 88, 16);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 104, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"{} -> {} · ASM {} · {} data bits",
|
||||
parsed.source_id.unwrap_or(0),
|
||||
parsed.destination_id.unwrap_or(0),
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_4(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Addressed");
|
||||
parsed.destination_id = read_bits_u32(bits, 45, 32);
|
||||
parsed.data_count = read_bits_u16(bits, 77, 11);
|
||||
parsed.asm_identifier = read_bits_u16(bits, 88, 16);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 104, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"{} -> {} · ITDMA ASM {} · {} data bits",
|
||||
parsed.source_id.unwrap_or(0),
|
||||
parsed.destination_id.unwrap_or(0),
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_5(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Acknowledge");
|
||||
parsed.destination_id = read_bits_u32(bits, 45, 32);
|
||||
parsed.ack_nack_mask = read_bits_u16(bits, 77, 16);
|
||||
parsed.channel_quality = read_bits_u8(bits, 95, 8);
|
||||
parsed.summary = Some(format!(
|
||||
"{} -> {} · ack 0x{:04X} · CQ {}",
|
||||
parsed.source_id.unwrap_or(0),
|
||||
parsed.destination_id.unwrap_or(0),
|
||||
parsed.ack_nack_mask.unwrap_or(0),
|
||||
parsed.channel_quality.unwrap_or(0)
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_msg_6(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Geo");
|
||||
let ne_lon = read_signed_bits(bits, 45, 18);
|
||||
let ne_lat = read_signed_bits(bits, 63, 17);
|
||||
let sw_lon = read_signed_bits(bits, 80, 18);
|
||||
let sw_lat = read_signed_bits(bits, 98, 17);
|
||||
parsed.data_count = read_bits_u16(bits, 115, 11);
|
||||
parsed.asm_identifier = read_bits_u16(bits, 128, 16);
|
||||
parsed.payload_bits = extract_counted_payload(bits, 144, parsed.data_count);
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
if let (Some(ne_lon), Some(ne_lat), Some(sw_lon), Some(sw_lat)) = (ne_lon, ne_lat, sw_lon, sw_lat)
|
||||
{
|
||||
let ne_lon_deg = ne_lon as f64 / 600.0;
|
||||
let ne_lat_deg = ne_lat as f64 / 600.0;
|
||||
let sw_lon_deg = sw_lon as f64 / 600.0;
|
||||
let sw_lat_deg = sw_lat as f64 / 600.0;
|
||||
parsed.lon = Some((ne_lon_deg + sw_lon_deg) * 0.5);
|
||||
parsed.lat = Some((ne_lat_deg + sw_lat_deg) * 0.5);
|
||||
parsed.summary = Some(format!(
|
||||
"Geo ASM {} · {} data bits · box {:.3},{:.3} to {:.3},{:.3}",
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len(),
|
||||
sw_lat_deg,
|
||||
sw_lon_deg,
|
||||
ne_lat_deg,
|
||||
ne_lon_deg
|
||||
));
|
||||
} else {
|
||||
parsed.summary = Some(format!(
|
||||
"Geo ASM {} · {} data bits",
|
||||
parsed.asm_identifier.unwrap_or(0),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
}
|
||||
parsed
|
||||
}
|
||||
|
||||
fn parse_unknown_msg(bits: &[u8], mut parsed: ParsedPayload) -> ParsedPayload {
|
||||
parsed.message_label = Some("Unknown");
|
||||
parsed.payload_bits = bits.to_vec();
|
||||
parsed.payload_preview = ascii_preview(&parsed.payload_bits);
|
||||
parsed.summary = Some(format!(
|
||||
"Message {} · {} bits",
|
||||
parsed.message_id.unwrap_or(255),
|
||||
parsed.payload_bits.len()
|
||||
));
|
||||
parsed
|
||||
}
|
||||
|
||||
fn viterbi_decode_rate_half(coded_bits: &[u8]) -> Vec<u8> {
|
||||
if coded_bits.len() < 2 {
|
||||
return Vec::new();
|
||||
@@ -399,6 +606,73 @@ fn parity6_7(value: u8) -> u8 {
|
||||
(value.count_ones() as u8) & 1
|
||||
}
|
||||
|
||||
fn extract_counted_payload(bits: &[u8], start: usize, count: Option<u16>) -> Vec<u8> {
|
||||
let Some(count) = count.map(usize::from) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let end = start.saturating_add(count).min(bits.len());
|
||||
if start >= end {
|
||||
return Vec::new();
|
||||
}
|
||||
bits[start..end].to_vec()
|
||||
}
|
||||
|
||||
fn ascii_preview(bits: &[u8]) -> Option<String> {
|
||||
let bytes = pack_bits_msb(bits);
|
||||
let mut out = String::new();
|
||||
for &byte in bytes.iter().take(24) {
|
||||
let ch = if byte.is_ascii_graphic() || byte == b' ' {
|
||||
byte as char
|
||||
} else {
|
||||
'.'
|
||||
};
|
||||
out.push(ch);
|
||||
}
|
||||
let trimmed = out.trim_matches('.').trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else if bytes.len() > 24 {
|
||||
Some(format!("{}...", trimmed))
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_bits_u8(bits: &[u8], start: usize, len: usize) -> Option<u8> {
|
||||
read_bits_u32(bits, start, len).and_then(|value| u8::try_from(value).ok())
|
||||
}
|
||||
|
||||
fn read_bits_u16(bits: &[u8], start: usize, len: usize) -> Option<u16> {
|
||||
read_bits_u32(bits, start, len).and_then(|value| u16::try_from(value).ok())
|
||||
}
|
||||
|
||||
fn read_bits_u32(bits: &[u8], start: usize, len: usize) -> Option<u32> {
|
||||
if len == 0 || len > 32 {
|
||||
return None;
|
||||
}
|
||||
let end = start.checked_add(len)?;
|
||||
let slice = bits.get(start..end)?;
|
||||
let mut value = 0u32;
|
||||
for &bit in slice {
|
||||
value = (value << 1) | u32::from(bit & 1);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn read_signed_bits(bits: &[u8], start: usize, len: usize) -> Option<i32> {
|
||||
let raw = read_bits_u32(bits, start, len)?;
|
||||
if len == 0 || len > 31 {
|
||||
return None;
|
||||
}
|
||||
let sign_mask = 1u32 << (len - 1);
|
||||
if raw & sign_mask == 0 {
|
||||
Some(raw as i32)
|
||||
} else {
|
||||
let extended = raw | (!0u32 << len);
|
||||
Some(extended as i32)
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_link_id_from_symbols(symbols: &[u8]) -> Option<u8> {
|
||||
let start = TER_MCS1_100_RAMP_SYMBOLS + TER_MCS1_100_SYNC_SYMBOLS;
|
||||
let end = start + TER_MCS1_100_LINK_ID_SYMBOLS;
|
||||
|
||||
@@ -79,15 +79,33 @@ function renderVdesRow(msg) {
|
||||
const title = msg.vessel_name || "VDES Burst";
|
||||
const label = msg.callsign || "VDES";
|
||||
const info = msg.destination || "";
|
||||
const labelText = msg.message_label || "";
|
||||
const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : "";
|
||||
const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : "";
|
||||
const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : "";
|
||||
const fecText = msg.fec_state || "";
|
||||
const srcText = Number.isFinite(msg.source_id) ? `SRC ${Number(msg.source_id)}` : "";
|
||||
const dstText = Number.isFinite(msg.destination_id) ? `DST ${Number(msg.destination_id)}` : "";
|
||||
const sessionText = Number.isFinite(msg.session_id) ? `S${Number(msg.session_id)}` : "";
|
||||
const asmText = Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : "";
|
||||
const countText = Number.isFinite(msg.data_count) ? `${Number(msg.data_count)} data bits` : "";
|
||||
const ackText = Number.isFinite(msg.ack_nack_mask) ? `ACK 0x${Number(msg.ack_nack_mask).toString(16).toUpperCase().padStart(4, "0")}` : "";
|
||||
const cqiText = Number.isFinite(msg.channel_quality) ? `CQ ${Number(msg.channel_quality)}` : "";
|
||||
const previewText = msg.payload_preview || "";
|
||||
const rawHex = vdesHexPreview(msg.raw_bytes);
|
||||
row.dataset.filterText = [
|
||||
title,
|
||||
label,
|
||||
labelText,
|
||||
info,
|
||||
srcText,
|
||||
dstText,
|
||||
sessionText,
|
||||
asmText,
|
||||
countText,
|
||||
ackText,
|
||||
cqiText,
|
||||
previewText,
|
||||
linkText,
|
||||
syncText,
|
||||
phaseText,
|
||||
@@ -104,7 +122,10 @@ function renderVdesRow(msg) {
|
||||
`<span class="vdes-time">${ts}</span>` +
|
||||
`<span class="vdes-call">${escapeMapHtml(title)}</span>` +
|
||||
`<span class="vdes-badge">${escapeMapHtml(label)}</span>` +
|
||||
(labelText ? `<span class="vdes-badge">${escapeMapHtml(labelText)}</span>` : "") +
|
||||
(linkText ? `<span class="vdes-badge">${escapeMapHtml(linkText)}</span>` : "") +
|
||||
(srcText ? `<span class="vdes-badge">${escapeMapHtml(srcText)}</span>` : "") +
|
||||
(dstText ? `<span class="vdes-badge">${escapeMapHtml(dstText)}</span>` : "") +
|
||||
(syncText ? `<span class="vdes-badge">${escapeMapHtml(syncText)}</span>` : "") +
|
||||
(phaseText ? `<span class="vdes-badge">${escapeMapHtml(phaseText)}</span>` : "") +
|
||||
`<span class="vdes-badge">T${escapeMapHtml(String(msg.message_type ?? "--"))}</span>` +
|
||||
@@ -112,11 +133,18 @@ function renderVdesRow(msg) {
|
||||
`<div class="vdes-row-meta">` +
|
||||
`<span>${escapeMapHtml(currentVdesCenterText())}</span>` +
|
||||
`<span>${escapeMapHtml(`${msg.bit_len || 0} bits`)}</span>` +
|
||||
(sessionText ? `<span>${escapeMapHtml(sessionText)}</span>` : "") +
|
||||
(asmText ? `<span>${escapeMapHtml(asmText)}</span>` : "") +
|
||||
(countText ? `<span>${escapeMapHtml(countText)}</span>` : "") +
|
||||
(ackText ? `<span>${escapeMapHtml(ackText)}</span>` : "") +
|
||||
(cqiText ? `<span>${escapeMapHtml(cqiText)}</span>` : "") +
|
||||
(info ? `<span>${escapeMapHtml(info)}</span>` : "") +
|
||||
(fecText ? `<span>${escapeMapHtml(fecText)}</span>` : "") +
|
||||
`<span>${escapeMapHtml(vdesAgeText(msg._tsMs))}</span>` +
|
||||
`</div>` +
|
||||
`<div class="vdes-row-detail">` +
|
||||
(previewText ? `<span>${escapeMapHtml(previewText)}</span>` : "") +
|
||||
(previewText ? `<span>·</span>` : "") +
|
||||
`<span class="vdes-raw">${escapeMapHtml(rawHex)}</span>` +
|
||||
`</div>`;
|
||||
applyVdesFilterToRow(row);
|
||||
@@ -142,7 +170,11 @@ function updateVdesBar() {
|
||||
const title = escapeMapHtml(msg.vessel_name || "Burst");
|
||||
const detail = [
|
||||
`${msg.bit_len || 0} bits`,
|
||||
msg.message_label ? escapeMapHtml(msg.message_label) : null,
|
||||
Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null,
|
||||
Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null,
|
||||
Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null,
|
||||
Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null,
|
||||
Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null,
|
||||
Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null,
|
||||
msg.destination ? escapeMapHtml(msg.destination) : null,
|
||||
@@ -215,6 +247,15 @@ window.onServerVdes = function(msg) {
|
||||
vessel_name: msg.vessel_name,
|
||||
callsign: msg.callsign,
|
||||
destination: msg.destination,
|
||||
message_label: msg.message_label,
|
||||
session_id: msg.session_id,
|
||||
source_id: msg.source_id,
|
||||
destination_id: msg.destination_id,
|
||||
data_count: msg.data_count,
|
||||
asm_identifier: msg.asm_identifier,
|
||||
ack_nack_mask: msg.ack_nack_mask,
|
||||
channel_quality: msg.channel_quality,
|
||||
payload_preview: msg.payload_preview,
|
||||
link_id: msg.link_id,
|
||||
sync_score: msg.sync_score,
|
||||
sync_errors: msg.sync_errors,
|
||||
|
||||
@@ -85,6 +85,24 @@ pub struct VdesMessage {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub destination: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub destination_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_count: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub asm_identifier: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ack_nack_mask: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub channel_quality: Option<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub payload_preview: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub link_id: Option<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_score: Option<f32>,
|
||||
|
||||
Reference in New Issue
Block a user