Skip to content
Draft
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
299 changes: 273 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
echonet-lite = { git = "https://github.com/marcelocf/echonet-lite-rs.git", branch = "marcelocf/1/singlefunctionlighting" }
echonet-lite = "0.1"
itertools = "0.10"
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
159 changes: 159 additions & 0 deletions src/echonet/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! Contains data types that extends he echonet-lite-rs types.
//! Special attention on handling [`Prorperty`]

/// Properties common to wide variety of devices.
#[repr(u8)]
pub(crate) enum GeneralPropertyCode {
/// Indicates the node operating status
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: required
/// states:
/// 0x30u8: Booting
/// 0x31u8: Not booting
OperatingStatus = 0x80u8,

/// Indicates ECHONET Lite versionused by communication middleware and message types supported by communication middleware
/// 1st byte: major version, 2nd byte: minor version, 3rd data: bitmap or data format, 4th byte: 0x00.
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
VersionInformation = 0x81u8,

/// Number to identify the node implementing the device object in the domain
/// 1st data is 0xFE, 2nd to 4th data is manufacture code. The rest should be unique to each device.
/// accesRule:
/// get: required
/// set: notApplicable
/// inf: optional
IndentificationNumber = 0x83u8,

/// indicates whether a fault has occurred or not
/// accessRule:
/// get: optional
/// set: notApplicable
/// inf: optional
/// states: YES/NO
FaultStatus = 0x88u8,

/// Fault description
/// accessRule:
/// get: optional
/// set: notApplicable
/// inf:optional
/// data: number between 0 and 1004.
FaultDescription = 0x89u8,

/// 3-byte Manufacturer code
/// accesRule:
/// get: required
/// set: notApplicable
/// inf: optional
ManufacturerCode = 0x8Au8,

/// 3-byte business facility code
/// accessRule:
/// get: optional
/// set: notApplicable
/// inf: optional
BusinessFacilityCode = 0x8Bu8,

/// Identifies the product using ASCII code
/// accessRule:
/// get: optional
/// set: notApplicable
/// inf: optional
/// raw_12
ProductCode = 0x8Cu8,

/// Indicates the product number using ASCII code
/// accesRule:
/// get: optional
/// set: notApplicable
/// inf: optional
/// raw_12
SerialNumber = 0x8Du8,

/// 4-byte production date code
/// accessRule:
/// get: optional
/// set: notApplicable
/// inf: optional
ProductionDate = 0x8Eu8,

/// Enumeration of EPC in case of the count is than 16, or bitmap in case of the count is more than 15.
/// 1st byte is the count of property.
/// accesRule:
/// get: required
/// set: notApplicable
/// inf: optional
StatusChangeAnnouncementPropertyMap = 0x9Du8,

/// Enumeration of EPC in case of the count is less than 16, or bitmap in case of the count is more than 15.
/// 1st byte is count of property.
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
SetPropertyMap = 0x9Eu8,

/// Enumeration of EPC in case of the count is less than 16, or bitmap in case of the count is more than 15.
/// 1st byte is count of property.
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
GetPropertyMap = 0x9Fu8,
}

/// Property code for profile node 0x0EF0
#[repr(u8)]
pub(crate) enum ProfilePropertyCode {
/// 2 byte data to identify each node in a domain
/// accessRule:
/// get: optional
/// set: optional
/// inf: optional
UniqueIdentifierData = 0xBFu8,

/// Total number of instances held by self-node
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
/// 3-byte data for integer. excluding node profile object instance.
SelfNodeInstances = 0xD3u8,

/// Total number of classes held by self-node
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
/// Including node profile class
SelfNodeClasses = 0xD4u8,

/// Instance list when self-node instance configuration is changed.
/// Number of instances + Instance list
/// accessRule:
/// get: Optional
/// set: notApplicable
/// inf: required
InstanceListNotification = 0xD5u8,

/// Number of Instances + Instance List
/// Instance list is an array of EOJ. (3 bytes)
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
SelfNodeInstanceListS = 0xd6u8,

/// Number of classess + class list, excluding node profile class
/// accessRule:
/// get: required
/// set: notApplicable
/// inf: optional
SelfNodeClassListS = 0xD7u8,
}
38 changes: 33 additions & 5 deletions src/discovery.rs → src/echonet/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@
// that went innactive.

use echonet_lite as el;
use el::prelude::*;
use std::io;
use el::{prelude::*, props};
use std::net::{Ipv4Addr, UdpSocket};
use std::time::Duration;

use tracing::info;

use crate::echonet::profile::InstanceList;
use crate::error::Error;

use super::data::ProfilePropertyCode;

const EL_MULTICAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 23, 0);

