use crate::types::NodeId;
use crate::{Error, Result};
pub const NMT_COMMAND_COB_ID: u16 = 0x000;
pub const HEARTBEAT_COB_BASE: u16 = 0x700;
pub const fn heartbeat_cob_id(node: NodeId) -> u16 {
HEARTBEAT_COB_BASE + node.raw() as u16
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NmtState {
Initialising = 0x00,
Stopped = 0x04,
Operational = 0x05,
PreOperational = 0x7F,
}
impl NmtState {
pub const fn from_wire(byte: u8) -> Result<Self> {
Ok(match byte & 0x7F {
0x00 => NmtState::Initialising,
0x04 => NmtState::Stopped,
0x05 => NmtState::Operational,
0x7F => NmtState::PreOperational,
_ => return Err(Error::UnknownState),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NmtCommand {
StartRemoteNode = 0x01,
StopRemoteNode = 0x02,
EnterPreOperational = 0x80,
ResetNode = 0x81,
ResetCommunication = 0x82,
}
impl NmtCommand {
pub const fn from_byte(byte: u8) -> Result<Self> {
Ok(match byte {
0x01 => NmtCommand::StartRemoteNode,
0x02 => NmtCommand::StopRemoteNode,
0x80 => NmtCommand::EnterPreOperational,
0x81 => NmtCommand::ResetNode,
0x82 => NmtCommand::ResetCommunication,
_ => return Err(Error::UnexpectedCommand),
})
}
}
pub type NmtCommandFrame = [u8; 2];
pub type HeartbeatFrame = [u8; 1];
pub const BOOTUP_FRAME: HeartbeatFrame = [0x00];
pub fn encode_command(command: NmtCommand, target: NodeId) -> NmtCommandFrame {
[command as u8, target.raw()]
}
pub fn decode_command(frame: &NmtCommandFrame) -> Result<(NmtCommand, NodeId)> {
let command = NmtCommand::from_byte(frame[0])?;
let target = if frame[1] == 0 {
NodeId::BROADCAST
} else {
NodeId::new(frame[1])?
};
Ok((command, target))
}
pub fn encode_heartbeat(state: NmtState) -> HeartbeatFrame {
[state as u8]
}
pub fn decode_heartbeat(frame: &HeartbeatFrame) -> Result<NmtState> {
NmtState::from_wire(frame[0])
}
#[derive(Debug)]
pub struct NmtStateMachine {
state: NmtState,
}
impl Default for NmtStateMachine {
fn default() -> Self {
Self::new()
}
}
impl NmtStateMachine {
pub const fn new() -> Self {
Self {
state: NmtState::Initialising,
}
}
pub const fn state(&self) -> NmtState {
self.state
}
pub fn boot(&mut self) -> NmtState {
if let NmtState::Initialising = self.state {
self.state = NmtState::PreOperational;
}
self.state
}
pub fn apply(&mut self, command: NmtCommand) -> NmtState {
self.state = match (self.state, command) {
(_, NmtCommand::ResetNode | NmtCommand::ResetCommunication) => NmtState::Initialising,
(NmtState::Initialising, _) => NmtState::Initialising,
(_, NmtCommand::StartRemoteNode) => NmtState::Operational,
(_, NmtCommand::StopRemoteNode) => NmtState::Stopped,
(_, NmtCommand::EnterPreOperational) => NmtState::PreOperational,
};
self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heartbeat_cob_id_follows_convention() {
assert_eq!(heartbeat_cob_id(NodeId::new(0x05).unwrap()), 0x705);
assert_eq!(NMT_COMMAND_COB_ID, 0x000);
}
#[test]
fn start_broadcast_matches_known_frame() {
assert_eq!(
encode_command(NmtCommand::StartRemoteNode, NodeId::BROADCAST),
[0x01, 0x00]
);
}
#[test]
fn start_single_node_matches_known_frame() {
let f = encode_command(NmtCommand::StartRemoteNode, NodeId::new(5).unwrap());
assert_eq!(f, [0x01, 0x05]);
}
#[test]
fn stop_single_node_matches_known_frame() {
let f = encode_command(NmtCommand::StopRemoteNode, NodeId::new(5).unwrap());
assert_eq!(f, [0x02, 0x05]);
}
#[test]
fn reset_commands_match_known_frames() {
let node = NodeId::new(0x7F).unwrap();
assert_eq!(
encode_command(NmtCommand::EnterPreOperational, node),
[0x80, 0x7F]
);
assert_eq!(encode_command(NmtCommand::ResetNode, node), [0x81, 0x7F]);
assert_eq!(
encode_command(NmtCommand::ResetCommunication, node),
[0x82, 0x7F]
);
}
#[test]
fn command_frame_roundtrips() {
let (cmd, target) = decode_command(&[0x01, 0x05]).unwrap();
assert_eq!(cmd, NmtCommand::StartRemoteNode);
assert_eq!(target, NodeId::new(5).unwrap());
let (cmd, target) = decode_command(&[0x82, 0x00]).unwrap();
assert_eq!(cmd, NmtCommand::ResetCommunication);
assert_eq!(target, NodeId::BROADCAST);
}
#[test]
fn decode_rejects_unknown_command() {
assert_eq!(decode_command(&[0x7E, 0x05]), Err(Error::UnexpectedCommand));
}
#[test]
fn heartbeat_states_match_known_frames() {
assert_eq!(encode_heartbeat(NmtState::Operational), [0x05]);
assert_eq!(encode_heartbeat(NmtState::Stopped), [0x04]);
assert_eq!(encode_heartbeat(NmtState::PreOperational), [0x7F]);
assert_eq!(BOOTUP_FRAME, [0x00]);
}
#[test]
fn heartbeat_decode_ignores_toggle_bit() {
assert_eq!(decode_heartbeat(&[0x05]).unwrap(), NmtState::Operational);
assert_eq!(decode_heartbeat(&[0x85]).unwrap(), NmtState::Operational);
}
#[test]
fn heartbeat_decode_rejects_unknown_state() {
assert_eq!(decode_heartbeat(&[0x42]), Err(Error::UnknownState));
}
#[test]
fn boots_from_init_to_preoperational() {
let mut sm = NmtStateMachine::new();
assert_eq!(sm.state(), NmtState::Initialising);
assert_eq!(sm.boot(), NmtState::PreOperational);
}
#[test]
fn ignores_operational_commands_while_initialising() {
let mut sm = NmtStateMachine::new();
assert_eq!(
sm.apply(NmtCommand::StartRemoteNode),
NmtState::Initialising
);
}
#[test]
fn full_operational_lifecycle() {
let mut sm = NmtStateMachine::new();
sm.boot();
assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
assert_eq!(
sm.apply(NmtCommand::EnterPreOperational),
NmtState::PreOperational
);
assert_eq!(sm.apply(NmtCommand::StopRemoteNode), NmtState::Stopped);
assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
}
#[test]
fn reset_returns_to_initialising_from_any_state() {
let mut sm = NmtStateMachine::new();
sm.boot();
sm.apply(NmtCommand::StartRemoteNode);
assert_eq!(sm.apply(NmtCommand::ResetNode), NmtState::Initialising);
assert_eq!(sm.boot(), NmtState::PreOperational);
sm.apply(NmtCommand::StartRemoteNode);
assert_eq!(
sm.apply(NmtCommand::ResetCommunication),
NmtState::Initialising
);
}
#[test]
fn repeated_command_is_idempotent() {
let mut sm = NmtStateMachine::new();
sm.boot();
sm.apply(NmtCommand::StartRemoteNode);
assert_eq!(sm.apply(NmtCommand::StartRemoteNode), NmtState::Operational);
}
}