feat: rewrite spec and proto

This commit is contained in:
Strix 2023-11-30 21:52:49 +01:00
parent 11e58c3d1d
commit 7999ac6103
No known key found for this signature in database
GPG key ID: 5F35B3B8537287A7
6 changed files with 298 additions and 144 deletions

View file

@ -1,10 +1,69 @@
> **Version:** `1`
> **Authored by:** `Raine <raine@ixvd.net>`
> **Status**: `planning`
>
# Table of contents
- [Table of contents](#table-of-contents)
- [Prelude](#prelude)
- [Interpretation of types](#interpretation-of-types)
- [Booleans](#booleans)
- [Reserved fields](#reserved-fields)
- [Structure](#structure)
- [Data types](#data-types)
- [Dynamic data (`dynamic_data`)](#dynamic-data-dynamic_data)
- [Commands](#commands)
- [`0x0*` - Node management](#0x0---node-management)
- [`0x00` - Ping](#0x00---ping)
- [Command data](#command-data)
- [`0x01` - Register node](#0x01---register-node)
- [Command data](#command-data-1)
- [`0x02` - Remove node](#0x02---remove-node)
- [Command data](#command-data-2)
- [`0x0A` - Acknowledge packets](#0x0a---acknowledge-packets)
- [`0x0E` - Error](#0x0e---error)
- [Command data](#command-data-3)
- [Error codes](#error-codes)
- [`0x1*` - Properties control](#0x1---properties-control)
- [`0x10` - Register property](#0x10---register-property)
- [Command data](#command-data-4)
- [`0x11` - Remove property](#0x11---remove-property)
- [Command data](#command-data-5)
- [`0x12` - Get property](#0x12---get-property)
- [Command data](#command-data-6)
- [`0x13` - Set property](#0x13---set-property)
- [Command data](#command-data-7)
- [`0x14` - Reset property](#0x14---reset-property)
- [Command data](#command-data-8)
- [`0x1A` - Subscribe to property](#0x1a---subscribe-to-property)
- [`0x1B` - Unsubscribe to property](#0x1b---unsubscribe-to-property)
- [`0x1F` - All properties](#0x1f---all-properties)
- [Command data](#command-data-9)
- [`0xF*` - Raw data transmission](#0xf---raw-data-transmission)
- [`0xF0` - Setup transfer](#0xf0---setup-transfer)
- [Command data](#command-data-10)
- [`0xF1` - Data](#0xf1---data)
- [Command data](#command-data-11)
# Prelude
Version 1 has zero security on itself.
This document only describes what the protocol looks like.
The specification can be found at `./specification.md`.
## Interpretation of types
### Booleans
If in the description of a property it's marked as a boolean the exact logic that should be executed is:
`value != 0`
## Reserved fields
The "reserved" column (if present) will state what should be filled in by each side.
- "no": this should be filled in by both sides. (default)
- "request": this should only be filled in the request.
- "response": this should only be filled in the response.
# Structure
@ -22,30 +81,6 @@ Packets are sent in big endian.
| `0x14` | `<0x11-0x12:data_length>` | `data` | This is the data of the command | `0x0000` |
| `0x14 + <0x11-0x12:data_length>` | 4 bytes | `checksum` | This is the CRC32 checksum of the packet without the checksum. | `0x00000000` |
# Packets in practice
## Statuses
When an error occurs the response should be a `0x0E` (error command).
This can contain error data.
To mark a success, you should just send back the expected response.
## Reserved fields
The "reserved" column (if present) will state what should be filled in by each side.
- "no": this should be filled in by both sides. (default)
- "request": this should only be filled in the request.
- "response": this should only be filled in the response.
# Error checking
If the CRC32 doesn't match up the receiver will send a `0x0E` (error) packet to the probable source.
The error code `0x01`/`net_broken_packet` should be used. The metadata may contain some textual info on what went
wrong.
The master will not track the broken packet's `packet_id`, so the slave can send it again.
# Data types
Domo has a data framework reliant on data types.
@ -64,13 +99,6 @@ These define what kind of data the property holds.
| `0x2*` | | | **Color** |
| `0x20` | 3 bytes | RGB | An RGB value. |
<!--
REQ prop/get "firmware"
RES prop/get <DataType::Identifier(0xABFF21FA)> // this is the setup packet id
RES raw/setup size=10B
RES raw/data [0x0000 (0x2710)] ....
-->
## Dynamic data (`dynamic_data`)
Dynamic data is a data snippet with a `prop_data_type` byte and the data next to it.
@ -78,9 +106,9 @@ Dynamic data is a data snippet with a `prop_data_type` byte and the data next to
> **Note**: this section uses relative offset
| offset | size | name | description | example |
| ------ | ----------------------------- | ---------------- | ----------------------- | ------- |
| ------ | --------------------------- | ---------------- | ----------------------- | ------- |
| `0x00` | 1 byte | `prop_data_type` | Describes the data type | `0x00` |
| `0x01` | depending on `prop_data_type` | `data` | The data | |
| `0x01` | depends on `prop_data_type` | `data` | The data | |
# Commands
@ -121,23 +149,13 @@ Remove node from network
there is no extra data required.
### `0x03` - Register property
### `0x0A` - Acknowledge packets
#### Command data
Acknowledge received packets.
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ------------------------------------------------------------------ | -------- | ------- |
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
| `0x34` | 1 byte | `data_type` | The type of data; see "Data types". | no | `0x01` |
| `0x35` | 1 byte | `read_only` | Whether the property is readonly. <br/>(`0x00` = no, `0x01` = yes) | no | `0x00` |
### `0x04` - Remove property
#### Command data
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ------------------------------------------- | -------- | ------- |
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
| ------ | ------------- | ----------- | ---------------------------------- | ----------- | ------- |
| `0x14` | 4 bytes * `n` | `packet_id` | list of packet_id's to acknowledge | origin node | |
### `0x0E` - Error
@ -146,9 +164,9 @@ Send a packet to state an error occurred.
#### Command data
| offset | size | name | description | example |
| ------ | ----------------------------- | ------------ | ------------------------------- | ------- |
| ------ | ------------- | ------------ | ------------------------------- | ------- |
| `0x14` | 1 byte | `error_code` | error code; check 'Error codes' | `0x00` |
| `0x15` | `<0x14-0x15:metadata_length>` | `metadata` | metadata | |
| `0x15` | `data_length` | `metadata` | metadata | |
#### Error codes
@ -167,20 +185,40 @@ Send a packet to state an error occurred.
| `0x20` | `node_invalid_property` | the property specified is invalid | no |
| `0x21` | `node_failed_request` | the request could not be completed. | consult metadata |
## `0x1*` - Properties control
### `0x10` - Get property
### `0x10` - Register property
#### Command data
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ---------------------------------------------- | -------- | ------- |
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
| `0x34` | 1 byte | `data_type` | The type of data; see "Data types". | no | `0x01` |
| `0x35` | 1 byte | `read_only` | Whether the property is readonly. (boolean) | no | `0x00` |
| `0x36` | 1 byte | `descriptive` | Whether the property is descriptive. (boolean) | no | `0x00` |
### `0x11` - Remove property
#### Command data
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ------------------------------------------- | -------- | ------- |
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
### `0x12` - Get property
Get a properties value
#### Command data
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ------------------------------------------- | -------- | -------- |
| ------ | -------- | --------------- | ------------------------------------------- | ---------------- | -------- |
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
| `0x34` | dynamic | `dynamic_data` | Dynamic data snippet | response | `0x0100` |
| `0x34` | dynamic | `dynamic_data` | Dynamic data snippet | destination node | `0x0100` |
### `0x11` - Set property
### `0x13` - Set property
#### Command data
@ -189,7 +227,7 @@ Get a properties value
| `0x14` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
| `0x34` | dynamic | `dynamic_data` | Dynamic data snippet | no | `0x0100` |
### `0x12` - Reset property
### `0x14` - Reset property
#### Command data
@ -204,13 +242,26 @@ Get a properties value
| `0x14` | 4 bytes | `device_id` | Who's property? | no | `0x00000000` |
| `0x18` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
### `0x1A` - Unsubscribe to property
### `0x1B` - Unsubscribe to property
| offset | size | name | description | reserved | example |
| ------ | -------- | --------------- | ------------------------------------------- | -------- | ------------ |
| `0x14` | 4 bytes | `device_id` | Who's property? | no | `0x00000000` |
| `0x18` | 32 bytes | `property_name` | The name of the property as a UTF-8 string. | no | "Power" |
### `0x1F` - All properties
Return all properties.
#### Command data
| offset | size | name | description | reserved | example |
| ------ | ------ | ------ | ------------------------- | -------- | ------- |
| `0x14` | 1 byte | `type` | | no | `0x00` |
| | | | `0x00` - all | | |
| | | | `0x01` - descriptive only | | |
| | | | `0x02` - m only | | |
## `0xF*` - Raw data transmission
This is useful for things like OTA updates.
@ -222,9 +273,9 @@ Since this procedure is quite expensive (network wise), the sides must shake han
#### Command data
| offset | size | name | description | reserved | example |
| ------ | --------- | ------ | -------------------- | -------- | ------- |
| `0x14` | 8 bytes | `size` | u64 size of the data | request | `0x00` |
| `0x1c` | 128 bytes | `mime` | media type of data | request | `0x00` |
| ------ | --------- | ------ | -------------------- | ----------- | ------- |
| `0x14` | 8 bytes | `size` | u64 size of the data | origin node | `0x00` |
| `0x1c` | 128 bytes | `mime` | media type of data | origin node | `0x00` |
### `0xF1` - Data
@ -236,6 +287,6 @@ This may be sent without the initial transfer setup for custom implementations.
The `sequence_number` is added for redundancy, all data sent will always reply to it's previous data segment.
| offset | size | name | description | example |
| ------ | -------- | ----------------- | ------------------------------- | ------------ |
| ------ | ------------- | ----------------- | ------------------------------- | ------------ |
| `0x14` | 4 bytes | `sequence_number` | the sequence number of the data | `0x00000000` |
| `0x18` | `<size>` | `data` | the data | |
| `0x18` | `data_length` | `data` | the data | |

46
doc/specification.md Normal file
View file

@ -0,0 +1,46 @@
# Protocol Design
In this document I will define how this document is designed and how it should be implemented.
# Prelude
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL",
"NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
RFC 2119.
"node" or "nodes" are referring to a device that implements the Domo protocol.
## Transactions
All interactions between 2 nodes is called a transaction; a request and response.
The initial packet sent in the transaction is called the "origin packet".
A initial packet MUST NOT have a `reply_to`.
A transaction consists of 2 nodes;
- "Origin node"; the node who sent the origin packet.
- "Destination node"; the node who is marked as the destination by the origin packet.
# Behaviour
This chapter describes the behaviour of a node.
## Statuses
When an error occurs on the destination node in result of a request, the response to the origin node MUST be a `0x0E` (error command).
This MAY contain error data.
To mark a success, you MUST send back the request packet altered with the data specified in the protocol.
## Reserved addresses
### Broadcast (`0xFFFFFFFF`)
Nodes MUST listen to this address.
Owner nodes MUST forward this address to their owners if applicable.
# Packet inspection
## Error checking
If the CRC32 doesn't match up the destination node MUST send a `0x0E` (error) packet to the origin node.
The error code `0x01`/`net_broken_packet` MUST be used. The metadata MAY contain some textual info on what went wrong.
The master MUST not track the broken packet's `packet_id`, so the slave can send it again.

View file

@ -12,13 +12,8 @@ pub enum NodeManagementCommand {
Ping,
RegisterNode { device_id: Identifier },
RemoveNode,
RegisterProperty {
property_name: String,
data_type: u8,
read_only: bool,
},
RemoveProperty {
property_name: String,
AcknowledgePackets {
packet_ids: Vec<Identifier>
},
Error {
error_code: u8,
@ -32,9 +27,8 @@ impl NodeManagementCommand {
NodeManagementCommand::Ping => 0x00,
NodeManagementCommand::RegisterNode { .. } => 0x01,
NodeManagementCommand::RemoveNode => 0x02,
NodeManagementCommand::RegisterProperty { .. } => 0x03,
NodeManagementCommand::RemoveProperty { .. } => 0x04,
NodeManagementCommand::Error { .. } => 0x0E
NodeManagementCommand::AcknowledgePackets { .. } => 0x0A,
NodeManagementCommand::Error { .. } => 0x0E,
}
}
}
@ -68,26 +62,15 @@ impl TryFrom<RawPacket> for NodeManagementCommand {
},
// remove node
0x02 => Ok(NodeManagementCommand::RemoveNode),
// register property
0x03 => if raw_packet.data_length == 34 {
Ok(NodeManagementCommand::RegisterProperty {
property_name: String::from_utf8(raw_packet.data[..32].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
data_type: raw_packet.data[33],
read_only: raw_packet.data[34] != 0,
0x0A => if raw_packet.data_length >= 4 && (raw_packet.data_length % 4 == 0) {
Ok(NodeManagementCommand::AcknowledgePackets {
packet_ids: raw_packet.data.chunks(4)
.map(|c| Identifier::from(c))
.collect()
})
} else {
build_invalid_input_err("expected 34 bytes")
},
// remove property
0x04 => if raw_packet.data_length == 32 {
Ok(NodeManagementCommand::RemoveProperty {
property_name: String::from_utf8(raw_packet.data.to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?
})
} else {
build_invalid_input_err("expected 32 bytes")
},
build_invalid_input_err("expected at least 4 bytes and data_length%4=0")
}
// error
0x0E => if raw_packet.data_length >= 1 {
Ok(NodeManagementCommand::Error {

View file

@ -6,18 +6,21 @@ impl Into<Vec<u8>> for NodeManagementCommand {
match self {
NodeManagementCommand::Ping => vec![],
NodeManagementCommand::RegisterNode { device_id } => {
vec![device_id.bytes[0], device_id.bytes[1], device_id.bytes[2], device_id.bytes[3]]
},
vec![
device_id.bytes[0],
device_id.bytes[1],
device_id.bytes[2],
device_id.bytes[3],
]
}
NodeManagementCommand::RemoveNode => vec![],
NodeManagementCommand::RegisterProperty { property_name, data_type, read_only } => {
let mut vec = vec![];
vec.extend(property_name.into_bytes());
vec.push(data_type);
vec.push(read_only as u8);
vec
},
NodeManagementCommand::RemoveProperty { property_name } => property_name.into_bytes(),
NodeManagementCommand::Error { error_code, metadata } => {
NodeManagementCommand::AcknowledgePackets { packet_ids } =>
packet_ids.iter().map(|i| i.bytes).flatten().collect(),
NodeManagementCommand::Error {
error_code,
metadata,
} => {
let mut vec = vec![];
vec.push(error_code);
vec.extend(metadata);

View file

@ -1,15 +1,24 @@
use std::io;
use crate::data_types::DataType;
use crate::identifier::Identifier;
use crate::packet::packet_data::PacketData;
use crate::packet::raw_packet::RawPacket;
use crate::packet::{Packet, ToPacket};
use crate::packet::packet_data::PacketData;
use crate::prelude::as_u32_be;
use std::io;
pub mod vec;
#[derive(Debug, Clone)]
pub enum PropertyControlCommand {
Register {
property_name: String,
data_type: u8,
read_only: bool,
// TODO: descriptive
},
Remove {
property_name: String,
},
Get {
property_name: String,
data: DataType,
@ -34,21 +43,32 @@ pub enum PropertyControlCommand {
impl PropertyControlCommand {
pub fn command(&self) -> u8 {
match self {
PropertyControlCommand::Get { .. } => 0x10,
PropertyControlCommand::Set { .. } => 0x11,
PropertyControlCommand::Reset { .. } => 0x12,
PropertyControlCommand::Register { .. } => 0x10,
PropertyControlCommand::Remove { .. } => 0x11,
PropertyControlCommand::Get { .. } => 0x12,
PropertyControlCommand::Set { .. } => 0x13,
PropertyControlCommand::Reset { .. } => 0x14,
PropertyControlCommand::Subscribe { .. } => 0x1A,
PropertyControlCommand::Unsubscribe { .. } => 0x1B
PropertyControlCommand::Unsubscribe { .. } => 0x1B,
}
}
}
impl ToPacket for PropertyControlCommand {
fn to_packet(self, src: Identifier, dest: Identifier, packet_id: Identifier, reply_to: Identifier) -> Packet {
fn to_packet(
self,
src: Identifier,
dest: Identifier,
packet_id: Identifier,
reply_to: Identifier,
) -> Packet {
Packet {
src, dest, packet_id, reply_to,
src,
dest,
packet_id,
reply_to,
command: self.command(),
data: PacketData::PropertyControl(self.clone())
data: PacketData::PropertyControl(self.clone()),
}
}
}
@ -56,26 +76,56 @@ impl ToPacket for PropertyControlCommand {
impl TryFrom<RawPacket> for PropertyControlCommand {
type Error = io::Error;
fn try_from(raw_packet: RawPacket) -> io::Result<Self> {
let build_invalid_input_err = |r: &str| {
Err(io::Error::new(io::ErrorKind::InvalidInput, r))
};
let build_invalid_input_err = |r: &str| Err(io::Error::new(io::ErrorKind::InvalidInput, r));
match raw_packet.command {
// remove node
0x10 => {
if raw_packet.data_length == 34 {
Ok(PropertyControlCommand::Register {
property_name: String::from_utf8(raw_packet.data[..32].to_vec()).map_err(
|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"),
)?,
data_type: raw_packet.data[33],
read_only: raw_packet.data[34] != 0,
})
} else {
build_invalid_input_err("expected 34 bytes")
}
}
// remove property
0x11 => {
if raw_packet.data_length == 32 {
Ok(PropertyControlCommand::Remove {
property_name: String::from_utf8(raw_packet.data.to_vec()).map_err(
|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"),
)?,
})
} else {
build_invalid_input_err("expected 32 bytes")
}
}
// get property
0x10 => if raw_packet.data_length == 33 {
0x12 => {
if raw_packet.data_length == 33 {
Ok(PropertyControlCommand::Get {
property_name: String::from_utf8(raw_packet.data[..0x34].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8")
})?,
data: DataType::try_from(raw_packet.data[0x34..].to_vec())?,
})
} else {
build_invalid_input_err("expected 33 bytes")
},
}
}
// set property
0x11 => {
0x13 => {
if raw_packet.data_length == 33 {
Ok(PropertyControlCommand::Set {
property_name: String::from_utf8(raw_packet.data[..0x34].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8")
})?,
data: DataType::try_from(raw_packet.data[0x34..].to_vec())?,
})
} else {
@ -83,11 +133,13 @@ impl TryFrom<RawPacket> for PropertyControlCommand {
}
}
// reset property
0x12 => {
0x14 => {
if raw_packet.data_length == 33 {
Ok(PropertyControlCommand::Reset {
property_name: String::from_utf8(raw_packet.data[..0x34].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8")
})?,
})
} else {
build_invalid_input_err("expected 33 bytes")
@ -97,9 +149,13 @@ impl TryFrom<RawPacket> for PropertyControlCommand {
0x1A => {
if raw_packet.data_length == 36 {
Ok(PropertyControlCommand::Subscribe {
device_id: Identifier::from(as_u32_be(raw_packet.data[0x14..0x18].as_ref())),
device_id: Identifier::from(as_u32_be(
raw_packet.data[0x14..0x18].as_ref(),
)),
property_name: String::from_utf8(raw_packet.data[0x18..0x4C].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8")
})?,
})
} else {
build_invalid_input_err("expected 36 bytes")
@ -109,15 +165,22 @@ impl TryFrom<RawPacket> for PropertyControlCommand {
0x1B => {
if raw_packet.data_length == 36 {
Ok(PropertyControlCommand::Unsubscribe {
device_id: Identifier::from(as_u32_be(raw_packet.data[0x14..0x18].as_ref())),
device_id: Identifier::from(as_u32_be(
raw_packet.data[0x14..0x18].as_ref(),
)),
property_name: String::from_utf8(raw_packet.data[0x18..0x4C].to_vec())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8"))?,
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "String is not UTF-8")
})?,
})
} else {
build_invalid_input_err("expected 36 bytes")
}
}
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "command group is unsupported"))
_ => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"command group is unsupported",
)),
}
}
}

View file

@ -4,6 +4,14 @@ use crate::commands::property_control::PropertyControlCommand;
impl Into<Vec<u8>> for PropertyControlCommand {
fn into(self) -> Vec<u8> {
match self {
PropertyControlCommand::Register { property_name, data_type, read_only } => {
let mut vec = vec![];
vec.extend(property_name.into_bytes());
vec.push(data_type);
vec.push(read_only as u8);
vec
},
PropertyControlCommand::Remove { property_name } => property_name.into_bytes(),
PropertyControlCommand::Get { property_name, data } => {
let mut v = vec![];
v.extend(property_name.into_bytes());