diff --git a/crates/dsview-cli/src/main.rs b/crates/dsview-cli/src/main.rs index c5e5fab..86b5456 100644 --- a/crates/dsview-cli/src/main.rs +++ b/crates/dsview-cli/src/main.rs @@ -32,10 +32,12 @@ use dsview_core::{ DecoderRuntimeError, DecoderRuntimeErrorCode, DeviceIdentitySnapshot, DeviceOptionApplyFailure, DeviceOptionValidationCapabilities, DeviceOptionValidationError, DeviceOptionsSnapshot, Discovery, EnumOptionSnapshot, MetadataAcquisitionInfo, MetadataArtifactInfo, + NativeLogLevel, MetadataCaptureInfo, MetadataToolInfo, NativeErrorCode, OfflineDecodeInput, OfflineDecodeInputError, OfflineDecodeRunError, OperationModeValidationCapabilities, RuntimeError, SelectionHandle, SupportedDevice, ThresholdCapabilitySnapshot, - ValidatedCaptureConfig, ValidatedDecodeConfig, decode_inspect as core_decode_inspect, + TriggerSpec, ValidatedCaptureConfig, ValidatedDecodeConfig, + decode_inspect as core_decode_inspect, decode_list as core_decode_list, decode_registry_for_config, describe_native_error, parse_decode_config_slice, resolve_capture_artifact_paths, run_offline_decode as core_run_offline_decode, validate_decode_config, @@ -136,6 +138,29 @@ struct SharedDecodeArgs { format: OutputFormat, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +enum LogLevelArg { + None, + Error, + Warn, + Info, + Debug, + Detail, +} + +impl From for NativeLogLevel { + fn from(value: LogLevelArg) -> Self { + match value { + LogLevelArg::None => NativeLogLevel::None, + LogLevelArg::Error => NativeLogLevel::Error, + LogLevelArg::Warn => NativeLogLevel::Warn, + LogLevelArg::Info => NativeLogLevel::Info, + LogLevelArg::Debug => NativeLogLevel::Debug, + LogLevelArg::Detail => NativeLogLevel::Detail, + } + } +} + #[derive(Args, Debug)] struct DecodeListArgs { #[command(flatten)] @@ -197,6 +222,16 @@ struct SharedRuntimeArgs { help = "Directory containing DSLogic Plus firmware and bitstream resources; bundled resources are used by default" )] resource_dir: Option, + #[arg( + long = "log-level", + value_enum, + default_value_t = LogLevelArg::Warn, + help = "Verbosity of the native library's stderr logging. The library's \ + own default is `info`, which prints scan chatter on every command; \ + this defaults to `warn` so stdout stays parseable. Raise it when \ + diagnosing a device that will not enumerate." + )] + log_level: LogLevelArg, #[arg( long, value_enum, @@ -265,6 +300,23 @@ struct CaptureArgs { help = "Comma-separated logic channel indexes to enable, for example 0,1,2,3" )] channels: Vec, + #[arg( + long = "trigger", + value_name = "CH=COND[,CH=COND...]", + help = "Arm a trigger and capture around it, for example 0=r,1=1. \ + Conditions: 0 low, 1 high, r rising, f falling, c either edge, \ + x don't care. Channels may be written 0 or D0. Without this the \ + capture starts immediately." + )] + trigger: Option, + #[arg( + long = "trigger-position", + value_name = "PERCENT", + default_value_t = 0, + help = "Percent of the capture buffer to keep from before the trigger \ + fires (0-100). 0 captures only what follows the trigger." + )] + trigger_position: u16, #[command(flatten)] device_options: CaptureDeviceOptionArgs, #[arg( @@ -281,8 +333,10 @@ struct CaptureArgs { metadata_output: Option, #[arg( long = "wait-timeout-ms", - default_value_t = 10_000, - help = "Maximum time to wait for capture completion before aborting" + default_value_t = 0, + help = "Maximum time to wait for capture completion before aborting. \ + 0 (the default) derives it from the sample rate and sample limit, \ + so large captures are not aborted while still running." )] wait_timeout_ms: u64, #[arg( @@ -409,6 +463,9 @@ struct CaptureResponse { cleanup_succeeded: bool, device_options: CaptureDeviceOptionFacts, artifacts: CaptureArtifactsResponse, + /// Settings the device silently coerced. Empty when it honoured everything. + #[serde(skip_serializing_if = "Vec::is_empty")] + warnings: Vec, } #[derive(Serialize)] @@ -1236,6 +1293,14 @@ fn run_list(args: ListArgs) -> Result<(), FailedCommand> { .list_supported_devices() .map_err(|error| command_error(args.runtime.format, classify_error(&error)))?; + // An empty list is a legitimate result when nothing is plugged in, but not + // when the bus is holding a DreamSourceLab device this build cannot drive. + if devices.is_empty() { + if let Some(error) = unsupported_device_present_error() { + return Err(command_error(args.runtime.format, error)); + } + } + let response = DeviceListResponse { devices: devices.iter().map(device_record).collect(), }; @@ -1335,10 +1400,44 @@ fn run_capture(args: CaptureArgs) -> Result<(), FailedCommand> { ); } let discovery = connect_runtime(&args.runtime)?; + let trigger = match args.trigger.as_deref() { + Some(spec) => { + let parsed = TriggerSpec::parse(spec, args.trigger_position).map_err(|error| { + command_error( + args.runtime.format, + plain_error_response("capture_trigger_invalid", &error.to_string()), + ) + })?; + // The device samples only the enabled channels, so a trigger that + // names a disabled one would simply never fire. Fail loudly here + // rather than let the capture sit until it times out. + let missing = parsed.channels_not_enabled(&args.channels); + if !missing.is_empty() { + let list = missing + .iter() + .map(|c| format!("D{c}")) + .collect::>() + .join(", "); + return Err(command_error( + args.runtime.format, + plain_error_response( + "capture_trigger_channel_not_enabled", + &format!( + "trigger references {list}, which --channels does not enable; \ + the trigger could never fire" + ), + ), + )); + } + Some(parsed) + } + None => None, + }; let config_request = CaptureConfigRequest { sample_rate_hz: args.sample_rate_hz, sample_limit: args.sample_limit, enabled_channels: args.channels.iter().copied().collect::>(), + trigger, }; let run_request = CaptureRunRequest { selection_handle: handle, @@ -1347,7 +1446,11 @@ fn run_capture(args: CaptureArgs) -> Result<(), FailedCommand> { stop_after: args.duration_ms.map(Duration::from_millis), stop_on_interrupt: interrupt_flag.clone(), stream_vcd_path: streaming_vcd_path.clone(), - wait_timeout: Duration::from_millis(args.wait_timeout_ms), + wait_timeout: Duration::from_millis(resolve_wait_timeout_ms( + args.wait_timeout_ms, + args.sample_rate_hz, + args.sample_limit, + )), poll_interval: Duration::from_millis(args.poll_interval_ms), }; let validated_config = if let Some(validated_device_options) = validated_device_options.as_ref() @@ -1395,12 +1498,18 @@ fn run_capture(args: CaptureArgs) -> Result<(), FailedCommand> { saw_end_packet: result.summary.saw_end_packet, saw_terminal_normal_end: result.summary.saw_terminal_normal_end, cleanup_succeeded: result.cleanup.succeeded(), + warnings: coercion_warnings(&export.metadata.device_options), device_options: export.metadata.device_options.clone(), artifacts: CaptureArtifactsResponse { vcd_path: export.vcd_path.display().to_string(), metadata_path: export.metadata_path.display().to_string(), }, }; + // Text mode prints only the artifact paths, so a coercion would be invisible + // there. Send warnings to stderr, leaving stdout clean for piping. + for warning in &response.warnings { + eprintln!("warning: {warning}"); + } render_capture_success(args.runtime.format, &response); Ok(()) } @@ -1488,6 +1597,7 @@ fn fixture_validated_capture_config( effective_sample_limit: args.sample_limit, enabled_channels: args.channels.clone(), channel_mode_id, + trigger: None, }) } @@ -1589,7 +1699,8 @@ fn run_capture_with_test_fixture( vcd_path: artifact_paths.vcd_path.display().to_string(), metadata_path: artifact_paths.metadata_path.display().to_string(), }, - }; + + warnings: Vec::new(),}; render_capture_success(format, &response); Ok(()) } @@ -1735,7 +1846,7 @@ fn run_options(args: OptionsArgs) -> Result<(), FailedCommand> { } fn connect_runtime(args: &SharedRuntimeArgs) -> Result { - Discovery::connect_auto(args.resource_dir.as_deref()) + Discovery::connect_auto_with_log_level(args.resource_dir.as_deref(), args.log_level.into()) .map_err(|error| command_error(args.format, classify_error(&error))) } @@ -2880,6 +2991,137 @@ fn capture_cleanup_response(cleanup: &CaptureCleanup) -> CaptureCleanupResponse } } +/// USB devices from DreamSourceLab (VID 0x2a0e) that the operating system can +/// see, regardless of whether the native library recognises them. +/// +/// The native scan logs unmatched devices at `info`, which is below the default +/// log level, so an unsupported board otherwise looks identical to no board at +/// all: an empty list and a success exit code. Probing the bus separately lets +/// the difference be reported. +#[cfg(target_os = "linux")] +fn dreamsourcelab_usb_devices() -> Vec { + let mut found = Vec::new(); + let Ok(entries) = std::fs::read_dir("/sys/bus/usb/devices") else { + return found; + }; + for entry in entries.flatten() { + let path = entry.path(); + let (Ok(vendor), Ok(product)) = ( + std::fs::read_to_string(path.join("idVendor")), + std::fs::read_to_string(path.join("idProduct")), + ) else { + continue; + }; + let vendor = vendor.trim().to_ascii_lowercase(); + if vendor == "2a0e" { + found.push(format!("{vendor}:{}", product.trim().to_ascii_lowercase())); + } + } + found.sort(); + found.dedup(); + found +} + +#[cfg(not(target_os = "linux"))] +fn dreamsourcelab_usb_devices() -> Vec { + Vec::new() +} + +/// Turn "no devices" into a specific error when the bus says otherwise. +fn unsupported_device_present_error() -> Option { + let present = dreamsourcelab_usb_devices(); + if present.is_empty() { + return None; + } + Some(plain_error_response( + "devices_present_but_unsupported", + &format!( + "found DreamSourceLab USB device(s) [{}] that this build does not recognise. \ + The device profile table in the bundled DSView submodule has no entry for \ + that product id, so the native scan skipped it. Re-run with \ + --log-level info to see the scan decision.", + present.join(", ") + ), + )) +} + +/// Report settings the device did not honour. +/// +/// The device quietly coerces values it cannot meet - selecting `internal-test` +/// forces its own sample rate, for instance. Both figures are already in the +/// JSON, but only if the caller thinks to diff them, so surface the mismatch +/// explicitly rather than let a capture be silently taken at the wrong rate. +fn coercion_warnings(facts: &CaptureDeviceOptionFacts) -> Vec { + let mut warnings = Vec::new(); + let (requested, effective) = (&facts.requested, &facts.effective); + + if requested.sample_rate_hz != effective.sample_rate_hz { + warnings.push(format!( + "sample rate coerced: requested {} Hz, device used {} Hz", + requested.sample_rate_hz, effective.sample_rate_hz + )); + } + // The device rounds the sample limit up to its own buffer granularity on + // essentially every capture (200000 -> 200704), which is expected and not + // worth a warning - a warning that always fires is one nobody reads. A + // clamp that actually matters, such as hitting the hardware capacity, is + // far larger than the alignment step, so only report differences above a + // small relative tolerance. + if requested.sample_limit != effective.sample_limit { + let (req, eff) = (requested.sample_limit, effective.sample_limit); + let delta = req.abs_diff(eff); + let significant = req == 0 || (delta.saturating_mul(100) / req.max(1)) >= 1; + if significant { + warnings.push(format!( + "sample limit coerced: requested {req}, device used {eff}" + )); + } + } + if requested.enabled_channels != effective.enabled_channels { + warnings.push(format!( + "enabled channels coerced: requested {:?}, device used {:?}", + requested.enabled_channels, effective.enabled_channels + )); + } + if requested.channel_mode_id != effective.channel_mode_id { + warnings.push(format!( + "channel mode coerced: requested {}, device used {}", + requested.channel_mode_id, effective.channel_mode_id + )); + } + warnings +} + +/// Wait long enough for the capture actually requested. +/// +/// A fixed 10 s default is fine for a small buffer capture and far too short +/// for a multi-million-sample one, where it aborts a capture that was working. +/// `0` means: derive it from how long the capture must take, with generous +/// headroom for arming and upload. +fn resolve_wait_timeout_ms(requested: u64, sample_rate_hz: u64, sample_limit: u64) -> u64 { + if requested != 0 { + return requested; + } + let capture_ms = if sample_rate_hz == 0 { + 0 + } else { + sample_limit.saturating_mul(1000) / sample_rate_hz + }; + // 3x the capture duration covers upload, plus a 10 s floor. + capture_ms.saturating_mul(3).saturating_add(10_000).max(10_000) +} + +fn plain_error_response(code: &'static str, message: &str) -> ErrorResponse { + ErrorResponse { + code, + message: message.to_string(), + detail: None, + native_error: None, + terminal_event: None, + cleanup: None, + } +} + fn command_error(format: OutputFormat, error: ErrorResponse) -> FailedCommand { FailedCommand { format, @@ -3805,12 +4047,15 @@ mod tests { CaptureArgs { runtime: SharedRuntimeArgs { resource_dir: None, + log_level: LogLevelArg::Warn, format: OutputFormat::Json, }, handle: 7, sample_rate_hz: 100_000_000, sample_limit: 4096, channels: vec![0, 1, 2, 3], + trigger: None, + trigger_position: 0, device_options: CaptureDeviceOptionArgs::default(), output: PathBuf::from("artifacts/run.vcd"), metadata_output: None, @@ -3860,7 +4105,8 @@ mod tests { metadata_path: "/tmp/run.json".to_string(), }, device_options: sample_capture_device_option_facts(), - }; + + warnings: Vec::new(),}; let payload = serde_json::to_value(response).unwrap(); @@ -3889,7 +4135,8 @@ mod tests { metadata_path: "/tmp/run.json".to_string(), }, device_options: sample_capture_device_option_facts(), - }; + + warnings: Vec::new(),}; let text = capture_success_text(&response); diff --git a/crates/dsview-core/src/capture_config.rs b/crates/dsview-core/src/capture_config.rs index 7e6ac85..ab66242 100644 --- a/crates/dsview-core/src/capture_config.rs +++ b/crates/dsview-core/src/capture_config.rs @@ -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; @@ -8,6 +10,145 @@ pub struct CaptureConfigRequest { pub sample_rate_hz: u64, pub sample_limit: u64, pub enabled_channels: BTreeSet, + /// Optional simple trigger. `None` starts the capture immediately. + pub trigger: Option, +} + +/// 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 { + 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, + 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 { + 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 { + self.conditions + .keys() + .copied() + .filter(|c| !enabled.contains(c)) + .collect() + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -35,6 +176,7 @@ pub struct ValidatedCaptureConfig { pub effective_sample_limit: u64, pub enabled_channels: Vec, pub channel_mode_id: i16, + pub trigger: Option, } impl CaptureConfigError { @@ -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, @@ -257,6 +400,7 @@ mod tests { sample_rate_hz, sample_limit, enabled_channels: enabled_channels.iter().copied().collect(), + trigger: None, } } diff --git a/crates/dsview-core/src/lib.rs b/crates/dsview-core/src/lib.rs index 3464163..59ebe78 100644 --- a/crates/dsview-core/src/lib.rs +++ b/crates/dsview-core/src/lib.rs @@ -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, @@ -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, resource_dir: impl AsRef, + ) -> Result { + Self::connect_with_log_level(library_path, resource_dir, NativeLogLevel::default()) + } + + pub fn connect_with_log_level( + library_path: impl AsRef, + resource_dir: impl AsRef, + log_level: NativeLogLevel, ) -> Result { 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)?; @@ -1947,8 +1990,15 @@ impl Discovery { } pub fn connect_auto(resource_override: Option>) -> Result { + Self::connect_auto_with_log_level(resource_override, NativeLogLevel::default()) + } + + pub fn connect_auto_with_log_level( + resource_override: Option>, + log_level: NativeLogLevel, + ) -> Result { 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( @@ -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, @@ -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, @@ -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 { @@ -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() @@ -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, diff --git a/crates/dsview-core/tests/acquisition.rs b/crates/dsview-core/tests/acquisition.rs index d8f898c..d751f3d 100644 --- a/crates/dsview-core/tests/acquisition.rs +++ b/crates/dsview-core/tests/acquisition.rs @@ -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, diff --git a/crates/dsview-core/tests/export_artifacts.rs b/crates/dsview-core/tests/export_artifacts.rs index a425183..30f50a5 100644 --- a/crates/dsview-core/tests/export_artifacts.rs +++ b/crates/dsview-core/tests/export_artifacts.rs @@ -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 { diff --git a/crates/dsview-sys/bridge_runtime.c b/crates/dsview-sys/bridge_runtime.c index 20b8648..5db5606 100644 --- a/crates/dsview-sys/bridge_runtime.c +++ b/crates/dsview-sys/bridge_runtime.c @@ -72,6 +72,14 @@ typedef int (*srd_inst_stack_fn)( struct srd_decoder_inst *di_bottom, struct srd_decoder_inst *di_top); +typedef void (*ds_log_level_fn)(int level); +typedef int (*ds_trigger_reset_fn)(void); +typedef int (*ds_trigger_probe_set_fn)(uint16_t probe, unsigned char trigger0, unsigned char trigger1); +typedef int (*ds_trigger_set_en_fn)(uint16_t enable); +typedef int (*ds_trigger_set_mode_fn)(uint16_t mode); +typedef int (*ds_trigger_set_pos_fn)(uint16_t position); +typedef int (*ds_trigger_set_stage_fn)(uint16_t stages); + typedef void (*dslib_event_callback_t)(int event); typedef void (*ds_datafeed_callback_t)(const struct sr_dev_inst *sdi, const struct sr_datafeed_packet *packet); @@ -112,6 +120,13 @@ struct dsview_bridge_api { ds_get_actived_device_config_list_fn ds_get_actived_device_config_list; ds_set_actived_device_config_fn ds_set_actived_device_config; ds_enable_device_channel_index_fn ds_enable_device_channel_index; + ds_log_level_fn ds_log_level; + ds_trigger_reset_fn ds_trigger_reset; + ds_trigger_probe_set_fn ds_trigger_probe_set; + ds_trigger_set_en_fn ds_trigger_set_en; + ds_trigger_set_mode_fn ds_trigger_set_mode; + ds_trigger_set_pos_fn ds_trigger_set_pos; + ds_trigger_set_stage_fn ds_trigger_set_stage; sr_output_find_fn sr_output_find; sr_output_new_fn sr_output_new; sr_output_send_fn sr_output_send; @@ -3075,6 +3090,55 @@ int dsview_bridge_load_library(const char *path) return status; } + g_bridge_api.ds_log_level = + (ds_log_level_fn)dsview_bridge_load_symbol("ds_log_level", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_reset = + (ds_trigger_reset_fn)dsview_bridge_load_symbol("ds_trigger_reset", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_probe_set = + (ds_trigger_probe_set_fn)dsview_bridge_load_symbol("ds_trigger_probe_set", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_set_en = + (ds_trigger_set_en_fn)dsview_bridge_load_symbol("ds_trigger_set_en", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_set_mode = + (ds_trigger_set_mode_fn)dsview_bridge_load_symbol("ds_trigger_set_mode", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_set_pos = + (ds_trigger_set_pos_fn)dsview_bridge_load_symbol("ds_trigger_set_pos", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + + g_bridge_api.ds_trigger_set_stage = + (ds_trigger_set_stage_fn)dsview_bridge_load_symbol("ds_trigger_set_stage", &status); + if (status != DSVIEW_BRIDGE_OK) { + dsview_bridge_unload_library(); + return status; + } + g_bridge_api.sr_output_find = (sr_output_find_fn)dsview_bridge_load_symbol("sr_output_find", &status); if (status != DSVIEW_BRIDGE_OK) { @@ -3710,6 +3774,84 @@ int dsview_bridge_ds_set_vth(double value) return g_bridge_api.ds_set_actived_device_config(NULL, NULL, SR_CONF_VTH, data); } +/* + * Native log verbosity. libsigrok4DSL defaults to INFO, which prints a dozen + * lines of scan chatter to stderr on every single command; that is noise in a + * tool meant to be scripted. Levels: 0 none, 1 error, 2 warn, 3 info, 4 debug, + * 5 detail. + */ +int dsview_bridge_ds_log_level(int level) +{ + if (g_bridge_api.ds_log_level == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + g_bridge_api.ds_log_level(level); + return 0; +} + +/* + * Trigger control. + * + * libsigrok4DSL keeps a simple (single-stage) trigger in the slot past the + * staged-trigger array, which is what ds_trigger_probe_set() writes to. Values + * are the uppercase characters the library matches on: 'X' don't care, '0'/'1' + * level, 'R' rising, 'F' falling, 'C' either edge. + */ +int dsview_bridge_ds_trigger_reset(void) +{ + if (g_bridge_api.ds_trigger_reset == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_reset(); +} + +int dsview_bridge_ds_trigger_probe_set(uint16_t probe, unsigned char trigger0, unsigned char trigger1) +{ + if (g_bridge_api.ds_trigger_probe_set == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_probe_set(probe, trigger0, trigger1); +} + +int dsview_bridge_ds_trigger_set_en(uint16_t enable) +{ + if (g_bridge_api.ds_trigger_set_en == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_set_en(enable); +} + +int dsview_bridge_ds_trigger_set_mode(uint16_t mode) +{ + if (g_bridge_api.ds_trigger_set_mode == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_set_mode(mode); +} + +int dsview_bridge_ds_trigger_set_pos(uint16_t position) +{ + if (g_bridge_api.ds_trigger_set_pos == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_set_pos(position); +} + +int dsview_bridge_ds_trigger_set_stage(uint16_t stages) +{ + if (g_bridge_api.ds_trigger_set_stage == NULL) { + return DSVIEW_BRIDGE_ERR_NOT_LOADED; + } + + return g_bridge_api.ds_trigger_set_stage(stages); +} + int dsview_bridge_ds_set_filter(int value) { return dsview_bridge_set_int16_config(SR_CONF_FILTER, value); diff --git a/crates/dsview-sys/src/lib.rs b/crates/dsview-sys/src/lib.rs index 959c9af..5c64cef 100644 --- a/crates/dsview-sys/src/lib.rs +++ b/crates/dsview-sys/src/lib.rs @@ -93,6 +93,13 @@ unsafe extern "C" { fn dsview_bridge_ds_set_samplerate(value: u64) -> c_int; fn dsview_bridge_ds_set_sample_limit(value: u64) -> c_int; fn dsview_bridge_ds_enable_channel(channel_index: c_int, enable: c_int) -> c_int; + fn dsview_bridge_ds_log_level(level: c_int) -> c_int; + fn dsview_bridge_ds_trigger_reset() -> c_int; + fn dsview_bridge_ds_trigger_probe_set(probe: u16, trigger0: u8, trigger1: u8) -> c_int; + fn dsview_bridge_ds_trigger_set_en(enable: u16) -> c_int; + fn dsview_bridge_ds_trigger_set_mode(mode: u16) -> c_int; + fn dsview_bridge_ds_trigger_set_pos(position: u16) -> c_int; + fn dsview_bridge_ds_trigger_set_stage(stages: u16) -> c_int; fn dsview_bridge_ds_register_acquisition_callbacks() -> c_int; fn dsview_bridge_ds_clear_acquisition_callbacks() -> c_int; fn dsview_bridge_ds_start_collect() -> c_int; @@ -1417,6 +1424,58 @@ impl RuntimeBridge { }) } + /// Set native library log verbosity: 0 none, 1 error, 2 warn, 3 info, + /// 4 debug, 5 detail. The library defaults to info, which is far too + /// chatty for a tool whose output is meant to be parsed. + pub fn set_log_level(&self, level: i32) -> Result<(), RuntimeError> { + native_call_status("ds_log_level", unsafe { + dsview_bridge_ds_log_level(level as c_int) + }) + } + + /// Clear any previously configured trigger back to "don't care" on every + /// probe. Always call this before programming a new trigger: the library + /// keeps trigger state in a global, so a stale probe from an earlier run + /// would otherwise still be armed. + pub fn trigger_reset(&self) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_reset", unsafe { dsview_bridge_ds_trigger_reset() }) + } + + /// Set the simple-trigger condition for one probe. `trigger0`/`trigger1` + /// are the uppercase characters libsigrok4DSL matches on: `X` don't care, + /// `0`/`1` level, `R` rising, `F` falling, `C` either edge. + pub fn trigger_probe_set(&self, probe: u16, trigger0: u8, trigger1: u8) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_probe_set", unsafe { + dsview_bridge_ds_trigger_probe_set(probe, trigger0, trigger1) + }) + } + + pub fn trigger_set_enabled(&self, enable: bool) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_set_en", unsafe { + dsview_bridge_ds_trigger_set_en(u16::from(enable)) + }) + } + + pub fn trigger_set_mode(&self, mode: u16) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_set_mode", unsafe { + dsview_bridge_ds_trigger_set_mode(mode) + }) + } + + /// Trigger position as a percentage of the capture buffer (0-100): how + /// much of the capture is kept from *before* the trigger fires. + pub fn trigger_set_position(&self, position: u16) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_set_pos", unsafe { + dsview_bridge_ds_trigger_set_pos(position) + }) + } + + pub fn trigger_set_stage(&self, stages: u16) -> Result<(), RuntimeError> { + native_call_status("ds_trigger_set_stage", unsafe { + dsview_bridge_ds_trigger_set_stage(stages) + }) + } + pub fn set_stop_option(&self, value: i16) -> Result<(), RuntimeError> { native_call_status("ds_set_stop_option", unsafe { dsview_bridge_ds_set_stop_option(value as c_int) diff --git a/patches/dsview/0002-add-dslogic-plus-u2basic-pgl12-2-profiles.patch b/patches/dsview/0002-add-dslogic-plus-u2basic-pgl12-2-profiles.patch new file mode 100644 index 0000000..a1b8e88 --- /dev/null +++ b/patches/dsview/0002-add-dslogic-plus-u2basic-pgl12-2-profiles.patch @@ -0,0 +1,66 @@ +diff --git a/libsigrok4DSL/hardware/DSL/dsl.h b/libsigrok4DSL/hardware/DSL/dsl.h +index cfb5355..b06dab6 100644 +--- a/libsigrok4DSL/hardware/DSL/dsl.h ++++ b/libsigrok4DSL/hardware/DSL/dsl.h +@@ -775,6 +775,61 @@ static const struct DSL_profile supported_DSLogic[] = { + SR_MHZ(400)} + }, + ++ {0x2A0E, 0x0034, LIBUSB_SPEED_HIGH, "DreamSourceLab", "DSLogic PLus", NULL, ++ "DSLogicPlus-pgl12-2.fw", ++ "DSLogicPlus-pgl12-2.bin", ++ "DSLogicPlus-pgl12-2.bin", ++ {CAPS_MODE_LOGIC, ++ CAPS_FEATURE_VTH | CAPS_FEATURE_BUF | CAPS_FEATURE_MAX25_VTH | CAPS_FEATURE_SECURITY, ++ (1 << DSL_STREAM20x16) | (1 << DSL_STREAM25x12) | (1 << DSL_STREAM50x6) | (1 << DSL_STREAM100x3) | ++ (1 << DSL_BUFFER100x16) | (1 << DSL_BUFFER200x8) | (1 << DSL_BUFFER400x4), ++ 16, ++ SR_MB(256), ++ 0, ++ DSL_BUFFER100x16, ++ 0, ++ samplerates400, ++ 0, ++ DSL_STREAM20x16, ++ SR_MHZ(1), ++ SR_Mn(1), ++ 0, ++ 0, ++ 0, ++ 0, ++ 0, ++ SR_MHZ(200), ++ SR_MHZ(400)} ++ }, ++ ++ {0x2A0E, 0x0035, LIBUSB_SPEED_HIGH, "DreamSourceLab", "DSLogic U2Basic", NULL, ++ "DSLogicU2Basic-pgl12-2.fw", ++ "DSLogicU2Basic-pgl12-2.bin", ++ "DSLogicU2Basic-pgl12-2.bin", ++ {CAPS_MODE_LOGIC, ++ CAPS_FEATURE_VTH | CAPS_FEATURE_BUF | CAPS_FEATURE_MAX25_VTH | CAPS_FEATURE_SECURITY, ++ (1 << DSL_STREAM20x16) | (1 << DSL_STREAM25x12) | (1 << DSL_STREAM50x6) | (1 << DSL_STREAM100x3) | ++ (1 << DSL_BUFFER100x16), ++ 16, ++ SR_MB(64), ++ 0, ++ DSL_BUFFER100x16, ++ 0, ++ samplerates100, ++ 0, ++ DSL_STREAM20x16, ++ SR_MHZ(1), ++ SR_Mn(1), ++ 0, ++ 0, ++ 0, ++ 0, ++ 0, ++ SR_MHZ(200), ++ SR_MHZ(400)} ++ }, ++ ++ + { 0, 0, LIBUSB_SPEED_UNKNOWN, 0, 0, 0, 0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}} + }; + diff --git a/patches/dsview/0003-security-check-pass-is-not-an-error.patch b/patches/dsview/0003-security-check-pass-is-not-an-error.patch new file mode 100644 index 0000000..91c0dd0 --- /dev/null +++ b/patches/dsview/0003-security-check-pass-is-not-an-error.patch @@ -0,0 +1,13 @@ +diff --git a/libsigrok4DSL/hardware/DSL/dsl.c b/libsigrok4DSL/hardware/DSL/dsl.c +index 38ffde6..4f56ecc 100644 +--- a/libsigrok4DSL/hardware/DSL/dsl.c ++++ b/libsigrok4DSL/hardware/DSL/dsl.c +@@ -1960,7 +1960,7 @@ SR_PRIV int dsl_dev_open(struct sr_dev_driver *di, struct sr_dev_inst *sdi, gboo + if (ret != SR_OK){ + sr_err("Security check failed!"); + } else { +- sr_err("Security check pass!"); ++ sr_info("Security check pass!"); + } + } +