├── mavlink ├── rustfmt.toml ├── tests │ ├── log.tlog │ ├── target_fields_tests.rs │ ├── helper_tests.rs │ ├── direct_serial_tests.rs │ ├── parse_test.rs │ ├── process_log_files.rs │ ├── random_test.rs │ ├── invalid_value_test.rs │ ├── udp_loopback_tests.rs │ ├── signing.rs │ ├── udp_loopback_async_tests.rs │ ├── mav_frame_tests.rs │ ├── file_connection_tests.rs │ ├── tcp_loopback_tests.rs │ ├── tcp_loopback_async_tests.rs │ ├── agnostic_decode_test.rs │ └── v1_encode_decode_tests.rs ├── examples │ ├── embedded │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── memory.x │ │ ├── README.md │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── embedded-async-read │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── README.md │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── mavlink-dump │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── build │ └── main.rs ├── src │ └── lib.rs └── Cargo.toml ├── .gitmodules ├── mavlink-bindgen ├── tests │ ├── definitions │ │ ├── no_field_description.xml │ │ ├── heartbeat.xml │ │ ├── mav_bool.xml │ │ ├── deprecated.xml │ │ └── parameters.xml │ ├── snapshots │ │ ├── e2e_snapshots__heartbeat.xml@mod.rs.snap │ │ ├── e2e_snapshots__parameters.xml@mod.rs.snap │ │ ├── e2e_snapshots__mav_bool.xml@mod.rs.snap │ │ ├── e2e_snapshots__deprecated.xml@mod.rs.snap │ │ ├── e2e_snapshots__no_field_description.xml@mod.rs.snap │ │ ├── e2e_snapshots__no_field_description.xml@no_field_description.rs.snap │ │ └── e2e_snapshots__heartbeat.xml@heartbeat.rs.snap │ └── e2e_snapshots.rs ├── src │ ├── main.rs │ ├── util.rs │ ├── binder.rs │ ├── cli.rs │ └── error.rs └── Cargo.toml ├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── Cargo.toml ├── mavlink-core ├── src │ ├── connection │ │ ├── file │ │ │ └── config.rs │ │ ├── tcp │ │ │ └── config.rs │ │ ├── udp │ │ │ └── config.rs │ │ ├── direct_serial │ │ │ └── config.rs │ │ ├── file.rs │ │ ├── tcp.rs │ │ └── direct_serial.rs │ ├── embedded.rs │ ├── utils.rs │ ├── error.rs │ ├── async_connection │ │ ├── file.rs │ │ ├── mod.rs │ │ ├── direct_serial.rs │ │ └── tcp.rs │ ├── connectable.rs │ ├── types.rs │ ├── bytes.rs │ ├── signing.rs │ └── bytes_mut.rs └── Cargo.toml ├── LICENSE-MIT └── README.md /mavlink/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" -------------------------------------------------------------------------------- /mavlink/tests/log.tlog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mavlink/rust-mavlink/HEAD/mavlink/tests/log.tlog -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mavlink/mavlink"] 2 | path = mavlink/mavlink 3 | url = https://github.com/mavlink/mavlink 4 | -------------------------------------------------------------------------------- /mavlink/examples/embedded/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.thumbv7em-none-eabihf] 2 | rustflags = [ 3 | "-C", "link-arg=-Tlink.x", 4 | ] 5 | 6 | [build] 7 | target = "thumbv7em-none-eabihf" 8 | -------------------------------------------------------------------------------- /mavlink/examples/embedded-async-read/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.thumbv7em-none-eabihf] 2 | rustflags = [ 3 | "-C", "link-arg=-Tlink.x", 4 | ] 5 | 6 | [build] 7 | target = "thumbv7em-none-eabihf" 8 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/definitions/no_field_description.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Raw RC Data 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | ignore: 8 | - dependency-name: "embedded-hal" 9 | versions: ["^0.2"] 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /mavlink/examples/embedded/memory.x: -------------------------------------------------------------------------------- 1 | /* Linker script for the STM32F303RE */ 2 | MEMORY 3 | { 4 | CCMRAM : ORIGIN = 0x10000000, LENGTH = 16K 5 | FLASH : ORIGIN = 0x08000000, LENGTH = 512K 6 | RAM : ORIGIN = 0x20000000, LENGTH = 64K 7 | } 8 | 9 | _stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM); 10 | _stack_end = ORIGIN(CCMRAM); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Editor 2 | *.swp 3 | *.swo 4 | Session.vim 5 | .cproject 6 | .idea 7 | *.iml 8 | .vscode 9 | .project 10 | .vim/ 11 | .helix/ 12 | .zed/ 13 | .favorites.json 14 | .settings/ 15 | .vs/ 16 | .dir-locals.el 17 | 18 | ## Rust 19 | target 20 | Cargo.lock 21 | 22 | 23 | # Typescript message generation 24 | mavlink/bindings 25 | -------------------------------------------------------------------------------- /mavlink/examples/embedded/README.md: -------------------------------------------------------------------------------- 1 | # rust-MAVLink Embedded example 2 | ### How to run: 3 | - Install cargo flash: 4 | - cargo install cargo-flash 5 | - Install target 6 | - rustup target add thumbv7em-none-eabihf 7 | - Check if we can build the project 8 | - cargo build 9 | - Connect your STM32f303Xe board 10 | - Flash it! 11 | - cargo flash --chip stm32f303RETx --release --log info 12 | -------------------------------------------------------------------------------- /mavlink/examples/embedded-async-read/README.md: -------------------------------------------------------------------------------- 1 | # rust-MAVLink Embedded async example (with reading loop) 2 | ### How to run: 3 | - Install cargo flash: 4 | - cargo install cargo-flash 5 | - Install target 6 | - rustup target add thumbv7em-none-eabihf 7 | - Check if we can build the project 8 | - cargo build 9 | - Connect your STM32f446re board 10 | - Flash it! 11 | - cargo flash --chip stm32f446RETx --release --log info 12 | -------------------------------------------------------------------------------- /mavlink/examples/mavlink-dump/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mavlink-dump" 3 | authors = [ 4 | "Patrick José Pereira ", 5 | ] 6 | license = "MIT/Apache-2.0" 7 | edition = "2018" 8 | version = "0.1.0" 9 | 10 | [profile.release] 11 | opt-level = 3 12 | lto = true # Performs "fat" LTO which attempts to perform optimizations across all crates within the dependency graph 13 | 14 | [dependencies.mavlink] # MAVLink library 15 | path = "../../" 16 | -------------------------------------------------------------------------------- /mavlink-bindgen/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "256"] 2 | 3 | use std::process::ExitCode; 4 | 5 | #[cfg(feature = "cli")] 6 | mod cli; 7 | 8 | fn main() -> ExitCode { 9 | #[cfg(feature = "cli")] 10 | if let Err(e) = cli::main() { 11 | eprintln!("{e}"); 12 | return ExitCode::FAILURE; 13 | } 14 | 15 | #[cfg(not(feature = "cli"))] 16 | panic!("Compiled without cli feature"); 17 | 18 | #[cfg(feature = "cli")] 19 | ExitCode::SUCCESS 20 | } 21 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@mod.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | expression: contents 4 | --- 5 | #[allow(non_camel_case_types)] 6 | #[allow(clippy::derive_partial_eq_without_eq)] 7 | #[allow(clippy::field_reassign_with_default)] 8 | #[allow(non_snake_case)] 9 | #[allow(clippy::unnecessary_cast)] 10 | #[allow(clippy::bad_bit_mask)] 11 | #[allow(clippy::suspicious_else_formatting)] 12 | #[cfg(feature = "heartbeat")] 13 | pub mod heartbeat; 14 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__parameters.xml@mod.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | expression: contents 4 | --- 5 | #[allow(non_camel_case_types)] 6 | #[allow(clippy::derive_partial_eq_without_eq)] 7 | #[allow(clippy::field_reassign_with_default)] 8 | #[allow(non_snake_case)] 9 | #[allow(clippy::unnecessary_cast)] 10 | #[allow(clippy::bad_bit_mask)] 11 | #[allow(clippy::suspicious_else_formatting)] 12 | #[cfg(feature = "parameters")] 13 | pub mod parameters; 14 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__mav_bool.xml@mod.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | assertion_line: 26 4 | expression: contents 5 | --- 6 | #[allow(non_camel_case_types)] 7 | #[allow(clippy::derive_partial_eq_without_eq)] 8 | #[allow(clippy::field_reassign_with_default)] 9 | #[allow(non_snake_case)] 10 | #[allow(clippy::unnecessary_cast)] 11 | #[allow(clippy::bad_bit_mask)] 12 | #[allow(clippy::suspicious_else_formatting)] 13 | #[cfg(feature = "mav_bool")] 14 | pub mod mav_bool; 15 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__deprecated.xml@mod.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | assertion_line: 26 4 | expression: contents 5 | --- 6 | #[allow(non_camel_case_types)] 7 | #[allow(clippy::derive_partial_eq_without_eq)] 8 | #[allow(clippy::field_reassign_with_default)] 9 | #[allow(non_snake_case)] 10 | #[allow(clippy::unnecessary_cast)] 11 | #[allow(clippy::bad_bit_mask)] 12 | #[allow(clippy::suspicious_else_formatting)] 13 | #[cfg(feature = "deprecated")] 14 | pub mod deprecated; 15 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@mod.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | assertion_line: 26 4 | expression: contents 5 | --- 6 | #[allow(non_camel_case_types)] 7 | #[allow(clippy::derive_partial_eq_without_eq)] 8 | #[allow(clippy::field_reassign_with_default)] 9 | #[allow(non_snake_case)] 10 | #[allow(clippy::unnecessary_cast)] 11 | #[allow(clippy::bad_bit_mask)] 12 | #[allow(clippy::suspicious_else_formatting)] 13 | #[cfg(feature = "no_field_description")] 14 | pub mod no_field_description; 15 | -------------------------------------------------------------------------------- /mavlink-bindgen/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub fn to_module_name>(file_name: P) -> String { 4 | file_name 5 | .into() 6 | .file_stem() // remove extension 7 | .unwrap() 8 | .to_string_lossy() // convert to string 9 | .to_lowercase() // all lowercase 10 | .replace(|c: char| !c.is_alphanumeric(), "_") // remove non alphanum 11 | } 12 | 13 | pub fn to_dialect_name>(file_name: P) -> String { 14 | file_name 15 | .into() 16 | .file_stem() // remove extension 17 | .unwrap() 18 | .to_string_lossy() 19 | .to_string() 20 | } 21 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/definitions/heartbeat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 3 | 130 4 | 5 | 6 | Custom mode 7 | Type 8 | Autopilot 9 | Base mode 10 | System status 11 | Mavlink version 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /mavlink/tests/target_fields_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(feature = "common")] 4 | mod target_fields_tests { 5 | use mavlink::{common::MavMessage, Message}; 6 | 7 | #[test] 8 | fn test_target_ids_present() { 9 | let data = crate::test_shared::get_cmd_nav_takeoff_msg(); 10 | let msg = MavMessage::COMMAND_INT(data); 11 | 12 | assert_eq!(msg.target_system_id(), Some(42)); 13 | assert_eq!(msg.target_component_id(), Some(84)); 14 | } 15 | 16 | #[test] 17 | fn test_target_ids_absent() { 18 | let data = crate::test_shared::get_heartbeat_msg(); 19 | let msg = MavMessage::HEARTBEAT(data); 20 | 21 | assert_eq!(msg.target_system_id(), None); 22 | assert_eq!(msg.target_component_id(), None); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /mavlink/tests/helper_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "std", feature = "common"))] 2 | mod helper_tests { 3 | use mavlink::{common::MavMessage, Message}; 4 | 5 | #[test] 6 | fn test_get_default_message_from_id() { 7 | let message_name = "PING"; 8 | let id: Option = MavMessage::message_id_from_name(message_name); 9 | let id = id.unwrap(); 10 | assert!(id == 4, "Invalid id for message name: PING"); 11 | let message = MavMessage::default_message_from_id(id); 12 | #[expect(deprecated)] 13 | if !matches!(message, Some(MavMessage::PING(_))) { 14 | unreachable!("Invalid message type.") 15 | } 16 | assert!( 17 | message.unwrap().message_name() == message_name, 18 | "Message name does not match" 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["mavlink", "mavlink-bindgen", "mavlink-core"] 3 | resolver = "1" 4 | 5 | [workspace.dependencies] 6 | crc-any = { version = "2.3.5", default-features = false } 7 | num-traits = { version = "0.2", default-features = false } 8 | num-derive = "0.4" 9 | bitflags = { version = "2.9.1", default-features = false } 10 | byteorder = { version = "1.3.4", default-features = false } 11 | 12 | [workspace.package] 13 | edition = "2021" 14 | rust-version = "1.80.0" 15 | keywords = ["mavlink", "parser", "protocol", "embedded"] 16 | categories = ["aerospace", "aerospace::protocols", "parsing", "embedded"] 17 | 18 | [workspace.lints.clippy] 19 | cargo = { level = "warn", priority = -1 } 20 | multiple_crate_versions = "allow" 21 | semicolon_if_nothing_returned = "warn" 22 | unnecessary_semicolon = "warn" 23 | uninlined_format_args = "warn" 24 | use_self = "warn" 25 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/definitions/mav_bool.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Enum used to indicate true or false (also: success or failure, enabled or disabled, active or inactive). 6 | 7 | False. 8 | 9 | 10 | True. 11 | 12 | 13 | 14 | 15 | 16 | A message with MAV_BOOL 17 | unsigned 18 | signed 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /mavlink-bindgen/src/binder.rs: -------------------------------------------------------------------------------- 1 | use quote::{format_ident, quote}; 2 | use std::io::Write; 3 | 4 | pub fn generate(modules: Vec<&str>, out: &mut W) { 5 | let modules_tokens = modules.into_iter().map(|module| { 6 | let module_ident = format_ident!("{}", module); 7 | 8 | quote! { 9 | #[allow(non_camel_case_types)] 10 | #[allow(clippy::derive_partial_eq_without_eq)] 11 | #[allow(clippy::field_reassign_with_default)] 12 | #[allow(non_snake_case)] 13 | #[allow(clippy::unnecessary_cast)] 14 | #[allow(clippy::bad_bit_mask)] 15 | #[allow(clippy::suspicious_else_formatting)] 16 | #[cfg(feature = #module)] 17 | pub mod #module_ident; 18 | } 19 | }); 20 | 21 | let tokens = quote! { 22 | #(#modules_tokens)* 23 | }; 24 | 25 | writeln!(out, "{tokens}").unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /mavlink/examples/embedded/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mavlink-embedded" 3 | edition = "2021" 4 | authors = [ 5 | "Patrick José Pereira ", 6 | ] 7 | version = "0.1.0" 8 | 9 | [profile.release] 10 | opt-level = 'z' # Optimize for binary size, but also turn off loop vectorization. 11 | lto = true # Performs "fat" LTO which attempts to perform optimizations across all crates within the dependency graph 12 | 13 | [dependencies] 14 | cortex-m = "0.7" # Low level access to Cortex-M processors 15 | cortex-m-rt = "0.7" # Startup code and minimal runtime for Cortex-M microcontrollers 16 | panic-halt = "0.2" # Panic handler 17 | stm32f3xx-hal = { version = "0.9", features = ["stm32f303xe"] } # HAL for stm32f303xe 18 | 19 | [dependencies.mavlink] # MAVLink library (wait for 0.9.0 version) 20 | path = "../../" 21 | features = ["embedded-hal-02", "common"] 22 | default-features = false 23 | 24 | [workspace] 25 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/file/config.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | use std::path::PathBuf; 3 | 4 | /// MAVLink connection address for a file input 5 | /// 6 | /// # Example 7 | /// 8 | /// ```ignore 9 | /// use mavlink::{Connectable, FileConfig}; 10 | /// use std::path::PathBuf; 11 | /// 12 | /// let config = FileConfig::new(PathBuf::from("/some/path")); 13 | /// config 14 | /// .connect::() 15 | /// .unwrap(); 16 | /// ``` 17 | #[derive(Debug, Clone)] 18 | pub struct FileConfig { 19 | pub(crate) address: PathBuf, 20 | } 21 | 22 | impl FileConfig { 23 | /// Creates a file input address from a file path string. 24 | pub fn new(address: PathBuf) -> Self { 25 | Self { address } 26 | } 27 | } 28 | impl Display for FileConfig { 29 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 30 | write!(f, "file:{}", self.address.display()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mavlink/tests/direct_serial_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "std", feature = "direct-serial", feature = "common"))] 2 | mod test_direct_serial { 3 | use mavlink::common::MavMessage; 4 | 5 | #[test] 6 | pub fn test_incomplete_address() { 7 | let conn_result = mavlink::connect::("serial:"); 8 | assert!(conn_result.is_err(), "Incomplete address should error"); 9 | } 10 | 11 | #[test] 12 | pub fn test_bogus_baud() { 13 | let conn_result = mavlink::connect::("serial:port1:badbaud"); 14 | assert!(conn_result.is_err(), "Invalid baud should error"); 15 | } 16 | 17 | #[test] 18 | pub fn test_nonexistent_port() { 19 | let bogus_port_str = "serial:8d73ba8c-eb87-4105-8d0c-2931940e13be:57600"; 20 | let conn_result = mavlink::connect::(bogus_port_str); 21 | assert!(conn_result.is_err(), "Invalid port should error"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 3D Robotics 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /mavlink-bindgen/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use mavlink_bindgen::{ 5 | emit_cargo_build_messages, format_generated_code, generate, BindGenError, XmlDefinitions, 6 | }; 7 | 8 | #[derive(Parser)] 9 | /// Generate Rust bindings from MAVLink message dialect XML files. 10 | struct Cli { 11 | /// Path to the directory containing the MAVLink dialect definitions. 12 | definitions_dir: PathBuf, 13 | /// Path to the directory where the code is generated into, must already exist. 14 | destination_dir: PathBuf, 15 | /// format code generated code, requires rustfmt to be installed 16 | #[arg(long)] 17 | format_generated_code: bool, 18 | /// prints cargo build messages indicating when the code has to be rebuild 19 | #[arg(long)] 20 | emit_cargo_build_messages: bool, 21 | } 22 | 23 | pub fn main() -> Result<(), BindGenError> { 24 | let args = Cli::parse(); 25 | let result = generate( 26 | XmlDefinitions::Directory(args.definitions_dir), 27 | args.destination_dir, 28 | )?; 29 | 30 | if args.format_generated_code { 31 | format_generated_code(&result); 32 | } 33 | 34 | if args.emit_cargo_build_messages { 35 | emit_cargo_build_messages(&result); 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/tcp/config.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | /// Type of TCP connection 4 | #[derive(Debug, Clone, Copy)] 5 | pub enum TcpMode { 6 | /// Connection will open a TCP server that binds to the provided address 7 | TcpIn, 8 | /// Connection will connect to the provided TCP server address 9 | TcpOut, 10 | } 11 | 12 | /// MAVLink connection address for a TCP server or client 13 | /// 14 | /// # Example 15 | /// 16 | /// ```ignore 17 | /// use mavlink::{Connectable, TcpConfig, TcpMode}; 18 | /// 19 | /// let config = TcpConfig::new("0.0.0.0:14551".to_owned(), false); 20 | /// config.connect::(); 21 | /// ``` 22 | #[derive(Debug, Clone)] 23 | pub struct TcpConfig { 24 | pub(crate) address: String, 25 | pub(crate) mode: TcpMode, 26 | } 27 | 28 | impl TcpConfig { 29 | /// Creates a TCP connection address. 30 | pub fn new(address: String, mode: TcpMode) -> Self { 31 | Self { address, mode } 32 | } 33 | } 34 | impl Display for TcpConfig { 35 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 36 | match self.mode { 37 | TcpMode::TcpIn => write!(f, "tcpin:{}", self.address), 38 | TcpMode::TcpOut => write!(f, "tcpout:{}", self.address), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mavlink-bindgen/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum BindGenError { 5 | /// Represents a failure to read the MAVLink definitions directory. 6 | #[error("Could not read definitions directory {}: {source}", path.display())] 7 | CouldNotReadDefinitionsDirectory { 8 | source: std::io::Error, 9 | path: std::path::PathBuf, 10 | }, 11 | /// Represents a failure to read a MAVLink definition file. 12 | #[error("Could not read definition file {}: {source}", path.display())] 13 | CouldNotReadDefinitionFile { 14 | source: std::io::Error, 15 | path: std::path::PathBuf, 16 | }, 17 | /// Represents a failure to read a directory entry in the MAVLink definitions directory. 18 | #[error("Could not read MAVLink definitions directory entry {}: {source}", path.display())] 19 | CouldNotReadDirectoryEntryInDefinitionsDirectory { 20 | source: std::io::Error, 21 | path: std::path::PathBuf, 22 | }, 23 | /// Represents a failure to create a Rust file for the generated MAVLink bindings. 24 | #[error("Could not create Rust bindings file {}: {source}", dest_path.display())] 25 | CouldNotCreateRustBindingsFile { 26 | source: std::io::Error, 27 | dest_path: std::path::PathBuf, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/e2e_snapshots.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use insta::{self, assert_snapshot, glob}; 5 | use mavlink_bindgen::{format_generated_code, generate, XmlDefinitions}; 6 | use tempfile::TempDir; 7 | 8 | fn definitions_dir() -> PathBuf { 9 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 10 | .join("tests") 11 | .join("definitions") 12 | } 13 | 14 | fn run_snapshot(def_file: &str) { 15 | let defs = definitions_dir(); 16 | let tmp = TempDir::new().expect("tmp dir"); 17 | let out_dir = tmp.path(); 18 | 19 | let xml = defs.join(def_file); 20 | let result = generate(XmlDefinitions::Files(vec![xml]), out_dir).expect("generate ok"); 21 | 22 | format_generated_code(&result); 23 | 24 | glob!(out_dir, "**/*.rs", |path| { 25 | let contents = fs::read_to_string(path).expect("read generated file"); 26 | assert_snapshot!(def_file, contents); 27 | }); 28 | } 29 | 30 | #[test] 31 | fn snapshot_heartbeat() { 32 | run_snapshot("heartbeat.xml"); 33 | } 34 | 35 | #[test] 36 | fn snapshot_parameters() { 37 | run_snapshot("parameters.xml"); 38 | } 39 | 40 | #[test] 41 | fn snapshot_deprecated() { 42 | run_snapshot("deprecated.xml"); 43 | } 44 | 45 | #[test] 46 | fn snapshot_no_field_description() { 47 | run_snapshot("no_field_description.xml"); 48 | } 49 | 50 | #[test] 51 | fn snapshot_mav_bool() { 52 | run_snapshot("mav_bool.xml"); 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | with: 14 | fetch-depth: 0 15 | ref: ${{ github.event.repository.default_branch }} 16 | - uses: actions-rs/toolchain@v1.0.7 17 | with: 18 | toolchain: stable 19 | override: true 20 | - name: Build 21 | run: cargo build 22 | - name: Extract version from tag 23 | id: get_version 24 | run: echo "::set-output name=version::${GITHUB_REF/refs\/tags\//}" 25 | - name: Commit version changes 26 | run: | 27 | git config --global user.name 'github-actions[bot]' 28 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' 29 | - name: Set and publish workspace crates 30 | env: 31 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO }} 32 | run: | 33 | cargo install cargo-workspaces 34 | cargo workspaces version custom ${{ steps.get_version.outputs.version }} \ 35 | --exact --yes --no-git-tag --no-git-push \ 36 | -m "Commit new release ${{ steps.get_version.outputs.version }}" --force "mavlink-*" 37 | cargo publish -p mavlink-core --no-verify 38 | cargo publish -p mavlink-bindgen --no-verify 39 | cargo publish -p mavlink --no-verify 40 | - name: Push commit 41 | run: | 42 | git push origin ${{ github.event.repository.default_branch }} 43 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/udp/config.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | /// Type of UDP connection 4 | /// 5 | /// # Example 6 | /// 7 | /// ```ignore 8 | /// use mavlink::{Connectable, UdpConfig, UdpMode}; 9 | /// 10 | /// let config = mavlink::UdpConfig::new("0.0.0.0:14552".to_owned(), UdpMode::Udpin); 11 | /// config 12 | /// .connect::() 13 | /// .unwrap(); 14 | /// ``` 15 | #[derive(Debug, Clone, Copy)] 16 | pub enum UdpMode { 17 | /// Server connection waiting for a client connection 18 | Udpin, 19 | /// Client connection connecting to a server 20 | Udpout, 21 | /// Client connection that is allowed to send to broadcast addresses 22 | Udpcast, 23 | } 24 | 25 | /// MAVLink address for a UDP server client or broadcast connection 26 | #[derive(Debug, Clone)] 27 | pub struct UdpConfig { 28 | pub(crate) address: String, 29 | pub(crate) mode: UdpMode, 30 | } 31 | 32 | impl UdpConfig { 33 | /// Creates a UDP connection address. 34 | /// 35 | /// The type of connection depends on the [`UdpMode`] 36 | pub fn new(address: String, mode: UdpMode) -> Self { 37 | Self { address, mode } 38 | } 39 | } 40 | 41 | impl Display for UdpConfig { 42 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 43 | let mode = match self.mode { 44 | UdpMode::Udpin => "udpin", 45 | UdpMode::Udpout => "udpout", 46 | UdpMode::Udpcast => "udpcast", 47 | }; 48 | write!(f, "{mode}:{}", self.address) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/direct_serial/config.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | /// MAVLink address for a serial connection 4 | /// 5 | /// # Example 6 | /// 7 | /// ```ignore 8 | /// use mavlink::{Connectable, SerialConfig}; 9 | /// 10 | /// let config = SerialConfig::new("/dev/ttyTHS1".to_owned(), 115200); 11 | /// config.connect::(); 12 | /// ``` 13 | #[derive(Debug, Clone)] 14 | pub struct SerialConfig { 15 | pub(crate) port_name: String, 16 | pub(crate) baud_rate: u32, 17 | read_buffer_capacity: usize, 18 | } 19 | 20 | impl SerialConfig { 21 | /// Creates a serial connection address with port name and baud rate. 22 | pub fn new(port_name: String, baud_rate: u32) -> Self { 23 | // Calculate a sane default buffer capacity based on the baud rate. 24 | let default_capacity = (baud_rate / 100).clamp(1024, 1024 * 8) as usize; 25 | 26 | Self { 27 | port_name, 28 | baud_rate, 29 | read_buffer_capacity: default_capacity, 30 | } 31 | } 32 | 33 | /// Updates the read buffer capacity. 34 | pub fn with_read_buffer_capacity(mut self, capacity: usize) -> Self { 35 | self.read_buffer_capacity = capacity; 36 | self 37 | } 38 | 39 | /// Returns the configured read buffer capacity. 40 | pub fn buffer_capacity(&self) -> usize { 41 | self.read_buffer_capacity 42 | } 43 | } 44 | 45 | impl Display for SerialConfig { 46 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 47 | write!(f, "serial:{}:{}", self.port_name, self.baud_rate) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /mavlink-bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mavlink-bindgen" 3 | version = "0.16.2" 4 | edition.workspace = true 5 | license = "MIT/Apache-2.0" 6 | description = "Library used by rust-mavlink." 7 | readme = "README.md" 8 | repository = "https://github.com/mavlink/rust-mavlink" 9 | rust-version.workspace = true 10 | keywords = ['mavlink', 'cli', "generator", "parser", "xml"] 11 | categories.workspace = true 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | anstyle = { version = "1.0.11", optional = true } 17 | anstyle-parse = { version = "0.2.7", optional = true } 18 | anstyle-query = { version = "1.1.4", optional = true } 19 | arbitrary = { version = "1.4", optional = true, features = ["derive"] } 20 | clap = { version = "~4.5.48", optional = true, default-features =false, features = ["derive", "help", "usage", "error-context"] } 21 | clap_builder = { version = "4.3.24", optional = true} 22 | clap_lex = { version = "0.7.5", optional = true } 23 | crc-any = { workspace = true, default-features = false } 24 | proc-macro2 = "1.0.43" 25 | quick-xml = "0.38" 26 | quote = "1" 27 | rand = { version = "0.9", optional = true, default-features = false, features = ["std", "std_rng"] } 28 | regex = { version = "1.0" } 29 | serde = { version = "1.0.115", optional = true, features = ["derive"] } 30 | thiserror = { version = "2.0.12", default-features = false } 31 | 32 | [features] 33 | cli = ["dep:clap", "dep:clap_lex", "dep:clap_builder", "dep:anstyle", "dep:anstyle-query", "dep:anstyle-parse"] 34 | arbitrary = ["dep:arbitrary", "dep:rand"] 35 | emit-extensions = [] 36 | 37 | [dev-dependencies] 38 | insta = { version = "1.39.0", features = ["glob"] } 39 | tempfile = "3.10.1" 40 | 41 | [lints] 42 | workspace = true 43 | -------------------------------------------------------------------------------- /mavlink/examples/embedded-async-read/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mavlink-embedded-async-read" 3 | edition = "2021" 4 | authors = ["Patrick José Pereira "] 5 | version = "0.1.0" 6 | 7 | [profile.release] 8 | opt-level = 'z' # Optimize for binary size, but also turn off loop vectorization. 9 | lto = true # Performs "fat" LTO which attempts to perform optimizations across all crates within the dependency graph 10 | 11 | [dependencies] 12 | cortex-m = { version = "0.7", features = [ 13 | "inline-asm", 14 | "critical-section-single-core", 15 | ] } # Low level access to Cortex-M processors 16 | cortex-m-rt = "0.7" # Startup code and minimal runtime for Cortex-M microcontrollers 17 | rtt-target = "0.5" 18 | panic-rtt-target = "0.1" # Panic handler 19 | static_cell = "2.1" 20 | 21 | embassy-time = { version = "0.3", features = ["tick-hz-32_768"] } 22 | embassy-executor = { version = "0.5", features = [ 23 | "arch-cortex-m", 24 | "executor-thread", 25 | "executor-interrupt", 26 | "integrated-timers", 27 | ] } 28 | embassy-stm32 = { version = "0.1", features = [ 29 | "memory-x", 30 | "stm32f446re", 31 | "time-driver-any", 32 | ] } 33 | 34 | [dependencies.mavlink] # MAVLink library (wait for 0.9.0 version) 35 | path = "../../" 36 | features = ["common", "embedded"] 37 | default-features = false 38 | 39 | [patch.crates-io] 40 | embassy-time-driver = { git = "https://github.com/embassy-rs/embassy.git", rev = "86c48dde4192cabcad22faa10cabb4dc5f035c0a" } 41 | embassy-time-queue-driver = { git = "https://github.com/embassy-rs/embassy.git", rev = "86c48dde4192cabcad22faa10cabb4dc5f035c0a" } 42 | embassy-stm32 = { git = "https://github.com/embassy-rs/embassy.git", rev = "86c48dde4192cabcad22faa10cabb4dc5f035c0a" } 43 | 44 | [workspace] 45 | -------------------------------------------------------------------------------- /mavlink/tests/parse_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "std", feature = "tokio-1"))] 2 | mod parse_tests { 3 | use mavlink::ConnectionAddress; 4 | 5 | fn assert_parse(addr: &str) { 6 | assert_eq!( 7 | format!("{}", ConnectionAddress::parse_address(addr).unwrap()), 8 | addr 9 | ); 10 | } 11 | 12 | #[cfg(feature = "tcp")] 13 | #[test] 14 | fn test_parse_tcp() { 15 | assert_parse("tcpin:example.com:99"); 16 | assert_parse("tcpout:127.0.0.1:14549"); 17 | } 18 | 19 | #[cfg(feature = "tcp")] 20 | #[test] 21 | fn test_parse_file() { 22 | assert_parse("file:/mnt/12_44-mav.bin"); 23 | assert_parse("file:C:\\mav_logs\\test.bin"); 24 | } 25 | 26 | #[cfg(feature = "udp")] 27 | #[test] 28 | fn test_parse_udp() { 29 | assert_parse("udpcast:[::1]:4567"); 30 | assert_parse("udpin:[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443"); 31 | assert_parse("udpout:1.1.1.1:1"); 32 | } 33 | 34 | #[cfg(feature = "direct-serial")] 35 | #[test] 36 | fn test_parse_serial() { 37 | assert_parse("serial:/dev/ttyUSB0:9600"); 38 | assert_parse("serial:COM0:115200"); 39 | } 40 | 41 | #[test] 42 | fn test_parse_errors() { 43 | assert!(ConnectionAddress::parse_address("serial:/dev/ttyUSB0").is_err()); 44 | assert!(ConnectionAddress::parse_address("updout:1.1.1.1:1").is_err()); 45 | assert!(ConnectionAddress::parse_address("tcp:127.0.0.1:14540").is_err()); 46 | assert!(ConnectionAddress::parse_address("tcpin127.0.0.1:14540").is_err()); 47 | assert!(ConnectionAddress::parse_address(" udpout:1.1.1.1:1 ").is_err()); 48 | assert!(ConnectionAddress::parse_address(":udpcast:[::1]:4567").is_err()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-mavlink 2 | 3 | [![Build status](https://github.com/mavlink/rust-mavlink/actions/workflows/test.yml/badge.svg)](https://github.com/mavlink/rust-mavlink/actions/workflows/test.yml) 4 | [![Crate info](https://img.shields.io/crates/v/mavlink.svg)](https://crates.io/crates/mavlink) 5 | [![Documentation](https://docs.rs/mavlink/badge.svg)](https://docs.rs/mavlink) 6 | 7 | Rust implementation of the [MAVLink](https://mavlink.io/en) UAV messaging protocol, 8 | with bindings for all message sets. 9 | 10 | Add to your Cargo.toml: 11 | 12 | ``` 13 | mavlink = "0.16" 14 | ``` 15 | 16 | Building this crate requires `git`. 17 | 18 | ## Examples 19 | See [examples/](mavlink/examples/mavlink-dump/src/main.rs) for different usage examples. 20 | 21 | ### mavlink-dump 22 | [examples/mavlink-dump](mavlink/examples/mavlink-dump/src/main.rs) contains an executable example that can be used to test message reception. 23 | 24 | It can be executed directly by running: 25 | ``` 26 | cargo run --example mavlink-dump [options] 27 | ``` 28 | 29 | It's also possible to install the working example via `cargo` command line: 30 | ```sh 31 | cargo install --path examples/mavlink-dump 32 | ``` 33 | 34 | It can then be executed by running: 35 | ``` 36 | mavlink-dump [options] 37 | ``` 38 | 39 | Execution call example: 40 | ```sh 41 | mavlink-dump udpin:127.0.0.1:14540 42 | ``` 43 | 44 | ### Community projects 45 | Check some projects built by the community: 46 | - [mavlink2rest](https://github.com/patrickelectric/mavlink2rest): A REST server that provides easy and friendly access to mavlink messages. 47 | - [mavlink-camera-manager](https://github.com/mavlink/mavlink-camera-manager): Extensible cross-platform camera server. 48 | 49 | ## License 50 | 51 | Licensed under either of 52 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 53 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 54 | at your option. 55 | 56 | -------------------------------------------------------------------------------- /mavlink/tests/process_log_files.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "default", feature = "ardupilotmega"))] 2 | mod process_files { 3 | use mavlink::ardupilotmega::MavMessage; 4 | use mavlink::error::MessageReadError; 5 | use mavlink::MavConnection; 6 | 7 | #[test] 8 | pub fn get_file() { 9 | // Get path for download script 10 | let tlog = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 11 | .join("tests/log.tlog") 12 | .canonicalize() 13 | .unwrap(); 14 | 15 | let tlog = tlog.to_str().unwrap(); 16 | 17 | let filename = std::path::Path::new(tlog); 18 | let filename = filename.to_str().unwrap(); 19 | dbg!(filename); 20 | 21 | println!("Processing file: {filename}"); 22 | let connection_string = format!("file:{filename}"); 23 | 24 | // Process file 25 | process_file(&connection_string); 26 | } 27 | 28 | pub fn process_file(connection_string: &str) { 29 | let vehicle = mavlink::connect::(connection_string); 30 | assert!(vehicle.is_ok(), "Incomplete address should error"); 31 | 32 | let vehicle = vehicle.unwrap(); 33 | let mut counter = 0; 34 | loop { 35 | match vehicle.recv() { 36 | Ok((_header, _msg)) => { 37 | counter += 1; 38 | } 39 | Err(MessageReadError::Io(e)) => { 40 | if e.kind() == std::io::ErrorKind::WouldBlock { 41 | continue; 42 | } else { 43 | println!("recv error: {e:?}"); 44 | break; 45 | } 46 | } 47 | _ => {} 48 | } 49 | } 50 | 51 | println!("Number of parsed messages: {counter}"); 52 | assert!( 53 | counter == 1426, 54 | "Unable to hit the necessary amount of matches" 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mavlink-core/src/embedded.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | 3 | #[cfg(all(feature = "embedded", feature = "embedded-hal-02"))] 4 | const _: () = panic!("Only one of 'embedded' and 'embedded-hal-02' features can be enabled."); 5 | 6 | /// Replacement for std::io::Read + byteorder::ReadBytesExt in no_std envs 7 | pub trait Read { 8 | fn read(&mut self, buf: &mut [u8]) -> Result { 9 | self.read_exact(buf).map(|_| buf.len()) 10 | } 11 | 12 | fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), MessageReadError>; 13 | } 14 | 15 | #[cfg(all(feature = "embedded", not(feature = "embedded-hal-02")))] 16 | impl Read for R { 17 | fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), MessageReadError> { 18 | embedded_io::Read::read_exact(self, buf).map_err(|_| MessageReadError::Io) 19 | } 20 | } 21 | 22 | #[cfg(all(feature = "embedded-hal-02", not(feature = "embedded")))] 23 | impl> Read for R { 24 | fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), MessageReadError> { 25 | for byte in buf { 26 | *byte = nb::block!(self.read()).map_err(|_| MessageReadError::Io)?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | /// Replacement for std::io::Write + byteorder::WriteBytesExt in no_std envs 34 | pub trait Write { 35 | fn write_all(&mut self, buf: &[u8]) -> Result<(), MessageWriteError>; 36 | } 37 | 38 | #[cfg(all(feature = "embedded", not(feature = "embedded-hal-02")))] 39 | impl Write for W { 40 | fn write_all(&mut self, buf: &[u8]) -> Result<(), MessageWriteError> { 41 | embedded_io::Write::write_all(self, buf).map_err(|_| MessageWriteError::Io) 42 | } 43 | } 44 | 45 | #[cfg(all(feature = "embedded-hal-02", not(feature = "embedded")))] 46 | impl> Write for W { 47 | fn write_all(&mut self, buf: &[u8]) -> Result<(), MessageWriteError> { 48 | for byte in buf { 49 | nb::block!(self.write(*byte)).map_err(|_| MessageWriteError::Io)?; 50 | } 51 | 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/definitions/deprecated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Use MAV_FRAME_GLOBAL in COMMAND_INT (and elsewhere) as a synonymous replacement. 7 | Global (WGS84) coordinate frame (scaled) + altitude relative to mean sea level (MSL). 8 | 9 | 10 | 11 | 12 | 13 | Enumeration of possible mount operation modes. This message is used by obsolete/deprecated gimbal messages. 14 | 15 | Load and keep safe position (Roll,Pitch,Yaw) from permanent memory and stop stabilization 16 | 17 | 18 | 19 | 20 | 21 | 22 | To be removed / merged with TIMESYNC 23 | A ping message either requesting or responding to a ping. This allows to measure the system latencies, including serial port, radio modem and UDP connections. The ping microservice is documented at https://mavlink.io/en/services/ping.html 24 | Timestamp (UNIX Epoch time or time since system boot). The receiving end can infer timestamp format (since 1.1.1970 or since system boot) by checking for the magnitude of the number. 25 | PING sequence 26 | 0: request ping from all receiving systems. If greater than 0: message is a ping response and number is the system id of the requesting system 27 | 0: request ping from all receiving components. If greater than 0: message is a ping response and number is the component id of the requesting component. 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /mavlink/tests/random_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "ardupilotmega", feature = "arbitrary"))] 2 | mod random_tests { 3 | use mavlink::{ardupilotmega::MavMessage, Message}; 4 | use rand::{rngs::StdRng, SeedableRng}; 5 | 6 | #[test] 7 | fn test_random_message_generation() { 8 | let seed = 42; 9 | let mut rng: StdRng = SeedableRng::seed_from_u64(seed); 10 | 11 | let known_ardupilotmega_ids = &[ 12 | 0, 1, 2, 4, 5, 6, 7, 8, 11, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 13 | 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 54, 55, 61, 62, 63, 14 | 64, 65, 66, 67, 69, 70, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84, 85, 86, 87, 89, 90, 91, 15 | 92, 93, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 16 | 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 17 | 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 146, 147, 148, 149, 150, 18 | 151, 152, 153, 154, 155, 156, 157, 158, 160, 161, 162, 163, 164, 165, 166, 167, 168, 19 | 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 20 | 186, 191, 192, 193, 194, 195, 200, 201, 214, 215, 216, 217, 218, 219, 225, 226, 230, 21 | 231, 232, 233, 234, 235, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 22 | 253, 254, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 23 | 271, 275, 276, 280, 281, 282, 283, 284, 285, 286, 287, 288, 290, 291, 299, 300, 301, 24 | 310, 311, 320, 321, 322, 323, 324, 330, 331, 332, 333, 334, 335, 336, 339, 340, 350, 25 | 360, 370, 373, 375, 380, 385, 386, 387, 388, 390, 395, 397, 400, 401, 410, 411, 412, 26 | 413, 9000, 9005, 10001, 10002, 10003, 11000, 11001, 11002, 11003, 11010, 11011, 11020, 27 | 11030, 11031, 11032, 11033, 11034, 11035, 11036, 11037, 11038, 11039, 12900, 12901, 28 | 12902, 12903, 12904, 12905, 12915, 12918, 12919, 12920, 42000, 42001, 50001, 50002, 29 | 50003, 50004, 50005, 30 | ]; 31 | 32 | for id in known_ardupilotmega_ids { 33 | let msg = MavMessage::random_message_from_id(*id, &mut rng).unwrap(); 34 | let id2 = msg.message_id(); 35 | assert_eq!(*id, id2, "Generated message had wrong ID"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mavlink-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mavlink-core" 3 | version = "0.16.2" 4 | authors = [ 5 | "Todd Stellanova", 6 | "Michal Podhradsky", 7 | "Kevin Mehall", 8 | "Tim Ryan", 9 | "Patrick José Pereira", 10 | "Ibiyemi Abiodun", 11 | ] 12 | description = "Implements the MAVLink data interchange format for UAVs." 13 | readme = "../README.md" 14 | license = "MIT/Apache-2.0" 15 | repository = "https://github.com/mavlink/rust-mavlink" 16 | edition.workspace = true 17 | rust-version.workspace = true 18 | keywords.workspace = true 19 | categories.workspace = true 20 | 21 | [dependencies] 22 | arbitrary = { version = "1.4", optional = true, features = ["derive"] } 23 | async-trait = { version = "0.1.18", optional = true } 24 | byteorder = { workspace = true, default-features = false } 25 | crc-any = { workspace = true, default-features = false } 26 | embedded-hal-02 = { version = "0.2", optional = true, package = "embedded-hal" } 27 | embedded-io = { version = "0.6.1", optional = true } 28 | embedded-io-async = { version = "0.6.1", optional = true } 29 | futures = { version = "0.3", default-features = false, optional = true } 30 | nb = { version = "1.0", optional = true } 31 | rand = { version = "0.9", optional = true, default-features = false, features = ["std", "std_rng"] } 32 | serde = { version = "1.0.115", optional = true, features = ["derive"] } 33 | serde_arrays = { version = "0.2.0", optional = true } 34 | serialport = { version = "4.7.2", default-features = false, optional = true } 35 | sha2 = { version = "0.10", optional = true } 36 | tokio = { version = "1.0", default-features = false, features = ["io-util", "net", "fs"], optional = true } 37 | tokio-serial = { version = "5.4.4", default-features = false, optional = true } 38 | 39 | [features] 40 | default = ["std", "tcp", "udp", "direct-serial", "serde"] 41 | 42 | std = ["byteorder/std"] 43 | udp = [] 44 | tcp = [] 45 | direct-serial = ["serialport"] 46 | # NOTE: Only one of 'embedded' and 'embedded-hal-02' features can be enabled. 47 | # Use "embedded' feature to enable embedded-hal=1.0 (embedded-io and embedded-io-async is part of embedded-hal). 48 | # Use 'embedded-hal-0.2' feature to enable deprecated embedded-hal=0.2.3 (some hals is not supports embedded-hal=1.0 yet). 49 | embedded = ["dep:embedded-io", "dep:embedded-io-async"] 50 | embedded-hal-02 = ["dep:nb", "dep:embedded-hal-02"] 51 | serde = ["dep:serde", "dep:serde_arrays"] 52 | tokio-1 = ["dep:tokio", "dep:async-trait", "dep:tokio-serial", "dep:futures"] 53 | signing = ["dep:sha2"] 54 | arbitrary = ["dep:arbitrary", "dep:rand"] 55 | 56 | [dev-dependencies] 57 | tokio = { version = "1.0", default-features = false, features = ["io-util", "net", "fs", "macros", "rt"] } 58 | 59 | [lints] 60 | workspace = true 61 | -------------------------------------------------------------------------------- /mavlink/tests/invalid_value_test.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(feature = "common")] 4 | mod helper_tests { 5 | use mavlink::{ 6 | calculate_crc, 7 | common::MavMessage, 8 | error::{MessageReadError, ParserError}, 9 | peek_reader::PeekReader, 10 | MavlinkVersion, MessageData, 11 | }; 12 | 13 | #[test] 14 | fn test_invalid_enum() { 15 | use crate::test_shared::HEARTBEAT_V2; 16 | 17 | let mut invalid_enum_buf = [0; HEARTBEAT_V2.len()]; 18 | invalid_enum_buf.copy_from_slice(HEARTBEAT_V2); 19 | // set autopilot to an invalid MavAutopilot value 20 | invalid_enum_buf[1 + 9 + 5] = 255; 21 | // update crc 22 | let crc = calculate_crc( 23 | &invalid_enum_buf[1..HEARTBEAT_V2.len() - 2], 24 | mavlink::common::HEARTBEAT_DATA::EXTRA_CRC, 25 | ); 26 | invalid_enum_buf[HEARTBEAT_V2.len() - 2..HEARTBEAT_V2.len()] 27 | .copy_from_slice(&crc.to_le_bytes()); 28 | 29 | let result = mavlink::read_v2_msg::(&mut PeekReader::new( 30 | invalid_enum_buf.as_slice(), 31 | )); 32 | assert!(matches!( 33 | result, 34 | Err(MessageReadError::Parse(ParserError::InvalidEnum { 35 | enum_type: "MavAutopilot", 36 | value: 255 37 | })) 38 | )); 39 | } 40 | 41 | #[test] 42 | fn test_invalid_bitflag() { 43 | use mavlink::common::HIL_ACTUATOR_CONTROLS_DATA; 44 | 45 | let msg = HIL_ACTUATOR_CONTROLS_DATA::DEFAULT; 46 | let mut invalid_flag_buf = [0; 1 + 9 + HIL_ACTUATOR_CONTROLS_DATA::ENCODED_LEN + 2]; 47 | let len = msg.ser( 48 | MavlinkVersion::V2, 49 | &mut invalid_flag_buf[10..10 + HIL_ACTUATOR_CONTROLS_DATA::ENCODED_LEN], 50 | ); 51 | invalid_flag_buf[0] = mavlink::MAV_STX_V2; 52 | invalid_flag_buf[1] = len as u8; 53 | invalid_flag_buf[7] = HIL_ACTUATOR_CONTROLS_DATA::ID as u8; 54 | // set flags to an invalid HilActuatorControlsFlags value 55 | invalid_flag_buf[1 + 9 + 8..1 + 9 + 16].copy_from_slice(&u64::MAX.to_le_bytes()); 56 | // update crc 57 | let crc = calculate_crc( 58 | &invalid_flag_buf[1..1 + 9 + len], 59 | HIL_ACTUATOR_CONTROLS_DATA::EXTRA_CRC, 60 | ); 61 | invalid_flag_buf[1 + 9 + len..1 + 9 + len + 2].copy_from_slice(&crc.to_le_bytes()); 62 | 63 | let result = mavlink::read_v2_msg::(&mut PeekReader::new( 64 | invalid_flag_buf.as_slice(), 65 | )); 66 | assert!(matches!( 67 | result, 68 | Err(MessageReadError::Parse(ParserError::InvalidFlag { 69 | flag_type: "HilActuatorControlsFlags", 70 | value: u64::MAX 71 | })) 72 | )); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mavlink-core/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for processing MAVLink messages 2 | 3 | /// Removes the trailing zeroes in the payload 4 | /// 5 | /// # Note: 6 | /// 7 | /// There must always be at least one remaining byte even if it is a 8 | /// zero byte. 9 | pub fn remove_trailing_zeroes(data: &[u8]) -> usize { 10 | let mut len = data.len(); 11 | 12 | while len > 1 && data[len - 1] == 0 { 13 | len -= 1; 14 | } 15 | 16 | len 17 | } 18 | 19 | /// A trait very similar to [`Default`] but is only implemented for the equivalent Rust types to 20 | /// `MavType`s. 21 | /// 22 | /// This is only needed because rust doesn't currently implement `Default` for arrays 23 | /// of all sizes. In particular this trait is only ever used when the "serde" feature is enabled. 24 | /// For more information, check out [this issue](https://users.rust-lang.org/t/issue-for-derives-for-arrays-greater-than-size-32/59055/3). 25 | pub trait RustDefault: Copy { 26 | fn rust_default() -> Self; 27 | } 28 | 29 | impl RustDefault for [T; N] { 30 | #[inline(always)] 31 | fn rust_default() -> Self { 32 | let val: T = RustDefault::rust_default(); 33 | [val; N] 34 | } 35 | } 36 | 37 | macro_rules! impl_rust_default { 38 | ($($t:ty => $val:expr),* $(,)?) => { 39 | $(impl RustDefault for $t { 40 | #[inline(always)] 41 | fn rust_default() -> Self { $val } 42 | })* 43 | }; 44 | } 45 | 46 | impl_rust_default! { 47 | u8 => 0, 48 | i8 => 0, 49 | u16 => 0, 50 | i16 => 0, 51 | u32 => 0, 52 | i32 => 0, 53 | u64 => 0, 54 | i64 => 0, 55 | f32 => 0.0, 56 | f64 => 0.0, 57 | char => '\0', 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn test_remove_trailing_zeroes_empty_slice() { 66 | remove_trailing_zeroes(&[]); 67 | } 68 | } 69 | 70 | #[cfg(feature = "serde")] 71 | pub mod nulstr { 72 | use serde::de::Deserializer; 73 | use serde::ser::Serializer; 74 | use serde::Deserialize; 75 | use std::str; 76 | 77 | pub fn serialize(value: &[u8; N], serializer: S) -> Result 78 | where 79 | S: Serializer, 80 | { 81 | let nul_pos = value.iter().position(|&b| b == 0).unwrap_or(N); 82 | let s = str::from_utf8(&value[..nul_pos]).map_err(serde::ser::Error::custom)?; 83 | serializer.serialize_str(s) 84 | } 85 | 86 | pub fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error> 87 | where 88 | D: Deserializer<'de>, 89 | { 90 | let s: String = Deserialize::deserialize(deserializer)?; 91 | let mut buf = [0u8; N]; 92 | let bytes = s.as_bytes(); 93 | let len = bytes.len().min(N); 94 | buf[..len].copy_from_slice(&bytes[..len]); 95 | Ok(buf) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mavlink/build/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "256"] 2 | 3 | use std::env; 4 | use std::fs::read_dir; 5 | use std::path::Path; 6 | use std::process::{Command, ExitCode}; 7 | 8 | use mavlink_bindgen::XmlDefinitions; 9 | 10 | fn main() -> ExitCode { 11 | let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 12 | 13 | // Check if git is installed 14 | if Command::new("git").arg("--version").status().is_err() { 15 | eprintln!("error: Git is not installed or could not be found."); 16 | return ExitCode::FAILURE; 17 | } 18 | 19 | // Update and init submodule 20 | if let Err(error) = Command::new("git") 21 | .arg("submodule") 22 | .arg("update") 23 | .arg("--init") 24 | .current_dir(src_dir) 25 | .status() 26 | { 27 | eprintln!("Failed to update MAVLink definitions submodule: {error}"); 28 | return ExitCode::FAILURE; 29 | } 30 | 31 | // find & apply patches to XML definitions to avoid crashes 32 | let patch_dir = src_dir.join("build/patches"); 33 | let mavlink_dir = src_dir.join("mavlink"); 34 | 35 | if let Ok(dir) = read_dir(patch_dir) { 36 | for entry in dir.flatten() { 37 | if let Err(error) = Command::new("git") 38 | .arg("apply") 39 | .arg(entry.path().as_os_str()) 40 | .current_dir(&mavlink_dir) 41 | .status() 42 | { 43 | eprintln!("Failed to apply MAVLink definitions patches: {error}"); 44 | return ExitCode::FAILURE; 45 | } 46 | } 47 | } 48 | 49 | let out_dir = env::var("OUT_DIR").unwrap(); 50 | 51 | let source_definitions_dir = src_dir.join("mavlink/message_definitions/v1.0"); 52 | 53 | let enabled_features: Vec = env::vars() 54 | .filter_map(|(key, _)| key.strip_prefix("CARGO_FEATURE_").map(str::to_lowercase)) 55 | .collect(); 56 | 57 | let mut definitions_to_bind = vec![]; 58 | 59 | if let Ok(dir) = read_dir(&source_definitions_dir) { 60 | for entry in dir.flatten() { 61 | let filename = entry 62 | .path() 63 | .file_stem() 64 | .unwrap() 65 | .to_string_lossy() 66 | .to_lowercase(); 67 | 68 | if enabled_features.contains(&filename) { 69 | definitions_to_bind.push(entry.path()); 70 | } 71 | } 72 | } 73 | 74 | let xml_definitions = if definitions_to_bind.is_empty() { 75 | XmlDefinitions::Directory(source_definitions_dir) 76 | } else { 77 | XmlDefinitions::Files(definitions_to_bind) 78 | }; 79 | 80 | let result = match mavlink_bindgen::generate(xml_definitions, out_dir) { 81 | Ok(r) => r, 82 | Err(e) => { 83 | eprintln!("{e}"); 84 | return ExitCode::FAILURE; 85 | } 86 | }; 87 | 88 | #[cfg(feature = "format-generated-code")] 89 | mavlink_bindgen::format_generated_code(&result); 90 | 91 | mavlink_bindgen::emit_cargo_build_messages(&result); 92 | 93 | ExitCode::SUCCESS 94 | } 95 | -------------------------------------------------------------------------------- /mavlink/tests/udp_loopback_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(all(feature = "std", feature = "udp", feature = "common"))] 4 | mod test_udp_connections { 5 | use std::thread; 6 | 7 | use mavlink::{MavConnection, MessageData}; 8 | 9 | /// Test whether we can send a message via UDP and receive it OK 10 | #[test] 11 | fn test_udp_loopback() { 12 | const RECEIVE_CHECK_COUNT: i32 = 3; 13 | 14 | let server = mavlink::connect("udpin:0.0.0.0:14551").expect("Couldn't create server"); 15 | 16 | // have the client send one heartbeat per second 17 | thread::spawn({ 18 | move || { 19 | let msg = 20 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 21 | let client = 22 | mavlink::connect("udpout:127.0.0.1:14551").expect("Couldn't create client"); 23 | loop { 24 | client.send_default(&msg).ok(); 25 | } 26 | } 27 | }); 28 | 29 | //TODO use std::sync::WaitTimeoutResult to timeout ourselves if recv fails? 30 | let mut recv_count = 0; 31 | for _i in 0..RECEIVE_CHECK_COUNT { 32 | match server.recv() { 33 | Ok((_header, msg)) => { 34 | if let mavlink::common::MavMessage::HEARTBEAT(_heartbeat_msg) = msg { 35 | recv_count += 1; 36 | } else { 37 | // one message parse failure fails the test 38 | break; 39 | } 40 | } 41 | Err(..) => { 42 | // one message read failure fails the test 43 | break; 44 | } 45 | } 46 | } 47 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 48 | } 49 | 50 | /// Test whether we can send a message via UDP and receive it OK using recv_raw 51 | #[test] 52 | fn test_udp_loopback_recv_raw() { 53 | const RECEIVE_CHECK_COUNT: i32 = 3; 54 | 55 | let server = mavlink::connect::("udpin:0.0.0.0:14561") 56 | .expect("Couldn't create server"); 57 | 58 | // have the client send one heartbeat per second 59 | thread::spawn({ 60 | move || { 61 | let msg = 62 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 63 | let client = 64 | mavlink::connect("udpout:127.0.0.1:14561").expect("Couldn't create client"); 65 | loop { 66 | client.send_default(&msg).ok(); 67 | } 68 | } 69 | }); 70 | 71 | //TODO use std::sync::WaitTimeoutResult to timeout ourselves if recv fails? 72 | let mut recv_count = 0; 73 | for _i in 0..RECEIVE_CHECK_COUNT { 74 | match server.recv_raw() { 75 | Ok(message) => { 76 | if message.message_id() == mavlink::common::HEARTBEAT_DATA::ID { 77 | recv_count += 1; 78 | } else { 79 | // one message parse failure fails the test 80 | break; 81 | } 82 | } 83 | Err(..) => { 84 | // one message read failure fails the test 85 | break; 86 | } 87 | } 88 | } 89 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /mavlink/examples/embedded-async-read/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Target board: stm32f446RETx (stm32nucleo) 2 | //! Manual: https://www.st.com/resource/en/reference_manual/dm00043574-stm32f303xb-c-d-e-stm32f303x6-8-stm32f328x8-stm32f358xc-stm32f398xe-advanced-arm-based-mcus-stmicroelectronics.pdf 3 | #![no_main] 4 | #![no_std] 5 | 6 | // Panic handler 7 | use panic_rtt_target as _; 8 | 9 | use embassy_executor::Spawner; 10 | use embassy_stm32::{bind_interrupts, mode::Async, peripherals::*, usart}; 11 | use embassy_time::Timer; 12 | use mavlink; 13 | use mavlink::common::{MavMessage, HEARTBEAT_DATA}; 14 | use mavlink::{read_v2_raw_message_async, MAVLinkV2MessageRaw, MavlinkVersion, MessageData}; 15 | use rtt_target::{rprintln, rtt_init_print}; 16 | use static_cell::ConstStaticCell; 17 | 18 | bind_interrupts!(struct Irqs { 19 | USART1 => usart::InterruptHandler; 20 | }); 21 | 22 | #[embassy_executor::main] 23 | async fn main(spawner: Spawner) { 24 | rtt_init_print!(); 25 | 26 | // Peripherals access 27 | let p = embassy_stm32::init(embassy_stm32::Config::default()); 28 | 29 | // Create an interface USART2 with 115200 baudrate 30 | let mut config = usart::Config::default(); 31 | config.baudrate = 115200; 32 | let serial = usart::Uart::new( 33 | p.USART1, p.PA10, p.PA9, Irqs, p.DMA2_CH7, p.DMA2_CH2, config, 34 | ) 35 | .unwrap(); 36 | 37 | // Break serial in TX and RX (not used) 38 | let (mut tx, rx) = serial.split(); 39 | 40 | // Create our mavlink header and heartbeat message 41 | let header = mavlink::MavHeader { 42 | system_id: 1, 43 | component_id: 1, 44 | sequence: 42, 45 | }; 46 | let heartbeat = mavlink::common::HEARTBEAT_DATA { 47 | custom_mode: 0, 48 | mavtype: mavlink::common::MavType::MAV_TYPE_SUBMARINE, 49 | autopilot: mavlink::common::MavAutopilot::MAV_AUTOPILOT_ARDUPILOTMEGA, 50 | base_mode: mavlink::common::MavModeFlag::empty(), 51 | system_status: mavlink::common::MavState::MAV_STATE_STANDBY, 52 | mavlink_version: 0x3, 53 | }; 54 | 55 | // Spawn Rx loop 56 | spawner.spawn(rx_task(rx)).unwrap(); 57 | 58 | // Main loop 59 | loop { 60 | // Write the raw heartbeat message to reduce firmware flash size (using Message::ser will be add ~70KB because 61 | // all *_DATA::ser methods will be add to firmware). 62 | let mut raw = MAVLinkV2MessageRaw::new(); 63 | raw.serialize_message_data(header, &heartbeat); 64 | tx.write(raw.raw_bytes()).await.unwrap(); 65 | 66 | // Delay for 1 second 67 | Timer::after_millis(1000).await; 68 | } 69 | } 70 | 71 | #[embassy_executor::task] 72 | pub async fn rx_task(rx: usart::UartRx<'static, Async>) { 73 | // Make ring-buffered RX (over DMA) 74 | static BUF_MEMORY: ConstStaticCell<[u8; 1024]> = ConstStaticCell::new([0; 1024]); 75 | let mut rx_buffered = rx.into_ring_buffered(BUF_MEMORY.take()); 76 | 77 | loop { 78 | // Read raw message to reduce firmware flash size (using read_v2_msg_async will be add ~80KB because 79 | // all *_DATA::deser methods will be add to firmware). 80 | let raw = read_v2_raw_message_async::(&mut rx_buffered) 81 | .await 82 | .unwrap(); 83 | rprintln!("Read raw message: msg_id={}", raw.message_id()); 84 | 85 | if raw.message_id() == HEARTBEAT_DATA::ID { 86 | let heartbeat = HEARTBEAT_DATA::deser(MavlinkVersion::V2, raw.payload()).unwrap(); 87 | rprintln!("heartbeat: {:?}", heartbeat); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /mavlink/tests/signing.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(feature = "signing")] 4 | mod signing { 5 | use mavlink::{ 6 | common::HEARTBEAT_DATA, peek_reader::PeekReader, read_v2_raw_message, MAVLinkV2MessageRaw, 7 | MavHeader, SigningConfig, SigningData, MAV_STX_V2, 8 | }; 9 | 10 | use crate::test_shared::SECRET_KEY; 11 | 12 | const HEARTBEAT_SIGNED: &[u8] = &[ 13 | MAV_STX_V2, 14 | 0x09, 15 | 0x01, // MAVLINK_IFLAG_SIGNED 16 | 0x00, 17 | crate::test_shared::COMMON_MSG_HEADER.sequence, 18 | crate::test_shared::COMMON_MSG_HEADER.system_id, 19 | crate::test_shared::COMMON_MSG_HEADER.component_id, 20 | 0x00, // msg ID 21 | 0x00, 22 | 0x00, 23 | 0x05, // payload 24 | 0x00, 25 | 0x00, 26 | 0x00, 27 | 0x02, 28 | 0x03, 29 | 0x59, 30 | 0x03, 31 | 0x03, 32 | 0xc9, // checksum 33 | 0x8b, 34 | 0x00, // link_id 35 | 0xff, // use max timestamp to ensure test will never fail against current time 36 | 0xff, 37 | 0xff, 38 | 0xff, 39 | 0xff, 40 | 0xff, 41 | 0x27, // signature 42 | 0x18, 43 | 0xb1, 44 | 0x68, 45 | 0xcc, 46 | 0xf5, 47 | ]; 48 | 49 | #[test] 50 | pub fn test_verify() { 51 | let signing_cfg = SigningConfig::new(SECRET_KEY, 0, true, false); 52 | let signing_data = SigningData::from_config(signing_cfg); 53 | let mut r = PeekReader::new(HEARTBEAT_SIGNED); 54 | let msg = read_v2_raw_message::(&mut r).unwrap(); 55 | assert!( 56 | signing_data.verify_signature(&msg), 57 | "Message verification failed" 58 | ); 59 | } 60 | 61 | #[test] 62 | pub fn test_invalid_ts() { 63 | let signing_cfg = SigningConfig::new(SECRET_KEY, 0, true, false); 64 | let signing_data = SigningData::from_config(signing_cfg); 65 | let mut r = PeekReader::new(HEARTBEAT_SIGNED); 66 | let mut msg = read_v2_raw_message::(&mut r).unwrap(); 67 | msg.signature_timestamp_bytes_mut() 68 | .copy_from_slice(&[0, 0, 0, 0, 0, 0]); // set timestamp to min causing the timestamp test to fail 69 | assert!( 70 | !signing_data.verify_signature(&msg), 71 | "Invalid message verified" 72 | ); 73 | } 74 | 75 | #[test] 76 | pub fn test_sign_verify() { 77 | use mavlink::common::MavMessage; 78 | let heartbeat_message = MavMessage::HEARTBEAT(HEARTBEAT_DATA::default()); 79 | let mut message = MAVLinkV2MessageRaw::new(); 80 | let header = MavHeader { 81 | system_id: 4, 82 | component_id: 3, 83 | sequence: 42, 84 | }; 85 | message.serialize_message_for_signing(header, &heartbeat_message); 86 | 87 | let signing_cfg = SigningConfig::new(SECRET_KEY, 0, true, false); 88 | let signing_data = SigningData::from_config(signing_cfg); 89 | signing_data.sign_message(&mut message); 90 | assert!( 91 | signing_data.verify_signature(&message), 92 | "Message verification failed" 93 | ); 94 | // the same message must not be allowed to be verified again 95 | assert!( 96 | !signing_data.verify_signature(&message), 97 | "Invalid message verified" 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /mavlink/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | 4 | //! Rust implementation of the MAVLink UAV messaging protocol, with bindings for all dialects. 5 | //! This crate provides message set code generation, packet building, parsing and connection handling for blocking and asynchronous I/O. 6 | //! 7 | //! # Feature flags 8 | //! The `mavlink` crate uses a number of [feature flags] to reduce the amount of compiled code by making certain functions and MAVLink message sets (dialects) optional. 9 | //! These feature flags are available to control the provided functionalities: 10 | //! 11 | //! - `std`: Enables the usage of `std` in `mavlink`, enabled by default, this can be disabled for embedded applications. 12 | //! - `direct-serial`: Enable serial MAVLink connections, enabled by default. 13 | //! - `udp`: Enables UDP based MAVLink connections, enabled by default. 14 | //! - `tcp`: Enables TCP based MAVLink connections, enabled by default. 15 | //! - `signing`: Enable support for [MAVLink 2 message signing] 16 | //! - `embedded`: Enables embedded support using the [embedded-io] crate, incompatible with `embedded-hal-02` and `tokio-1`. 17 | //! - `embedded-hal-02`: Enables embedded support using version 0.2 of the [embedded-hal] crate, incompatible with `embedded`. 18 | //! - `tokio-1`: Enable support for asynchronous I/O using [tokio], incompatible with `embedded`. 19 | //! - `serde`: Enables [serde] support in generated message sets, enabled by default. 20 | //! - `format-generated-code`: Generated MAVLink message set code will be formatted, requires `rustfmt` to be installed, enabled by default. 21 | //! - `emit-extensions`: Generated MAVLink message set code will include [MAVLink 2 message extensions]. 22 | //! - `arbitrary`: Enable support for the [arbitrary] crate. 23 | //! - `ts`: Enable support for [ts-rs] typescript generation. 24 | //! 25 | //! Either `std`, `embedded` or `embedded-hal-02` must be enabled. 26 | //! 27 | //! Each MAVlink message set (dialect) can be enabled using its feature flag. The following message set feature flags are available: 28 | //! - `ardupilotmega`, enabled by default 29 | //! - `common`, enabled by default 30 | //! - `all`, this includes all other sets in the same message set 31 | //! - `asluav` 32 | //! - `avssuas` 33 | //! - `cubepilot` 34 | //! - `csairlink` 35 | //! - `development` 36 | //! - `icarous` 37 | //! - `loweheiser` 38 | //! - `matrixpilot` 39 | //! - `minimal` 40 | //! - `paparazzi` 41 | //! - `python_array_test` 42 | //! - `slugs` 43 | //! - `standard` 44 | //! - `storm32` 45 | //! - `test` 46 | //! - `ualberta` 47 | //! - `uavionix` 48 | //! 49 | //! The `all-dialects` feature enables all message sets except `all`. 50 | //! 51 | //! [feature flags]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section 52 | //! [MAVLink 2 message signing]: https://mavlink.io/en/guide/message_signing.html 53 | //! [MAVLink 2 message extensions]: https://mavlink.io/en/guide/define_xml_element.html#message_extensions 54 | //! [embedded-io]: https://crates.io/crates/embedded-io 55 | //! [embedded-hal]: https://crates.io/crates/embedded-hal 56 | //! [tokio]: https://crates.io/crates/tokio 57 | //! [serde]: https://crates.io/crates/serde 58 | //! [arbitrary]: https://crates.io/crates/arbitrary 59 | //! [ts-rs]: https://crates.io/crates/ts-rs 60 | 61 | // include generate definitions 62 | include!(concat!(env!("OUT_DIR"), "/mod.rs")); 63 | 64 | pub use mavlink_core::*; 65 | 66 | #[cfg(feature = "emit-extensions")] 67 | #[allow(unused_imports)] 68 | pub(crate) use mavlink_core::utils::RustDefault; 69 | 70 | #[cfg(feature = "serde")] 71 | #[allow(unused_imports)] 72 | pub(crate) use mavlink_core::utils::nulstr; 73 | -------------------------------------------------------------------------------- /mavlink/examples/mavlink-dump/src/main.rs: -------------------------------------------------------------------------------- 1 | use mavlink::error::MessageReadError; 2 | use mavlink::MavConnection; 3 | use std::{env, sync::Arc, thread, time::Duration}; 4 | 5 | fn main() { 6 | let args: Vec<_> = env::args().collect(); 7 | 8 | if args.len() < 2 { 9 | println!( 10 | "Usage: mavlink-dump (tcpout|tcpin|udpout|udpin|udpbcast|serial|file):(ip|dev|path):(port|baud)" 11 | ); 12 | return; 13 | } 14 | 15 | // It's possible to change the mavlink dialect to be used in the connect call 16 | let mut mavconn = mavlink::connect::(&args[1]).unwrap(); 17 | 18 | // here as an example we force the protocol version to mavlink V1: 19 | // the default for this library is mavlink V2 20 | mavconn.set_protocol_version(mavlink::MavlinkVersion::V1); 21 | 22 | let vehicle = Arc::new(mavconn); 23 | vehicle 24 | .send(&mavlink::MavHeader::default(), &request_parameters()) 25 | .unwrap(); 26 | vehicle 27 | .send(&mavlink::MavHeader::default(), &request_stream()) 28 | .unwrap(); 29 | 30 | thread::spawn({ 31 | let vehicle = vehicle.clone(); 32 | move || loop { 33 | let res = vehicle.send_default(&heartbeat_message()); 34 | if res.is_ok() { 35 | thread::sleep(Duration::from_secs(1)); 36 | } else { 37 | println!("send failed: {res:?}"); 38 | } 39 | } 40 | }); 41 | 42 | loop { 43 | match vehicle.recv() { 44 | Ok((_header, msg)) => { 45 | println!("received: {msg:?}"); 46 | } 47 | Err(MessageReadError::Io(e)) => { 48 | if e.kind() == std::io::ErrorKind::WouldBlock { 49 | //no messages currently available to receive -- wait a while 50 | thread::sleep(Duration::from_secs(1)); 51 | continue; 52 | } else { 53 | println!("recv error: {e:?}"); 54 | break; 55 | } 56 | } 57 | // messages that didn't get through due to parser errors are ignored 58 | _ => {} 59 | } 60 | } 61 | } 62 | 63 | /// Create a heartbeat message using 'ardupilotmega' dialect 64 | pub fn heartbeat_message() -> mavlink::ardupilotmega::MavMessage { 65 | mavlink::ardupilotmega::MavMessage::HEARTBEAT(mavlink::ardupilotmega::HEARTBEAT_DATA { 66 | custom_mode: 0, 67 | mavtype: mavlink::ardupilotmega::MavType::MAV_TYPE_QUADROTOR, 68 | autopilot: mavlink::ardupilotmega::MavAutopilot::MAV_AUTOPILOT_ARDUPILOTMEGA, 69 | base_mode: mavlink::ardupilotmega::MavModeFlag::empty(), 70 | system_status: mavlink::ardupilotmega::MavState::MAV_STATE_STANDBY, 71 | mavlink_version: 0x3, 72 | }) 73 | } 74 | 75 | /// Create a message requesting the parameters list 76 | pub fn request_parameters() -> mavlink::ardupilotmega::MavMessage { 77 | mavlink::ardupilotmega::MavMessage::PARAM_REQUEST_LIST( 78 | mavlink::ardupilotmega::PARAM_REQUEST_LIST_DATA { 79 | target_system: 0, 80 | target_component: 0, 81 | }, 82 | ) 83 | } 84 | 85 | /// Create a message enabling data streaming 86 | pub fn request_stream() -> mavlink::ardupilotmega::MavMessage { 87 | #[expect(deprecated)] 88 | mavlink::ardupilotmega::MavMessage::REQUEST_DATA_STREAM( 89 | mavlink::ardupilotmega::REQUEST_DATA_STREAM_DATA { 90 | target_system: 0, 91 | target_component: 0, 92 | req_stream_id: 0, 93 | req_message_rate: 10, 94 | start_stop: 1, 95 | }, 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /mavlink/tests/udp_loopback_async_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(all(feature = "tokio-1", feature = "udp", feature = "common"))] 4 | mod test_udp_connections { 5 | use mavlink::MessageData; 6 | 7 | /// Test whether we can send a message via UDP and receive it OK using async_connect 8 | #[tokio::test] 9 | async fn test_udp_loopback() { 10 | const RECEIVE_CHECK_COUNT: i32 = 3; 11 | 12 | let server = mavlink::connect_async("udpin:0.0.0.0:14552") 13 | .await 14 | .expect("Couldn't create server"); 15 | 16 | // have the client send one heartbeat per second 17 | tokio::spawn({ 18 | async move { 19 | let msg = 20 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 21 | let client = mavlink::connect_async("udpout:127.0.0.1:14552") 22 | .await 23 | .expect("Couldn't create client"); 24 | loop { 25 | client.send_default(&msg).await.ok(); 26 | } 27 | } 28 | }); 29 | 30 | //TODO use std::sync::WaitTimeoutResult to timeout ourselves if recv fails? 31 | let mut recv_count = 0; 32 | for _i in 0..RECEIVE_CHECK_COUNT { 33 | match server.recv().await { 34 | Ok((_header, msg)) => { 35 | if let mavlink::common::MavMessage::HEARTBEAT(_heartbeat_msg) = msg { 36 | recv_count += 1; 37 | } else { 38 | // one message parse failure fails the test 39 | break; 40 | } 41 | } 42 | Err(..) => { 43 | // one message read failure fails the test 44 | break; 45 | } 46 | } 47 | } 48 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 49 | } 50 | 51 | /// Test whether we can send a message via UDP and receive it OK using async_connect recv_raw 52 | #[tokio::test] 53 | async fn test_udp_loopback_recv_raw() { 54 | const RECEIVE_CHECK_COUNT: i32 = 3; 55 | 56 | let server = mavlink::connect_async::("udpin:0.0.0.0:14562") 57 | .await 58 | .expect("Couldn't create server"); 59 | 60 | // have the client send one heartbeat per second 61 | tokio::spawn({ 62 | async move { 63 | let msg = 64 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 65 | let client = mavlink::connect_async("udpout:127.0.0.1:14562") 66 | .await 67 | .expect("Couldn't create client"); 68 | loop { 69 | client.send_default(&msg).await.ok(); 70 | } 71 | } 72 | }); 73 | 74 | //TODO use std::sync::WaitTimeoutResult to timeout ourselves if recv fails? 75 | let mut recv_count = 0; 76 | for _i in 0..RECEIVE_CHECK_COUNT { 77 | match server.recv_raw().await { 78 | Ok(message) => { 79 | if message.message_id() == mavlink::common::HEARTBEAT_DATA::ID { 80 | recv_count += 1; 81 | } else { 82 | // one message parse failure fails the test 83 | break; 84 | } 85 | } 86 | Err(..) => { 87 | // one message read failure fails the test 88 | break; 89 | } 90 | } 91 | } 92 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mavlink/examples/embedded/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Target board: stm32f303RETx (stm32nucleo) 2 | //! Manual: https://www.st.com/resource/en/reference_manual/dm00043574-stm32f303xb-c-d-e-stm32f303x6-8-stm32f328x8-stm32f358xc-stm32f398xe-advanced-arm-based-mcus-stmicroelectronics.pdf 3 | #![no_main] 4 | #![no_std] 5 | 6 | // Panic handler 7 | use panic_halt as _; 8 | 9 | use cortex_m_rt::entry; 10 | use hal::pac; 11 | use hal::prelude::*; 12 | use mavlink; 13 | use stm32f3xx_hal as hal; 14 | 15 | #[entry] 16 | fn main() -> ! { 17 | // Peripherals access 18 | let dp = pac::Peripherals::take().unwrap(); 19 | let cp = cortex_m::Peripherals::take().unwrap(); 20 | 21 | // 9: RCC: Reset and clock control (RCC) 22 | let mut rcc = dp.RCC.constrain(); 23 | 24 | // Configure GPIOA using AHB 25 | // 9.4.6: AHB peripheral clock enable register (RCC_AHBENR) 26 | // Bit 17 IOPAEN: I/O port A clock enable 27 | let mut gpioa = dp.GPIOA.split(&mut rcc.ahb); 28 | 29 | // stm32nucleo has a LED on pin PA5 30 | let mut led = gpioa 31 | .pa5 32 | .into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper); 33 | 34 | // Constrains the FLASH peripheral to play nicely with the other abstractions 35 | let mut flash = dp.FLASH.constrain(); 36 | 37 | // Freezes the clock configuration 38 | // This function internally calculates the specific 39 | // divisors for the different clock peripheries 40 | // 4.5.1 Flash access control register (FLASH_ACR) 41 | let clocks = rcc.cfgr.freeze(&mut flash.acr); 42 | 43 | // USART2 uses Pins A9 and A10 44 | // We don't need the datasheet to check which alternative function to use 45 | // https://docs.rs/stm32f3xx-hal/0.6.1/stm32f3xx_hal/gpio/gpioa/struct.PA2.html#impl-TxPin%3CUSART2%3E 46 | // The documentation provide the necessary information about each possible hardware configuration 47 | let pin_tx = gpioa.pa2.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl); 48 | let pin_rx = gpioa.pa3.into_af_push_pull(&mut gpioa.moder, &mut gpioa.otyper, &mut gpioa.afrl); 49 | 50 | // Create an interface USART2 with 115200 baudrate 51 | let serial = hal::serial::Serial::new( 52 | dp.USART2, 53 | (pin_tx, pin_rx), 54 | 115_200.Bd(), 55 | clocks, 56 | &mut rcc.apb1, 57 | ); 58 | 59 | // Break serial in TX and RX (not used) 60 | let (mut tx, _) = serial.split(); 61 | 62 | // Create our mavlink header and heartbeat message 63 | let header = mavlink_header(); 64 | let heartbeat = mavlink_heartbeat_message(); 65 | 66 | // Create a delay object based on SysTick 67 | let mut delay = hal::delay::Delay::new(cp.SYST, clocks); 68 | 69 | // Main loop 70 | loop { 71 | // Write the mavlink message via serial 72 | mavlink::write_versioned_msg(&mut tx, mavlink::MavlinkVersion::V2, header, &heartbeat) 73 | .unwrap(); 74 | 75 | // Toggle the LED 76 | led.toggle().unwrap(); 77 | 78 | // Delay for 1 second 79 | delay.delay_ms(1_000u32); 80 | } 81 | } 82 | 83 | fn mavlink_header() -> mavlink::MavHeader { 84 | mavlink::MavHeader { 85 | system_id: 1, 86 | component_id: 1, 87 | sequence: 42, 88 | } 89 | } 90 | 91 | pub fn mavlink_heartbeat_message() -> mavlink::common::MavMessage { 92 | mavlink::common::MavMessage::HEARTBEAT(mavlink::common::HEARTBEAT_DATA { 93 | custom_mode: 0, 94 | mavtype: mavlink::common::MavType::MAV_TYPE_SUBMARINE, 95 | autopilot: mavlink::common::MavAutopilot::MAV_AUTOPILOT_ARDUPILOTMEGA, 96 | base_mode: mavlink::common::MavModeFlag::empty(), 97 | system_status: mavlink::common::MavState::MAV_STATE_STANDBY, 98 | mavlink_version: 0x3, 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /mavlink/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "mavlink" 4 | version = "0.16.2" 5 | authors = [ 6 | "Todd Stellanova", 7 | "Michal Podhradsky", 8 | "Kevin Mehall", 9 | "Tim Ryan", 10 | "Patrick José Pereira", 11 | "Ibiyemi Abiodun", 12 | ] 13 | build = "build/main.rs" 14 | description = "Implements the MAVLink data interchange format for UAVs." 15 | readme = "../README.md" 16 | license = "MIT/Apache-2.0" 17 | repository = "https://github.com/mavlink/rust-mavlink" 18 | edition.workspace = true 19 | rust-version.workspace = true 20 | keywords.workspace = true 21 | categories.workspace = true 22 | 23 | [build-dependencies] 24 | mavlink-bindgen = { version = "=0.16.2", path = "../mavlink-bindgen", default-features = false } 25 | 26 | [[example]] 27 | name = "mavlink-dump" 28 | path = "examples/mavlink-dump/src/main.rs" 29 | required-features = ["ardupilotmega"] 30 | 31 | [dependencies] 32 | mavlink-core = { version="=0.16.2", path = "../mavlink-core", default-features = false } 33 | num-traits = { workspace = true, default-features = false } 34 | num-derive = { workspace = true } 35 | bitflags = { workspace = true } 36 | serde = { version = "1.0.115", optional = true, features = ["derive"] } 37 | serde_arrays = { version = "0.2.0", optional = true } 38 | arbitrary = { version = "1.4", optional = true, features = ["derive"] } 39 | rand = { version = "0.9", optional = true, default-features = false, features = ["std", "std_rng"] } 40 | ts-rs = { version = "11.0.1", optional = true } 41 | 42 | [features] 43 | default = ["std", "tcp", "udp", "direct-serial", "serde", "ardupilotmega", "common", "format-generated-code"] 44 | 45 | all = [] 46 | ardupilotmega = [] 47 | asluav = [] 48 | avssuas = [] 49 | development = [] 50 | matrixpilot = [] 51 | minimal = [] 52 | paparazzi = [] 53 | python_array_test = [] 54 | standard = [] 55 | test = [] 56 | ualberta = [] 57 | uavionix = [] 58 | icarous = [] 59 | common = [] 60 | cubepilot = [] 61 | csairlink = [] 62 | loweheiser = [] 63 | storm32 = [] 64 | 65 | all-dialects = [ 66 | "ardupilotmega", 67 | "asluav", 68 | "avssuas", 69 | "development", 70 | "matrixpilot", 71 | "minimal", 72 | "paparazzi", 73 | "python_array_test", 74 | "standard", 75 | "test", 76 | "ualberta", 77 | "uavionix", 78 | "icarous", 79 | "common", 80 | "cubepilot", 81 | "storm32", 82 | "csairlink", 83 | "loweheiser" 84 | ] 85 | 86 | format-generated-code = [] 87 | emit-extensions = ["mavlink-bindgen/emit-extensions"] 88 | std = ["mavlink-core/std"] 89 | udp = ["mavlink-core/udp"] 90 | tcp = ["mavlink-core/tcp"] 91 | signing = ["mavlink-core/signing"] 92 | direct-serial = ["mavlink-core/direct-serial"] 93 | # NOTE: Only one of 'embedded' and 'embedded-hal-02' features can be enabled. 94 | # Use "embedded' feature to enable embedded-hal=1.0 (embedded-io and embedded-io-async is part of embedded-hal). 95 | # Use 'embedded-hal-0.2' feature to enable deprecated embedded-hal=0.2.3 (some hals is not supports embedded-hal=1.0 yet). 96 | embedded = ["mavlink-core/embedded"] 97 | embedded-hal-02 = ["mavlink-core/embedded-hal-02"] 98 | serde = ["bitflags/serde", "dep:serde", "dep:serde_arrays", "mavlink-core/serde"] 99 | tokio-1 = ["mavlink-core/tokio-1"] 100 | arbitrary = ["dep:arbitrary", "dep:rand", "mavlink-bindgen/arbitrary", "mavlink-core/arbitrary", "bitflags/arbitrary"] 101 | # Used for typescript generation 102 | ts = ["dep:ts-rs"] 103 | 104 | # build with all features on docs.rs so that users viewing documentation 105 | # can see everything 106 | [package.metadata.docs.rs] 107 | features = [ 108 | "default", 109 | "arbitrary", 110 | "all-dialects", 111 | "emit-extensions", 112 | "format-generated-code", 113 | "tokio-1", 114 | "signing" 115 | ] 116 | 117 | [dev-dependencies] 118 | tokio = { version = "1.0", default-features = false, features = ["macros", "rt", "time" ] } 119 | serde_json = { version = "1.0", features = ["preserve_order"] } 120 | serde_test = "1.0" 121 | 122 | [lints] 123 | workspace = true 124 | -------------------------------------------------------------------------------- /mavlink/tests/mav_frame_tests.rs: -------------------------------------------------------------------------------- 1 | pub mod test_shared; 2 | 3 | mod mav_frame_tests { 4 | use mavlink::MavFrame; 5 | 6 | // NOTE: No STX, length, or flag fields in the header 7 | pub const HEARTBEAT_V2: &[u8] = &[ 8 | // Currently [`MavFrame::deser`] and [`MavFrame::ser`] does not account for the first four fields. 9 | // 0xfd, // STX V2 10 | // 0x09, // len 11 | // 0x00, // incompat_flags 12 | // 0x00, // compat_flags 13 | crate::test_shared::COMMON_MSG_HEADER.sequence, 14 | crate::test_shared::COMMON_MSG_HEADER.system_id, 15 | crate::test_shared::COMMON_MSG_HEADER.component_id, 16 | 0x00, // msg ID 17 | 0x00, 18 | 0x00, 19 | 0x05, // payload 20 | 0x00, 21 | 0x00, 22 | 0x00, 23 | 0x02, 24 | 0x03, 25 | 0x59, 26 | 0x03, 27 | 0x03, 28 | 0x10, // checksum 29 | 0xf0, 30 | ]; 31 | 32 | #[cfg(feature = "common")] 33 | #[test] 34 | pub fn test_deser_ser() { 35 | use mavlink::{common::MavMessage, MavlinkVersion}; 36 | let frame = MavFrame::::deser(MavlinkVersion::V2, HEARTBEAT_V2) 37 | .expect("failed to parse message"); 38 | 39 | assert_eq!(frame.header, crate::test_shared::COMMON_MSG_HEADER); 40 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 41 | 42 | let mut buffer = [0u8; HEARTBEAT_V2.len()]; 43 | frame.ser(&mut buffer); 44 | assert_eq!(buffer[..buffer.len() - 2], HEARTBEAT_V2[..buffer.len() - 2]); 45 | 46 | let msg = match frame.msg { 47 | MavMessage::HEARTBEAT(msg) => msg, 48 | _ => panic!("Decoded wrong message type"), 49 | }; 50 | assert_eq!(msg.custom_mode, heartbeat_msg.custom_mode); 51 | assert_eq!(msg.mavtype, heartbeat_msg.mavtype); 52 | assert_eq!(msg.autopilot, heartbeat_msg.autopilot); 53 | assert_eq!(msg.base_mode, heartbeat_msg.base_mode); 54 | assert_eq!(msg.system_status, heartbeat_msg.system_status); 55 | assert_eq!(msg.mavlink_version, heartbeat_msg.mavlink_version); 56 | } 57 | 58 | #[cfg(feature = "ardupilotmega")] 59 | #[test] 60 | pub fn test_deser_ser_message() { 61 | let buf: &mut [u8; 255] = &mut [0; 255]; 62 | 63 | let mavlink_message = mavlink_message(); 64 | let mavlink_frame = new(mavlink_message); 65 | 66 | let _len = mavlink_frame.ser(buf); 67 | 68 | let parsed_mavlink_frame = 69 | MavFrame::::deser(mavlink::MavlinkVersion::V2, buf) 70 | .unwrap(); 71 | 72 | assert_eq!( 73 | format!("{mavlink_frame:?}"), 74 | format!("{parsed_mavlink_frame:?}") 75 | ); 76 | } 77 | 78 | #[cfg(feature = "ardupilotmega")] 79 | fn mavlink_message() -> mavlink::ardupilotmega::MavMessage { 80 | mavlink::ardupilotmega::MavMessage::LINK_NODE_STATUS( 81 | mavlink::ardupilotmega::LINK_NODE_STATUS_DATA { 82 | timestamp: 92197916, 83 | tx_rate: 0x11223344, 84 | rx_rate: 0x55667788, 85 | messages_sent: 0x99001122, 86 | messages_received: 0x33445566, 87 | messages_lost: 0x77889900, 88 | rx_parse_err: 0x1122, 89 | tx_overflows: 0x3355, 90 | rx_overflows: 0x5566, 91 | tx_buf: 0xff, 92 | rx_buf: 0x11, 93 | }, 94 | ) 95 | } 96 | 97 | #[cfg(feature = "ardupilotmega")] 98 | fn new( 99 | msg: mavlink::ardupilotmega::MavMessage, 100 | ) -> MavFrame { 101 | use mavlink::MavHeader; 102 | MavFrame { 103 | header: MavHeader { 104 | system_id: 1, 105 | component_id: 2, 106 | sequence: 84, 107 | }, 108 | msg, 109 | protocol_version: mavlink::MavlinkVersion::V2, 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /mavlink/tests/file_connection_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(feature = "common")] 4 | mod test_file_connections { 5 | use mavlink::ardupilotmega::MavMessage; 6 | use mavlink::MavConnection; 7 | 8 | /// Test whether we can send a message via TCP and receive it OK using async_connect. 9 | /// This also test signing as a property of a MavConnection if the signing feature is enabled. 10 | #[cfg(feature = "tokio-1")] 11 | #[tokio::test] 12 | pub async fn test_file_async_read_raw() { 13 | let tlog = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 14 | .join("tests/log.tlog") 15 | .canonicalize() 16 | .unwrap(); 17 | 18 | let tlog = tlog.to_str().unwrap(); 19 | 20 | let filename = std::path::Path::new(tlog); 21 | let filename = filename.to_str().unwrap(); 22 | dbg!(filename); 23 | 24 | println!("Processing file: {filename}"); 25 | let connection_string = format!("file:{filename}"); 26 | 27 | println!("connection_string - {connection_string}"); 28 | 29 | let vehicle = mavlink::connect_async::(&connection_string) 30 | .await 31 | .expect("Couldn't read from file"); 32 | 33 | let mut counter = 0; 34 | loop { 35 | match vehicle.recv_raw().await { 36 | Ok(raw_msg) => { 37 | println!( 38 | "raw_msg.component_id() {} | sequence number {} | message_id {:?}", 39 | raw_msg.component_id(), 40 | raw_msg.sequence(), 41 | raw_msg.message_id() 42 | ); 43 | 44 | counter += 1; 45 | } 46 | Err(mavlink::error::MessageReadError::Io(e)) => { 47 | if e.kind() == tokio::io::ErrorKind::UnexpectedEof { 48 | break; 49 | } 50 | } 51 | _ => { 52 | break; 53 | } 54 | } 55 | } 56 | 57 | println!("Number of parsed messages: {counter}"); 58 | assert!( 59 | counter == 1426, 60 | "Unable to hit the necessary amount of matches" 61 | ); 62 | } 63 | 64 | #[test] 65 | pub fn test_file_read_raw() { 66 | let tlog = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 67 | .join("tests/log.tlog") 68 | .canonicalize() 69 | .unwrap(); 70 | 71 | let tlog = tlog.to_str().unwrap(); 72 | 73 | let filename = std::path::Path::new(tlog); 74 | let filename = filename.to_str().unwrap(); 75 | dbg!(filename); 76 | 77 | println!("Processing file: {filename}"); 78 | let connection_string = format!("file:{filename}"); 79 | 80 | println!("connection_string - {connection_string}"); 81 | 82 | let vehicle = 83 | mavlink::connect::(&connection_string).expect("Couldn't read from file"); 84 | 85 | let mut counter = 0; 86 | loop { 87 | match vehicle.recv_raw() { 88 | Ok(raw_msg) => { 89 | println!( 90 | "raw_msg.component_id() {} | sequence number {} | message_id {:?}", 91 | raw_msg.component_id(), 92 | raw_msg.sequence(), 93 | raw_msg.message_id() 94 | ); 95 | 96 | counter += 1; 97 | } 98 | Err(mavlink::error::MessageReadError::Io(e)) => { 99 | if e.kind() == tokio::io::ErrorKind::UnexpectedEof { 100 | break; 101 | } 102 | } 103 | _ => { 104 | break; 105 | } 106 | } 107 | } 108 | 109 | println!("Number of parsed messages: {counter}"); 110 | assert!( 111 | counter == 1426, 112 | "Unable to hit the necessary amount of matches" 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /mavlink-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::bytes; 2 | use core::fmt::{Display, Formatter}; 3 | #[cfg(feature = "std")] 4 | use std::error::Error; 5 | 6 | /// Error while parsing a MAVLink message 7 | #[derive(Debug)] 8 | pub enum ParserError { 9 | /// Bit flag for this type is invalid 10 | InvalidFlag { flag_type: &'static str, value: u64 }, 11 | /// Enum value for this enum type does not exist 12 | InvalidEnum { enum_type: &'static str, value: u64 }, 13 | /// Message ID does not exist in this message set 14 | UnknownMessage { id: u32 }, 15 | /// Errors that occurred in the bytes module. 16 | BytesError(bytes::Error), 17 | } 18 | 19 | impl From for ParserError { 20 | fn from(error: bytes::Error) -> Self { 21 | Self::BytesError(error) 22 | } 23 | } 24 | 25 | impl Display for ParserError { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 27 | match self { 28 | Self::InvalidFlag { flag_type, value } => write!( 29 | f, 30 | "Invalid flag value for flag type {flag_type:?}, got {value:?}" 31 | ), 32 | Self::InvalidEnum { enum_type, value } => write!( 33 | f, 34 | "Invalid enum value for enum type {enum_type:?}, got {value:?}" 35 | ), 36 | Self::UnknownMessage { id } => write!(f, "Unknown message with ID {id:?}"), 37 | Self::BytesError(error) => write!(f, "{error}"), 38 | } 39 | } 40 | } 41 | 42 | #[cfg(feature = "std")] 43 | impl Error for ParserError {} 44 | 45 | /// Error while reading and parsing a MAVLink message 46 | #[derive(Debug)] 47 | pub enum MessageReadError { 48 | /// IO Error while reading 49 | #[cfg(feature = "std")] 50 | Io(std::io::Error), 51 | /// IO Error while reading 52 | #[cfg(any(feature = "embedded", feature = "embedded-hal-02"))] 53 | Io, 54 | /// Error while parsing 55 | Parse(ParserError), 56 | } 57 | 58 | impl MessageReadError { 59 | pub fn eof() -> Self { 60 | #[cfg(feature = "std")] 61 | return Self::Io(std::io::ErrorKind::UnexpectedEof.into()); 62 | #[cfg(any(feature = "embedded", feature = "embedded-hal-02"))] 63 | return Self::Io; 64 | } 65 | } 66 | 67 | impl Display for MessageReadError { 68 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 69 | match self { 70 | #[cfg(feature = "std")] 71 | Self::Io(e) => write!(f, "Failed to read message: {e:#?}"), 72 | #[cfg(any(feature = "embedded", feature = "embedded-hal-02"))] 73 | Self::Io => write!(f, "Failed to read message"), 74 | Self::Parse(e) => write!(f, "Failed to read message: {e:#?}"), 75 | } 76 | } 77 | } 78 | 79 | #[cfg(feature = "std")] 80 | impl Error for MessageReadError {} 81 | 82 | #[cfg(feature = "std")] 83 | impl From for MessageReadError { 84 | fn from(e: std::io::Error) -> Self { 85 | Self::Io(e) 86 | } 87 | } 88 | 89 | impl From for MessageReadError { 90 | fn from(e: ParserError) -> Self { 91 | Self::Parse(e) 92 | } 93 | } 94 | 95 | /// Error while writing a MAVLink message 96 | #[derive(Debug)] 97 | pub enum MessageWriteError { 98 | /// IO Error while writing 99 | #[cfg(feature = "std")] 100 | Io(std::io::Error), 101 | /// IO Error while writing 102 | #[cfg(any(feature = "embedded", feature = "embedded-hal-02"))] 103 | Io, 104 | /// Message does not support MAVLink 1 105 | MAVLink2Only, 106 | } 107 | 108 | impl Display for MessageWriteError { 109 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 110 | match self { 111 | #[cfg(feature = "std")] 112 | Self::Io(e) => write!(f, "Failed to write message: {e:#?}"), 113 | #[cfg(any(feature = "embedded", feature = "embedded-hal-02"))] 114 | Self::Io => write!(f, "Failed to write message"), 115 | Self::MAVLink2Only => write!(f, "Message is not supported in MAVLink 1"), 116 | } 117 | } 118 | } 119 | 120 | #[cfg(feature = "std")] 121 | impl Error for MessageWriteError {} 122 | 123 | #[cfg(feature = "std")] 124 | impl From for MessageWriteError { 125 | fn from(e: std::io::Error) -> Self { 126 | Self::Io(e) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test all targets 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | formatting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v6 10 | - uses: dtolnay/rust-toolchain@stable 11 | with: 12 | components: rustfmt 13 | - name: Run rustfmt 14 | run: cargo fmt --all -- --check 15 | 16 | linting: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: dtolnay/rust-toolchain@nightly 21 | with: 22 | components: clippy 23 | - uses: actions-rs-plus/clippy-check@v2 24 | with: 25 | args: --all --all-targets --features format-generated-code --features signing --features tokio-1 26 | 27 | internal-tests: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | dialect: ["ardupilotmega", "asluav", "matrixpilot", "minimal", "paparazzi", "python_array_test", "standard", "test", "ualberta", "uavionix", "icarous", "common", "storm32", "csairlink", "loweheiser"] 33 | signing: ["", "--features signing"] 34 | steps: 35 | - uses: actions/checkout@v6 36 | - uses: dtolnay/rust-toolchain@stable 37 | with: 38 | components: rustfmt 39 | - name: Run internal tests 40 | run: cargo test --verbose --features arbitrary,${{ matrix.dialect }} ${{ matrix.signing }} -- --nocapture 41 | 42 | mavlink-dump: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v6 46 | - uses: dtolnay/rust-toolchain@stable 47 | - name: Build mavlink-dump 48 | run: cargo build --verbose --example mavlink-dump 49 | 50 | msrv: 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | features: ["", "--features serde,tokio-1", "--features signing"] 55 | steps: 56 | - uses: actions/checkout@v6 57 | - uses: taiki-e/install-action@cargo-hack 58 | - run: cargo hack --no-dev-deps --rust-version check --package mavlink ${{ matrix.features }} 59 | 60 | build: 61 | needs: [formatting, linting, internal-tests, mavlink-dump, msrv] 62 | runs-on: ${{ matrix.os }} 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | include: 67 | - os: macos-latest 68 | TARGET: x86_64-apple-darwin 69 | FEATURES: --features ardupilotmega 70 | 71 | - os: ubuntu-latest 72 | TARGET: arm-unknown-linux-musleabihf 73 | FLAGS: --features ardupilotmega 74 | 75 | - os: ubuntu-latest 76 | TARGET: armv7-unknown-linux-musleabihf 77 | FLAGS: --features ardupilotmega 78 | 79 | - os: ubuntu-latest 80 | TARGET: x86_64-unknown-linux-musl 81 | FLAGS: --features ardupilotmega 82 | 83 | - os: ubuntu-latest 84 | TARGET: x86_64-unknown-linux-musl 85 | FLAGS: --features ardupilotmega,emit-extensions 86 | 87 | - os: ubuntu-latest 88 | TARGET: thumbv7m-none-eabi 89 | FLAGS: --no-default-features --features embedded 90 | 91 | - os: windows-latest 92 | TARGET: x86_64-pc-windows-msvc 93 | FLAGS: --features ardupilotmega 94 | 95 | steps: 96 | - name: Building ${{ matrix.TARGET }} 97 | run: echo "${{ matrix.TARGET }}" 98 | - uses: actions/checkout@v6 99 | - uses: dtolnay/rust-toolchain@stable 100 | with: 101 | target: ${{ matrix.TARGET }} 102 | - uses: actions-rs/cargo@v1 103 | with: 104 | use-cross: true 105 | command: build 106 | args: --verbose --release --package=mavlink --target=${{ matrix.TARGET }} ${{ matrix.FLAGS }} 107 | 108 | test-embedded-size: 109 | needs: build 110 | runs-on: ubuntu-latest 111 | steps: 112 | - uses: actions/checkout@v6 113 | - uses: dtolnay/rust-toolchain@nightly 114 | with: 115 | target: thumbv7em-none-eabihf 116 | - name: Build 117 | working-directory: ./mavlink/examples/embedded 118 | run: cargo +nightly build --out-dir $PWD/../../.. --release -Z unstable-options 119 | 120 | docs: 121 | needs: internal-tests 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v6 125 | - uses: dtolnay/rust-toolchain@nightly 126 | with: 127 | components: rustfmt 128 | - name: Build docs 129 | run: RUSTDOCFLAGS=--cfg=docsrs cargo doc --no-deps --features "default all-dialects emit-extensions format-generated-code tokio-1 signing" 130 | - name: Deploy 131 | uses: peaceiris/actions-gh-pages@v3 132 | if: ${{ github.ref == 'refs/heads/master' }} 133 | with: 134 | github_token: ${{ secrets.GITHUB_TOKEN }} 135 | publish_dir: ./target/doc 136 | -------------------------------------------------------------------------------- /mavlink-core/src/async_connection/file.rs: -------------------------------------------------------------------------------- 1 | //! Async File MAVLINK connection 2 | use core::ops::DerefMut; 3 | use std::io; 4 | use std::path::PathBuf; 5 | 6 | use super::{AsyncConnectable, AsyncMavConnection}; 7 | use crate::connection::file::config::FileConfig; 8 | use crate::error::{MessageReadError, MessageWriteError}; 9 | use crate::{ 10 | async_peek_reader::AsyncPeekReader, MAVLinkMessageRaw, MavHeader, MavlinkVersion, Message, 11 | ReadVersion, 12 | }; 13 | 14 | use async_trait::async_trait; 15 | use futures::lock::Mutex; 16 | use tokio::fs::File; 17 | 18 | #[cfg(not(feature = "signing"))] 19 | use crate::{read_versioned_msg_async, read_versioned_raw_message_async}; 20 | 21 | #[cfg(feature = "signing")] 22 | use crate::{ 23 | read_versioned_msg_async_signed, read_versioned_raw_message_async_signed, SigningConfig, 24 | SigningData, 25 | }; 26 | 27 | pub async fn open(file_path: &PathBuf) -> io::Result { 28 | let file = File::open(file_path).await?; 29 | Ok(AsyncFileConnection { 30 | file: Mutex::new(AsyncPeekReader::new(file)), 31 | protocol_version: MavlinkVersion::V2, 32 | recv_any_version: false, 33 | #[cfg(feature = "signing")] 34 | signing_data: None, 35 | }) 36 | } 37 | 38 | pub struct AsyncFileConnection { 39 | file: Mutex>, 40 | protocol_version: MavlinkVersion, 41 | recv_any_version: bool, 42 | #[cfg(feature = "signing")] 43 | signing_data: Option, 44 | } 45 | 46 | #[async_trait::async_trait] 47 | impl AsyncMavConnection for AsyncFileConnection { 48 | async fn recv_raw(&self) -> Result { 49 | let mut file = self.file.lock().await; 50 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 51 | loop { 52 | #[cfg(not(feature = "signing"))] 53 | let result = read_versioned_raw_message_async::(file.deref_mut(), version).await; 54 | #[cfg(feature = "signing")] 55 | let result = read_versioned_raw_message_async_signed::( 56 | file.deref_mut(), 57 | version, 58 | self.signing_data.as_ref(), 59 | ) 60 | .await; 61 | match result { 62 | ok @ Ok(..) => { 63 | return ok; 64 | } 65 | Err(MessageReadError::Io(e)) => { 66 | if e.kind() == io::ErrorKind::UnexpectedEof { 67 | return Err(MessageReadError::Io(e)); 68 | } 69 | } 70 | _ => {} 71 | } 72 | } 73 | } 74 | 75 | async fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 76 | let mut file = self.file.lock().await; 77 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 78 | loop { 79 | #[cfg(not(feature = "signing"))] 80 | let result = read_versioned_msg_async(file.deref_mut(), version).await; 81 | #[cfg(feature = "signing")] 82 | let result = read_versioned_msg_async_signed( 83 | file.deref_mut(), 84 | version, 85 | self.signing_data.as_ref(), 86 | ) 87 | .await; 88 | match result { 89 | ok @ Ok(..) => { 90 | return ok; 91 | } 92 | Err(MessageReadError::Io(e)) => { 93 | if e.kind() == io::ErrorKind::UnexpectedEof { 94 | return Err(MessageReadError::Io(e)); 95 | } 96 | } 97 | _ => {} 98 | } 99 | } 100 | } 101 | 102 | async fn send(&self, _header: &MavHeader, _data: &M) -> Result { 103 | Ok(0) 104 | } 105 | 106 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 107 | self.protocol_version = version; 108 | } 109 | 110 | fn protocol_version(&self) -> MavlinkVersion { 111 | self.protocol_version 112 | } 113 | 114 | fn set_allow_recv_any_version(&mut self, allow: bool) { 115 | self.recv_any_version = allow; 116 | } 117 | 118 | fn allow_recv_any_version(&self) -> bool { 119 | self.recv_any_version 120 | } 121 | 122 | #[cfg(feature = "signing")] 123 | fn setup_signing(&mut self, signing_data: Option) { 124 | self.signing_data = signing_data.map(SigningData::from_config); 125 | } 126 | } 127 | 128 | #[async_trait] 129 | impl AsyncConnectable for FileConfig { 130 | async fn connect_async(&self) -> io::Result + Sync + Send>> 131 | where 132 | M: Message + Sync + Send, 133 | { 134 | Ok(Box::new(open(&self.address).await?)) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/file.rs: -------------------------------------------------------------------------------- 1 | //! File MAVLINK connection 2 | 3 | use crate::connection::{Connection, MavConnection}; 4 | use crate::error::{MessageReadError, MessageWriteError}; 5 | use crate::peek_reader::PeekReader; 6 | use crate::{Connectable, MAVLinkMessageRaw}; 7 | use crate::{MavHeader, MavlinkVersion, Message, ReadVersion}; 8 | use core::ops::DerefMut; 9 | use std::fs::File; 10 | use std::io; 11 | use std::path::PathBuf; 12 | use std::sync::Mutex; 13 | 14 | #[cfg(not(feature = "signing"))] 15 | use crate::{read_versioned_msg, read_versioned_raw_message}; 16 | #[cfg(feature = "signing")] 17 | use crate::{ 18 | read_versioned_msg_signed, read_versioned_raw_message_signed, SigningConfig, SigningData, 19 | }; 20 | 21 | pub mod config; 22 | 23 | use config::FileConfig; 24 | 25 | pub fn open(file_path: &PathBuf) -> io::Result { 26 | let file = File::open(file_path)?; 27 | 28 | Ok(FileConnection { 29 | file: Mutex::new(PeekReader::new(file)), 30 | protocol_version: MavlinkVersion::V2, 31 | #[cfg(feature = "signing")] 32 | signing_data: None, 33 | recv_any_version: false, 34 | }) 35 | } 36 | 37 | pub struct FileConnection { 38 | file: Mutex>, 39 | protocol_version: MavlinkVersion, 40 | recv_any_version: bool, 41 | #[cfg(feature = "signing")] 42 | signing_data: Option, 43 | } 44 | 45 | impl MavConnection for FileConnection { 46 | fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 47 | let mut file = self.file.lock().unwrap(); 48 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 49 | 50 | loop { 51 | #[cfg(not(feature = "signing"))] 52 | let result = read_versioned_msg(file.deref_mut(), version); 53 | #[cfg(feature = "signing")] 54 | let result = 55 | read_versioned_msg_signed(file.deref_mut(), version, self.signing_data.as_ref()); 56 | match result { 57 | ok @ Ok(..) => { 58 | return ok; 59 | } 60 | Err(MessageReadError::Io(e)) => { 61 | if e.kind() == io::ErrorKind::UnexpectedEof { 62 | return Err(MessageReadError::Io(e)); 63 | } 64 | } 65 | _ => {} 66 | } 67 | } 68 | } 69 | 70 | fn recv_raw(&self) -> Result { 71 | let mut file = self.file.lock().unwrap(); 72 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 73 | 74 | loop { 75 | #[cfg(not(feature = "signing"))] 76 | let result = read_versioned_raw_message::(file.deref_mut(), version); 77 | #[cfg(feature = "signing")] 78 | let result = read_versioned_raw_message_signed::( 79 | file.deref_mut(), 80 | version, 81 | self.signing_data.as_ref(), 82 | ); 83 | match result { 84 | ok @ Ok(..) => { 85 | return ok; 86 | } 87 | Err(MessageReadError::Io(e)) => { 88 | if e.kind() == io::ErrorKind::UnexpectedEof { 89 | return Err(MessageReadError::Io(e)); 90 | } 91 | } 92 | _ => {} 93 | } 94 | } 95 | } 96 | 97 | fn try_recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 98 | let mut file = self.file.lock().unwrap(); 99 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 100 | 101 | #[cfg(not(feature = "signing"))] 102 | let result = read_versioned_msg(file.deref_mut(), version); 103 | #[cfg(feature = "signing")] 104 | let result = 105 | read_versioned_msg_signed(file.deref_mut(), version, self.signing_data.as_ref()); 106 | 107 | result 108 | } 109 | 110 | fn send(&self, _header: &MavHeader, _data: &M) -> Result { 111 | Ok(0) 112 | } 113 | 114 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 115 | self.protocol_version = version; 116 | } 117 | 118 | fn protocol_version(&self) -> MavlinkVersion { 119 | self.protocol_version 120 | } 121 | 122 | fn set_allow_recv_any_version(&mut self, allow: bool) { 123 | self.recv_any_version = allow; 124 | } 125 | 126 | fn allow_recv_any_version(&self) -> bool { 127 | self.recv_any_version 128 | } 129 | 130 | #[cfg(feature = "signing")] 131 | fn setup_signing(&mut self, signing_data: Option) { 132 | self.signing_data = signing_data.map(SigningData::from_config); 133 | } 134 | } 135 | 136 | impl Connectable for FileConfig { 137 | fn connect(&self) -> io::Result> { 138 | Ok(open(&self.address)?.into()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /mavlink-core/src/connectable.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | use std::io; 3 | use std::path::PathBuf; 4 | 5 | #[cfg(feature = "direct-serial")] 6 | use crate::connection::direct_serial::config::SerialConfig; 7 | use crate::connection::file::config::FileConfig; 8 | #[cfg(feature = "tcp")] 9 | use crate::connection::tcp::config::{TcpConfig, TcpMode}; 10 | #[cfg(feature = "udp")] 11 | use crate::connection::udp::config::{UdpConfig, UdpMode}; 12 | 13 | /// A parsed MAVLink connection address 14 | pub enum ConnectionAddress { 15 | /// TCP client or server address 16 | #[cfg(feature = "tcp")] 17 | Tcp(TcpConfig), 18 | /// UDP client, server or broadcast address 19 | #[cfg(feature = "udp")] 20 | Udp(UdpConfig), 21 | /// Serial port address 22 | #[cfg(feature = "direct-serial")] 23 | Serial(SerialConfig), 24 | /// File input address 25 | File(FileConfig), 26 | } 27 | 28 | #[cfg(feature = "tcp")] 29 | impl From for ConnectionAddress { 30 | fn from(value: TcpConfig) -> Self { 31 | Self::Tcp(value) 32 | } 33 | } 34 | 35 | #[cfg(feature = "udp")] 36 | impl From for ConnectionAddress { 37 | fn from(value: UdpConfig) -> Self { 38 | Self::Udp(value) 39 | } 40 | } 41 | 42 | #[cfg(feature = "direct-serial")] 43 | impl From for ConnectionAddress { 44 | fn from(value: SerialConfig) -> Self { 45 | Self::Serial(value) 46 | } 47 | } 48 | 49 | impl From for ConnectionAddress { 50 | fn from(value: FileConfig) -> Self { 51 | Self::File(value) 52 | } 53 | } 54 | 55 | impl Display for ConnectionAddress { 56 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 57 | match self { 58 | #[cfg(feature = "tcp")] 59 | Self::Tcp(connectable) => write!(f, "{connectable}"), 60 | #[cfg(feature = "udp")] 61 | Self::Udp(connectable) => write!(f, "{connectable}"), 62 | #[cfg(feature = "direct-serial")] 63 | Self::Serial(connectable) => write!(f, "{connectable}"), 64 | Self::File(connectable) => write!(f, "{connectable}"), 65 | } 66 | } 67 | } 68 | 69 | impl ConnectionAddress { 70 | /// Parse a MAVLink address string. 71 | /// 72 | /// The address must be in one of the following formats: 73 | /// 74 | /// * `tcpin::` to create a TCP server, listening for an incoming connection 75 | /// * `tcpout::` to create a TCP client 76 | /// * `udpin::` to create a UDP server, listening for incoming packets 77 | /// * `udpout::` to create a UDP client 78 | /// * `udpbcast::` to create a UDP broadcast 79 | /// * `serial::` to create a serial connection 80 | /// * `file:` to extract file data, writing to such a connection does nothing 81 | /// 82 | /// # Errors 83 | /// 84 | /// - [`AddrNotAvailable`] if the address string could not be parsed as a valid MAVLink address 85 | /// 86 | /// [`AddrNotAvailable`]: io::ErrorKind::AddrNotAvailable 87 | pub fn parse_address(address: &str) -> Result { 88 | let (protocol, address) = address.split_once(':').ok_or(io::Error::new( 89 | io::ErrorKind::AddrNotAvailable, 90 | "Protocol unsupported", 91 | ))?; 92 | let conn = match protocol { 93 | #[cfg(feature = "direct-serial")] 94 | "serial" => { 95 | let (port_name, baud) = address.split_once(':').ok_or(io::Error::new( 96 | io::ErrorKind::AddrNotAvailable, 97 | "Incomplete port settings", 98 | ))?; 99 | Self::Serial(SerialConfig::new( 100 | port_name.to_string(), 101 | baud.parse().map_err(|_| { 102 | io::Error::new(io::ErrorKind::AddrNotAvailable, "Invalid baud rate") 103 | })?, 104 | )) 105 | } 106 | #[cfg(feature = "tcp")] 107 | "tcpin" | "tcpout" => { 108 | let mode = if protocol == "tcpout" { 109 | TcpMode::TcpOut 110 | } else { 111 | TcpMode::TcpIn 112 | }; 113 | Self::Tcp(TcpConfig::new(address.to_string(), mode)) 114 | } 115 | #[cfg(feature = "udp")] 116 | "udpin" | "udpout" | "udpcast" => Self::Udp(UdpConfig::new( 117 | address.to_string(), 118 | match protocol { 119 | "udpin" => UdpMode::Udpin, 120 | "udpout" => UdpMode::Udpout, 121 | "udpcast" => UdpMode::Udpcast, 122 | _ => unreachable!(), 123 | }, 124 | )), 125 | "file" => Self::File(FileConfig::new(PathBuf::from(address))), 126 | _ => { 127 | return Err(io::Error::new( 128 | io::ErrorKind::AddrNotAvailable, 129 | "Protocol unsupported", 130 | )) 131 | } 132 | }; 133 | Ok(conn) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /mavlink/tests/tcp_loopback_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(all(feature = "std", feature = "tcp", feature = "common"))] 4 | mod test_tcp_connections { 5 | use std::thread; 6 | 7 | #[cfg(feature = "signing")] 8 | use crate::test_shared; 9 | #[cfg(feature = "signing")] 10 | use mavlink::SigningConfig; 11 | use mavlink::{MavConnection, MessageData}; 12 | 13 | /// Test whether we can send a message via TCP and receive it OK. This also test signing as a property of a MavConnection if the signing feature is enabled. 14 | #[test] 15 | fn test_tcp_loopback() { 16 | const RECEIVE_CHECK_COUNT: i32 = 5; 17 | 18 | #[cfg(feature = "signing")] 19 | let singing_cfg_server = SigningConfig::new(test_shared::SECRET_KEY, 0, true, false); 20 | #[cfg(feature = "signing")] 21 | let singing_cfg_client = singing_cfg_server.clone(); 22 | 23 | let server_thread = thread::spawn(move || { 24 | //TODO consider using get_available_port to use a random port 25 | #[allow(unused_mut)] 26 | let mut server = 27 | mavlink::connect("tcpin:0.0.0.0:14550").expect("Couldn't create server"); 28 | 29 | #[cfg(feature = "signing")] 30 | server.setup_signing(Some(singing_cfg_server)); 31 | 32 | let mut recv_count = 0; 33 | for _i in 0..RECEIVE_CHECK_COUNT { 34 | match server.recv() { 35 | Ok((_header, msg)) => { 36 | if let mavlink::common::MavMessage::HEARTBEAT(_heartbeat_msg) = msg { 37 | recv_count += 1; 38 | } else { 39 | // one message parse failure fails the test 40 | break; 41 | } 42 | } 43 | Err(..) => { 44 | // one message read failure fails the test 45 | break; 46 | } 47 | } 48 | } 49 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 50 | }); 51 | 52 | // Give some time for the server to connect 53 | thread::sleep(std::time::Duration::from_millis(100)); 54 | 55 | // have the client send a few hearbeats 56 | thread::spawn(move || { 57 | let msg = 58 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 59 | #[allow(unused_mut)] 60 | let mut client = 61 | mavlink::connect("tcpout:127.0.0.1:14550").expect("Couldn't create client"); 62 | 63 | #[cfg(feature = "signing")] 64 | client.setup_signing(Some(singing_cfg_client)); 65 | 66 | for _i in 0..RECEIVE_CHECK_COUNT { 67 | client.send_default(&msg).ok(); 68 | } 69 | }); 70 | 71 | server_thread.join().unwrap(); 72 | } 73 | 74 | /// Test whether we can send a message via TCP and receive it OK as a raw message. This also test signing as a property of a MavConnection if the signing feature is enabled. 75 | #[test] 76 | fn test_tcp_loopback_recv_raw() { 77 | const RECEIVE_CHECK_COUNT: i32 = 5; 78 | 79 | #[cfg(feature = "signing")] 80 | let singing_cfg_server = SigningConfig::new(test_shared::SECRET_KEY, 0, true, false); 81 | #[cfg(feature = "signing")] 82 | let singing_cfg_client = singing_cfg_server.clone(); 83 | 84 | let server_thread = thread::spawn(move || { 85 | //TODO consider using get_available_port to use a random port 86 | #[allow(unused_mut)] 87 | let mut server = mavlink::connect::("tcpin:0.0.0.0:14560") 88 | .expect("Couldn't create server"); 89 | 90 | #[cfg(feature = "signing")] 91 | server.setup_signing(Some(singing_cfg_server)); 92 | 93 | let mut recv_count = 0; 94 | for _i in 0..RECEIVE_CHECK_COUNT { 95 | match server.recv_raw() { 96 | Ok(message) => { 97 | if message.message_id() == mavlink::common::HEARTBEAT_DATA::ID { 98 | recv_count += 1; 99 | } else { 100 | // one message parse failure fails the test 101 | break; 102 | } 103 | } 104 | Err(..) => { 105 | // one message read failure fails the test 106 | break; 107 | } 108 | } 109 | } 110 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 111 | }); 112 | 113 | // Give some time for the server to connect 114 | thread::sleep(std::time::Duration::from_millis(100)); 115 | 116 | // have the client send a few hearbeats 117 | thread::spawn(move || { 118 | let msg = 119 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 120 | #[allow(unused_mut)] 121 | let mut client = 122 | mavlink::connect("tcpout:127.0.0.1:14560").expect("Couldn't create client"); 123 | 124 | #[cfg(feature = "signing")] 125 | client.setup_signing(Some(singing_cfg_client)); 126 | 127 | for _i in 0..RECEIVE_CHECK_COUNT { 128 | client.send_default(&msg).ok(); 129 | } 130 | }); 131 | 132 | server_thread.join().unwrap(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /mavlink-core/src/types.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Deref, DerefMut}; 2 | 3 | #[cfg(feature = "serde")] 4 | use crate::utils::nulstr; 5 | #[cfg(feature = "serde")] 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[cfg(feature = "arbitrary")] 9 | use arbitrary::{Arbitrary, Unstructured}; 10 | 11 | /// Abstraction around a byte array that represents a string. 12 | /// 13 | /// MAVLink encodes strings as C char arrays and the handling is field dependent. 14 | /// This abstration allows to choose if one wants to handle the field as 15 | /// a raw byte array or if one wants the convenience of a str that stops at the first null byte. 16 | /// 17 | /// # Example 18 | /// ``` 19 | /// use mavlink_core::types::CharArray; 20 | /// 21 | /// let data = [0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x00, 0x57, 0x4f, 0x52, 0x4c, 0x44, 0x00, 0x00, 0x00]; 22 | /// let ca = CharArray::new(data); 23 | /// assert_eq!(ca.to_str().unwrap(), "HELLO"); 24 | /// // or using the from str method 25 | /// let ca: CharArray<10> = "HELLO!".into(); 26 | /// assert_eq!(ca.to_str().unwrap(), "HELLO!"); 27 | /// 28 | /// ``` 29 | #[cfg_attr(feature = "serde", derive(Serialize))] 30 | #[cfg_attr(feature = "serde", serde(transparent))] 31 | #[derive(Debug, PartialEq, Clone, Copy)] 32 | pub struct CharArray { 33 | #[cfg_attr( 34 | feature = "serde", 35 | serde(serialize_with = "nulstr::serialize::<_, N>",) 36 | )] 37 | data: [u8; N], 38 | 39 | #[cfg_attr(feature = "serde", serde(skip))] 40 | str_len: usize, 41 | } 42 | 43 | impl CharArray { 44 | pub const fn new(data: [u8; N]) -> Self { 45 | // Note: The generated code uses this in const contexts, so this is a const fn 46 | // and so we can't use iterators or other fancy stuff unfortunately. 47 | let mut first_null = N; 48 | let mut i = 0; 49 | loop { 50 | if i >= N { 51 | break; 52 | } 53 | if data[i] == 0 { 54 | first_null = i; 55 | break; 56 | } 57 | i += 1; 58 | } 59 | Self { 60 | data, 61 | str_len: first_null, 62 | } 63 | } 64 | 65 | /// Get the string representation of the char array. 66 | /// Returns the string stopping at the first null byte and if the string is not valid utf8 67 | /// the returned string will be empty. 68 | pub fn to_str(&self) -> Result<&str, core::str::Utf8Error> { 69 | core::str::from_utf8(&self.data[..self.str_len]) 70 | } 71 | } 72 | 73 | impl Deref for CharArray { 74 | type Target = [u8; N]; 75 | 76 | fn deref(&self) -> &Self::Target { 77 | &self.data 78 | } 79 | } 80 | 81 | impl DerefMut for CharArray { 82 | fn deref_mut(&mut self) -> &mut Self::Target { 83 | &mut self.data 84 | } 85 | } 86 | 87 | impl<'a, const N: usize> IntoIterator for &'a CharArray { 88 | type Item = &'a u8; 89 | type IntoIter = core::slice::Iter<'a, u8>; 90 | 91 | fn into_iter(self) -> Self::IntoIter { 92 | self.data.iter() 93 | } 94 | } 95 | 96 | impl From<[u8; N]> for CharArray { 97 | fn from(data: [u8; N]) -> Self { 98 | Self::new(data) 99 | } 100 | } 101 | 102 | impl From> for [u8; N] { 103 | fn from(value: CharArray) -> Self { 104 | value.data 105 | } 106 | } 107 | 108 | impl From<&str> for CharArray { 109 | fn from(s: &str) -> Self { 110 | let mut data = [0u8; N]; 111 | let bytes = s.as_bytes(); 112 | let len = bytes.len().min(N); 113 | data[..len].copy_from_slice(&bytes[..len]); 114 | Self::new(data) 115 | } 116 | } 117 | 118 | impl crate::utils::RustDefault for CharArray { 119 | #[inline(always)] 120 | fn rust_default() -> Self { 121 | Self::new([0u8; N]) 122 | } 123 | } 124 | 125 | #[cfg(feature = "serde")] 126 | impl<'de, const N: usize> Deserialize<'de> for CharArray { 127 | fn deserialize(deserializer: D) -> Result 128 | where 129 | D: serde::Deserializer<'de>, 130 | { 131 | let data: [u8; N] = nulstr::deserialize(deserializer)?; 132 | Ok(Self::new(data)) 133 | } 134 | } 135 | 136 | #[cfg(feature = "arbitrary")] 137 | impl<'a, const N: usize> Arbitrary<'a> for CharArray { 138 | fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { 139 | let mut data = [0u8; N]; 140 | 141 | for b in &mut data { 142 | // Take a char from the printable ASCII range. 143 | *b = u.int_in_range(32..=126)?; 144 | } 145 | 146 | Ok(Self::new(data)) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::CharArray; 153 | 154 | #[test] 155 | fn char_array_to_str_handles_no_nulls() { 156 | let data = *b"HELLOWORLD"; 157 | let ca = CharArray::new(data); 158 | assert_eq!(ca.len(), 10); 159 | assert_eq!(ca.to_str().unwrap(), "HELLOWORLD"); 160 | } 161 | 162 | #[test] 163 | fn char_array_to_str_trims_after_first_null() { 164 | let mut data = [0u8; 10]; 165 | data[..3].copy_from_slice(b"abc"); 166 | // data[3..] are zeros 167 | let ca = CharArray::new(data); 168 | assert_eq!(ca.len(), 10); 169 | assert_eq!(ca.to_str().unwrap(), "abc"); 170 | } 171 | 172 | #[test] 173 | fn char_array_from_str_into_str() { 174 | let ca: CharArray<10> = "HELLOWORLD".into(); 175 | assert_eq!(ca.len(), 10); 176 | assert_eq!(ca.to_str().unwrap(), "HELLOWORLD"); 177 | 178 | let ca: CharArray<10> = "abc".into(); 179 | assert_eq!(ca.len(), 10); 180 | assert_eq!(ca.to_str().unwrap(), "abc"); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /mavlink/tests/tcp_loopback_async_tests.rs: -------------------------------------------------------------------------------- 1 | mod test_shared; 2 | 3 | #[cfg(all(feature = "tokio-1", feature = "tcp", feature = "common"))] 4 | mod test_tcp_connections { 5 | #[cfg(feature = "signing")] 6 | use crate::test_shared; 7 | use mavlink::MessageData; 8 | #[cfg(feature = "signing")] 9 | use mavlink::SigningConfig; 10 | 11 | /// Test whether we can send a message via TCP and receive it OK using async_connect. 12 | /// This also test signing as a property of a MavConnection if the signing feature is enabled. 13 | #[tokio::test] 14 | async fn test_tcp_loopback() { 15 | const RECEIVE_CHECK_COUNT: i32 = 5; 16 | 17 | #[cfg(feature = "signing")] 18 | let singing_cfg_server = SigningConfig::new(test_shared::SECRET_KEY, 0, true, false); 19 | #[cfg(feature = "signing")] 20 | let singing_cfg_client = singing_cfg_server.clone(); 21 | 22 | let server_thread = tokio::spawn(async move { 23 | //TODO consider using get_available_port to use a random port 24 | #[allow(unused_mut)] 25 | let mut server = mavlink::connect_async("tcpin:0.0.0.0:14551") 26 | .await 27 | .expect("Couldn't create server"); 28 | 29 | #[cfg(feature = "signing")] 30 | server.setup_signing(Some(singing_cfg_server)); 31 | 32 | let mut recv_count = 0; 33 | for _i in 0..RECEIVE_CHECK_COUNT { 34 | match server.recv().await { 35 | Ok((_header, msg)) => { 36 | if let mavlink::common::MavMessage::HEARTBEAT(_heartbeat_msg) = msg { 37 | recv_count += 1; 38 | } else { 39 | // one message parse failure fails the test 40 | break; 41 | } 42 | } 43 | Err(..) => { 44 | // one message read failure fails the test 45 | break; 46 | } 47 | } 48 | } 49 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 50 | }); 51 | 52 | // Give some time for the server to connect 53 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 54 | 55 | // have the client send a few hearbeats 56 | tokio::spawn(async move { 57 | let msg = 58 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 59 | #[allow(unused_mut)] 60 | let mut client = mavlink::connect_async("tcpout:127.0.0.1:14551") 61 | .await 62 | .expect("Couldn't create client"); 63 | 64 | #[cfg(feature = "signing")] 65 | client.setup_signing(Some(singing_cfg_client)); 66 | 67 | for _i in 0..RECEIVE_CHECK_COUNT { 68 | client.send_default(&msg).await.ok(); 69 | } 70 | }); 71 | 72 | server_thread.await.unwrap(); 73 | } 74 | 75 | /// Test whether we can send a message via TCP and receive it OK using async_connect recv_raw. 76 | /// This also test signing as a property of a MavConnection if the signing feature is enabled. 77 | #[tokio::test] 78 | async fn test_tcp_loopback_recv_raw() { 79 | const RECEIVE_CHECK_COUNT: i32 = 5; 80 | 81 | #[cfg(feature = "signing")] 82 | let singing_cfg_server = SigningConfig::new(test_shared::SECRET_KEY, 0, true, false); 83 | #[cfg(feature = "signing")] 84 | let singing_cfg_client = singing_cfg_server.clone(); 85 | 86 | let server_thread = tokio::spawn(async move { 87 | //TODO consider using get_available_port to use a random port 88 | #[allow(unused_mut)] 89 | let mut server = 90 | mavlink::connect_async::("tcpin:0.0.0.0:14561") 91 | .await 92 | .expect("Couldn't create server"); 93 | 94 | #[cfg(feature = "signing")] 95 | server.setup_signing(Some(singing_cfg_server)); 96 | 97 | let mut recv_count = 0; 98 | for _i in 0..RECEIVE_CHECK_COUNT { 99 | match server.recv_raw().await { 100 | Ok(message) => { 101 | if message.message_id() == mavlink::common::HEARTBEAT_DATA::ID { 102 | recv_count += 1; 103 | } else { 104 | // one message parse failure fails the test 105 | break; 106 | } 107 | } 108 | Err(..) => { 109 | // one message read failure fails the test 110 | break; 111 | } 112 | } 113 | } 114 | assert_eq!(recv_count, RECEIVE_CHECK_COUNT); 115 | }); 116 | 117 | // Give some time for the server to connect 118 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 119 | 120 | // have the client send a few hearbeats 121 | tokio::spawn(async move { 122 | let msg = 123 | mavlink::common::MavMessage::HEARTBEAT(crate::test_shared::get_heartbeat_msg()); 124 | #[allow(unused_mut)] 125 | let mut client = mavlink::connect_async("tcpout:127.0.0.1:14561") 126 | .await 127 | .expect("Couldn't create client"); 128 | 129 | #[cfg(feature = "signing")] 130 | client.setup_signing(Some(singing_cfg_client)); 131 | 132 | for _i in 0..RECEIVE_CHECK_COUNT { 133 | client.send_default(&msg).await.ok(); 134 | } 135 | }); 136 | 137 | server_thread.await.unwrap(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /mavlink-core/src/async_connection/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use std::io; 3 | 4 | use crate::{ 5 | connectable::ConnectionAddress, MAVLinkMessageRaw, MavFrame, MavHeader, MavlinkVersion, Message, 6 | }; 7 | #[cfg(feature = "tcp")] 8 | mod tcp; 9 | 10 | #[cfg(feature = "udp")] 11 | mod udp; 12 | 13 | #[cfg(feature = "direct-serial")] 14 | mod direct_serial; 15 | 16 | mod file; 17 | 18 | #[cfg(feature = "signing")] 19 | use crate::SigningConfig; 20 | 21 | /// An async MAVLink connection 22 | #[async_trait::async_trait] 23 | pub trait AsyncMavConnection { 24 | /// Receive a mavlink message. 25 | /// 26 | /// Yield until a valid frame is received, ignoring invalid messages. 27 | async fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError>; 28 | 29 | /// Receive a raw, unparsed mavlink message. 30 | /// 31 | /// Yield until a valid frame is received, ignoring invalid messages. 32 | async fn recv_raw(&self) -> Result; 33 | 34 | /// Send a mavlink message 35 | async fn send( 36 | &self, 37 | header: &MavHeader, 38 | data: &M, 39 | ) -> Result; 40 | 41 | /// Sets the MAVLink version to use for receiving (when `allow_recv_any_version()` is `false`) and sending messages. 42 | fn set_protocol_version(&mut self, version: MavlinkVersion); 43 | /// Gets the currently used MAVLink version 44 | fn protocol_version(&self) -> MavlinkVersion; 45 | 46 | /// Set wether MAVLink messages of either version may be received. 47 | /// 48 | /// If set to false only messages of the version configured with `set_protocol_version()` are received. 49 | fn set_allow_recv_any_version(&mut self, allow: bool); 50 | /// Wether messages of any MAVLink version may be received 51 | fn allow_recv_any_version(&self) -> bool; 52 | 53 | /// Write whole frame 54 | async fn send_frame( 55 | &self, 56 | frame: &MavFrame, 57 | ) -> Result { 58 | self.send(&frame.header, &frame.msg).await 59 | } 60 | 61 | /// Read whole frame 62 | async fn recv_frame(&self) -> Result, crate::error::MessageReadError> { 63 | let (header, msg) = self.recv().await?; 64 | let protocol_version = self.protocol_version(); 65 | Ok(MavFrame { 66 | header, 67 | msg, 68 | protocol_version, 69 | }) 70 | } 71 | 72 | /// Send a message with default header 73 | async fn send_default(&self, data: &M) -> Result { 74 | let header = MavHeader::default(); 75 | self.send(&header, data).await 76 | } 77 | 78 | /// Setup secret key used for message signing, or disable message signing 79 | #[cfg(feature = "signing")] 80 | fn setup_signing(&mut self, signing_data: Option); 81 | } 82 | 83 | /// Connect asynchronously to a MAVLink node by address string. 84 | /// 85 | /// The address must be in one of the following formats: 86 | /// 87 | /// * `tcpin::` to create a TCP server, listening for an incoming connection 88 | /// * `tcpout::` to create a TCP client 89 | /// * `udpin::` to create a UDP server, listening for incoming packets 90 | /// * `udpout::` to create a UDP client 91 | /// * `udpbcast::` to create a UDP broadcast 92 | /// * `serial::` to create a serial connection 93 | /// * `file:` to extract file data, writing to such a connection does nothing 94 | /// 95 | /// The type of the connection is determined at runtime based on the address type, so the 96 | /// connection is returned as a trait object. 97 | /// 98 | /// # Errors 99 | /// 100 | /// - [`AddrNotAvailable`] if the address string could not be parsed as a valid MAVLink address 101 | /// - When the connection could not be established a corresponding [`io::Error`] is returned 102 | /// 103 | /// [`AddrNotAvailable`]: io::ErrorKind::AddrNotAvailable 104 | pub async fn connect_async( 105 | address: &str, 106 | ) -> io::Result + Sync + Send>> { 107 | ConnectionAddress::parse_address(address)? 108 | .connect_async::() 109 | .await 110 | } 111 | 112 | /// Returns the socket address for the given address. 113 | #[cfg(any(feature = "tcp", feature = "udp"))] 114 | pub(crate) fn get_socket_addr( 115 | address: T, 116 | ) -> Result { 117 | let addr = match address.to_socket_addrs()?.next() { 118 | Some(addr) => addr, 119 | None => { 120 | return Err(io::Error::other("Host address lookup failed")); 121 | } 122 | }; 123 | Ok(addr) 124 | } 125 | 126 | /// A MAVLink connection address that can be connected to, establishing an [`AsyncMavConnection`] 127 | /// 128 | /// This is the `async` version of `Connectable`. 129 | #[async_trait] 130 | pub trait AsyncConnectable { 131 | /// Attempt to establish an asynchronous MAVLink connection 132 | async fn connect_async(&self) -> io::Result + Sync + Send>> 133 | where 134 | M: Message + Sync + Send; 135 | } 136 | 137 | #[async_trait] 138 | impl AsyncConnectable for ConnectionAddress { 139 | async fn connect_async(&self) -> io::Result + Sync + Send>> 140 | where 141 | M: Message + Sync + Send, 142 | { 143 | match self { 144 | #[cfg(feature = "tcp")] 145 | Self::Tcp(connectable) => connectable.connect_async::().await, 146 | #[cfg(feature = "udp")] 147 | Self::Udp(connectable) => connectable.connect_async::().await, 148 | #[cfg(feature = "direct-serial")] 149 | Self::Serial(connectable) => connectable.connect_async::().await, 150 | Self::File(connectable) => connectable.connect_async::().await, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /mavlink-core/src/async_connection/direct_serial.rs: -------------------------------------------------------------------------------- 1 | //! Async Serial MAVLink connection 2 | 3 | use core::ops::DerefMut; 4 | use core::sync::atomic::{self, AtomicU8}; 5 | use std::io; 6 | 7 | use async_trait::async_trait; 8 | use futures::lock::Mutex; 9 | use tokio::io::BufReader; 10 | use tokio_serial::{SerialPort, SerialPortBuilderExt, SerialStream}; 11 | 12 | use super::AsyncConnectable; 13 | use crate::connection::direct_serial::config::SerialConfig; 14 | use crate::MAVLinkMessageRaw; 15 | use crate::{async_peek_reader::AsyncPeekReader, MavHeader, MavlinkVersion, Message, ReadVersion}; 16 | 17 | #[cfg(not(feature = "signing"))] 18 | use crate::{ 19 | read_versioned_msg_async, read_versioned_raw_message_async, write_versioned_msg_async, 20 | }; 21 | #[cfg(feature = "signing")] 22 | use crate::{ 23 | read_versioned_msg_async_signed, read_versioned_raw_message_async_signed, 24 | write_versioned_msg_async_signed, SigningConfig, SigningData, 25 | }; 26 | 27 | use super::AsyncMavConnection; 28 | 29 | pub struct AsyncSerialConnection { 30 | port: Mutex>>, 31 | sequence: AtomicU8, 32 | protocol_version: MavlinkVersion, 33 | recv_any_version: bool, 34 | #[cfg(feature = "signing")] 35 | signing_data: Option, 36 | } 37 | 38 | #[async_trait::async_trait] 39 | impl AsyncMavConnection for AsyncSerialConnection { 40 | async fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 41 | let mut port = self.port.lock().await; 42 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 43 | #[cfg(not(feature = "signing"))] 44 | let result = read_versioned_msg_async(port.deref_mut(), version).await; 45 | #[cfg(feature = "signing")] 46 | let result = 47 | read_versioned_msg_async_signed(port.deref_mut(), version, self.signing_data.as_ref()) 48 | .await; 49 | result 50 | } 51 | 52 | async fn recv_raw(&self) -> Result { 53 | let mut port = self.port.lock().await; 54 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 55 | #[cfg(not(feature = "signing"))] 56 | let result = read_versioned_raw_message_async::(port.deref_mut(), version).await; 57 | #[cfg(feature = "signing")] 58 | let result = read_versioned_raw_message_async_signed::( 59 | port.deref_mut(), 60 | version, 61 | self.signing_data.as_ref(), 62 | ) 63 | .await; 64 | result 65 | } 66 | 67 | async fn send( 68 | &self, 69 | header: &MavHeader, 70 | data: &M, 71 | ) -> Result { 72 | let mut port = self.port.lock().await; 73 | 74 | let sequence = self.sequence.fetch_add( 75 | 1, 76 | // Safety: 77 | // 78 | // We are using `Ordering::Relaxed` here because: 79 | // - We only need a unique sequence number per message 80 | // - `Mutex` on `self.port` already makes sure the rest of the code is synchronized 81 | // - No other thread reads or writes `self.sequence` without going through this `Mutex` 82 | // 83 | // Warning: 84 | // 85 | // If we later change this code to access `self.sequence` without locking `self.port` with the `Mutex`, 86 | // then we should upgrade this ordering to `Ordering::SeqCst`. 87 | atomic::Ordering::Relaxed, 88 | ); 89 | 90 | let header = MavHeader { 91 | sequence, 92 | system_id: header.system_id, 93 | component_id: header.component_id, 94 | }; 95 | 96 | #[cfg(not(feature = "signing"))] 97 | let result = 98 | write_versioned_msg_async(port.reader_mut(), self.protocol_version, header, data).await; 99 | #[cfg(feature = "signing")] 100 | let result = write_versioned_msg_async_signed( 101 | port.reader_mut(), 102 | self.protocol_version, 103 | header, 104 | data, 105 | self.signing_data.as_ref(), 106 | ) 107 | .await; 108 | result 109 | } 110 | 111 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 112 | self.protocol_version = version; 113 | } 114 | 115 | fn protocol_version(&self) -> MavlinkVersion { 116 | self.protocol_version 117 | } 118 | 119 | fn set_allow_recv_any_version(&mut self, allow: bool) { 120 | self.recv_any_version = allow; 121 | } 122 | 123 | fn allow_recv_any_version(&self) -> bool { 124 | self.recv_any_version 125 | } 126 | 127 | #[cfg(feature = "signing")] 128 | fn setup_signing(&mut self, signing_data: Option) { 129 | self.signing_data = signing_data.map(SigningData::from_config); 130 | } 131 | } 132 | 133 | #[async_trait] 134 | impl AsyncConnectable for SerialConfig { 135 | async fn connect_async(&self) -> io::Result + Sync + Send>> 136 | where 137 | M: Message + Sync + Send, 138 | { 139 | let mut port = tokio_serial::new(&self.port_name, self.baud_rate).open_native_async()?; 140 | port.set_data_bits(tokio_serial::DataBits::Eight)?; 141 | port.set_parity(tokio_serial::Parity::None)?; 142 | port.set_stop_bits(tokio_serial::StopBits::One)?; 143 | port.set_flow_control(tokio_serial::FlowControl::None)?; 144 | 145 | let read_buffer_capacity = self.buffer_capacity(); 146 | let buf_reader = BufReader::with_capacity(read_buffer_capacity, port); 147 | 148 | Ok(Box::new(AsyncSerialConnection { 149 | port: Mutex::new(AsyncPeekReader::new(buf_reader)), 150 | sequence: AtomicU8::new(0), 151 | protocol_version: MavlinkVersion::V2, 152 | recv_any_version: false, 153 | #[cfg(feature = "signing")] 154 | signing_data: None, 155 | })) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /mavlink-core/src/bytes.rs: -------------------------------------------------------------------------------- 1 | pub struct Bytes<'a> { 2 | data: &'a [u8], 3 | pos: usize, 4 | } 5 | 6 | /// Simple error types for the bytes module. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 | pub enum Error { 9 | /// Attempted to to read more bytes than available. 10 | NotEnoughBuffer { requested: usize, available: usize }, 11 | } 12 | 13 | impl Error { 14 | #[inline] 15 | fn not_enough_buffer(requested: usize, bytes: &Bytes) -> Self { 16 | Self::NotEnoughBuffer { 17 | requested, 18 | available: bytes.remaining(), 19 | } 20 | } 21 | } 22 | 23 | impl core::fmt::Display for Error { 24 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 25 | match self { 26 | Self::NotEnoughBuffer { 27 | requested, 28 | available, 29 | } => write!( 30 | f, 31 | "Attempted to read {requested} bytes but only {available} available.", 32 | ), 33 | } 34 | } 35 | } 36 | 37 | impl<'a> Bytes<'a> { 38 | pub fn new(data: &'a [u8]) -> Self { 39 | Self { data, pos: 0 } 40 | } 41 | 42 | #[inline] 43 | fn remaining(&self) -> usize { 44 | self.data.len() - self.pos 45 | } 46 | 47 | #[inline] 48 | pub fn remaining_bytes(&self) -> &'a [u8] { 49 | &self.data[self.pos..] 50 | } 51 | 52 | /// # Errors 53 | /// 54 | /// Will return an error if not at least `count` bytes remain in the buffer 55 | #[inline] 56 | pub fn get_bytes(&mut self, count: usize) -> Result<&[u8], Error> { 57 | let bytes = &self 58 | .data 59 | .get(self.pos..(self.pos + count)) 60 | .ok_or_else(|| Error::not_enough_buffer(count, self))?; 61 | self.pos += count; 62 | Ok(bytes) 63 | } 64 | 65 | /// # Errors 66 | /// 67 | /// Will return an error if not at least `SIZE` bytes remain in the buffer 68 | #[inline] 69 | pub fn get_array(&mut self) -> Result<[u8; SIZE], Error> { 70 | let bytes = self.get_bytes(SIZE)?; 71 | let mut arr = [0u8; SIZE]; 72 | 73 | arr.copy_from_slice(bytes); 74 | 75 | debug_assert_eq!(arr.as_slice(), bytes); 76 | 77 | Ok(arr) 78 | } 79 | 80 | /// # Errors 81 | /// 82 | /// Will return an error if nothing is remaining in the buffer 83 | #[inline] 84 | pub fn get_u8(&mut self) -> Result { 85 | let val = *self 86 | .data 87 | .get(self.pos) 88 | .ok_or_else(|| Error::not_enough_buffer(1, self))?; 89 | self.pos += 1; 90 | Ok(val) 91 | } 92 | 93 | /// # Errors 94 | /// 95 | /// Will return an error if nothing is remaining in the buffer 96 | #[inline] 97 | pub fn get_i8(&mut self) -> Result { 98 | let val = *self 99 | .data 100 | .get(self.pos) 101 | .ok_or_else(|| Error::not_enough_buffer(1, self))? as i8; 102 | self.pos += 1; 103 | Ok(val) 104 | } 105 | 106 | /// # Errors 107 | /// 108 | /// Will return an error if less then the 2 required bytes for a `u16` remain 109 | #[inline] 110 | pub fn get_u16_le(&mut self) -> Result { 111 | Ok(u16::from_le_bytes(self.get_array()?)) 112 | } 113 | 114 | /// # Errors 115 | /// 116 | /// Will return an error if less then the 2 required bytes for a `i16` remain 117 | #[inline] 118 | pub fn get_i16_le(&mut self) -> Result { 119 | Ok(i16::from_le_bytes(self.get_array()?)) 120 | } 121 | 122 | /// # Errors 123 | /// 124 | /// Will return an error if not at least 3 bytes remain 125 | #[inline] 126 | pub fn get_u24_le(&mut self) -> Result { 127 | const SIZE: usize = 3; 128 | 129 | let mut val = [0u8; SIZE + 1]; 130 | val[..3].copy_from_slice( 131 | self.data 132 | .get(self.pos..self.pos + SIZE) 133 | .ok_or_else(|| Error::not_enough_buffer(SIZE, self))?, 134 | ); 135 | self.pos += SIZE; 136 | 137 | debug_assert_eq!(val[3], 0); 138 | Ok(u32::from_le_bytes(val)) 139 | } 140 | 141 | /// # Errors 142 | /// 143 | /// Will return an error if not at least 3 bytes remain 144 | #[inline] 145 | pub fn get_i24_le(&mut self) -> Result { 146 | const SIZE: usize = 3; 147 | 148 | let mut val = [0u8; SIZE + 1]; 149 | val[..3].copy_from_slice( 150 | self.data 151 | .get(self.pos..self.pos + SIZE) 152 | .ok_or_else(|| Error::not_enough_buffer(SIZE, self))?, 153 | ); 154 | self.pos += SIZE; 155 | 156 | debug_assert_eq!(val[3], 0); 157 | Ok(i32::from_le_bytes(val)) 158 | } 159 | 160 | /// # Errors 161 | /// 162 | /// Will return an error if less then the 4 required bytes for a `u32` remain 163 | #[inline] 164 | pub fn get_u32_le(&mut self) -> Result { 165 | Ok(u32::from_le_bytes(self.get_array()?)) 166 | } 167 | 168 | /// # Errors 169 | /// 170 | /// Will return an error if less then the 4 required bytes for a `i32` remain 171 | #[inline] 172 | pub fn get_i32_le(&mut self) -> Result { 173 | Ok(i32::from_le_bytes(self.get_array()?)) 174 | } 175 | 176 | /// # Errors 177 | /// 178 | /// Will return an error if less then the 8 required bytes for a `u64` remain 179 | #[inline] 180 | pub fn get_u64_le(&mut self) -> Result { 181 | Ok(u64::from_le_bytes(self.get_array()?)) 182 | } 183 | 184 | /// # Errors 185 | /// 186 | /// Will return an error if less then the 8 required bytes for a `i64` remain 187 | #[inline] 188 | pub fn get_i64_le(&mut self) -> Result { 189 | Ok(i64::from_le_bytes(self.get_array()?)) 190 | } 191 | 192 | /// # Errors 193 | /// 194 | /// Will return an error if less then the 4 required bytes for a `f32` remain 195 | #[inline] 196 | pub fn get_f32_le(&mut self) -> Result { 197 | Ok(f32::from_le_bytes(self.get_array()?)) 198 | } 199 | 200 | /// # Errors 201 | /// 202 | /// Will return an error if less then the 8 required bytes for a `f64` remain 203 | #[inline] 204 | pub fn get_f64_le(&mut self) -> Result { 205 | Ok(f64::from_le_bytes(self.get_array()?)) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/definitions/parameters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Specifies the datatype of a MAVLink parameter. 5 | 6 | 8-bit unsigned integer 7 | 8 | 9 | 8-bit signed integer 10 | 11 | 12 | 16-bit unsigned integer 13 | 14 | 15 | 16-bit signed integer 16 | 17 | 18 | 32-bit unsigned integer 19 | 20 | 21 | 32-bit signed integer 22 | 23 | 24 | 64-bit unsigned integer 25 | 26 | 27 | 64-bit signed integer 28 | 29 | 30 | 32-bit floating-point 31 | 32 | 33 | 64-bit floating-point 34 | 35 | 36 | 37 | 38 | 39 | Request to read the onboard parameter with the param_id string id. Onboard 40 | parameters are stored as key[const char*] -> value[float]. This allows to send a 41 | parameter to any other component (such as the GCS) without the need of previous 42 | knowledge of possible parameter names. Thus the same GCS can store different 43 | parameters 44 | for different autopilots. See also https://mavlink.io/en/services/parameter.html for 45 | a 46 | full documentation of QGroundControl and IMU code. 47 | System ID 48 | Component ID 49 | Onboard parameter id, terminated by NULL if the 50 | length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte 51 | if 52 | the length is exactly 16 chars - applications have to provide 16+1 bytes storage if 53 | the 54 | ID is stored as string 55 | Parameter index. Send -1 to use 56 | the 57 | param ID field as identifier (else the param id will be ignored) 58 | 59 | 60 | Request all parameters of this component. After this request, all 61 | parameters 62 | are emitted. The parameter microservice is documented at 63 | https://mavlink.io/en/services/parameter.html 64 | System ID 65 | Component ID 66 | 67 | 68 | Emit the value of a onboard parameter. The inclusion of param_count and 69 | param_index in the message allows the recipient to keep track of received parameters 70 | and 71 | allows him to re-request missing parameters after a loss or timeout. The parameter 72 | microservice is documented at https://mavlink.io/en/services/parameter.html 73 | Onboard parameter id, terminated by NULL if the 74 | length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte 75 | if 76 | the length is exactly 16 chars - applications have to provide 16+1 bytes storage if 77 | the 78 | ID is stored as string 79 | Onboard parameter value 80 | Onboard parameter type. 81 | Total number of onboard parameters 82 | Index of this onboard parameter 83 | 84 | 85 | Set a parameter value (write new value to permanent storage). 86 | The receiving component should acknowledge the new parameter value by broadcasting a 87 | PARAM_VALUE message (broadcasting ensures that multiple GCS all have an up-to-date 88 | list 89 | of all parameters). If the sending GCS did not receive a PARAM_VALUE within its 90 | timeout 91 | time, it should re-send the PARAM_SET message. The parameter microservice is 92 | documented 93 | at https://mavlink.io/en/services/parameter.html. 94 | 95 | System ID 96 | Component ID 97 | Onboard parameter id, terminated by NULL if the 98 | length is less than 16 human-readable chars and WITHOUT null termination (NULL) byte 99 | if 100 | the length is exactly 16 chars - applications have to provide 16+1 bytes storage if 101 | the 102 | ID is stored as string 103 | Onboard parameter value 104 | Onboard parameter type. 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /mavlink/tests/agnostic_decode_test.rs: -------------------------------------------------------------------------------- 1 | pub mod test_shared; 2 | 3 | use mavlink::MAV_STX; 4 | use mavlink::MAV_STX_V2; 5 | 6 | // 100 randomly generted bytes with 10 extra MAV_STX/MAV_STX_V2 each inserted 7 | const GARBAGE: [u8; 120] = [ 8 | 0xfe, 0x43, 0x2d, MAV_STX, MAV_STX, 0x26, 0x1e, 0x33, 0x85, 0x38, 0x1d, 0x20, 0x20, 0x90, 0xd9, 9 | 0x24, 0xb6, 0xd7, 0xb1, 0x22, 0x3b, 0xaf, 0x7c, 0x2f, MAV_STX, 0x9d, 0x1a, 0x13, 0x16, 0x2b, 10 | 0xf8, 0x6f, 0xf4, 0xdc, 0x66, 0xff, 0x2d, MAV_STX_V2, 0xe2, 0x2c, 0xb1, MAV_STX_V2, 0x4e, 0xc9, 11 | 0xc6, 0xcb, 0x3e, 0x3e, 0xf4, MAV_STX_V2, MAV_STX_V2, 0x49, 0xbc, 0x11, 0xb7, 0xd4, 0x5e, 12 | MAV_STX, 0x46, 0x6a, 0xd3, 0xb9, MAV_STX, 0xe3, 0x81, 0x1d, MAV_STX_V2, 0x80, 0x47, 0xfc, 0xff, 13 | 0x0c, 0xaa, 0xf3, MAV_STX, MAV_STX_V2, 0x87, 0x2f, 0x9a, 0x15, MAV_STX_V2, MAV_STX, 0x06, 0xc9, 14 | 0xe1, 0xc0, 0x98, 0xf5, 0x71, 0x78, 0x1c, 0x4a, 0xe3, 0xf1, MAV_STX_V2, 0x5f, 0xdb, 0x0e, 0x3f, 15 | MAV_STX, 0x2e, MAV_STX_V2, 0x08, 0x39, 0x6e, 0x15, 0x3c, 0x55, 0xcb, 0x78, 0xe0, MAV_STX, 0x5a, 16 | 0xb3, 0x1b, 0xf9, MAV_STX, 0xe0, 0xa0, MAV_STX_V2, 17 | ]; 18 | 19 | #[cfg(feature = "common")] 20 | mod test_agnostic_encode_decode { 21 | use crate::GARBAGE; 22 | use mavlink_core::peek_reader::PeekReader; 23 | use std::io::Write; 24 | 25 | #[test] 26 | pub fn test_read_heartbeats() { 27 | let mut buf = vec![]; 28 | _ = buf.write(crate::test_shared::HEARTBEAT_V1); 29 | _ = buf.write(crate::test_shared::HEARTBEAT_V2); 30 | let mut r = PeekReader::new(buf.as_slice()); 31 | // read 2 messages 32 | for _ in 0..2 { 33 | let (header, msg) = mavlink::read_any_msg(&mut r).expect("Failed to parse message"); 34 | 35 | assert_eq!(header, crate::test_shared::COMMON_MSG_HEADER); 36 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 37 | 38 | if let mavlink::common::MavMessage::HEARTBEAT(msg) = msg { 39 | assert_eq!(msg.custom_mode, heartbeat_msg.custom_mode); 40 | assert_eq!(msg.mavtype, heartbeat_msg.mavtype); 41 | assert_eq!(msg.autopilot, heartbeat_msg.autopilot); 42 | assert_eq!(msg.base_mode, heartbeat_msg.base_mode); 43 | assert_eq!(msg.system_status, heartbeat_msg.system_status); 44 | assert_eq!(msg.mavlink_version, heartbeat_msg.mavlink_version); 45 | } else { 46 | panic!("Decoded wrong message type") 47 | } 48 | } 49 | } 50 | 51 | #[test] 52 | pub fn test_read_inbetween_garbage() { 53 | // write some garbage bytes as well as 2 heartbeats and attempt to read them 54 | 55 | let mut buf = vec![]; 56 | _ = buf.write(&GARBAGE); 57 | _ = buf.write(crate::test_shared::HEARTBEAT_V1); 58 | _ = buf.write(&GARBAGE); 59 | // only part of message 60 | _ = buf.write(&crate::test_shared::HEARTBEAT_V1[..5]); 61 | _ = buf.write(crate::test_shared::HEARTBEAT_V2); 62 | _ = buf.write(&GARBAGE); 63 | // only part of message 64 | _ = buf.write(&crate::test_shared::HEARTBEAT_V1[5..]); 65 | // add some zeros to prevent invalid package sizes from causing a read error 66 | _ = buf.write(&[0; 100]); 67 | 68 | let mut r = PeekReader::new(buf.as_slice()); 69 | _ = mavlink::read_any_msg::(&mut r).unwrap(); 70 | _ = mavlink::read_any_msg::(&mut r).unwrap(); 71 | assert!( 72 | mavlink::read_any_msg::(&mut r).is_err(), 73 | "Parsed message from garbage data" 74 | ); 75 | } 76 | } 77 | 78 | #[cfg(all(feature = "std", feature = "tokio-1", feature = "common"))] 79 | mod test_agnostic_encode_decode_async { 80 | use crate::GARBAGE; 81 | use mavlink_core::async_peek_reader::AsyncPeekReader; 82 | use std::io::Write; 83 | 84 | #[tokio::test] 85 | pub async fn test_read_heartbeats() { 86 | let mut buf = vec![]; 87 | _ = buf.write(crate::test_shared::HEARTBEAT_V1); 88 | _ = buf.write(crate::test_shared::HEARTBEAT_V2); 89 | let mut r = AsyncPeekReader::new(buf.as_slice()); 90 | // read 2 messages 91 | for _ in 0..2 { 92 | let (header, msg) = mavlink::read_any_msg_async(&mut r) 93 | .await 94 | .expect("Failed to parse message"); 95 | 96 | assert_eq!(header, crate::test_shared::COMMON_MSG_HEADER); 97 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 98 | 99 | if let mavlink::common::MavMessage::HEARTBEAT(msg) = msg { 100 | assert_eq!(msg.custom_mode, heartbeat_msg.custom_mode); 101 | assert_eq!(msg.mavtype, heartbeat_msg.mavtype); 102 | assert_eq!(msg.autopilot, heartbeat_msg.autopilot); 103 | assert_eq!(msg.base_mode, heartbeat_msg.base_mode); 104 | assert_eq!(msg.system_status, heartbeat_msg.system_status); 105 | assert_eq!(msg.mavlink_version, heartbeat_msg.mavlink_version); 106 | } else { 107 | panic!("Decoded wrong message type") 108 | } 109 | } 110 | } 111 | 112 | #[tokio::test] 113 | pub async fn test_read_inbetween_garbage() { 114 | // write some garbage bytes as well as 2 heartbeats and attempt to read them 115 | 116 | let mut buf = vec![]; 117 | _ = buf.write(&GARBAGE); 118 | _ = buf.write(crate::test_shared::HEARTBEAT_V1); 119 | _ = buf.write(&GARBAGE); 120 | // only part of message 121 | _ = buf.write(&crate::test_shared::HEARTBEAT_V1[..5]); 122 | _ = buf.write(crate::test_shared::HEARTBEAT_V2); 123 | _ = buf.write(&GARBAGE); 124 | // only part of message 125 | _ = buf.write(&crate::test_shared::HEARTBEAT_V1[5..]); 126 | // add some zeros to prevent invalid package sizes from causing a read error 127 | _ = buf.write(&[0; 100]); 128 | 129 | let mut r = AsyncPeekReader::new(buf.as_slice()); 130 | _ = mavlink::read_any_msg_async::(&mut r) 131 | .await 132 | .unwrap(); 133 | _ = mavlink::read_any_msg_async::(&mut r) 134 | .await 135 | .unwrap(); 136 | assert!( 137 | mavlink::read_any_msg_async::(&mut r) 138 | .await 139 | .is_err(), 140 | "Parsed message from garbage data" 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /mavlink-core/src/async_connection/tcp.rs: -------------------------------------------------------------------------------- 1 | //! Async TCP MAVLink connection 2 | 3 | use std::io; 4 | 5 | use super::{get_socket_addr, AsyncConnectable, AsyncMavConnection}; 6 | use crate::async_peek_reader::AsyncPeekReader; 7 | use crate::connection::tcp::config::{TcpConfig, TcpMode}; 8 | use crate::{MAVLinkMessageRaw, MavHeader, MavlinkVersion, Message, ReadVersion}; 9 | 10 | use async_trait::async_trait; 11 | use core::ops::DerefMut; 12 | use futures::lock::Mutex; 13 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 14 | use tokio::net::{TcpListener, TcpStream}; 15 | 16 | #[cfg(not(feature = "signing"))] 17 | use crate::{ 18 | read_versioned_msg_async, read_versioned_raw_message_async, write_versioned_msg_async, 19 | }; 20 | #[cfg(feature = "signing")] 21 | use crate::{ 22 | read_versioned_msg_async_signed, read_versioned_raw_message_async_signed, 23 | write_versioned_msg_async_signed, SigningConfig, SigningData, 24 | }; 25 | 26 | pub async fn tcpout(address: T) -> io::Result { 27 | let addr = get_socket_addr(address)?; 28 | 29 | let socket = TcpStream::connect(addr).await?; 30 | 31 | let (reader, writer) = socket.into_split(); 32 | 33 | Ok(AsyncTcpConnection { 34 | reader: Mutex::new(AsyncPeekReader::new(reader)), 35 | writer: Mutex::new(TcpWrite { 36 | socket: writer, 37 | sequence: 0, 38 | }), 39 | protocol_version: MavlinkVersion::V2, 40 | recv_any_version: false, 41 | #[cfg(feature = "signing")] 42 | signing_data: None, 43 | }) 44 | } 45 | 46 | pub async fn tcpin(address: T) -> io::Result { 47 | let addr = get_socket_addr(address)?; 48 | let listener = TcpListener::bind(addr).await?; 49 | 50 | //For now we only accept one incoming stream: this yields until we get one 51 | match listener.accept().await { 52 | Ok((socket, _)) => { 53 | let (reader, writer) = socket.into_split(); 54 | return Ok(AsyncTcpConnection { 55 | reader: Mutex::new(AsyncPeekReader::new(reader)), 56 | writer: Mutex::new(TcpWrite { 57 | socket: writer, 58 | sequence: 0, 59 | }), 60 | protocol_version: MavlinkVersion::V2, 61 | recv_any_version: false, 62 | #[cfg(feature = "signing")] 63 | signing_data: None, 64 | }); 65 | } 66 | Err(e) => { 67 | //TODO don't println in lib 68 | println!("listener err: {e}"); 69 | } 70 | } 71 | Err(io::Error::new( 72 | io::ErrorKind::NotConnected, 73 | "No incoming connections!", 74 | )) 75 | } 76 | 77 | pub struct AsyncTcpConnection { 78 | reader: Mutex>, 79 | writer: Mutex, 80 | protocol_version: MavlinkVersion, 81 | recv_any_version: bool, 82 | #[cfg(feature = "signing")] 83 | signing_data: Option, 84 | } 85 | 86 | struct TcpWrite { 87 | socket: OwnedWriteHalf, 88 | sequence: u8, 89 | } 90 | 91 | #[async_trait::async_trait] 92 | impl AsyncMavConnection for AsyncTcpConnection { 93 | async fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 94 | let mut reader = self.reader.lock().await; 95 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 96 | #[cfg(not(feature = "signing"))] 97 | let result = read_versioned_msg_async(reader.deref_mut(), version).await; 98 | #[cfg(feature = "signing")] 99 | let result = read_versioned_msg_async_signed( 100 | reader.deref_mut(), 101 | version, 102 | self.signing_data.as_ref(), 103 | ) 104 | .await; 105 | result 106 | } 107 | 108 | async fn recv_raw(&self) -> Result { 109 | let mut reader = self.reader.lock().await; 110 | let version = ReadVersion::from_async_conn_cfg::<_, M>(self); 111 | #[cfg(not(feature = "signing"))] 112 | let result = read_versioned_raw_message_async::(reader.deref_mut(), version).await; 113 | #[cfg(feature = "signing")] 114 | let result = read_versioned_raw_message_async_signed::( 115 | reader.deref_mut(), 116 | version, 117 | self.signing_data.as_ref(), 118 | ) 119 | .await; 120 | result 121 | } 122 | 123 | async fn send( 124 | &self, 125 | header: &MavHeader, 126 | data: &M, 127 | ) -> Result { 128 | let mut lock = self.writer.lock().await; 129 | 130 | let header = MavHeader { 131 | sequence: lock.sequence, 132 | system_id: header.system_id, 133 | component_id: header.component_id, 134 | }; 135 | 136 | lock.sequence = lock.sequence.wrapping_add(1); 137 | #[cfg(not(feature = "signing"))] 138 | let result = 139 | write_versioned_msg_async(&mut lock.socket, self.protocol_version, header, data).await; 140 | #[cfg(feature = "signing")] 141 | let result = write_versioned_msg_async_signed( 142 | &mut lock.socket, 143 | self.protocol_version, 144 | header, 145 | data, 146 | self.signing_data.as_ref(), 147 | ) 148 | .await; 149 | result 150 | } 151 | 152 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 153 | self.protocol_version = version; 154 | } 155 | 156 | fn protocol_version(&self) -> MavlinkVersion { 157 | self.protocol_version 158 | } 159 | 160 | fn set_allow_recv_any_version(&mut self, allow: bool) { 161 | self.recv_any_version = allow; 162 | } 163 | 164 | fn allow_recv_any_version(&self) -> bool { 165 | self.recv_any_version 166 | } 167 | 168 | #[cfg(feature = "signing")] 169 | fn setup_signing(&mut self, signing_data: Option) { 170 | self.signing_data = signing_data.map(SigningData::from_config); 171 | } 172 | } 173 | 174 | #[async_trait] 175 | impl AsyncConnectable for TcpConfig { 176 | async fn connect_async(&self) -> io::Result + Sync + Send>> 177 | where 178 | M: Message + Sync + Send, 179 | { 180 | let conn = match self.mode { 181 | TcpMode::TcpIn => tcpin(&self.address).await, 182 | TcpMode::TcpOut => tcpout(&self.address).await, 183 | }; 184 | 185 | Ok(Box::new(conn?)) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /mavlink-core/src/signing.rs: -------------------------------------------------------------------------------- 1 | use crate::MAVLinkV2MessageRaw; 2 | 3 | use std::time::SystemTime; 4 | use std::{collections::HashMap, sync::Mutex}; 5 | 6 | use crate::MAVLINK_IFLAG_SIGNED; 7 | 8 | /// Configuration used for MAVLink 2 messages signing as defined in . 9 | /// 10 | /// To use a [`SigningConfig`] for sending and reciving messages create a [`SigningData`] object using `SigningData::from_config`. 11 | /// 12 | /// # Examples 13 | /// Creating `SigningData`: 14 | /// ``` 15 | /// # use mavlink_core::{SigningData, SigningConfig}; 16 | /// let config = SigningConfig::new([0u8; 32], 0, true, false); 17 | /// let sign_data = SigningData::from_config(config); 18 | /// ``` 19 | /// 20 | #[derive(Debug, Clone)] 21 | pub struct SigningConfig { 22 | secret_key: [u8; 32], 23 | link_id: u8, 24 | pub(crate) sign_outgoing: bool, 25 | pub(crate) allow_unsigned: bool, 26 | } 27 | 28 | // mutable state of signing per connection 29 | pub(crate) struct SigningState { 30 | timestamp: u64, 31 | stream_timestamps: HashMap<(u8, u8, u8), u64>, 32 | } 33 | 34 | /// MAVLink 2 message signing data 35 | /// 36 | /// Contains a [`SigningConfig`] as well as a mutable state that is reused for all messages in a connection. 37 | pub struct SigningData { 38 | pub(crate) config: SigningConfig, 39 | pub(crate) state: Mutex, 40 | } 41 | 42 | impl SigningConfig { 43 | /// Creates a new signing configuration. 44 | /// 45 | /// If `sign_outgoing` is set messages send using this configuration will be signed. 46 | /// If `allow_unsigned` is set, when receiving messages, all unsigned messages are accepted, this may also includes MAVLink 1 messages. 47 | pub fn new( 48 | secret_key: [u8; 32], 49 | link_id: u8, 50 | sign_outgoing: bool, 51 | allow_unsigned: bool, 52 | ) -> Self { 53 | Self { 54 | secret_key, 55 | link_id, 56 | sign_outgoing, 57 | allow_unsigned, 58 | } 59 | } 60 | } 61 | 62 | impl SigningData { 63 | /// Initializes signing data from a given [`SigningConfig`] 64 | pub fn from_config(config: SigningConfig) -> Self { 65 | Self { 66 | config, 67 | state: Mutex::new(SigningState { 68 | timestamp: 0, 69 | stream_timestamps: HashMap::new(), 70 | }), 71 | } 72 | } 73 | 74 | /// Verify the signature of a MAVLink 2 message. 75 | /// 76 | /// This respects the `allow_unsigned` parameter in [`SigningConfig`]. 77 | pub fn verify_signature(&self, message: &MAVLinkV2MessageRaw) -> bool { 78 | if message.incompatibility_flags() & MAVLINK_IFLAG_SIGNED > 0 { 79 | // The code that holds the mutex lock is not expected to panic, therefore the expect is justified. 80 | // The only issue that might cause a panic, presuming the opertions on the message buffer are sound, 81 | // is the `SystemTime::now()` call in `get_current_timestamp()`. 82 | let mut state = self 83 | .state 84 | .lock() 85 | .expect("Code holding MutexGuard should not panic."); 86 | state.timestamp = u64::max(state.timestamp, Self::get_current_timestamp()); 87 | let timestamp = message.signature_timestamp(); 88 | let src_system = message.system_id(); 89 | let src_component = message.component_id(); 90 | let stream_key = (message.signature_link_id(), src_system, src_component); 91 | match state.stream_timestamps.get(&stream_key) { 92 | Some(stream_timestamp) => { 93 | if timestamp <= *stream_timestamp { 94 | // reject old timestamp 95 | return false; 96 | } 97 | } 98 | None => { 99 | if timestamp + 60 * 1000 * 100 < state.timestamp { 100 | // bad new stream, more then a minute older the the last one 101 | return false; 102 | } 103 | } 104 | } 105 | 106 | let mut signature_buffer = [0u8; 6]; 107 | message.calculate_signature(&self.config.secret_key, &mut signature_buffer); 108 | let result = signature_buffer == message.signature_value(); 109 | if result { 110 | // if signature is valid update timestamps 111 | state.stream_timestamps.insert(stream_key, timestamp); 112 | state.timestamp = u64::max(state.timestamp, timestamp); 113 | } 114 | result 115 | } else { 116 | self.config.allow_unsigned 117 | } 118 | } 119 | 120 | /// Sign a MAVLink 2 message if its incompatibility flag is set accordingly. 121 | pub fn sign_message(&self, message: &mut MAVLinkV2MessageRaw) { 122 | if message.incompatibility_flags() & MAVLINK_IFLAG_SIGNED > 0 { 123 | // The code that holds the mutex lock is not expected to panic, therefore the expect is justified. 124 | // The only issue that might cause a panic, presuming the opertions on the message buffer are sound, 125 | // is the `SystemTime::now()` call in `get_current_timestamp()`. 126 | let mut state = self 127 | .state 128 | .lock() 129 | .expect("Code holding MutexGuard should not panic."); 130 | state.timestamp = u64::max(state.timestamp, Self::get_current_timestamp()); 131 | let ts_bytes = u64::to_le_bytes(state.timestamp); 132 | message 133 | .signature_timestamp_bytes_mut() 134 | .copy_from_slice(&ts_bytes[0..6]); 135 | *message.signature_link_id_mut() = self.config.link_id; 136 | 137 | let mut signature_buffer = [0u8; 6]; 138 | message.calculate_signature(&self.config.secret_key, &mut signature_buffer); 139 | 140 | message 141 | .signature_value_mut() 142 | .copy_from_slice(&signature_buffer); 143 | state.timestamp += 1; 144 | } 145 | } 146 | 147 | fn get_current_timestamp() -> u64 { 148 | // fallback to 0 if the system time appears to be before epoch 149 | let now = SystemTime::now() 150 | .duration_since(SystemTime::UNIX_EPOCH) 151 | .map(|n| n.as_micros()) 152 | .unwrap_or(0); 153 | // use 1st January 2015 GMT as offset, fallback to 0 if before that date, the used 48 bit of this will overflow in 2104 154 | (now.saturating_sub(1420070400u128 * 1000000u128) / 10u128) as u64 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/tcp.rs: -------------------------------------------------------------------------------- 1 | //! TCP MAVLink connection 2 | 3 | use crate::connection::get_socket_addr; 4 | use crate::connection::{Connection, MavConnection}; 5 | use crate::peek_reader::PeekReader; 6 | #[cfg(not(feature = "signing"))] 7 | use crate::read_versioned_raw_message; 8 | #[cfg(feature = "signing")] 9 | use crate::read_versioned_raw_message_signed; 10 | use crate::Connectable; 11 | use crate::MAVLinkMessageRaw; 12 | use crate::{MavHeader, MavlinkVersion, Message, ReadVersion}; 13 | use core::ops::DerefMut; 14 | use std::io; 15 | use std::net::ToSocketAddrs; 16 | use std::net::{TcpListener, TcpStream}; 17 | use std::sync::Mutex; 18 | use std::time::Duration; 19 | 20 | #[cfg(not(feature = "signing"))] 21 | use crate::{read_versioned_msg, write_versioned_msg}; 22 | 23 | #[cfg(feature = "signing")] 24 | use crate::{read_versioned_msg_signed, write_versioned_msg_signed, SigningConfig, SigningData}; 25 | 26 | pub mod config; 27 | 28 | use config::{TcpConfig, TcpMode}; 29 | 30 | pub fn tcpout(address: T) -> io::Result { 31 | let addr = get_socket_addr(&address)?; 32 | 33 | let socket = TcpStream::connect(addr)?; 34 | socket.set_read_timeout(Some(Duration::from_millis(100)))?; 35 | 36 | Ok(TcpConnection { 37 | reader: Mutex::new(PeekReader::new(socket.try_clone()?)), 38 | writer: Mutex::new(TcpWrite { 39 | socket, 40 | sequence: 0, 41 | }), 42 | protocol_version: MavlinkVersion::V2, 43 | recv_any_version: false, 44 | #[cfg(feature = "signing")] 45 | signing_data: None, 46 | }) 47 | } 48 | 49 | pub fn tcpin(address: T) -> io::Result { 50 | let addr = get_socket_addr(&address)?; 51 | let listener = TcpListener::bind(addr)?; 52 | 53 | //For now we only accept one incoming stream: this blocks until we get one 54 | for incoming in listener.incoming() { 55 | match incoming { 56 | Ok(socket) => { 57 | return Ok(TcpConnection { 58 | reader: Mutex::new(PeekReader::new(socket.try_clone()?)), 59 | writer: Mutex::new(TcpWrite { 60 | socket, 61 | sequence: 0, 62 | }), 63 | protocol_version: MavlinkVersion::V2, 64 | recv_any_version: false, 65 | #[cfg(feature = "signing")] 66 | signing_data: None, 67 | }) 68 | } 69 | Err(e) => { 70 | //TODO don't println in lib 71 | println!("listener err: {e}"); 72 | } 73 | } 74 | } 75 | Err(io::Error::new( 76 | io::ErrorKind::NotConnected, 77 | "No incoming connections!", 78 | )) 79 | } 80 | 81 | pub struct TcpConnection { 82 | reader: Mutex>, 83 | writer: Mutex, 84 | protocol_version: MavlinkVersion, 85 | recv_any_version: bool, 86 | #[cfg(feature = "signing")] 87 | signing_data: Option, 88 | } 89 | 90 | struct TcpWrite { 91 | socket: TcpStream, 92 | sequence: u8, 93 | } 94 | 95 | impl MavConnection for TcpConnection { 96 | fn recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 97 | let mut reader = self.reader.lock().unwrap(); 98 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 99 | #[cfg(not(feature = "signing"))] 100 | let result = read_versioned_msg(reader.deref_mut(), version); 101 | #[cfg(feature = "signing")] 102 | let result = 103 | read_versioned_msg_signed(reader.deref_mut(), version, self.signing_data.as_ref()); 104 | result 105 | } 106 | 107 | fn recv_raw(&self) -> Result { 108 | let mut reader = self.reader.lock().unwrap(); 109 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 110 | #[cfg(not(feature = "signing"))] 111 | let result = read_versioned_raw_message::(reader.deref_mut(), version); 112 | #[cfg(feature = "signing")] 113 | let result = read_versioned_raw_message_signed::( 114 | reader.deref_mut(), 115 | version, 116 | self.signing_data.as_ref(), 117 | ); 118 | result 119 | } 120 | 121 | fn try_recv(&self) -> Result<(MavHeader, M), crate::error::MessageReadError> { 122 | let mut reader = self.reader.lock().unwrap(); 123 | reader.reader_mut().set_nonblocking(true)?; 124 | 125 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 126 | 127 | #[cfg(not(feature = "signing"))] 128 | let result = read_versioned_msg(reader.deref_mut(), version); 129 | 130 | #[cfg(feature = "signing")] 131 | let result = 132 | read_versioned_msg_signed(reader.deref_mut(), version, self.signing_data.as_ref()); 133 | 134 | reader.reader_mut().set_nonblocking(false)?; 135 | 136 | result 137 | } 138 | 139 | fn send(&self, header: &MavHeader, data: &M) -> Result { 140 | let mut lock = self.writer.lock().unwrap(); 141 | 142 | let header = MavHeader { 143 | sequence: lock.sequence, 144 | system_id: header.system_id, 145 | component_id: header.component_id, 146 | }; 147 | 148 | lock.sequence = lock.sequence.wrapping_add(1); 149 | #[cfg(not(feature = "signing"))] 150 | let result = write_versioned_msg(&mut lock.socket, self.protocol_version, header, data); 151 | #[cfg(feature = "signing")] 152 | let result = write_versioned_msg_signed( 153 | &mut lock.socket, 154 | self.protocol_version, 155 | header, 156 | data, 157 | self.signing_data.as_ref(), 158 | ); 159 | result 160 | } 161 | 162 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 163 | self.protocol_version = version; 164 | } 165 | 166 | fn protocol_version(&self) -> MavlinkVersion { 167 | self.protocol_version 168 | } 169 | 170 | fn set_allow_recv_any_version(&mut self, allow: bool) { 171 | self.recv_any_version = allow; 172 | } 173 | 174 | fn allow_recv_any_version(&self) -> bool { 175 | self.recv_any_version 176 | } 177 | 178 | #[cfg(feature = "signing")] 179 | fn setup_signing(&mut self, signing_data: Option) { 180 | self.signing_data = signing_data.map(SigningData::from_config); 181 | } 182 | } 183 | 184 | impl Connectable for TcpConfig { 185 | fn connect(&self) -> io::Result> { 186 | let conn = match self.mode { 187 | TcpMode::TcpIn => tcpin(&self.address), 188 | TcpMode::TcpOut => tcpout(&self.address), 189 | }; 190 | 191 | Ok(conn?.into()) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__no_field_description.xml@no_field_description.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | assertion_line: 26 4 | expression: contents 5 | --- 6 | #![doc = "MAVLink no_field_description dialect."] 7 | #![doc = ""] 8 | #![doc = "This file was automatically generated, do not edit."] 9 | #![allow(deprecated)] 10 | #![allow(clippy::match_single_binding)] 11 | #[cfg(feature = "arbitrary")] 12 | use arbitrary::Arbitrary; 13 | #[allow(unused_imports)] 14 | use bitflags::{bitflags, Flags}; 15 | #[allow(unused_imports)] 16 | use mavlink_core::{ 17 | bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, 18 | }; 19 | #[allow(unused_imports)] 20 | use num_derive::{FromPrimitive, ToPrimitive}; 21 | #[allow(unused_imports)] 22 | use num_traits::{FromPrimitive, ToPrimitive}; 23 | #[cfg(feature = "serde")] 24 | use serde::{Deserialize, Serialize}; 25 | #[cfg(feature = "ts")] 26 | use ts_rs::TS; 27 | #[doc = "Raw RC Data."] 28 | #[doc = ""] 29 | #[doc = "ID: 50001"] 30 | #[derive(Debug, Clone, PartialEq)] 31 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 32 | #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] 33 | #[cfg_attr(feature = "ts", derive(TS))] 34 | #[cfg_attr(feature = "ts", ts(export))] 35 | pub struct CUBEPILOT_RAW_RC_DATA { 36 | #[cfg_attr(feature = "serde", serde(with = "serde_arrays"))] 37 | #[cfg_attr(feature = "ts", ts(type = "Array"))] 38 | pub rc_raw: [u8; 32], 39 | } 40 | impl CUBEPILOT_RAW_RC_DATA { 41 | pub const ENCODED_LEN: usize = 32usize; 42 | pub const DEFAULT: Self = Self { 43 | rc_raw: [0_u8; 32usize], 44 | }; 45 | #[cfg(feature = "arbitrary")] 46 | pub fn random(rng: &mut R) -> Self { 47 | use arbitrary::{Arbitrary, Unstructured}; 48 | let mut buf = [0u8; 1024]; 49 | rng.fill_bytes(&mut buf); 50 | let mut unstructured = Unstructured::new(&buf); 51 | Self::arbitrary(&mut unstructured).unwrap_or_default() 52 | } 53 | } 54 | impl Default for CUBEPILOT_RAW_RC_DATA { 55 | fn default() -> Self { 56 | Self::DEFAULT.clone() 57 | } 58 | } 59 | impl MessageData for CUBEPILOT_RAW_RC_DATA { 60 | type Message = MavMessage; 61 | const ID: u32 = 50001u32; 62 | const NAME: &'static str = "CUBEPILOT_RAW_RC"; 63 | const EXTRA_CRC: u8 = 246u8; 64 | const ENCODED_LEN: usize = 32usize; 65 | fn deser( 66 | _version: MavlinkVersion, 67 | __input: &[u8], 68 | ) -> Result { 69 | let avail_len = __input.len(); 70 | let mut payload_buf = [0; Self::ENCODED_LEN]; 71 | let mut buf = if avail_len < Self::ENCODED_LEN { 72 | payload_buf[0..avail_len].copy_from_slice(__input); 73 | Bytes::new(&payload_buf) 74 | } else { 75 | Bytes::new(__input) 76 | }; 77 | let mut __struct = Self::default(); 78 | for v in &mut __struct.rc_raw { 79 | let val = buf.get_u8()?; 80 | *v = val; 81 | } 82 | Ok(__struct) 83 | } 84 | fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize { 85 | let mut __tmp = BytesMut::new(bytes); 86 | #[allow(clippy::absurd_extreme_comparisons)] 87 | #[allow(unused_comparisons)] 88 | if __tmp.remaining() < Self::ENCODED_LEN { 89 | panic!( 90 | "buffer is too small (need {} bytes, but got {})", 91 | Self::ENCODED_LEN, 92 | __tmp.remaining(), 93 | ) 94 | } 95 | for val in &self.rc_raw { 96 | __tmp.put_u8(*val); 97 | } 98 | if matches!(version, MavlinkVersion::V2) { 99 | let len = __tmp.len(); 100 | ::mavlink_core::utils::remove_trailing_zeroes(&bytes[..len]) 101 | } else { 102 | __tmp.len() 103 | } 104 | } 105 | } 106 | #[derive(Clone, PartialEq, Debug)] 107 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 108 | #[cfg_attr(feature = "serde", serde(tag = "type"))] 109 | #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] 110 | #[cfg_attr(feature = "ts", derive(TS))] 111 | #[cfg_attr(feature = "ts", ts(export))] 112 | #[repr(u32)] 113 | pub enum MavMessage { 114 | #[doc = "Raw RC Data."] 115 | #[doc = ""] 116 | #[doc = "ID: 50001"] 117 | CUBEPILOT_RAW_RC(CUBEPILOT_RAW_RC_DATA), 118 | } 119 | impl MavMessage { 120 | pub const fn all_ids() -> &'static [u32] { 121 | &[50001u32] 122 | } 123 | pub const fn all_messages() -> &'static [(&'static str, u32)] { 124 | &[(CUBEPILOT_RAW_RC_DATA::NAME, CUBEPILOT_RAW_RC_DATA::ID)] 125 | } 126 | } 127 | impl Message for MavMessage { 128 | fn parse( 129 | version: MavlinkVersion, 130 | id: u32, 131 | payload: &[u8], 132 | ) -> Result { 133 | match id { 134 | CUBEPILOT_RAW_RC_DATA::ID => { 135 | CUBEPILOT_RAW_RC_DATA::deser(version, payload).map(Self::CUBEPILOT_RAW_RC) 136 | } 137 | _ => Err(::mavlink_core::error::ParserError::UnknownMessage { id }), 138 | } 139 | } 140 | fn message_name(&self) -> &'static str { 141 | match self { 142 | Self::CUBEPILOT_RAW_RC(..) => CUBEPILOT_RAW_RC_DATA::NAME, 143 | } 144 | } 145 | fn message_id(&self) -> u32 { 146 | match self { 147 | Self::CUBEPILOT_RAW_RC(..) => CUBEPILOT_RAW_RC_DATA::ID, 148 | } 149 | } 150 | fn message_id_from_name(name: &str) -> Option { 151 | match name { 152 | CUBEPILOT_RAW_RC_DATA::NAME => Some(CUBEPILOT_RAW_RC_DATA::ID), 153 | _ => None, 154 | } 155 | } 156 | fn default_message_from_id(id: u32) -> Option { 157 | match id { 158 | CUBEPILOT_RAW_RC_DATA::ID => { 159 | Some(Self::CUBEPILOT_RAW_RC(CUBEPILOT_RAW_RC_DATA::default())) 160 | } 161 | _ => None, 162 | } 163 | } 164 | #[cfg(feature = "arbitrary")] 165 | fn random_message_from_id(id: u32, rng: &mut R) -> Option { 166 | match id { 167 | CUBEPILOT_RAW_RC_DATA::ID => { 168 | Some(Self::CUBEPILOT_RAW_RC(CUBEPILOT_RAW_RC_DATA::random(rng))) 169 | } 170 | _ => None, 171 | } 172 | } 173 | fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize { 174 | match self { 175 | Self::CUBEPILOT_RAW_RC(body) => body.ser(version, bytes), 176 | } 177 | } 178 | fn extra_crc(id: u32) -> u8 { 179 | match id { 180 | CUBEPILOT_RAW_RC_DATA::ID => CUBEPILOT_RAW_RC_DATA::EXTRA_CRC, 181 | _ => 0, 182 | } 183 | } 184 | fn target_system_id(&self) -> Option { 185 | match self { 186 | _ => None, 187 | } 188 | } 189 | fn target_component_id(&self) -> Option { 190 | match self { 191 | _ => None, 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /mavlink/tests/v1_encode_decode_tests.rs: -------------------------------------------------------------------------------- 1 | pub mod test_shared; 2 | 3 | #[cfg(feature = "common")] 4 | mod test_v1_encode_decode { 5 | use crate::test_shared::HEARTBEAT_V1; 6 | use mavlink_core::peek_reader::PeekReader; 7 | 8 | #[test] 9 | pub fn test_read_heartbeat() { 10 | let mut r = PeekReader::new(HEARTBEAT_V1); 11 | let (header, msg) = mavlink::read_v1_msg(&mut r).expect("Failed to parse message"); 12 | //println!("{:?}, {:?}", header, msg); 13 | 14 | assert_eq!(header, crate::test_shared::COMMON_MSG_HEADER); 15 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 16 | 17 | if let mavlink::common::MavMessage::HEARTBEAT(msg) = msg { 18 | assert_eq!(msg.custom_mode, heartbeat_msg.custom_mode); 19 | assert_eq!(msg.mavtype, heartbeat_msg.mavtype); 20 | assert_eq!(msg.autopilot, heartbeat_msg.autopilot); 21 | assert_eq!(msg.base_mode, heartbeat_msg.base_mode); 22 | assert_eq!(msg.system_status, heartbeat_msg.system_status); 23 | assert_eq!(msg.mavlink_version, heartbeat_msg.mavlink_version); 24 | } else { 25 | panic!("Decoded wrong message type") 26 | } 27 | } 28 | 29 | #[test] 30 | pub fn test_write_heartbeat() { 31 | let mut b = [0u8; 280]; 32 | let mut v: &mut [u8] = &mut b; 33 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 34 | mavlink::write_v1_msg( 35 | &mut v, 36 | crate::test_shared::COMMON_MSG_HEADER, 37 | &mavlink::common::MavMessage::HEARTBEAT(heartbeat_msg), 38 | ) 39 | .expect("Failed to write message"); 40 | 41 | assert_eq!(&b[..HEARTBEAT_V1.len()], HEARTBEAT_V1); 42 | } 43 | 44 | #[test] 45 | #[cfg(not(feature = "emit-extensions"))] 46 | pub fn test_echo_servo_output_raw() { 47 | use mavlink::Message; 48 | 49 | let mut b = [0u8; 280]; 50 | let mut v: &mut [u8] = &mut b; 51 | let send_msg = crate::test_shared::get_servo_output_raw_v1(); 52 | 53 | mavlink::write_v2_msg( 54 | &mut v, 55 | crate::test_shared::COMMON_MSG_HEADER, 56 | &mavlink::common::MavMessage::SERVO_OUTPUT_RAW(send_msg), 57 | ) 58 | .expect("Failed to write message"); 59 | 60 | let mut c = PeekReader::new(b.as_slice()); 61 | let (_header, recv_msg): (mavlink::MavHeader, mavlink::common::MavMessage) = 62 | mavlink::read_v2_msg(&mut c).expect("Failed to read"); 63 | 64 | assert_eq!( 65 | mavlink::common::MavMessage::extra_crc(recv_msg.message_id()), 66 | 222_u8 67 | ); 68 | 69 | if let mavlink::common::MavMessage::SERVO_OUTPUT_RAW(recv_msg) = recv_msg { 70 | assert_eq!(recv_msg.port, 123_u8); 71 | assert_eq!(recv_msg.servo4_raw, 1400_u16); 72 | } else { 73 | panic!("Decoded wrong message type") 74 | } 75 | } 76 | 77 | #[test] 78 | pub fn test_serialize_to_raw() { 79 | let heartbeat_msg = crate::test_shared::get_heartbeat_msg(); 80 | let mut raw_msg = mavlink::MAVLinkV1MessageRaw::new(); 81 | 82 | raw_msg.serialize_message_data(crate::test_shared::COMMON_MSG_HEADER, &heartbeat_msg); 83 | 84 | assert_eq!(raw_msg.raw_bytes(), HEARTBEAT_V1); 85 | assert!(raw_msg.has_valid_crc::()); 86 | } 87 | 88 | #[test] 89 | #[cfg(feature = "std")] 90 | pub fn test_read_error() { 91 | use std::io::ErrorKind; 92 | 93 | use mavlink_core::error::MessageReadError; 94 | 95 | let mut reader = PeekReader::new(crate::test_shared::BlockyReader::new(HEARTBEAT_V1)); 96 | 97 | loop { 98 | match mavlink::read_v1_msg::(&mut reader) { 99 | Ok((header, _)) => { 100 | assert_eq!(header, crate::test_shared::COMMON_MSG_HEADER); 101 | break; 102 | } 103 | Err(MessageReadError::Io(err)) if err.kind() == ErrorKind::WouldBlock => {} 104 | Err(err) => panic!("{err}"), 105 | } 106 | } 107 | } 108 | 109 | #[test] 110 | #[cfg(feature = "emit-extensions")] 111 | pub fn test_extensions_v1() { 112 | use mavlink::common::COMMAND_ACK_DATA; 113 | // test if "Extension fields are not sent when a message is encoded using the MAVLink 1 protocol" holds 114 | let ack_command = COMMAND_ACK_DATA { 115 | command: mavlink::common::MavCmd::MAV_CMD_NAV_WAYPOINT, 116 | result: mavlink::common::MavResult::MAV_RESULT_TEMPORARILY_REJECTED, 117 | progress: 2, 118 | result_param2: 3, 119 | target_system: 4, 120 | target_component: 5, 121 | }; 122 | let ack_msg_data = mavlink::common::MavMessage::COMMAND_ACK(ack_command); 123 | let mut buf = vec![]; 124 | mavlink::write_v1_msg( 125 | &mut buf, 126 | crate::test_shared::COMMON_MSG_HEADER, 127 | &ack_msg_data, 128 | ) 129 | .unwrap(); 130 | // check expected len of serialized buffer 131 | // expected is 1 byte STX, 5 byte header, 3 bytes for message content and 2 byte crc 132 | assert_eq!(buf.len(), 1 + 5 + 3 + 2); 133 | 134 | let mut reader = PeekReader::new(&*buf); 135 | let (_, read_msg) = 136 | mavlink::read_v1_msg::(&mut reader).unwrap(); 137 | if let mavlink::common::MavMessage::COMMAND_ACK(read_ack_command) = read_msg { 138 | // chech if the deserialized message has extension fields set to 0 139 | assert_eq!( 140 | read_ack_command.command, 141 | mavlink::common::MavCmd::MAV_CMD_NAV_WAYPOINT 142 | ); 143 | assert_eq!( 144 | read_ack_command.result, 145 | mavlink::common::MavResult::MAV_RESULT_TEMPORARILY_REJECTED 146 | ); 147 | assert_eq!(read_ack_command.progress, 0); 148 | assert_eq!(read_ack_command.result_param2, 0); 149 | assert_eq!(read_ack_command.target_system, 0); 150 | assert_eq!(read_ack_command.target_component, 0); 151 | } else { 152 | panic!("Read invalid message") 153 | } 154 | } 155 | 156 | #[test] 157 | pub fn test_overflowing_msg_id() { 158 | // test behaivior for message ids that are not valid for MAVLink 1 159 | let msg_data = mavlink::common::MavMessage::SETUP_SIGNING( 160 | mavlink::common::SETUP_SIGNING_DATA::default(), 161 | ); 162 | let mut buf = vec![]; 163 | assert!( 164 | matches!( 165 | mavlink::write_v1_msg(&mut buf, crate::test_shared::COMMON_MSG_HEADER, &msg_data,), 166 | Err(mavlink::error::MessageWriteError::MAVLink2Only) 167 | ), 168 | "Writing a message with id 256 should return an error for MAVLink 1" 169 | ); 170 | assert!(buf.is_empty(), "No bytes should be written"); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /mavlink-core/src/bytes_mut.rs: -------------------------------------------------------------------------------- 1 | pub struct BytesMut<'a> { 2 | data: &'a mut [u8], 3 | len: usize, 4 | } 5 | 6 | impl<'a> BytesMut<'a> { 7 | pub fn new(data: &'a mut [u8]) -> Self { 8 | Self { data, len: 0 } 9 | } 10 | 11 | #[inline] 12 | pub fn len(&self) -> usize { 13 | self.len 14 | } 15 | 16 | #[inline] 17 | pub fn is_empty(&self) -> bool { 18 | self.len() == 0 19 | } 20 | 21 | #[inline] 22 | pub fn remaining(&self) -> usize { 23 | self.data.len() - self.len 24 | } 25 | 26 | #[inline] 27 | fn check_remaining(&self, count: usize) { 28 | assert!( 29 | self.remaining() >= count, 30 | "write buffer overflow; remaining {} bytes, try add {count} bytes", 31 | self.remaining(), 32 | ); 33 | } 34 | 35 | /// # Panics 36 | /// 37 | /// Will panic if not enough space is remaining in the buffer to store the whole slice 38 | #[inline] 39 | pub fn put_slice(&mut self, src: &[u8]) { 40 | self.check_remaining(src.len()); 41 | 42 | unsafe { 43 | core::ptr::copy_nonoverlapping(src.as_ptr(), &mut self.data[self.len], src.len()); 44 | } 45 | self.len += src.len(); 46 | } 47 | 48 | /// # Panics 49 | /// 50 | /// Will panic if no space is remaing in the buffer 51 | #[inline] 52 | pub fn put_u8(&mut self, val: u8) { 53 | self.check_remaining(1); 54 | 55 | self.data[self.len] = val; 56 | self.len += 1; 57 | } 58 | 59 | /// # Panics 60 | /// 61 | /// Will panic if no space is remaing in the buffer 62 | #[inline] 63 | pub fn put_i8(&mut self, val: i8) { 64 | self.check_remaining(1); 65 | 66 | self.data[self.len] = val as u8; 67 | self.len += 1; 68 | } 69 | 70 | /// # Panics 71 | /// 72 | /// Will panic if less space then the 2 bytes required by a `u16` remain in the buffer 73 | #[inline] 74 | pub fn put_u16_le(&mut self, val: u16) { 75 | const SIZE: usize = core::mem::size_of::(); 76 | self.check_remaining(SIZE); 77 | 78 | let src = val.to_le_bytes(); 79 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 80 | self.len += SIZE; 81 | } 82 | 83 | /// # Panics 84 | /// 85 | /// Will panic if less space then the 2 bytes required by a `i16` remain in the buffer 86 | #[inline] 87 | pub fn put_i16_le(&mut self, val: i16) { 88 | const SIZE: usize = core::mem::size_of::(); 89 | self.check_remaining(SIZE); 90 | 91 | let src = val.to_le_bytes(); 92 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 93 | self.len += SIZE; 94 | } 95 | 96 | /// # Panics 97 | /// 98 | /// Will panic if `val` is not a valid 24 bit unsigned integer or if not 99 | /// enough space is remaing in the buffer to store 3 bytes 100 | #[inline] 101 | pub fn put_u24_le(&mut self, val: u32) { 102 | const SIZE: usize = 3; 103 | const MAX: u32 = 2u32.pow(24) - 1; 104 | 105 | assert!( 106 | val <= MAX, 107 | "Attempted to put value that is too large for 24 bits, \ 108 | attempted to push: {val}, max allowed: {MAX}", 109 | ); 110 | 111 | let src = val.to_le_bytes(); 112 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..3]); 113 | self.len += SIZE; 114 | } 115 | 116 | /// # Panics 117 | /// 118 | /// Will panic if `val` is not a valid 24 bit signed integer or if not 119 | /// enough space is remaing in the buffer to store 3 bytes 120 | #[inline] 121 | pub fn put_i24_le(&mut self, val: i32) { 122 | const SIZE: usize = 3; 123 | const MIN: i32 = 2i32.pow(23); 124 | const MAX: i32 = 2i32.pow(23) - 1; 125 | 126 | assert!( 127 | val <= MAX, 128 | "Attempted to put value that is too large for 24 bits, \ 129 | attempted to push: {val}, max allowed: {MAX}", 130 | ); 131 | assert!( 132 | val >= MIN, 133 | "Attempted to put value that is too negative for 24 bits, \ 134 | attempted to push: {val}, min allowed: {MIN}", 135 | ); 136 | 137 | let src = val.to_le_bytes(); 138 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..3]); 139 | self.len += SIZE; 140 | } 141 | 142 | /// # Panics 143 | /// 144 | /// Will panic if less space then the 4 bytes required by a `u32` remain in the buffer 145 | #[inline] 146 | pub fn put_u32_le(&mut self, val: u32) { 147 | const SIZE: usize = core::mem::size_of::(); 148 | self.check_remaining(SIZE); 149 | 150 | let src = val.to_le_bytes(); 151 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 152 | self.len += SIZE; 153 | } 154 | 155 | /// # Panics 156 | /// 157 | /// Will panic if less space then the 4 bytes required by a `i32` remain in the buffer 158 | #[inline] 159 | pub fn put_i32_le(&mut self, val: i32) { 160 | const SIZE: usize = core::mem::size_of::(); 161 | self.check_remaining(SIZE); 162 | 163 | let src = val.to_le_bytes(); 164 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 165 | self.len += SIZE; 166 | } 167 | 168 | /// # Panics 169 | /// 170 | /// Will panic if less space then the 8 bytes required by a `u64` remain in the buffer 171 | #[inline] 172 | pub fn put_u64_le(&mut self, val: u64) { 173 | const SIZE: usize = core::mem::size_of::(); 174 | self.check_remaining(SIZE); 175 | 176 | let src = val.to_le_bytes(); 177 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 178 | self.len += SIZE; 179 | } 180 | 181 | /// # Panics 182 | /// 183 | /// Will panic if less space then the 8 bytes required by a `i64` remain in the buffer 184 | #[inline] 185 | pub fn put_i64_le(&mut self, val: i64) { 186 | const SIZE: usize = core::mem::size_of::(); 187 | self.check_remaining(SIZE); 188 | 189 | let src = val.to_le_bytes(); 190 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 191 | self.len += SIZE; 192 | } 193 | 194 | /// # Panics 195 | /// 196 | /// Will panic if less space then the 4 bytes required by a `f32` remain in the buffer 197 | #[inline] 198 | pub fn put_f32_le(&mut self, val: f32) { 199 | const SIZE: usize = core::mem::size_of::(); 200 | self.check_remaining(SIZE); 201 | 202 | let src = val.to_le_bytes(); 203 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 204 | self.len += SIZE; 205 | } 206 | 207 | /// # Panics 208 | /// 209 | /// Will panic if less space then the 8 bytes required by a `f64` remain in the buffer 210 | #[inline] 211 | pub fn put_f64_le(&mut self, val: f64) { 212 | const SIZE: usize = core::mem::size_of::(); 213 | self.check_remaining(SIZE); 214 | 215 | let src = val.to_le_bytes(); 216 | self.data[self.len..self.len + SIZE].copy_from_slice(&src[..]); 217 | self.len += SIZE; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /mavlink-core/src/connection/direct_serial.rs: -------------------------------------------------------------------------------- 1 | //! Serial MAVLINK connection 2 | 3 | use crate::connection::{Connection, MavConnection}; 4 | use crate::error::{MessageReadError, MessageWriteError}; 5 | use crate::peek_reader::PeekReader; 6 | use crate::Connectable; 7 | use crate::{MAVLinkMessageRaw, MavHeader, MavlinkVersion, Message, ReadVersion}; 8 | use core::ops::DerefMut; 9 | use core::sync::atomic::{self, AtomicU8}; 10 | use std::io::{self, BufReader}; 11 | use std::sync::Mutex; 12 | 13 | use serialport::{DataBits, FlowControl, Parity, SerialPort, StopBits}; 14 | 15 | #[cfg(not(feature = "signing"))] 16 | use crate::{read_versioned_msg, read_versioned_raw_message, write_versioned_msg}; 17 | #[cfg(feature = "signing")] 18 | use crate::{ 19 | read_versioned_msg_signed, read_versioned_raw_message_signed, write_versioned_msg_signed, 20 | SigningConfig, SigningData, 21 | }; 22 | 23 | pub mod config; 24 | 25 | use config::SerialConfig; 26 | 27 | pub struct SerialConnection { 28 | // Separate ports for reading and writing as it's safe to use concurrently. 29 | // See the official ref: https://github.com/serialport/serialport-rs/blob/321f85e1886eaa1302aef8a600a631bc1c88703a/examples/duplex.rs 30 | read_port: Mutex>>>, 31 | write_port: Mutex>, 32 | sequence: AtomicU8, 33 | protocol_version: MavlinkVersion, 34 | recv_any_version: bool, 35 | #[cfg(feature = "signing")] 36 | signing_data: Option, 37 | } 38 | 39 | impl MavConnection for SerialConnection { 40 | fn recv(&self) -> Result<(MavHeader, M), MessageReadError> { 41 | let mut port = self.read_port.lock().unwrap(); 42 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 43 | 44 | loop { 45 | #[cfg(not(feature = "signing"))] 46 | let result = read_versioned_msg(port.deref_mut(), version); 47 | #[cfg(feature = "signing")] 48 | let result = 49 | read_versioned_msg_signed(port.deref_mut(), version, self.signing_data.as_ref()); 50 | match result { 51 | ok @ Ok(..) => { 52 | return ok; 53 | } 54 | Err(MessageReadError::Io(e)) => { 55 | if e.kind() == io::ErrorKind::UnexpectedEof { 56 | return Err(MessageReadError::Io(e)); 57 | } 58 | } 59 | _ => {} 60 | } 61 | } 62 | } 63 | 64 | fn recv_raw(&self) -> Result { 65 | let mut port = self.read_port.lock().unwrap(); 66 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 67 | 68 | loop { 69 | #[cfg(not(feature = "signing"))] 70 | let result = read_versioned_raw_message::(port.deref_mut(), version); 71 | #[cfg(feature = "signing")] 72 | let result = read_versioned_raw_message_signed::( 73 | port.deref_mut(), 74 | version, 75 | self.signing_data.as_ref(), 76 | ); 77 | match result { 78 | ok @ Ok(..) => { 79 | return ok; 80 | } 81 | Err(MessageReadError::Io(e)) => { 82 | if e.kind() == io::ErrorKind::UnexpectedEof { 83 | return Err(MessageReadError::Io(e)); 84 | } 85 | } 86 | _ => {} 87 | } 88 | } 89 | } 90 | 91 | fn try_recv(&self) -> Result<(MavHeader, M), MessageReadError> { 92 | let mut port = self.read_port.lock().unwrap(); 93 | let version = ReadVersion::from_conn_cfg::<_, M>(self); 94 | 95 | #[cfg(not(feature = "signing"))] 96 | let result = read_versioned_msg(port.deref_mut(), version); 97 | 98 | #[cfg(feature = "signing")] 99 | let result = 100 | read_versioned_msg_signed(port.deref_mut(), version, self.signing_data.as_ref()); 101 | 102 | result 103 | } 104 | 105 | fn send(&self, header: &MavHeader, data: &M) -> Result { 106 | let mut port = self.write_port.lock().unwrap(); 107 | 108 | let sequence = self.sequence.fetch_add( 109 | 1, 110 | // Safety: 111 | // 112 | // We are using `Ordering::Relaxed` here because: 113 | // - We only need a unique sequence number per message 114 | // - `Mutex` on `self.write_port` already makes sure the rest of the code is synchronized 115 | // - No other thread reads or writes `self.sequence` without going through this `Mutex` 116 | // 117 | // Warning: 118 | // 119 | // If we later change this code to access `self.sequence` without locking `self.write_port` with the `Mutex`, 120 | // then we should upgrade this ordering to `Ordering::SeqCst`. 121 | atomic::Ordering::Relaxed, 122 | ); 123 | 124 | let header = MavHeader { 125 | sequence, 126 | system_id: header.system_id, 127 | component_id: header.component_id, 128 | }; 129 | 130 | #[cfg(not(feature = "signing"))] 131 | let result = write_versioned_msg(port.deref_mut(), self.protocol_version, header, data); 132 | #[cfg(feature = "signing")] 133 | let result = write_versioned_msg_signed( 134 | port.deref_mut(), 135 | self.protocol_version, 136 | header, 137 | data, 138 | self.signing_data.as_ref(), 139 | ); 140 | result 141 | } 142 | 143 | fn set_protocol_version(&mut self, version: MavlinkVersion) { 144 | self.protocol_version = version; 145 | } 146 | 147 | fn protocol_version(&self) -> MavlinkVersion { 148 | self.protocol_version 149 | } 150 | 151 | fn set_allow_recv_any_version(&mut self, allow: bool) { 152 | self.recv_any_version = allow; 153 | } 154 | 155 | fn allow_recv_any_version(&self) -> bool { 156 | self.recv_any_version 157 | } 158 | 159 | #[cfg(feature = "signing")] 160 | fn setup_signing(&mut self, signing_data: Option) { 161 | self.signing_data = signing_data.map(SigningData::from_config); 162 | } 163 | } 164 | 165 | impl Connectable for SerialConfig { 166 | fn connect(&self) -> io::Result> { 167 | let read_port = serialport::new(&self.port_name, self.baud_rate) 168 | .data_bits(DataBits::Eight) 169 | .parity(Parity::None) 170 | .stop_bits(StopBits::One) 171 | .flow_control(FlowControl::None) 172 | .open()?; 173 | 174 | let write_port = read_port.try_clone()?; 175 | 176 | let read_buffer_capacity = self.buffer_capacity(); 177 | let buf_reader = BufReader::with_capacity(read_buffer_capacity, read_port); 178 | 179 | Ok(SerialConnection { 180 | read_port: Mutex::new(PeekReader::new(buf_reader)), 181 | write_port: Mutex::new(write_port), 182 | sequence: AtomicU8::new(0), 183 | protocol_version: MavlinkVersion::V2, 184 | #[cfg(feature = "signing")] 185 | signing_data: None, 186 | recv_any_version: false, 187 | } 188 | .into()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /mavlink-bindgen/tests/snapshots/e2e_snapshots__heartbeat.xml@heartbeat.rs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: mavlink-bindgen/tests/e2e_snapshots.rs 3 | assertion_line: 26 4 | expression: contents 5 | --- 6 | #![doc = "MAVLink heartbeat dialect."] 7 | #![doc = ""] 8 | #![doc = "This file was automatically generated, do not edit."] 9 | #![allow(deprecated)] 10 | #![allow(clippy::match_single_binding)] 11 | #[cfg(feature = "arbitrary")] 12 | use arbitrary::Arbitrary; 13 | #[allow(unused_imports)] 14 | use bitflags::{bitflags, Flags}; 15 | #[allow(unused_imports)] 16 | use mavlink_core::{ 17 | bytes::Bytes, bytes_mut::BytesMut, types::CharArray, MavlinkVersion, Message, MessageData, 18 | }; 19 | #[allow(unused_imports)] 20 | use num_derive::{FromPrimitive, ToPrimitive}; 21 | #[allow(unused_imports)] 22 | use num_traits::{FromPrimitive, ToPrimitive}; 23 | #[cfg(feature = "serde")] 24 | use serde::{Deserialize, Serialize}; 25 | #[cfg(feature = "ts")] 26 | use ts_rs::TS; 27 | pub const MINOR_MAVLINK_VERSION: u8 = 3u8; 28 | pub const DIALECT_NUMBER: u8 = 130u8; 29 | #[doc = "ID: 0"] 30 | #[derive(Debug, Clone, PartialEq)] 31 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 32 | #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] 33 | #[cfg_attr(feature = "ts", derive(TS))] 34 | #[cfg_attr(feature = "ts", ts(export))] 35 | pub struct HEARTBEAT_DATA { 36 | #[doc = "Custom mode"] 37 | pub custom_mode: u32, 38 | #[doc = "Type"] 39 | pub mavtype: u8, 40 | #[doc = "Autopilot"] 41 | pub autopilot: u8, 42 | #[doc = "Base mode"] 43 | pub base_mode: u8, 44 | #[doc = "System status"] 45 | pub system_status: u8, 46 | #[doc = "Mavlink version"] 47 | pub mavlink_version: u8, 48 | } 49 | impl HEARTBEAT_DATA { 50 | pub const ENCODED_LEN: usize = 9usize; 51 | pub const DEFAULT: Self = Self { 52 | custom_mode: 0_u32, 53 | mavtype: 0_u8, 54 | autopilot: 0_u8, 55 | base_mode: 0_u8, 56 | system_status: 0_u8, 57 | mavlink_version: MINOR_MAVLINK_VERSION, 58 | }; 59 | #[cfg(feature = "arbitrary")] 60 | pub fn random(rng: &mut R) -> Self { 61 | use arbitrary::{Arbitrary, Unstructured}; 62 | let mut buf = [0u8; 1024]; 63 | rng.fill_bytes(&mut buf); 64 | let mut unstructured = Unstructured::new(&buf); 65 | Self::arbitrary(&mut unstructured).unwrap_or_default() 66 | } 67 | } 68 | impl Default for HEARTBEAT_DATA { 69 | fn default() -> Self { 70 | Self::DEFAULT.clone() 71 | } 72 | } 73 | impl MessageData for HEARTBEAT_DATA { 74 | type Message = MavMessage; 75 | const ID: u32 = 0u32; 76 | const NAME: &'static str = "HEARTBEAT"; 77 | const EXTRA_CRC: u8 = 50u8; 78 | const ENCODED_LEN: usize = 9usize; 79 | fn deser( 80 | _version: MavlinkVersion, 81 | __input: &[u8], 82 | ) -> Result { 83 | let avail_len = __input.len(); 84 | let mut payload_buf = [0; Self::ENCODED_LEN]; 85 | let mut buf = if avail_len < Self::ENCODED_LEN { 86 | payload_buf[0..avail_len].copy_from_slice(__input); 87 | Bytes::new(&payload_buf) 88 | } else { 89 | Bytes::new(__input) 90 | }; 91 | let mut __struct = Self::default(); 92 | __struct.custom_mode = buf.get_u32_le()?; 93 | __struct.mavtype = buf.get_u8()?; 94 | __struct.autopilot = buf.get_u8()?; 95 | __struct.base_mode = buf.get_u8()?; 96 | __struct.system_status = buf.get_u8()?; 97 | __struct.mavlink_version = buf.get_u8()?; 98 | Ok(__struct) 99 | } 100 | fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize { 101 | let mut __tmp = BytesMut::new(bytes); 102 | #[allow(clippy::absurd_extreme_comparisons)] 103 | #[allow(unused_comparisons)] 104 | if __tmp.remaining() < Self::ENCODED_LEN { 105 | panic!( 106 | "buffer is too small (need {} bytes, but got {})", 107 | Self::ENCODED_LEN, 108 | __tmp.remaining(), 109 | ) 110 | } 111 | __tmp.put_u32_le(self.custom_mode); 112 | __tmp.put_u8(self.mavtype); 113 | __tmp.put_u8(self.autopilot); 114 | __tmp.put_u8(self.base_mode); 115 | __tmp.put_u8(self.system_status); 116 | __tmp.put_u8(self.mavlink_version); 117 | if matches!(version, MavlinkVersion::V2) { 118 | let len = __tmp.len(); 119 | ::mavlink_core::utils::remove_trailing_zeroes(&bytes[..len]) 120 | } else { 121 | __tmp.len() 122 | } 123 | } 124 | } 125 | #[derive(Clone, PartialEq, Debug)] 126 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 127 | #[cfg_attr(feature = "serde", serde(tag = "type"))] 128 | #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] 129 | #[cfg_attr(feature = "ts", derive(TS))] 130 | #[cfg_attr(feature = "ts", ts(export))] 131 | #[repr(u32)] 132 | pub enum MavMessage { 133 | #[doc = "ID: 0"] 134 | HEARTBEAT(HEARTBEAT_DATA), 135 | } 136 | impl MavMessage { 137 | pub const fn all_ids() -> &'static [u32] { 138 | &[0u32] 139 | } 140 | pub const fn all_messages() -> &'static [(&'static str, u32)] { 141 | &[(HEARTBEAT_DATA::NAME, HEARTBEAT_DATA::ID)] 142 | } 143 | } 144 | impl Message for MavMessage { 145 | fn parse( 146 | version: MavlinkVersion, 147 | id: u32, 148 | payload: &[u8], 149 | ) -> Result { 150 | match id { 151 | HEARTBEAT_DATA::ID => HEARTBEAT_DATA::deser(version, payload).map(Self::HEARTBEAT), 152 | _ => Err(::mavlink_core::error::ParserError::UnknownMessage { id }), 153 | } 154 | } 155 | fn message_name(&self) -> &'static str { 156 | match self { 157 | Self::HEARTBEAT(..) => HEARTBEAT_DATA::NAME, 158 | } 159 | } 160 | fn message_id(&self) -> u32 { 161 | match self { 162 | Self::HEARTBEAT(..) => HEARTBEAT_DATA::ID, 163 | } 164 | } 165 | fn message_id_from_name(name: &str) -> Option { 166 | match name { 167 | HEARTBEAT_DATA::NAME => Some(HEARTBEAT_DATA::ID), 168 | _ => None, 169 | } 170 | } 171 | fn default_message_from_id(id: u32) -> Option { 172 | match id { 173 | HEARTBEAT_DATA::ID => Some(Self::HEARTBEAT(HEARTBEAT_DATA::default())), 174 | _ => None, 175 | } 176 | } 177 | #[cfg(feature = "arbitrary")] 178 | fn random_message_from_id(id: u32, rng: &mut R) -> Option { 179 | match id { 180 | HEARTBEAT_DATA::ID => Some(Self::HEARTBEAT(HEARTBEAT_DATA::random(rng))), 181 | _ => None, 182 | } 183 | } 184 | fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize { 185 | match self { 186 | Self::HEARTBEAT(body) => body.ser(version, bytes), 187 | } 188 | } 189 | fn extra_crc(id: u32) -> u8 { 190 | match id { 191 | HEARTBEAT_DATA::ID => HEARTBEAT_DATA::EXTRA_CRC, 192 | _ => 0, 193 | } 194 | } 195 | fn target_system_id(&self) -> Option { 196 | match self { 197 | _ => None, 198 | } 199 | } 200 | fn target_component_id(&self) -> Option { 201 | match self { 202 | _ => None, 203 | } 204 | } 205 | } 206 | --------------------------------------------------------------------------------