Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 255 additions & 8 deletions crates/dsview-cli/src/main.rs

Large diffs are not rendered by default.

146 changes: 145 additions & 1 deletion crates/dsview-core/src/capture_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::str::FromStr;

use crate::{NativeErrorCode, RuntimeError};
use thiserror::Error;
Expand All @@ -8,6 +10,145 @@ pub struct CaptureConfigRequest {
pub sample_rate_hz: u64,
pub sample_limit: u64,
pub enabled_channels: BTreeSet<u16>,
/// Optional simple trigger. `None` starts the capture immediately.
pub trigger: Option<TriggerSpec>,
}

/// Condition a single channel must meet for the trigger to fire.
///
/// These map onto the uppercase characters libsigrok4DSL matches on; the
/// lowercase spellings are what a user types on the command line.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerCondition {
DontCare,
Low,
High,
Rising,
Falling,
Either,
}

impl TriggerCondition {
pub fn as_native_char(self) -> u8 {
match self {
TriggerCondition::DontCare => b'X',
TriggerCondition::Low => b'0',
TriggerCondition::High => b'1',
TriggerCondition::Rising => b'R',
TriggerCondition::Falling => b'F',
TriggerCondition::Either => b'C',
}
}

pub fn token(self) -> &'static str {
match self {
TriggerCondition::DontCare => "x",
TriggerCondition::Low => "0",
TriggerCondition::High => "1",
TriggerCondition::Rising => "r",
TriggerCondition::Falling => "f",
TriggerCondition::Either => "c",
}
}
}

impl fmt::Display for TriggerCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.token())
}
}

impl FromStr for TriggerCondition {
type Err = TriggerParseError;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"x" => Ok(TriggerCondition::DontCare),
"0" | "low" => Ok(TriggerCondition::Low),
"1" | "high" => Ok(TriggerCondition::High),
"r" | "rising" => Ok(TriggerCondition::Rising),
"f" | "falling" => Ok(TriggerCondition::Falling),
"c" | "change" | "either" => Ok(TriggerCondition::Either),
other => Err(TriggerParseError::UnknownCondition(other.to_string())),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TriggerParseError {
#[error("trigger term '{0}' is not CHANNEL=CONDITION")]
MalformedTerm(String),
#[error("'{0}' is not a channel index")]
BadChannel(String),
#[error("unknown trigger condition '{0}'; expected one of 0 1 r f c x")]
UnknownCondition(String),
#[error("channel {0} appears more than once")]
DuplicateChannel(u16),
#[error("trigger position must be 0-100, got {0}")]
BadPosition(u32),
#[error("every channel is 'x', so the trigger would never fire")]
AllDontCare,
}

/// A simple (single-stage) trigger: a condition per channel, plus how much of
/// the capture buffer to keep from before the trigger fires.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TriggerSpec {
pub conditions: BTreeMap<u16, TriggerCondition>,
pub position_percent: u16,
}

