feat: Simple client/server implementation to control lights

This commit is contained in:
Arne Dußin 2024-08-22 22:01:48 +02:00
parent 6202f86265
commit 797ab60b8b
8 changed files with 620 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

286
Cargo.lock generated Normal file
View file

@ -0,0 +1,286 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rppal"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b37e992f3222e304708025de77c9e395068a347449d0d7164f52d3beccdbd8d"
dependencies = [
"libc",
]
[[package]]
name = "serde"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zwote_sonne"
version = "0.1.0"
dependencies = [
"bincode",
"clap",
"clap_derive",
"rppal",
"serde",
"serde_derive",
]

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "zwote_sonne"
version = "0.1.0"
edition = "2021"
default-run = "client"
[[bin]]
name = "server"
path = "src/server.rs"
[[bin]]
name = "client"
path = "src/client.rs"
[dependencies]
bincode = "1.3.3"
clap = { version = "4.5.16", features = ["derive"] }
clap_derive = "4.5.13"
rppal = "0.19.0"
serde = "1.0.208"
serde_derive = "1.0.208"

13
rustfmt.toml Normal file
View file

@ -0,0 +1,13 @@
brace_style = "AlwaysNextLine"
condense_wildcard_suffixes = true
control_brace_style = "ClosingNextLine"
fn_single_line = true
format_strings = true
hard_tabs = true # Please don't cancel me
hex_literal_case = "Lower"
imports_granularity = "Module"
match_block_trailing_comma = true
newline_style = "Unix"
group_imports = "StdExternalCrate"
struct_field_align_threshold = 15
wrap_comments = true

65
src/client.rs Normal file
View file

