Skip to main content

omnitiles/protocol/
parser.rs

1// SPDX-License-Identifier: MIT
2// © 2025–2026 Christopher Liu
3
4//! Message parser for the OmniTiles command protocol.
5//!
6//! This module provides functionality to parse incoming command messages
7//! and convert them into actionable commands for the OmniTiles system.
8
9use crate::protocol::messages::*;
10
11/// Maximum payload size for any message.
12const MAX_PAYLOAD: usize = 3;
13
14enum State {
15    WaitStart,
16    WaitId,
17    WaitPayload {
18        id: u8,
19        buf: [u8; MAX_PAYLOAD],
20        received: u8,
21        expected: u8,
22    },
23    WaitChecksum {
24        id: u8,
25        buf: [u8; MAX_PAYLOAD],
26        len: u8,
27    },
28}
29
30pub struct Parser {
31    state: State,
32    checksum: u8,
33}
34
35fn payload_len(id: u8) -> Option<u8> {
36    match id {
37        MSG_M1_EXTEND | MSG_M1_RETRACT | MSG_M1_SET_POSITION | MSG_M2_EXTEND | MSG_M2_RETRACT
38        | MSG_M2_SET_POSITION => Some(1),
39        MSG_M1_BRAKE | MSG_M2_BRAKE | MSG_PING | MSG_BASE_BRAKE => Some(0),
40        MSG_BASE_VELOCITY => Some(3),
41        _ => None,
42    }
43}
44
45impl Parser {
46    pub fn new() -> Self {
47        Self {
48            state: State::WaitStart,
49            checksum: 0,
50        }
51    }
52
53    /// Process a single incoming byte. Returns `Some(Command)` if a complete packet is received.
54    pub fn push(&mut self, byte: u8) -> Option<Command> {
55        match self.state {
56            State::WaitStart => {
57                if byte == START_BYTE {
58                    self.state = State::WaitId;
59                    self.checksum = 0;
60                }
61            }
62            State::WaitId => {
63                self.checksum = self.checksum.wrapping_add(byte);
64
65                match payload_len(byte) {
66                    Some(0) => {
67                        self.state = State::WaitChecksum {
68                            id: byte,
69                            buf: [0; MAX_PAYLOAD],
70                            len: 0,
71                        };
72                    }
73                    Some(n) => {
74                        self.state = State::WaitPayload {
75                            id: byte,
76                            buf: [0; MAX_PAYLOAD],
77                            received: 0,
78                            expected: n,
79                        };
80                    }
81                    None => {
82                        self.state = State::WaitStart;
83                    }
84                }
85            }
86            State::WaitPayload {
87                id,
88                mut buf,
89                received,
90                expected,
91            } => {
92                self.checksum = self.checksum.wrapping_add(byte);
93                buf[received as usize] = byte;
94                let received = received + 1;
95
96                if received >= expected {
97                    self.state = State::WaitChecksum {
98                        id,
99                        buf,
100                        len: received,
101                    };
102                } else {
103                    self.state = State::WaitPayload {
104                        id,
105                        buf,
106                        received,
107                        expected,
108                    };
109                }
110            }
111            State::WaitChecksum { id, buf, len } => {
112                let valid = byte == self.checksum;
113                self.state = State::WaitStart;
114
115                if valid {
116                    return match id {
117                        MSG_M1_EXTEND => Some(Command::M1Extend(buf[0])),
118                        MSG_M1_RETRACT => Some(Command::M1Retract(buf[0])),
119                        MSG_M1_BRAKE => Some(Command::M1Brake),
120                        MSG_M1_SET_POSITION => Some(Command::M1SetPosition(buf[0])),
121                        MSG_M2_EXTEND => Some(Command::M2Extend(buf[0])),
122                        MSG_M2_RETRACT => Some(Command::M2Retract(buf[0])),
123                        MSG_M2_BRAKE => Some(Command::M2Brake),
124                        MSG_M2_SET_POSITION => Some(Command::M2SetPosition(buf[0])),
125                        MSG_PING => Some(Command::Ping),
126                        MSG_BASE_VELOCITY if len >= 3 => Some(Command::BaseVelocity {
127                            vx: buf[0] as i8,
128                            vy: buf[1] as i8,
129                            omega: buf[2] as i8,
130                        }),
131                        MSG_BASE_BRAKE => Some(Command::BaseBrake),
132                        _ => None,
133                    };
134                }
135            }
136        }
137        None
138    }
139}