pub fn find() -> io::Result<()> {
struct Discovery {}

impl super::Listener for Discovery {
// do something with the incoming packet
// returns a vectors of new discovery packets to send.
fn process(_request: super::Request) -> Vec<super::Response> {
vec![]
}
}

pub fn find() -> Result<(), Error> {
let socket = UdpSocket::bind("0.0.0.0:3610")?;
socket.set_read_timeout(Some(Duration::from_secs(2)))?;
socket.set_multicast_loop_v4(true)?;
Expand All @@ -22,7 +37,13 @@ pub fn find() -> io::Result<()> {
.seoj([0x05u8, 0xFFu8, 0x01u8])
.deoj([0x0Eu8, 0xF0u8, 0x01u8])
.esv(el::ServiceCode::Get)
.props(el::props!([0x80, []], [0xD6, []]))
.props(el::props!(
[ProfilePropertyCode::UniqueIdentifierData as u8, []],
[ProfilePropertyCode::SelfNodeClasses as u8, []],
[ProfilePropertyCode::SelfNodeClassListS as u8, []],
[ProfilePropertyCode::SelfNodeInstances as u8, []],
[ProfilePropertyCode::SelfNodeInstanceListS as u8, []]
))
.build();
let bytes = packet.serialize().expect("fail to serialize");

Expand All @@ -34,9 +55,16 @@ pub fn find() -> io::Result<()> {
Ok((_, src_addr)) => {
if let Ok((_, response)) = el::ElPacket::from_bytes(&buffer) {
if response.is_response_for(&packet) {
let obj: ClassPacket = response.into();
let obj: ClassPacket = response.clone().into();
info!("got response from {}", src_addr);
info!("{:}", obj);
if let ClassPacket::Profile(_) = obj {
let instances = InstanceList::from(response.props)?;
for instance in instances {
let obj = ClassPacket::new(instance, props![]);
info!(" * instance: {obj}")
}
}
}
}
}
Expand Down
98 changes: 98 additions & 0 deletions src/echonet/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use echonet_lite::{prelude::ClassPacket, ElPacket};
use std::{
io,
net::{IpAddr, Ipv4Addr},
};
use tokio::net::UdpSocket;
use tracing::info;

pub mod data;
pub mod discovery;
pub mod profile;

pub const EL_MULTICAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 23, 0);

// TODO: this probably need the following refactoring:
// mod echonet::io{Request, Response,server::{Server}}
// and then all the complex types within their own modules

// Represents an Echonet Lite request. Including the packet and source address
pub struct Request {
packet: ElPacket,
source: IpAddr,
}

impl Request {
fn new(packet: ElPacket, source: IpAddr) -> Self {
Request { packet, source }
}
}

impl From<Request> for ElPacket {
fn from(req: Request) -> ElPacket {
req.packet
}
}

impl From<Request> for IpAddr {
fn from(req: Request) -> Self {
req.source
}
}

// Response to echonet lite requests.
pub struct Response {
packet: ElPacket,
destination: IpAddr,
}

impl Response {
fn new(packet: ElPacket, destination: IpAddr) -> Self {
Self {
packet,
destination,
}
}
}

// Represents an Echonet Lite server

pub struct Server {
socket: UdpSocket,
}

trait Listener {
// a listener can have multiple responses
// for example, when sending a discovery package it can trigger multiple
// different messages
fn process(request: Request) -> Vec<Response>;
}

// Server implementation using Tokio for async code
impl Server {
async fn new() -> io::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:3610").await?;
socket.set_multicast_loop_v4(true)?;
socket.join_multicast_v4(EL_MULTICAST_ADDR, [0, 0, 0, 0].into())?;

Ok(Server { socket })
}

async fn listen(self) -> io::Result<()> {
loop {
let mut buffer = [0u8; 1024];
match self.socket.recv_from(&mut buffer).await {
Err(_) => break,
Ok((_, src_addr)) => {
if let Ok((_, packet)) = ElPacket::from_bytes(&buffer) {
let obj: ClassPacket = packet.into();
info!("got response from {}", src_addr);
info!("{:}", obj);
}
}
}
}

Ok(())
}
}
56 changes: 56 additions & 0 deletions src/echonet/profile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Useful tools for managing profile types.
use std::ops::Deref;

use super::data::ProfilePropertyCode;
use crate::error::Error;
use echonet_lite::{EchonetObject, Properties};
use itertools::Itertools;

#[derive(Clone, Debug)]
pub struct InstanceList(Vec<EchonetObject>);

impl InstanceList {
/// Converts from a list of properties into a list of instances, erroring in case
/// the property type is not available *or* malformed.
pub fn from(properties: Properties) -> Result<Self, Error> {
let prop = properties
.iter()
.find(|&p| (ProfilePropertyCode::SelfNodeInstanceListS as u8).eq(&p.epc))
.ok_or_else(|| Error::UnsetProperty("InstanceList".to_owned()))?;

let mut instances: Vec<EchonetObject> = Vec::new();

let mut it = prop.edt.iter();

let count = it.next().map(|&v| v as usize).unwrap_or(0);

while let Some((a, b, c)) = it.next_tuple() {
instances.push([*a, *b, *c].into());
}

if !count.eq(&instances.len()) {
Err(Error::MalformedProperty(format!(
"property count doesn't match: {count} != {}",
instances.len()
)))?;
}

Ok(InstanceList(instances))
}
}

impl IntoIterator for InstanceList {
type Item = EchonetObject;
type IntoIter = <Vec<EchonetObject> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl Deref for InstanceList {
type Target = Vec<EchonetObject>;

fn deref(&self) -> &Self::Target {
&self.0
}
}
Loading