Skip to content

Commit 4a6247c

Browse files
authored
Merge pull request #4 from physwkim/fix/pvinfo-and-get-init
fix: pvinfo GET_FIELD bug, ChannelConn server_addr, GET init for data-less PVs Good fixes, I re-isntated and updated some old p4p (pvxs under the hood) interops test I had, I plan to have full interop with all major pvaccess libraries in the long term as well as full protocol completness when needed.
2 parents 52b2182 + cde9286 commit 4a6247c

11 files changed

Lines changed: 211 additions & 42 deletions

File tree

spvirit-client/src/client.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::auth::{resolved_authnz_host, resolved_authnz_user};
66
use crate::search::resolve_pv_server;
77
use crate::transport::{read_packet, read_until};
88
use crate::types::{PvGetError, PvGetOptions, PvGetResult};
9+
use spvirit_codec::spvd_encode::encode_pv_request;
910
use spvirit_codec::epics_decode::{
1011
PvaPacket, PvaPacketCommand, decode_op_response_status as codec_decode_op_response_status,
1112
};
@@ -49,6 +50,7 @@ pub struct ChannelConn {
4950
pub sid: u32,
5051
pub version: u8,
5152
pub is_be: bool,
53+
pub server_addr: std::net::SocketAddr,
5254
}
5355

5456
pub async fn establish_channel(
@@ -129,10 +131,23 @@ pub async fn establish_channel(
129131
sid,
130132
version,
131133
is_be,
134+
server_addr: target,
132135
})
133136
}
134137