impl TriggerSpec {
pub const DEFAULT_POSITION_PERCENT: u16 = 0;

/// Parse a spec like `0=r,1=1,3=f`. Whitespace around terms is ignored.
pub fn parse(spec: &str, position_percent: u16) -> Result<Self, TriggerParseError> {
if position_percent > 100 {
return Err(TriggerParseError::BadPosition(u32::from(position_percent)));
}

let mut conditions = BTreeMap::new();
for term in spec.split(',').map(str::trim).filter(|t| !t.is_empty()) {
let (channel_text, condition_text) = term
.split_once('=')
.ok_or_else(|| TriggerParseError::MalformedTerm(term.to_string()))?;
let channel_text = channel_text.trim();
// accept both "0" and the "D0" spelling the VCD output uses
let normalised = channel_text
.strip_prefix(['D', 'd'])
.unwrap_or(channel_text);
let channel: u16 = normalised
.parse()
.map_err(|_| TriggerParseError::BadChannel(channel_text.to_string()))?;
let condition: TriggerCondition = condition_text.parse()?;
if conditions.insert(channel, condition).is_some() {
return Err(TriggerParseError::DuplicateChannel(channel));
}
}

if conditions
.values()
.all(|c| matches!(c, TriggerCondition::DontCare))
{
return Err(TriggerParseError::AllDontCare);
}

Ok(Self {
conditions,
position_percent,
})
}

/// Channels referenced by the trigger that are not in `enabled`. The device
/// cannot trigger on a channel it is not sampling, so this is an error the
/// caller should surface rather than let the capture hang.
pub fn channels_not_enabled(&self, enabled: &[u16]) -> Vec<u16> {
self.conditions
.keys()
.copied()
.filter(|c| !enabled.contains(c))
.collect()
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -35,6 +176,7 @@ pub struct ValidatedCaptureConfig {
pub effective_sample_limit: u64,
pub enabled_channels: Vec<u16>,
pub channel_mode_id: i16,
pub trigger: Option<TriggerSpec>,
}

impl CaptureConfigError {
Expand Down Expand Up @@ -159,6 +301,7 @@ impl CaptureCapabilities {
}

Ok(ValidatedCaptureConfig {
trigger: request.trigger.clone(),
sample_rate_hz: request.sample_rate_hz,
requested_sample_limit: request.sample_limit,
effective_sample_limit,
Expand Down Expand Up @@ -257,6 +400,7 @@ mod tests {
sample_rate_hz,
sample_limit,
enabled_channels: enabled_channels.iter().copied().collect(),
trigger: None,
}
}

Expand Down
97 changes: 96 additions & 1 deletion crates/dsview-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod device_options;
pub use capture_config::{
CaptureCapabilities, CaptureConfigError, CaptureConfigRequest, ChannelModeCapability,
ValidatedCaptureConfig,
TriggerCondition, TriggerParseError, TriggerSpec,
};
pub use device_option_validation::{
ChannelModeValidationCapabilities, DeviceOptionValidationCapabilities,
Expand Down Expand Up @@ -1931,13 +1932,55 @@ pub struct DecodeDiscovery {
paths: DecodeDiscoveryPaths,
}

/// Verbosity of the native libsigrok4DSL logger, which writes to stderr.
///
/// The library's own default is `Info`, which emits a dozen lines of scan
/// chatter on every command. That is unhelpful in a tool whose output is meant
/// to be piped, so this crate defaults to `Warn`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NativeLogLevel {
None,
Error,
#[default]
Warn,
Info,
Debug,
Detail,
}

impl NativeLogLevel {
pub fn as_native(self) -> i32 {
match self {
NativeLogLevel::None => 0,
NativeLogLevel::Error => 1,
NativeLogLevel::Warn => 2,
NativeLogLevel::Info => 3,
NativeLogLevel::Debug => 4,
NativeLogLevel::Detail => 5,
}
}
}

impl Discovery {
pub fn connect(
library_path: impl AsRef<Path>,
resource_dir: impl AsRef<Path>,
) -> Result<Self, BringUpError> {
Self::connect_with_log_level(library_path, resource_dir, NativeLogLevel::default())
}

pub fn connect_with_log_level(
library_path: impl AsRef<Path>,
resource_dir: impl AsRef<Path>,
log_level: NativeLogLevel,
) -> Result<Self, BringUpError> {
let resources = ResourceDirectory::discover(resource_dir)?;
let runtime = RuntimeBridge::load(library_path).map_err(BringUpError::Runtime)?;
// Set verbosity before init(): the device scan happens inside init(),
// and that scan is where most of the native chatter comes from.
runtime
.set_log_level(log_level.as_native())
.map_err(BringUpError::Runtime)?;
runtime
.set_firmware_resource_dir(resources.path())
.map_err(BringUpError::Runtime)?;
Expand All @@ -1947,8 +1990,15 @@ impl Discovery {
}

pub fn connect_auto(resource_override: Option<impl AsRef<Path>>) -> Result<Self, BringUpError> {
Self::connect_auto_with_log_level(resource_override, NativeLogLevel::default())
}

pub fn connect_auto_with_log_level(
resource_override: Option<impl AsRef<Path>>,
log_level: NativeLogLevel,
) -> Result<Self, BringUpError> {
let paths = RuntimeDiscoveryPaths::discover(resource_override)?;
Self::connect(&paths.runtime_library, &paths.resource_dir)
Self::connect_with_log_level(&paths.runtime_library, &paths.resource_dir, log_level)
}

pub fn discovery_paths(
Expand Down Expand Up @@ -2132,6 +2182,7 @@ pub fn validated_capture_config_from_device_options(
request: &ValidatedDeviceOptionRequest,
) -> ValidatedCaptureConfig {
ValidatedCaptureConfig {
trigger: None,
sample_rate_hz: request.sample_rate_hz,
requested_sample_limit: request.requested_sample_limit,
effective_sample_limit: request.effective_sample_limit,
Expand Down Expand Up @@ -2407,6 +2458,42 @@ impl Discovery {
Ok(())
}

/// Program the simple trigger, or clear it when `spec` is `None`.
///
/// libsigrok4DSL keeps trigger state in a process-global, so this always
/// resets first: without that, a channel armed by an earlier capture in the
/// same process would still be live and the device would wait on it.
fn apply_trigger(&self, spec: Option<&TriggerSpec>) -> Result<(), BringUpError> {
self.runtime.trigger_reset().map_err(BringUpError::Runtime)?;

let Some(spec) = spec else {
self.runtime
.trigger_set_enabled(false)
.map_err(BringUpError::Runtime)?;
return Ok(());
};

for (channel, condition) in &spec.conditions {
let native = condition.as_native_char();
// Simple mode evaluates both comparator banks; giving them the same
// character makes the pair express one condition.
self.runtime
.trigger_probe_set(*channel, native, native)
.map_err(BringUpError::Runtime)?;
}

// 0 = SIMPLE_TRIGGER, and a simple trigger occupies no staged slots.
self.runtime.trigger_set_stage(0).map_err(BringUpError::Runtime)?;
self.runtime.trigger_set_mode(0).map_err(BringUpError::Runtime)?;
self.runtime
.trigger_set_position(spec.position_percent)
.map_err(BringUpError::Runtime)?;
self.runtime
.trigger_set_enabled(true)
.map_err(BringUpError::Runtime)?;
Ok(())
}

pub fn acquisition_preflight(
&self,
selection_handle: SelectionHandle,
Expand Down Expand Up @@ -2480,6 +2567,7 @@ impl Discovery {
sample_rate_hz: config.sample_rate_hz,
sample_limit: config.requested_sample_limit,
enabled_channels: config.enabled_channels.iter().copied().collect(),
trigger: config.trigger.clone(),
};
let preflight = self.acquisition_preflight(selection_handle, &request);
if preflight.status != PreflightStatus::Ready {
Expand Down Expand Up @@ -2578,6 +2666,12 @@ impl Discovery {
(session, None)
};

// Both preparation paths above converge here, so the trigger is
// programmed in exactly one place. Doing it inside apply_capture_config
// would silently skip every capture that supplies device options,
// because those take the other path.
self.apply_trigger(request.config.trigger.as_ref())?;

let started = self
.runtime
.start_collect()
Expand Down Expand Up @@ -3820,6 +3914,7 @@ mod tests {
effective_sample_limit: 2048,
enabled_channels: vec![0, 1, 2, 3],
channel_mode_id: 20,
trigger: None,
},
vcd_path,
metadata_path: None,
Expand Down
3 changes: 2 additions & 1 deletion crates/dsview-core/tests/acquisition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ fn capture_without_validated_device_options_keeps_config_only_baseline() {
sample_rate_hz: 25_000_000,
sample_limit: 2048,
enabled_channels: BTreeSet::from([0_u16, 1_u16]),
},

trigger: None,},
validated_device_options: None,
stop_after: None,
stop_on_interrupt: None,
Expand Down
4 changes: 3 additions & 1 deletion crates/dsview-core/tests/export_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ fn validated_config() -> ValidatedCaptureConfig {
effective_sample_limit: 2048,
enabled_channels: vec![0, 1, 2, 3],
channel_mode_id: 20,
}


trigger: None,}
}

fn device_option_snapshot() -> DeviceOptionsSnapshot {
Expand Down
Loading