├── .gitignore
├── Cargo.toml
├── .idea
├── .gitignore
├── vcs.xml
├── misc.xml
└── modules.xml
├── .vscode
└── settings.json
├── uhd
├── src
│ ├── transmitter
│ │ ├── mod.rs
│ │ ├── info.rs
│ │ ├── metadata.rs
│ │ └── streamer.rs
│ ├── receiver
│ │ ├── mod.rs
│ │ ├── error.rs
│ │ ├── info.rs
│ │ ├── streamer.rs
│ │ └── metadata.rs
│ ├── lib.rs
│ ├── motherboard_eeprom.rs
│ ├── tune_result.rs
│ ├── tune_request.rs
│ ├── daughter_board_eeprom.rs
│ ├── error.rs
│ ├── utils.rs
│ ├── range.rs
│ ├── stream
│ │ └── mod.rs
│ ├── string_vector.rs
│ └── usrp.rs
├── uhd.iml
├── Cargo.toml
├── CHANGELOG.md
└── examples
│ ├── transmit.rs
│ ├── receive.rs
│ └── probe.rs
├── uhd-sys
├── CHANGELOG.md
├── src
│ └── lib.rs
├── uhd-sys.iml
├── README.md
├── Cargo.toml
└── build.rs
└── uhd-rust.iml
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | Cargo.lock
3 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 |
3 | members = ['uhd-sys', 'uhd']
4 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.inlayHints.enable": false
3 | }
--------------------------------------------------------------------------------
/uhd/src/transmitter/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod info;
2 | pub mod metadata;
3 | pub mod streamer;
4 |
--------------------------------------------------------------------------------
/uhd/src/receiver/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod error;
2 | pub mod info;
3 | pub mod metadata;
4 | pub mod streamer;
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/uhd-sys/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.1.3 - 2024-08-13
2 |
3 | * Updated build script to make uhd-sys compile on Apple Silicon macOS devices
4 |
5 | # 0.1.2 - 2021-03-30
6 |
7 | * Updated build script to make uhd-sys compile on Raspberry Pi devices
8 |
--------------------------------------------------------------------------------
/uhd-sys/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(non_camel_case_types)]
2 |
3 | //!
4 | //! Bindings to UHD
5 | //!
6 | //! UHD repository: [https://github.com/ettusresearch/uhd](https://github.com/ettusresearch/uhd)
7 | //!
8 | //! UHD documentation: [https://files.ettus.com/manual/](https://files.ettus.com/manual/)
9 | //!
10 |
11 | include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/uhd-rust.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/uhd-sys/uhd-sys.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/uhd/uhd.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/uhd/src/receiver/error.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug)]
2 | pub struct ReceiveError {
3 | pub kind: ReceiveErrorKind,
4 | pub message: Option,
5 | }
6 |
7 | impl ReceiveError {
8 | pub fn kind(&self) -> ReceiveErrorKind {
9 | self.kind.clone()
10 | }
11 | pub fn message(&self) -> Option<&str> {
12 | self.message.as_deref()
13 | }
14 | }
15 |
16 | impl std::error::Error for ReceiveError {}
17 |
18 | #[non_exhaustive]
19 | #[derive(Debug, Clone)]
20 | pub enum ReceiveErrorKind {
21 | Timeout,
22 | LateCommand,
23 | BrokenChain,
24 | Overflow,
25 | OutOfSequence,
26 | Alignment,
27 | BadPacket,
28 | Other,
29 | }
30 |
--------------------------------------------------------------------------------
/uhd-sys/README.md:
--------------------------------------------------------------------------------
1 | # Rust UHD bindings
2 |
3 | This crate provides low-level bindings to the UHD (USRP Hardware Driver) library, which provides support for Ettus Research / National Instruments Universal Software Radio Peripheral devices
4 |
5 | ## Requirements
6 |
7 | The UHD library must be installed, and pkg-config must be able to find it.
8 |
9 | ## License
10 |
11 | This crate is released under the MIT and Apache 2.0 licenses. You may redistribute it according to either license.
12 |
13 | The UHD library itself is mostly licensed under GPLv3 (see [https://github.com/EttusResearch/uhd/blob/master/LICENSE.md](https://github.com/EttusResearch/uhd/blob/master/LICENSE.md)). Depending on how you link the UHD library, this may have licensing implications.
14 |
--------------------------------------------------------------------------------
/uhd/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "uhd"
3 | version = "0.3.0"
4 | authors = ["Sam Crow "]
5 | edition = "2018"
6 | description = "Bindings to the UHD (USRP Hardware Driver) library, which provides support for Ettus Research / National Instruments Universal Software Radio Peripheral devices"
7 | repository = "https://github.com/samcrow/uhd-rust"
8 | license = "MIT OR Apache-2.0"
9 | keywords = ["sdr", "usrp", "radio"]
10 | categories = ["hardware-support"]
11 |
12 |
13 | [dependencies]
14 | num-complex = "0.4.0"
15 | libc = "0.2"
16 | thiserror = "1.0.24"
17 | anyhow = "1.0.39"
18 |
19 | [dependencies.uhd-sys]
20 | version = "0.1.3"
21 | path = "../uhd-sys"
22 |
23 | [dev-dependencies]
24 | tap = "1.0.1"
25 | log = "0.4.13"
26 | env_logger = "0.11.0"
27 |
--------------------------------------------------------------------------------
/uhd-sys/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "uhd-sys"
3 | version = "0.1.3"
4 | authors = ["Sam Crow "]
5 | repository = "https://github.com/samcrow/uhd-rust"
6 | edition = "2018"
7 | license = "MIT OR Apache-2.0"
8 | description = "Low-level bindings to the UHD (USRP Hardware Driver) library, which provides support for Ettus Research / National Instruments Universal Software Radio Peripheral devices"
9 | keywords = ["sdr"]
10 | categories = ["external-ffi-bindings", "hardware-support"]
11 | links = "uhd"
12 | build = "build.rs"
13 |
14 | [lib]
15 | # output of UHD C++ documentation from bindgen sometimes has 4 spaces, which rustdoc
16 | # interprets as markdown-style code, which then triggers failures during doctest
17 | doctest = false
18 |
19 | [dependencies]
20 |
21 | [build-dependencies]
22 | metadeps = "1.1.2"
23 | bindgen = "0.55.1"
24 |
25 | [package.metadata.pkg-config]
26 | uhd = "*"
27 |
--------------------------------------------------------------------------------
/uhd/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Unreleased
2 |
3 | ## Added
4 |
5 | * Time-related methods [#14](https://github.com/samcrow/uhd-rust/pull/14)
6 |
7 | # [0.3.0](https://github.com/samcrow/uhd-rust/tree/uhd-v0.3.0) - 2024-05-17
8 |
9 | ## Changed
10 |
11 | * Remove `enumerate_registers` from `Usrp` since the low level register apis have been removed from libuhd 4.0 .
12 | * Update `env-logger` dev-dependency to 0.11
13 |
14 | ## Added
15 |
16 | * Add `Usrp::set_clock_source` [#10](https://github.com/samcrow/uhd-rust/pull/10)
17 |
18 | # [0.2.0](https://github.com/samcrow/uhd-rust/tree/v0.2.0) - 2023-02-09
19 |
20 | * Added support for transmitting with TransmitStreamer
21 | * Remove kind() and message() methods on Error. Error is now implemented with the ThisError
22 | crate internally. Error message strings from UHD can instead be retrieved using
23 | `uhd::last_error_message()`.
24 | * Bump num-complex crate to v0.4
25 | * The following methods on `Ursp` now require a mutable reference:
26 | `clear_command_time`
27 | `get_rx_stream`
28 | `set_rx_agc_enabled`
29 | `set_rx_antenna`
30 | `set_rx_bandwidth`
31 | `set_rx_dc_offset_enabled`
32 | `set_rx_frequency`
33 | `set_rx_gain`
34 | `set_rx_sample_rate`
35 | `set_tx_antenna`
36 | * Use `sc16` over-the-wire format in transmit and receive examples
37 |
38 | # 0.1.1 - 2021-03-30
39 |
40 | * Fixes to compile with the version of UHD in the Raspberry Pi repositories (no public API changes, except panics in some situations)
41 |
42 |
--------------------------------------------------------------------------------
/uhd/src/lib.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! # `uhd`: Bindings to the USRP Hardware Driver library
3 | //!
4 | //! ## Status
5 | //!
6 | //! Basic functionality for configuring some USRP settings and receiving samples is working.
7 | //!
8 | //! Some things are not yet implemented:
9 | //!
10 | //! * Various configuration options related to transmitting
11 | //! * Some configuration options related to receiving and time synchronization
12 | //! * Sending samples to transmit
13 | //!
14 |
15 | extern crate libc;
16 | extern crate num_complex;
17 | extern crate uhd_sys;
18 |
19 | mod daughter_board_eeprom;
20 | mod error;
21 | mod motherboard_eeprom;
22 | pub mod range;
23 | mod receiver;
24 | mod stream;
25 | mod string_vector;
26 | mod transmitter;
27 | mod tune_request;
28 | mod tune_result;
29 | mod usrp;
30 | mod utils;
31 |
32 | // Re-export many public items at the root
33 | pub use daughter_board_eeprom::DaughterBoardEeprom;
34 | pub use error::*;
35 | pub use motherboard_eeprom::MotherboardEeprom;
36 | pub use receiver::{info::ReceiveInfo, metadata::*, streamer::ReceiveStreamer};
37 | pub use stream::*;
38 | pub use transmitter::{info::TransmitInfo, metadata::*, streamer::TransmitStreamer};
39 | pub use tune_request::*;
40 | pub use tune_result::TuneResult;
41 | pub use usrp::Usrp;
42 | pub use utils::alloc_boxed_slice;
43 | // Common definitions
44 |
45 | /// A time value, represented as an integer number of seconds and a floating-point fraction of
46 | /// a second
47 | #[derive(Debug, Clone, Default, PartialOrd, PartialEq)]
48 | pub struct TimeSpec {
49 | // In some versions of UHD, the corresponding field of uhd::time_spec_t is a time_t.
50 | // In other versions, it's a int64_t. The Rust code does conversion to keep this
51 | // an i64.
52 | pub seconds: i64,
53 | pub fraction: f64,
54 | }
55 |
--------------------------------------------------------------------------------
/uhd/examples/transmit.rs:
--------------------------------------------------------------------------------
1 | use core::f32::consts;
2 | use std::env::set_var;
3 |
4 | use anyhow::{Context, Result};
5 | use num_complex::{Complex, Complex32};
6 | use tap::Pipe;
7 | use uhd::{self, TuneRequest, Usrp};
8 |
9 | const CHANNEL: usize = 0;
10 | const NUM_SAMPLES: usize = 1_000_000;
11 |
12 | pub fn main() -> Result<()> {
13 | set_var("RUST_LOG", "DEBUG");
14 | env_logger::init();
15 |
16 | log::info!("Starting transmit test");
17 |
18 | let mut usrp = Usrp::find("")
19 | .context("Failed to open device list")?
20 | .drain(..)
21 | .next()
22 | .context("Failed to find a valid USRP to attach to")?
23 | .pipe(|addr| Usrp::open(&addr))
24 | .context("Failed to find properly open the USRP")?;
25 |
26 | // Set properties
27 | usrp.set_tx_sample_rate(1e6, CHANNEL)?;
28 | usrp.set_tx_gain(77.5, CHANNEL, "PGA")?; // -10dB gain
29 | usrp.set_tx_frequency(&TuneRequest::with_frequency(2.404e9), CHANNEL)?;
30 |
31 | // Check properties
32 | log::info!("Tx gain {}", usrp.get_tx_gain(CHANNEL, "PGA")?);
33 | log::info!("Tx freq {}", usrp.get_tx_frequency(CHANNEL)?);
34 |
35 | // Get TransmitStreamer
36 | let mut transmitter = usrp
37 | .get_tx_stream(&uhd::StreamArgs::>::new("sc16"))
38 | .unwrap();
39 |
40 | // Generate a sine wave at Fs/4
41 | let mut single_chan = uhd::alloc_boxed_slice::, NUM_SAMPLES>();
42 | for i in 0..NUM_SAMPLES {
43 | let t = i as f32 / 4.;
44 | // z = e^j*2π*theta
45 | let z = (Complex32::i() * 2. * consts::PI * t).expf(consts::E);
46 | single_chan[i] = Complex::new((8192. * z.re) as i16, (8192. * z.im) as i16);
47 | }
48 |
49 | // Transmit
50 | log::info!("Transmitting..");
51 | let stat = transmitter.transmit_simple(single_chan.as_mut())?;
52 | log::info!("{:?}", stat);
53 |
54 | Ok(())
55 | }
56 |
--------------------------------------------------------------------------------
/uhd-sys/build.rs:
--------------------------------------------------------------------------------
1 | extern crate bindgen;
2 | extern crate metadeps;
3 |
4 | use std::env;
5 | use std::path::{Path, PathBuf};
6 |
7 | fn main() {
8 | // This reads the metadata in Cargo.toml and sends Cargo the appropriate output to link the
9 | // libraries
10 | let libraries = metadeps::probe().unwrap();
11 |
12 | let uhd_include_path = libraries
13 | .get("uhd")
14 | .expect("uhd library not in map")
15 | .include_paths
16 | .first()
17 | .expect("no include path for UHD headers");
18 | generate_bindings(uhd_include_path);
19 | }
20 |
21 | fn generate_bindings(include_path: &Path) {
22 | let usrp_header = include_path.join("uhd.h");
23 |
24 | let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
25 | let out_path = out_dir.join("bindgen.rs");
26 |
27 | let mut builder = bindgen::builder()
28 | .whitelist_function("^uhd.+")
29 | .default_enum_style(bindgen::EnumVariation::ModuleConsts)
30 | .header(usrp_header.to_string_lossy().clone())
31 | // Add the include directory to ensure that #includes in the header work correctly
32 | .clang_arg(format!("-I{}", include_path.to_string_lossy().clone()));
33 |
34 | // On Raspberry Pi devices, the include directories require some adjustment.
35 | let target = env::var("TARGET").expect("No TARGET environment variable");
36 | if target == "armv7-unknown-linux-gnueabihf" {
37 | builder = builder.clang_arg("-I/usr/lib/gcc/arm-linux-gnueabihf/8/include");
38 | } else if target == "aarch64-apple-darwin" {
39 | // On macOS Apple Silicon, boost libs from `brew install boost` are at this path
40 | println!("cargo:rustc-link-search=/opt/homebrew/lib/");
41 | }
42 |
43 | let bindings = builder.generate().expect("Failed to generate bindings");
44 | bindings
45 | .write_to_file(out_path)
46 | .expect("Failed to write bindings to file");
47 | }
48 |
--------------------------------------------------------------------------------
/uhd/examples/receive.rs:
--------------------------------------------------------------------------------
1 | use std::env::set_var;
2 |
3 | use anyhow::{Context, Result};
4 | use num_complex::Complex;
5 | use tap::Pipe;
6 | use uhd::{self, StreamCommand, StreamCommandType, StreamTime, TuneRequest, Usrp};
7 |
8 | const CHANNEL: usize = 0;
9 | const NUM_SAMPLES: usize = 1000;
10 |
11 | pub fn main() -> Result<()> {
12 | set_var("RUST_LOG", "DEBUG");
13 | env_logger::init();
14 |
15 | log::info!("Starting receive test");
16 |
17 | let mut usrp = Usrp::find("")
18 | .context("Failed to open device list")?
19 | .drain(..)
20 | .next()
21 | .context("Failed to find a valid USRP to attach to")?
22 | .pipe(|addr| Usrp::open(&addr))
23 | .context("Failed to find properly open the USRP")?;
24 |
25 | let _ = usrp.set_clock_source("external", 0);
26 | let clock_source = usrp.get_clock_source(0).unwrap();
27 | println!("Clock source: {:?}", clock_source);
28 | assert_eq!(clock_source, "external");
29 | let _ = usrp.set_clock_source("internal", 0);
30 | let clock_source = usrp.get_clock_source(0).unwrap();
31 | println!("Clock source: {:?}", clock_source);
32 | assert_eq!(clock_source, "internal");
33 |
34 | usrp.set_rx_sample_rate(1e6, CHANNEL)?;
35 | usrp.set_rx_antenna("TX/RX", CHANNEL)?;
36 | usrp.set_rx_frequency(&TuneRequest::with_frequency(2.4e9), CHANNEL)?;
37 |
38 | let mut receiver = usrp
39 | .get_rx_stream(&uhd::StreamArgs::>::new("sc16"))
40 | .unwrap();
41 |
42 | let mut buffer = uhd::alloc_boxed_slice::, NUM_SAMPLES>();
43 |
44 | receiver.send_command(&StreamCommand {
45 | command_type: StreamCommandType::CountAndDone(buffer.len() as u64),
46 | time: StreamTime::Now,
47 | })?;
48 | let status = receiver.receive_simple(buffer.as_mut())?;
49 |
50 | log::info!("{:?}", status);
51 | log::info!("{:?}", &buffer[..16]);
52 |
53 | Ok(())
54 | }
55 |
--------------------------------------------------------------------------------
/uhd/src/receiver/info.rs:
--------------------------------------------------------------------------------
1 | use std::ffi::CStr;
2 | use std::str::Utf8Error;
3 |
4 | /// Information about a receive channel
5 | #[derive(Debug, Clone)]
6 | pub struct ReceiveInfo {
7 | motherboard_id: String,
8 | motherboard_name: String,
9 | motherboard_serial: String,
10 | daughterboard_id: String,
11 | daughterboard_serial: String,
12 | subdev_name: String,
13 | subdev_spec: String,
14 | antenna: String,
15 | }
16 |
17 | impl ReceiveInfo {
18 | pub fn motherboard_id(&self) -> &str {
19 | &self.motherboard_id
20 | }
21 | pub fn motherboard_name(&self) -> &str {
22 | &self.motherboard_name
23 | }
24 | pub fn motherboard_serial(&self) -> &str {
25 | &self.motherboard_serial
26 | }
27 | pub fn daughterboard_id(&self) -> &str {
28 | &self.daughterboard_id
29 | }
30 | pub fn daughterboard_serial(&self) -> &str {
31 | &self.daughterboard_serial
32 | }
33 | pub fn subdev_name(&self) -> &str {
34 | &self.subdev_name
35 | }
36 | pub fn subdev_spec(&self) -> &str {
37 | &self.subdev_spec
38 | }
39 | pub fn antenna(&self) -> &str {
40 | &self.antenna
41 | }
42 |
43 | pub(crate) unsafe fn from_c(info_c: &uhd_sys::uhd_usrp_rx_info_t) -> Result {
44 | Ok(ReceiveInfo {
45 | motherboard_id: CStr::from_ptr(info_c.mboard_id).to_str()?.into(),
46 | motherboard_name: CStr::from_ptr(info_c.mboard_name).to_str()?.into(),
47 | motherboard_serial: CStr::from_ptr(info_c.mboard_serial).to_str()?.into(),
48 | daughterboard_id: CStr::from_ptr(info_c.rx_id).to_str()?.into(),
49 | daughterboard_serial: CStr::from_ptr(info_c.rx_serial).to_str()?.into(),
50 | subdev_name: CStr::from_ptr(info_c.rx_subdev_name).to_str()?.into(),
51 | subdev_spec: CStr::from_ptr(info_c.rx_subdev_spec).to_str()?.into(),
52 | antenna: CStr::from_ptr(info_c.rx_antenna).to_str()?.into(),
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/uhd/src/transmitter/info.rs:
--------------------------------------------------------------------------------
1 | use std::ffi::CStr;
2 | use std::str::Utf8Error;
3 |
4 | /// Information about a transmit channel
5 | #[derive(Debug, Clone)]
6 | pub struct TransmitInfo {
7 | motherboard_id: String,
8 | motherboard_name: String,
9 | motherboard_serial: String,
10 | daughterboard_id: String,
11 | daughterboard_serial: String,
12 | subdev_name: String,
13 | subdev_spec: String,
14 | antenna: String,
15 | }
16 |
17 | impl TransmitInfo {
18 | pub fn motherboard_id(&self) -> &str {
19 | &self.motherboard_id
20 | }
21 | pub fn motherboard_name(&self) -> &str {
22 | &self.motherboard_name
23 | }
24 | pub fn motherboard_serial(&self) -> &str {
25 | &self.motherboard_serial
26 | }
27 | pub fn daughterboard_id(&self) -> &str {
28 | &self.daughterboard_id
29 | }
30 | pub fn daughterboard_serial(&self) -> &str {
31 | &self.daughterboard_serial
32 | }
33 | pub fn subdev_name(&self) -> &str {
34 | &self.subdev_name
35 | }
36 | pub fn subdev_spec(&self) -> &str {
37 | &self.subdev_spec
38 | }
39 | pub fn antenna(&self) -> &str {
40 | &self.antenna
41 | }
42 |
43 | pub(crate) unsafe fn from_c(info_c: &uhd_sys::uhd_usrp_tx_info_t) -> Result {
44 | Ok(TransmitInfo {
45 | motherboard_id: CStr::from_ptr(info_c.mboard_id).to_str()?.into(),
46 | motherboard_name: CStr::from_ptr(info_c.mboard_name).to_str()?.into(),
47 | motherboard_serial: CStr::from_ptr(info_c.mboard_serial).to_str()?.into(),
48 | daughterboard_id: CStr::from_ptr(info_c.tx_id).to_str()?.into(),
49 | daughterboard_serial: CStr::from_ptr(info_c.tx_serial).to_str()?.into(),
50 | subdev_name: CStr::from_ptr(info_c.tx_subdev_name).to_str()?.into(),
51 | subdev_spec: CStr::from_ptr(info_c.tx_subdev_spec).to_str()?.into(),
52 | antenna: CStr::from_ptr(info_c.tx_antenna).to_str()?.into(),
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/uhd/src/motherboard_eeprom.rs:
--------------------------------------------------------------------------------
1 | use crate::error::{check_status, Error};
2 | use crate::utils::copy_string;
3 | use std::ffi::CString;
4 | use std::ptr;
5 |
6 | /// Information stored in the USRP motherboard EEPROM
7 | pub struct MotherboardEeprom(uhd_sys::uhd_mboard_eeprom_handle);
8 |
9 | impl MotherboardEeprom {
10 | pub fn get(&self, key: &str) -> Result