@ -0,0 +1,65 @@
use std::net::{SocketAddr, TcpStream};
use clap::Parser;
use clap_derive::Subcommand;
use packet::Packet;
mod packet;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args
{
#[arg(short, long)]
connect: SocketAddr,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command
{
/// Set a constant brightness in Range 0.0..=1.0
Constant
{
brightness: f64
},
/// Sinusoidal curve with a period of 0.0.. seconds
Sine
{
period: f64
},
/// Triangle curve with a period of 0.0.. seconds
Triangle
{
period: f64
},
/// Sawtooth curve with a period of 0.0.. seconds
Sawtooth
{
period: f64
},
/// Square curve witha period of 0.0.. seconds and a duty cycle of 0.0..1.0
Square
{
period: f64, duty: f64
},
}
fn main()
{
let args = Args::parse();
let mut stream = TcpStream::connect(args.connect).expect("Unable to connect to server");
let packet = match args.command {
Command::Constant { brightness } => Packet::Constant(brightness),
Command::Sine { period } => Packet::Sine(period),
Command::Triangle { period } => Packet::Triangle(period),
Command::Sawtooth { period } => Packet::Sawtooth(period),
Command::Square { period, duty } => Packet::Square { period, duty },
};
packet
.write_to_stream(&mut stream)
.expect("Unable to send packet");
}

39
src/main.rs Normal file
View file

@ -0,0 +1,39 @@
use core::f64;
use std::f64::consts::TAU;
use std::thread;
use std::time::{Duration, Instant};
use rppal::pwm::{Channel, Polarity, Pwm};
/// 1 kHz PWM frequency
const PWM_FREQUENCY: f64 = 1000.;
fn triangle_wave(t: f64) -> f64 { (t - (t + 0.5).floor()).abs() * 2. }
fn sine_wave(t: f64) -> f64 { (f64::sin(TAU * t) + 1.) * 0.5 }
fn square_wave(t: f64, high_time: f64) -> f64
{
if t - t.floor() < high_time {
1.
}
else {
0.
}
}
fn sawtooth_wave(t: f64) -> f64 { t - t.floor() }
fn main()
{
println!("Starting light control");
let pwm = Pwm::with_frequency(Channel::Pwm0, PWM_FREQUENCY, 0.5, Polarity::Normal, true)
.expect("Unable to initialise PWM");
let start = Instant::now();
loop {
let time = start.elapsed().as_secs_f64();
let brightness = sawtooth_wave(time * 0.5);
pwm.set_duty_cycle(brightness).unwrap();
thread::sleep(Duration::from_millis(5));
}
}

42
src/packet.rs Normal file
View file

@ -0,0 +1,42 @@
use std::net::TcpStream;
use bincode::config::{Bounded, WithOtherLimit};
use bincode::{DefaultOptions, Options};
use serde_derive::{Deserialize, Serialize};
const MAX_PACKET_SIZE_BYTES: u64 = 1024;
#[derive(Serialize, Deserialize)]
pub enum Packet
{
/// Constant brightness in range 0.0..1.0
Constant(f64),
/// Sinusoidal curve with a period of 0.0.. seconds
Sine(f64),
/// Triangle curve with a period of 0.0.. seconds
Triangle(f64),
/// Sawtooth curve with a period of 0.0.. seconds
Sawtooth(f64),
/// Square curve witha period of 0.0.. seconds and a duty cycle of 0.0..1.0
Square
{
period: f64, duty: f64
},
}
#[inline]
fn ser_options() -> WithOtherLimit<DefaultOptions, Bounded>
{
DefaultOptions::new().with_limit(MAX_PACKET_SIZE_BYTES)
}
impl Packet
{
pub fn write_to_stream(&self, stream: &mut TcpStream) -> Result<(), Box<bincode::ErrorKind>>
{
ser_options().serialize_into(stream, &self)
}
pub fn read_from_stream(stream: &mut TcpStream) -> Result<Self, Box<bincode::ErrorKind>>
{
ser_options().deserialize_from(stream)
}
}

153
src/server.rs Normal file
View file

@ -0,0 +1,153 @@
use core::f64;
use std::f64::consts::TAU;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, TcpListener, TcpStream};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::{Duration, Instant};
use rppal::pwm::{Channel, Polarity, Pwm};
mod packet;
use packet::Packet;
const ADDRESS: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
const PORT: u16 = 6969;
const CLIENT_TIMEOUT: Duration = Duration::from_secs(5);
/// 1 kHz PWM frequency
const PWM_FREQUENCY: f64 = 1000.;
fn triangle_wave(t: f64) -> f64 { (t - (t + 0.5).floor()).abs() * 2. }
fn sine_wave(t: f64) -> f64 { (f64::sin(TAU * t) + 1.) * 0.5 }
fn square_wave(t: f64, high_time: f64) -> f64
{
if t - t.floor() < high_time {
1.
}
else {
0.
}
}
fn sawtooth_wave(t: f64) -> f64 { t - t.floor() }
enum LightMode
{
Constant(f64),
Sine(f64),
Triangle(f64),
Sawtooth(f64),
Square(f64, f64),
}
impl LightMode
{
/// The brightness that the LEDs should have at the given instant
pub fn brightness(&self, time: Duration) -> f64
{
let t = time.as_secs_f64();
match self {
Self::Constant(b) => *b,
Self::Sine(p) => sine_wave(t / p),
Self::Triangle(p) => triangle_wave(t / p),
Self::Sawtooth(p) => sawtooth_wave(t / p),
Self::Square(period, duty) => square_wave(t / period, *duty),
}
}
}
struct ArgumentOutOfRange;
impl TryFrom<Packet> for LightMode
{
type Error = ArgumentOutOfRange;
fn try_from(p: Packet) -> Result<Self, Self::Error>
{
match p {
Packet::Constant(b @ 0.0..=1.0) => Ok(Self::Constant(b)),
Packet::Sine(0.0) => Err(ArgumentOutOfRange),
Packet::Sine(p @ 0.0..) => Ok(Self::Sine(p)),
Packet::Triangle(0.0) => Err(ArgumentOutOfRange),
Packet::Triangle(p @ 0.0..) => Ok(Self::Triangle(p)),
Packet::Sawtooth(0.0) => Err(ArgumentOutOfRange),
Packet::Sawtooth(p @ 0.0..) => Ok(Self::Sawtooth(p)),
Packet::Square { period: 0.0, .. } => Err(ArgumentOutOfRange),
Packet::Square {
period: p @ 0.0..,
duty: d @ 0.0..=1.0,
} => Ok(Self::Square(p, d)),
_ => Err(ArgumentOutOfRange),
}
}
}
/// Thread to run the light control, meaning the actual PWM signal. Through the
/// packet receiver it can be influenced to change the light mode.
fn light_control(rx: Receiver<Packet>)
{
thread::spawn(move || {
let pwm = Pwm::with_frequency(Channel::Pwm0, PWM_FREQUENCY, 0.5, Polarity::Normal, true)
.expect("Unable to initialise PWM");
let mut mode = LightMode::Constant(0.0);
let mut out_of_sync = true;
let start = Instant::now();
loop {
if let Ok(p) = rx.try_recv() {
match LightMode::try_from(p) {
Ok(lm) => {
mode = lm;
out_of_sync = true;
},
Err(_) => eprintln!("Rejecting invalid light mode"),
}
}
if out_of_sync {
pwm.set_duty_cycle(mode.brightness(start.elapsed()))
.unwrap();
}
thread::sleep(Duration::from_millis(10));
}
});
}
fn main()
{
println!("Starting light control");
let (tx, rx) = mpsc::channel();
light_control(rx);
listen(tx);
}
fn listen(tx: Sender<Packet>)
{
let listener =
TcpListener::bind(SocketAddrV6::new(ADDRESS, PORT, 0, 0)).expect("Unable to open server");
for stream in listener.incoming() {
if let Ok(stream) = stream {
handle_client(stream, tx.clone());
}
}
}
fn handle_client(mut stream: TcpStream, tx: Sender<Packet>)
{
thread::spawn(move || {
if let Err(e) = stream.set_read_timeout(Some(CLIENT_TIMEOUT)) {
eprintln!(
"Unable to set client stream timeout. Dropping client. {}",
e
);
}
match Packet::read_from_stream(&mut stream) {
Ok(p) => tx.send(p).unwrap(),
Err(e) => eprintln!(
"Unable to read command packet from stream. Dropping client. {}",
e
),
}
});
}