138+
/// Convenience wrapper: GET with no field filtering.
135139
pub async fn pvget(opts: &PvGetOptions) -> Result<PvGetResult, PvGetError> {
140+
pvget_fields(opts, &[]).await
141+
}
142+
143+
/// GET with optional field filtering.
144+
///
145+
/// If `fields` is empty, requests all fields (equivalent to `-r ""`).
146+
/// Otherwise, encodes a pvRequest like `field(value,alarm,timeStamp)`.
147+
pub async fn pvget_fields(
148+
opts: &PvGetOptions,
149+
fields: &[&str],
150+
) -> Result<PvGetResult, PvGetError> {
136151
let target = resolve_pv_server(opts).await?;
137152

138153
let conn = establish_channel(target, opts).await?;
@@ -141,15 +156,21 @@ pub async fn pvget(opts: &PvGetOptions) -> Result<PvGetResult, PvGetError> {
141156
sid,
142157
version,
143158
is_be,
159+
..
144160
} = conn;
145161

146162
let ioid = 1u32;
147-
// Match EPICS pvget GET init payload (extra pvRequest bytes observed in capture).
163+
let pv_request = if fields.is_empty() {
164+
// Empty pvRequest — request all fields
165+
vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
166+
} else {
167+
encode_pv_request(fields, is_be)
168+
};
148169
let get_init_req = encode_get_request(
149170
sid,
150171
ioid,
151172
0x08,
152-
&[0xfd, 0x02, 0x00, 0x80, 0x00, 0x00],
173+
&pv_request,
153174
version,
154175
is_be,
155176
);

spvirit-client/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub use pva_client::{
5252
pub use pvlist::PvListSource;
5353

5454
// --- Re-exports: client core ---
55-
pub use client::{ChannelConn, build_client_validation, establish_channel, pvget};
55+
pub use client::{ChannelConn, build_client_validation, establish_channel, pvget, pvget_fields};
5656
pub use format::{OutputFormat, RenderOptions, format_output};
5757
pub use search::{SearchTarget, build_auto_broadcast_targets, resolve_pv_server, search_pv};
5858
pub use transport::read_packet;

spvirit-client/src/pva_client.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ impl PvaClient {
233233
low_level_pvget(&opts).await
234234
}
235235

236+
/// Fetch a PV with field filtering (equivalent to `pvget -r "field(value,alarm)"`).
237+
pub async fn pvget_fields(
238+
&self,
239+
pv_name: &str,
240+
fields: &[&str],
241+
) -> Result<PvGetResult, PvGetError> {
242+
let opts = self.opts(pv_name);
243+
crate::client::pvget_fields(&opts, fields).await
244+
}
245+
236246
// ─── pvput ───────────────────────────────────────────────────────────
237247

238248
/// Write a value to a PV.
@@ -250,6 +260,7 @@ impl PvaClient {
250260
sid,
251261
version: _,
252262
is_be,
263+
..
253264
} = self.open_channel(pv_name).await?;
254265

255266
let ioid = alloc_ioid();
@@ -298,6 +309,7 @@ impl PvaClient {
298309
sid,
299310
version,
300311
is_be,
312+
..
301313
} = self.open_channel(pv_name).await?;
302314

303315
let ioid = alloc_ioid();
@@ -383,6 +395,7 @@ impl PvaClient {
383395
sid,
384396
version: _,
385397
is_be,
398+
..
386399
} = self.open_channel(pv_name).await?;
387400

388401
let ioid = alloc_ioid();
@@ -445,11 +458,21 @@ impl PvaClient {
445458

446459
/// Retrieve the field/structure description (introspection) for a PV.
447460
pub async fn pvinfo(&self, pv_name: &str) -> Result<StructureDesc, PvGetError> {
461+
let result = self.pvinfo_full(pv_name).await?;
462+
Ok(result.0)
463+
}
464+
465+
/// Retrieve introspection and server address for a PV.
466+
pub async fn pvinfo_full(
467+
&self,
468+
pv_name: &str,
469+
) -> Result<(StructureDesc, SocketAddr), PvGetError> {
448470
let ChannelConn {
449471
mut stream,
450472
sid,
451473
version: _,
452474
is_be,
475+
server_addr,
453476
} = self.open_channel(pv_name).await?;
454477

455478
let ioid = alloc_ioid();
@@ -459,11 +482,34 @@ impl PvaClient {
459482
let resp_bytes = read_until(
460483
&mut stream,
461484
self.timeout,
462-
|cmd| matches!(cmd, PvaPacketCommand::Op(op) if op.command == 17),
485+
|cmd| matches!(cmd, PvaPacketCommand::GetField(_)),
463486
)
464487
.await?;
465488

466-
decode_init_introspection(&resp_bytes, "GET_FIELD")
489+
let mut pkt = PvaPacket::new(&resp_bytes);
490+
let cmd = pkt
491+
.decode_payload()
492+
.ok_or_else(|| PvGetError::Decode("GET_FIELD response decode failed".to_string()))?;
493+
match cmd {
494+
PvaPacketCommand::GetField(payload) => {
495+
if let Some(ref st) = payload.status {
496+
if st.is_error() {
497+
let msg = st
498+
.message
499+
.clone()
500+
.unwrap_or_else(|| format!("code={}", st.code));
501+
return Err(PvGetError::Protocol(format!("GET_FIELD error: {msg}")));
502+
}
503+
}
504+
let desc = payload
505+
.introspection
506+
.ok_or_else(|| PvGetError::Decode("missing GET_FIELD introspection".to_string()))?;
507+
Ok((desc, server_addr))
508+
}
509+
_ => Err(PvGetError::Protocol(
510+
"unexpected GET_FIELD response".to_string(),
511+
)),
512+
}
467513
}
468514

469515
// ─── pvlist ──────────────────────────────────────────────────────────

spvirit-client/src/pvlist.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ async fn list_pvs_via_server_rpc_channel(
306306
sid,
307307
version,
308308
is_be,
309+
..
309310
} = establish_channel(server_addr, &rpc_opts).await?;
310311

311312
let ioid = 1u32;
@@ -421,6 +422,7 @@ pub async fn list_pvs_via_server_get(
421422
sid,
422423
version,
423424
is_be,
425+
..
424426
} = establish_channel(server_addr, &get_opts).await?;
425427

426428
let ioid = 1u32;

spvirit-codec/src/spvd_decode.rs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,17 @@ impl fmt::Display for DecodedValue {
293293
/// PVD Decoder state
294294
pub struct PvdDecoder {
295295
is_be: bool,
296+
/// IntrospectionRegistry: maps int16 keys to previously seen FieldTypes.
297+
/// Populated when parsing `0xFD` (full-with-id) entries, looked up on `0xFE` (only-id).
298+
registry: std::cell::RefCell<std::collections::HashMap<u16, FieldType>>,
296299
}
297300

298301
impl PvdDecoder {
299302
pub fn new(is_be: bool) -> Self {
300-
Self { is_be }
303+
Self {
304+
is_be,
305+
registry: std::cell::RefCell::new(std::collections::HashMap::new()),
306+
}
301307
}
302308

303309
/// Decode a size value (PVA variable-length encoding)
@@ -382,24 +388,36 @@ impl PvdDecoder {
382388
// Full-with-id from IntrospectionRegistry:
383389
// 0xFD + int16 key + type descriptor payload.
384390
if type_byte == 0xFD {
385-
if data.len() < 4 {
391+
if data.len() < 3 {
386392
return None;
387393
}
388-
// Preferred parsing path: skip tag + int16 key.
394+
let key = if self.is_be {
395+
u16::from_be_bytes([data[1], data[2]])
396+
} else {
397+
u16::from_le_bytes([data[1], data[2]])
398+
};
389399
if let Some((field_type, consumed)) = self.parse_type_desc(&data[3..]) {
400+
self.registry.borrow_mut().insert(key, field_type.clone());
390401
return Some((field_type, 3 + consumed));
391402
}
392-
// Legacy fallback for older non-keyed streams.
393-
if let Some((field_type, consumed)) = self.parse_type_desc(&data[1..]) {
394-
return Some((field_type, 1 + consumed));
395-
}
396403
return None;
397404
}
398405

399406
// Only-id from IntrospectionRegistry:
400-
// 0xFE + int16 key, requires connection-level registry state.
407+
// 0xFE + int16 key — reference to a previously seen type.
401408
if type_byte == 0xFE {
402-
debug!("Type descriptor uses ONLY_ID (0xFE) without registry context");
409+
if data.len() < 3 {
410+
return None;
411+
}
412+
let key = if self.is_be {
413+
u16::from_be_bytes([data[1], data[2]])
414+
} else {
415+
u16::from_le_bytes([data[1], data[2]])
416+
};
417+
if let Some(ft) = self.registry.borrow().get(&key) {
418+
return Some((ft.clone(), 3));
419+
}
420+
debug!("Type descriptor ONLY_ID (0xFE) key={} not found in registry", key);
403421
return None;
404422
}
405423

@@ -549,26 +567,49 @@ impl PvdDecoder {
549567
// Full-with-id from IntrospectionRegistry:
550568
// 0xFD + int16 key + field type descriptor payload.
551569
if type_byte == 0xFD {
552-
if data.len() < 4 {
570+
if data.len() < 3 {
553571
return None;
554572
}
555-
// Preferred parsing path: skip tag + int16 key.
573+
let key = if self.is_be {
574+
u16::from_be_bytes([data[1], data[2]])
575+
} else {
576+
u16::from_le_bytes([data[1], data[2]])
577+
};
556578
if let Some((desc, consumed)) = self.parse_introspection_with_len(&data[3..]) {
579+
// Register this structure type for later 0xFE references
580+
if !desc.fields.is_empty() {
581+
self.registry.borrow_mut().insert(
582+
key,
583+
FieldType::Structure(desc.clone()),
584+
);
585+
} else {
586+
self.registry.borrow_mut().insert(
587+
key,
588+
FieldType::Structure(desc.clone()),
589+
);
590+
}
557591
return Some((desc, 3 + consumed));
558592
}
559-
// Legacy fallback for older non-keyed streams.
560-
if let Some((desc, consumed)) = self.parse_introspection_with_len(&data[1..]) {
561-
return Some((desc, 1 + consumed));
562-
}
563593
return None;
564594
}
565595

566596
// Only-id from IntrospectionRegistry:
567-
// 0xFE + int16 key, requires connection-level registry state.
597+
// 0xFE + int16 key — reference to a previously seen type.
568598
if type_byte == 0xFE {
569-
debug!(
570-
"Introspection uses ONLY_ID (0xFE), but no registry is available in this decoder"
571-
);
599+
if data.len() < 3 {
600+
return None;
601+
}
602+
let key = if self.is_be {
603+
u16::from_be_bytes([data[1], data[2]])
604+
} else {
605+
u16::from_le_bytes([data[1], data[2]])
606+
};
607+
if let Some(ft) = self.registry.borrow().get(&key) {
608+
if let FieldType::Structure(desc) = ft {
609+
return Some((desc.clone(), 3));
610+
}
611+
}
612+
debug!("Introspection ONLY_ID (0xFE) key={} not found in registry", key);
572613
return None;
573614
}
574615

spvirit-codec/src/spvd_encode.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,10 @@ fn encode_codec_parameters(
889889
out
890890
}
891891

892+
pub fn nt_ndarray_desc_default() -> StructureDesc {
893+
nt_ndarray_desc(&NtNdArray::empty())
894+
}
895+
892896
pub fn nt_ndarray_desc(_nt: &NtNdArray) -> StructureDesc {
893897
StructureDesc {
894898
struct_id: Some("epics:nt/NTNDArray:1.0".to_string()),

0 commit comments

Comments
 (0)