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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ application that needs to communicate with VESC motor controllers.

| Command ID | Command Name | Status |
|:----------:|-----------------------------------|--------|
| `0` | `FwVersion` | ✅ |
| `4` | `GetValues` | ✅ |
| `6` | `SetCurrent` | ✅ |
| `7` | `SetCurrentBrake` | ✅ |
Expand All @@ -32,6 +33,7 @@ application that needs to communicate with VESC motor controllers.

| Command ID | Command Name | Status |
|------------|-----------------------------------|--------|
| `0` | `FwVersion` | ✅ |
| `4` | `GetValues` | ✅ |
| `50` | `GetValuesSelective` | ✅ |

Expand Down
134 changes: 134 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use bitflags::bitflags;
use core::ffi::CStr;

use super::packer::{Packer, Unpacker};

const CRC16: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_XMODEM);
const FRAME_END: u8 = 3;
const FRAME_START_SHORT: u8 = 2;

// The VESC firmware caps the FwVersion payload at 65 bytes.
// This decoder allows each name buffer to hold up to 39 bytes (including NUL).
// The two names still share the same 65-byte payload budget with fixed fields.
const FW_VERSION_NAME_MAX_LEN: usize = 39;

/// Errors that can occur during command encoding.
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
Expand Down Expand Up @@ -35,6 +41,7 @@ pub enum DecodeError {

#[repr(u8)]
enum CommandId {
FwVersion = 0,
GetValues = 4,
SetCurrent = 6,
SetCurrentBrake = 7,
Expand All @@ -49,6 +56,7 @@ impl TryFrom<u8> for CommandId {

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
id if id == CommandId::FwVersion as u8 => Ok(CommandId::FwVersion),
id if id == CommandId::GetValues as u8 => Ok(CommandId::GetValues),
id if id == CommandId::SetCurrent as u8 => Ok(CommandId::SetCurrent),
id if id == CommandId::SetCurrentBrake as u8 => Ok(CommandId::SetCurrentBrake),
Expand Down Expand Up @@ -120,6 +128,9 @@ bitflags! {
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Command<'a> {
/// Requests firmware version information from the VESC.
FwVersion,

/// Requests the complete set of telemetry data from the VESC.
GetValues,

Expand Down Expand Up @@ -155,6 +166,9 @@ pub enum Command<'a> {
impl<'a> Command<'a> {
fn pack_into(&self, packer: &mut Packer) -> Result<(), EncodeError> {
match self {
Self::FwVersion => {
packer.pack_u8(CommandId::FwVersion as u8)?;
}
Self::GetValues => {
packer.pack_u8(CommandId::GetValues as u8)?;
}
Expand Down Expand Up @@ -310,6 +324,102 @@ impl From<u8> for FaultCode {
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum HwType {
Vesc = 0,
VescBms = 1,
CustomModule = 2,
Unknown = 255,
}

impl From<u8> for HwType {
fn from(value: u8) -> Self {
match value {
v if v == HwType::Vesc as u8 => HwType::Vesc,
v if v == HwType::VescBms as u8 => HwType::VescBms,
v if v == HwType::CustomModule as u8 => HwType::CustomModule,
_ => HwType::Unknown,
}
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum QmlHw {
None = 0,
Embedded = 1,
Fullscreen = 2,
Unknown = 255,
}

impl From<u8> for QmlHw {
fn from(value: u8) -> Self {
match value {
v if v == QmlHw::None as u8 => QmlHw::None,
v if v == QmlHw::Embedded as u8 => QmlHw::Embedded,
v if v == QmlHw::Fullscreen as u8 => QmlHw::Fullscreen,
_ => QmlHw::Unknown,
}
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct QmlAppFlags(u8);

bitflags! {
impl QmlAppFlags: u8 {
const EMBEDDED = 1 << 0;
const FULLSCREEN = 1 << 1;
const _ = !0;
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct NrfFlags(u8);

bitflags! {
impl NrfFlags: u8 {
const _ = !0;
}
}

/// Firmware version data returned by the motor controller.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FwVersion {
pub major: u8,
pub minor: u8,
pub hw_name: [u8; FW_VERSION_NAME_MAX_LEN],
pub uuid: [u8; 12],
pub pairing_done: bool,
pub test_version_number: u8,
pub hw_type: HwType,
pub custom_config_num: u8,
pub has_phase_filters: bool,
pub qml_hw: QmlHw,
pub qml_app: QmlAppFlags,
pub nrf_flags: NrfFlags,
pub fw_name: [u8; FW_VERSION_NAME_MAX_LEN],
pub hw_crc: u32,
}

impl FwVersion {
pub fn hw_name(&self) -> Option<&str> {
let cstr = CStr::from_bytes_until_nul(&self.hw_name).ok()?;
cstr.to_str().ok()
}

pub fn fw_name(&self) -> Option<&str> {
let cstr = CStr::from_bytes_until_nul(&self.fw_name).ok()?;
cstr.to_str().ok()
}
}

/// Telemetry data returned by the motor controller.
///
/// Contains temperatures, currents, voltages, rpm, and so on. Returned by
Expand Down Expand Up @@ -353,6 +463,9 @@ pub struct Values {
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CommandReply {
/// Firmware version in response to [`Command::FwVersion`].
FwVersion(FwVersion),

/// Complete telemetry data in response to [`Command::GetValues`]. Contains
/// all available sensor readings and status information.
GetValues(Values),
Expand All @@ -366,12 +479,33 @@ pub enum CommandReply {
impl CommandReply {
fn unpack_from(unpacker: &mut Unpacker) -> Result<Self, DecodeError> {
Ok(match unpacker.unpack_u8()?.try_into()? {
CommandId::FwVersion => Self::unpack_fw_version(unpacker)?,
CommandId::GetValues => Self::unpack_get_values(unpacker)?,
CommandId::GetValuesSelective => Self::unpack_get_values_selective(unpacker)?,
id => return Err(DecodeError::UnknownPacket { id: id as u8 }),
})
}

fn unpack_fw_version(unpacker: &mut Unpacker) -> Result<Self, DecodeError> {
let fw_version = FwVersion {
major: unpacker.unpack_u8()?,
minor: unpacker.unpack_u8()?,
hw_name: unpacker.unpack_c_string::<FW_VERSION_NAME_MAX_LEN>()?,
uuid: unpacker.unpack_uuid()?,
pairing_done: unpacker.unpack_u8()? != 0,
test_version_number: unpacker.unpack_u8()?,
hw_type: unpacker.unpack_u8()?.into(),
custom_config_num: unpacker.unpack_u8()?,
has_phase_filters: unpacker.unpack_u8()? != 0,
qml_hw: unpacker.unpack_u8()?.into(),
qml_app: QmlAppFlags::from_bits_retain(unpacker.unpack_u8()?),
nrf_flags: NrfFlags::from_bits_retain(unpacker.unpack_u8()?),
fw_name: unpacker.unpack_c_string::<FW_VERSION_NAME_MAX_LEN>()?,
hw_crc: unpacker.unpack_u32()?,
};
Ok(CommandReply::FwVersion(fw_version))
}

fn unpack_get_values(unpacker: &mut Unpacker) -> Result<Self, DecodeError> {
let values = Values {
temp_mosfet: unpacker.unpack_f16(10.0)?,
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ pub use command::{
DecodeError,
EncodeError,
FaultCode,
FwVersion,
HwType,
NrfFlags,
QmlAppFlags,
QmlHw,
Values,
ValuesMask,
decode,
Expand Down
18 changes: 18 additions & 0 deletions src/packer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ impl<'a> Unpacker<'a> {
Ok(self.unpack_i16()? as f32 / scale)
}

#[inline]
pub fn unpack_c_string<const N: usize>(&mut self) -> Result<[u8; N], DecodeError> {
let mut buf = [0u8; N];
for slot in buf.iter_mut() {
*slot = self.unpack_u8()?;
if *slot == 0 {
return Ok(buf);
}
}
Err(DecodeError::InvalidFrame)
}

#[inline]
pub fn unpack_uuid(&mut self) -> Result<[u8; 12], DecodeError> {
// SAFETY: `consume(12)` returns a slice with exactly 12 bytes.
Ok(self.consume(12)?.try_into().unwrap())
}

#[inline]
fn consume(&mut self, amount: usize) -> Result<&[u8], DecodeError> {
if self.pos + amount > self.buf.len() {
Expand Down
8 changes: 8 additions & 0 deletions tests/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ use googletest::prelude::*;

use vesc::{self, Command, EncodeError, ValuesMask};

#[test]
fn encode_fw_version() {
let mut buf = [0u8; 16];

let size = vesc::encode(Command::FwVersion, &mut buf).unwrap();
assert_that!(buf[..size], eq([2, 1, 0, 0, 0, 3]));
}

#[test]
fn encode_get_values() {
let mut buf = [0u8; 16];
Expand Down
49 changes: 48 additions & 1 deletion tests/command_reply.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,53 @@
use googletest::prelude::*;

use vesc::{CommandReply, DecodeError, FaultCode, Values};
use vesc::{CommandReply, DecodeError, FaultCode, HwType, NrfFlags, QmlAppFlags, QmlHw, Values};

#[test]
fn decode_fw_version_incomplete_data() {
let input = [2, 3, 0, 7, 0, 153, 151, 3];
assert_that!(vesc::decode(&input), err(eq(&DecodeError::IncompleteData)));
}

#[test]
fn decode_fw_version_incomplete_uuid() {
let input = [2, 5, 0, 7, 0, 97, 0, 105, 54, 3];
assert_that!(vesc::decode(&input), err(eq(&DecodeError::IncompleteData)));
}

#[test]
fn decode_fw_version_full_reply() {
let input = [
2, 41, 0, 7, 0, 86, 69, 83, 67, 54, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 0, 0, 2,
1, 2, 3, 3, 100, 101, 102, 97, 117, 108, 116, 0, 18, 52, 86, 120, 225, 223, 3,
];

let expected = (
eq(&46),
pat!(&CommandReply::FwVersion(pat!(vesc::FwVersion {
major: eq(7),
minor: eq(0),
uuid: eq([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
pairing_done: eq(true),
test_version_number: eq(0),
hw_type: eq(HwType::Vesc),
custom_config_num: eq(2),
has_phase_filters: eq(true),
qml_hw: eq(QmlHw::Fullscreen),
qml_app: eq(QmlAppFlags::from_bits_retain(3)),
nrf_flags: eq(NrfFlags::from_bits_retain(3)),
hw_crc: eq(0x12345678),
..
}))),
);
assert_that!(vesc::decode(&input), ok(expected));

let (_, reply) = vesc::decode(&input).unwrap();
let CommandReply::FwVersion(version) = reply else {
panic!("expected fw version reply");
};
assert_that!(version.hw_name(), some(eq("VESC6")));
assert_that!(version.fw_name(), some(eq("default")));
}

#[test]
fn decode_get_values_zero_rpm() {
Expand Down
Loading