Skip to content

Commit 8a6fc8d

Browse files
feat: smv encoding
1 parent 97b2cd9 commit 8a6fc8d

File tree

5 files changed

+1686
-33
lines changed

5 files changed

+1686
-33
lines changed

benches/smv_decode.rs

Lines changed: 171 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
2-
use iec_61850_lib::decode_smv::{decode_smv, is_smv_frame};
2+
use iec_61850_lib::decode_smv::decode_smv;
3+
use iec_61850_lib::encode_smv::encode_smv;
4+
use iec_61850_lib::types::{EthernetHeader, Sample, SavAsdu, SavPdu};
35

46
/// Diagnostic function to validate packet structure
57
fn validate_packet(packet: &[u8], name: &str) {
68
println!("\n=== Validating {} ===", name);
79
println!("Packet size: {} bytes", packet.len());
810

911
match decode_smv(packet, 22) {
10-
Ok(num_parsed) => {
11-
println!("✓ Successfully decoded {} bytes", num_parsed);
12-
println!(" Total parsed: {} bytes", num_parsed);
12+
Ok(pdu) => {
13+
println!("✓ Successfully decoded PDU");
14+
println!(" Number of ASDUs: {}", pdu.no_asdu);
15+
println!(" Simulation: {}", pdu.sim);
1316
}
1417
Err(e) => {
1518
println!("✗ Decode error at index {}: {}", e.buffer_index, e.message);
@@ -392,14 +395,6 @@ fn create_sample_smv_packet() -> Vec<u8> {
392395
packet
393396
}
394397

395-
fn benchmark_smv_detection(c: &mut Criterion) {
396-
let packet = create_sample_smv_packet();
397-
398-
c.bench_function("smv_frame_detection", |b| {
399-
b.iter(|| is_smv_frame(black_box(&packet)));
400-
});
401-
}
402-
403398
fn benchmark_full_smv_decode(c: &mut Criterion) {
404399
let packet = create_sample_smv_packet();
405400

@@ -480,7 +475,7 @@ fn benchmark_max_stress_decode(c: &mut Criterion) {
480475
group.finish();
481476
}
482477

483-
fn benchmark_comparison(c: &mut Criterion) {
478+
fn benchmark_decode_comparison(c: &mut Criterion) {
484479
let small_packet = create_sample_smv_packet(); // 1 ASDU, 8 samples
485480
let realistic_packet = create_max_realistic_smv_packet(); // 8 ASDUs, 12 samples each
486481
let large_packet = create_max_stress_smv_packet(); // 8 ASDUs, 32 samples each
@@ -530,12 +525,173 @@ fn benchmark_comparison(c: &mut Criterion) {
530525
group.finish();
531526
}
532527

528+
/// Helper function to create sample data for encoding benchmarks
529+
fn create_sample_pdu(num_asdus: usize, samples_per_asdu: usize) -> SavPdu {
530+
let mut sav_asdu = Vec::new();
531+
532+
for i in 0..num_asdus {
533+
let mut samples = Vec::new();
534+
for j in 0..samples_per_asdu {
535+
samples.push(Sample::new(
536+
1000 + (i * 100 + j) as i32,
537+
0x0000, // good quality
538+
));
539+
}
540+
541+
sav_asdu.push(SavAsdu {
542+
msv_id: format!("IED1/LLN0$MSVCB{:02}", i + 1),
543+
dat_set: Some(format!("IED1/LLN0$DATASET{:02}", i + 1)),
544+
smp_cnt: (i * 1000) as u16,
545+
conf_rev: 1,
546+
refr_tm: Some([0x20, 0x21, 0x06, 0x12, 0x0A, 0x30, 0x00, 0x00]),
547+
smp_synch: 1,
548+
smp_rate: Some(4000),
549+
all_data: samples,
550+
smp_mod: None,
551+
gm_identity: None,
552+
});
553+
}
554+
555+
SavPdu {
556+
sim: false,
557+
no_asdu: num_asdus as u16,
558+
security: None,
559+
sav_asdu,
560+
}
561+
}
562+
563+
fn benchmark_smv_encode(c: &mut Criterion) {
564+
let header = EthernetHeader {
565+
dst_addr: [0x01, 0x0c, 0xcd, 0x04, 0x00, 0x01],
566+
src_addr: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
567+
tpid: None,
568+
tci: None,
569+
ether_type: [0x88, 0xba], // SMV EtherType
570+
appid: [0x40, 0x00],
571+
length: [0x00, 0x00],
572+
};
573+
574+
let small_pdu = create_sample_pdu(1, 8); // 1 ASDU × 8 samples
575+
let realistic_pdu = create_sample_pdu(8, 12); // 8 ASDUs × 12 samples
576+
let stress_pdu = create_sample_pdu(8, 32); // 8 ASDUs × 32 samples
577+
578+
let mut group = c.benchmark_group("smv_encode");
579+
580+
group.bench_function("small_1x8", |b| {
581+
b.iter(|| encode_smv(black_box(&header), black_box(&small_pdu)));
582+
});
583+
584+
group.bench_function("realistic_8x12", |b| {
585+
b.iter(|| encode_smv(black_box(&header), black_box(&realistic_pdu)));
586+
});
587+
588+
group.bench_function("stress_8x32", |b| {
589+
b.iter(|| encode_smv(black_box(&header), black_box(&stress_pdu)));
590+
});
591+
592+
group.finish();
593+
}
594+
595+
fn benchmark_smv_encode_comparison(c: &mut Criterion) {
596+
let header = EthernetHeader {
597+
dst_addr: [0x01, 0x0c, 0xcd, 0x04, 0x00, 0x01],
598+
src_addr: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
599+
tpid: None,
600+
tci: None,
601+
ether_type: [0x88, 0xba],
602+
appid: [0x40, 0x00],
603+
length: [0x00, 0x00],
604+
};
605+
606+
let small_pdu = create_sample_pdu(1, 8);
607+
let realistic_pdu = create_sample_pdu(8, 12);
608+
let stress_pdu = create_sample_pdu(8, 32);
609+
610+
println!("\n=== SMV Encoding Performance (Zero-Copy) ===");
611+
println!("Testing zero-copy encoding with exact allocation");
612+
println!("================================================\n");
613+
614+
let mut group = c.benchmark_group("smv_encode_comparison");
615+
616+
// Small packet benchmarks
617+
group.bench_with_input(
618+
BenchmarkId::new("zero_copy", "small_1x8"),
619+
&small_pdu,
620+
|b, pdu| {
621+
b.iter(|| encode_smv(black_box(&header), black_box(pdu)));
622+
},
623+
);
624+
625+
// Realistic packet benchmarks
626+
group.bench_with_input(
627+
BenchmarkId::new("zero_copy", "realistic_8x12"),
628+
&realistic_pdu,
629+
|b, pdu| {
630+
b.iter(|| encode_smv(black_box(&header), black_box(pdu)));
631+
},
632+
);
633+
634+
// Stress packet benchmarks
635+
group.bench_with_input(
636+
BenchmarkId::new("zero_copy", "stress_8x32"),
637+
&stress_pdu,
638+
|b, pdu| {
639+
b.iter(|| encode_smv(black_box(&header), black_box(pdu)));
640+
},
641+
);
642+
643+
group.finish();
644+
}
645+
646+
fn benchmark_smv_roundtrip(c: &mut Criterion) {
647+
let header = EthernetHeader {
648+
dst_addr: [0x01, 0x0c, 0xcd, 0x04, 0x00, 0x01],
649+
src_addr: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
650+
tpid: None,
651+
tci: None,
652+
ether_type: [0x88, 0xba],
653+
appid: [0x40, 0x00],
654+
length: [0x00, 0x00],
655+
};
656+
657+
let small_pdu = create_sample_pdu(1, 8);
658+
let realistic_pdu = create_sample_pdu(8, 12);
659+
let stress_pdu = create_sample_pdu(8, 32);
660+
661+
let mut group = c.benchmark_group("smv_roundtrip");
662+
663+
group.bench_function("small_1x8", |b| {
664+
b.iter(|| {
665+
let encoded = encode_smv(black_box(&header), black_box(&small_pdu)).unwrap();
666+
decode_smv(black_box(&encoded), black_box(22))
667+
});
668+
});
669+
670+
group.bench_function("realistic_8x12", |b| {
671+
b.iter(|| {
672+
let encoded = encode_smv(black_box(&header), black_box(&realistic_pdu)).unwrap();
673+
decode_smv(black_box(&encoded), black_box(22))
674+
});
675+
});
676+
677+
group.bench_function("stress_8x32", |b| {
678+
b.iter(|| {
679+
let encoded = encode_smv(black_box(&header), black_box(&stress_pdu)).unwrap();
680+
decode_smv(black_box(&encoded), black_box(22))
681+
});
682+
});
683+
684+
group.finish();
685+
}
686+
533687
criterion_group!(
534688
benches,
535-
benchmark_smv_detection,
536689
benchmark_full_smv_decode,
537690
benchmark_throughput,
538691
benchmark_max_stress_decode,
539-
benchmark_comparison
692+
benchmark_decode_comparison,
693+
benchmark_smv_encode,
694+
benchmark_smv_encode_comparison,
695+
benchmark_smv_roundtrip
540696
);
541697
criterion_main!(benches);

src/decode_smv.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,18 @@ fn decompress_integer(
8686
buffer_index,
8787
));
8888
}
89-
if length > value.len() {
89+
90+
// Handle unsigned integers with leading 0x00 byte (for MSB set prevention)
91+
// If the encoded value has a leading 0x00 and the next byte has MSB set,
92+
// and the length is exactly one more than our target size, skip the leading 0x00
93+
let (actual_start, actual_length) = if length == value.len() + 1
94+
&& length >= 2
95+
&& buffer[buffer_index] == 0x00
96+
&& (buffer[buffer_index + 1] & 0x80) != 0
97+
{
98+
// Skip the leading 0x00 byte for unsigned integers
99+
(buffer_index + 1, length - 1)
100+
} else if length > value.len() {
90101
return Err(DecodeError::new(
91102
&format!(
92103
"Mismatch value length {} vs buffer length {}",
@@ -95,23 +106,25 @@ fn decompress_integer(
95106
),
96107
buffer_index,
97108
));
98-
}
109+
} else {
110+
(buffer_index, length)
111+
};
99112

100113
// Determine fill byte for sign extension (0xFF for negative, 0x00 for positive)
101-
let fill = if buffer[buffer_index] & 0x80 == 0x80 {
114+
let fill = if buffer[actual_start] & 0x80 == 0x80 {
102115
0xFF
103116
} else {
104117
0x00
105118
};
106119

107120
// Fill the leading bytes with the sign extension
108-
let fill_length = value.len() - length;
121+
let fill_length = value.len() - actual_length;
109122
for item in value.iter_mut().take(fill_length) {
110123
*item = fill;
111124
}
112125

113126
// Copy the encoded integer bytes into the lower part of the output buffer
114-
value[fill_length..].copy_from_slice(&buffer[buffer_index..buffer_index + length]);
127+
value[fill_length..].copy_from_slice(&buffer[actual_start..actual_start + actual_length]);
115128
Ok(())
116129
}
117130

@@ -285,7 +298,7 @@ fn decode_tag_length(
285298
///
286299
/// # Returns
287300
/// The new buffer position after decoding the PDU.
288-
pub fn decode_smv(buffer: &[u8], pos: usize) -> Result<usize, DecodeError> {
301+
pub fn decode_smv(buffer: &[u8], pos: usize) -> Result<SavPdu, DecodeError> {
289302
let mut pdu = SavPdu::default();
290303
let mut new_pos = pos;
291304

@@ -301,23 +314,26 @@ pub fn decode_smv(buffer: &[u8], pos: usize) -> Result<usize, DecodeError> {
301314
new_pos = decode_tag_length(&mut _tag, &mut _length, buffer, new_pos)?;
302315
new_pos = decode_unsigned_16(&mut pdu.no_asdu, buffer, new_pos, _length)?;
303316

304-
// Optional field security
317+
// Optional field security (ANY OPTIONAL - reserved for future use)
305318
let tag = buffer[new_pos];
306319
if tag == 0x81 {
307-
pdu.security = true;
308-
new_pos += 12; // Skip the security tag and length field
320+
let mut _length = 0usize;
321+
new_pos = decode_tag_length(&mut _tag, &mut _length, buffer, new_pos)?;
322+
let mut sec_buf = vec![0u8; _length];
323+
new_pos = decode_octet_string(&mut sec_buf, buffer, new_pos, _length)?;
324+
pdu.security = Some(sec_buf);
309325
} else {
310-
pdu.security = false;
326+
pdu.security = None;
311327
}
312328

313329
// sequence of ASDU
314330
let mut length = 0usize;
315331
new_pos = decode_tag_length(&mut _tag, &mut length, buffer, new_pos)?;
316332

317333
pdu.sav_asdu.clear();
318-
new_pos = decode_smv_asdus(&mut pdu.sav_asdu, buffer, new_pos, pdu.no_asdu)?;
334+
decode_smv_asdus(&mut pdu.sav_asdu, buffer, new_pos, pdu.no_asdu)?;
319335

320-
Ok(new_pos)
336+
Ok(pdu)
321337
}
322338

323339
/// Determines if the provided Ethernet frame buffer contains a Sampled Values (SMV) frame

0 commit comments

Comments
 (0)