libsparkypi/
lib.rs

1//! Generate and transmit radio signals that can be used to control
2//! mains socket switches, doorbells and similar radio controlled
3//! devices.
4
5use std::{thread, time};
6use gpio_cdev::{Chip, LineRequestFlags, LineHandle};
7
8pub mod builder;
9use crate::builder::{TransmissionBuilder, ProtocolBuilder};
10
11/// A transmission consists of a sequence of short and long radio pulses.
12/// The binary sequence is represented by a string of literal
13/// '0' and '1' characters.
14/// That way you can compose the signal simply by concatenating
15/// multiple strings.
16///
17/// The pulse length is the smallest time unit of the signal.
18/// It will be multiplied by the respective values specified
19/// in the `ProtocolProperties` struct. The pulse length is
20/// given in microseconds. Usually, a lot of fine tuning is 
21/// required.
22///
23/// Usually, sending the signal once is not enough,
24/// so we need to specify the number of repeats.
25#[derive(Debug, Default, Clone, PartialEq)]
26pub struct Transmission {
27    pub sequence: String,
28    pub pulse_length: u16,
29    pub repeats: u8,
30    pub protocol: ProtocolProperties,
31}
32
33/// In the protocol we define the smallest parts of the radio signal.
34/// Usually a short pulse with a long pause resembles a binary zero,
35/// and a long pulse followed by a short pause resembles a binary one.
36/// A sync bit / sync gap combination marks the beginning of the radio transmission.
37#[derive(Debug, Default, Copy, Clone, PartialEq)]
38pub struct ProtocolProperties {
39    pub short: u8,
40    pub long: u8,
41    pub sync_bit: u8,
42    pub sync_gap: u8,
43}
44
45impl ProtocolProperties {
46    
47    /// Initialize a custom protocol. Every field is set to zero.    
48    pub fn new() -> Self {
49        Default::default()
50    }
51
52    /// Invoke the builder pattern.
53    pub fn builder() -> ProtocolBuilder {
54        ProtocolBuilder::default()
55    }
56}
57
58/// Resembles 'protocol1' of sui77's brilliant
59/// [rc-switch library](https://github.com/sui77/rc-switch) 
60/// for the arduino - probably the most common protocol.
61pub const P1: ProtocolProperties = ProtocolProperties {
62    short: 1,
63    long: 3,
64    sync_bit: 1,
65    sync_gap: 31,
66};
67
68/// Resembles 'protocol2'.
69/// Untested - your mileage may vary.
70pub const P2: ProtocolProperties = ProtocolProperties {
71    short: 1,
72    long: 2,
73    sync_bit: 1,
74    sync_gap: 10,
75};
76
77/// Protocol for the 'Gmornxen' RC socket switches, very similar to 'protocol2'.
78pub const XEN: ProtocolProperties = ProtocolProperties {
79    short: 1,
80    long: 2,
81    sync_bit: 1,
82    sync_gap: 11,
83};
84
85impl Transmission {
86    
87    /// Output the signal on a gpio pin on the specified gpio device via the
88    /// [gpio character device ABI](https://www.kernel.org/doc/Documentation/ABI/testing/gpio-cdev).
89    /// Finding the right device can sometimes be a bit tricky.
90    /// Further information might be found in the documentation of your SBC.
91    /// On a Raspberry Pi 4B it is `/dev/gpiochip0`, but on a
92    /// Raspberry Pi 5 the appropiate device is `/dev/gpiochip4`.
93    /// `gpio_pin` is the number of the gpio pin the radio transmitter module,
94    /// e.g. an FS1000A module, is connected to.
95    ///
96    /// The sequence is transmitted by iterating over the characters of a string slice.
97    /// If the character is '1' a binary one will be transmitted, or a binary zero if the character is
98    /// '0', respectively.
99    /// All other characters will result in a sync bit.
100    ///
101    /// # Examples
102    /// ```rust
103    /// use libsparkypi::*;
104    ///
105    /// let my_signal = Transmission::builder()
106    ///     .sequence("s000000000000010101010001")
107    ///     .pulse_length(320)
108    ///     .repeats(10)
109    ///     .protocol(P1)
110    ///     .build();
111    ///
112    /// // output on device /dev/gpiochip0, gpio pin 18
113    /// my_signal.send_to("/dev/gpiochip0", 18).unwrap();
114    /// ```
115    pub fn send_to(&self, gpio_dev: &str, gpio_pin: u8) -> Result<(), gpio_cdev::Error> {
116        
117        let mut chip = Chip::new(gpio_dev)?;
118
119        let lh = chip
120            .get_line(gpio_pin as u32)?
121            .request(LineRequestFlags::OUTPUT, 0, "tx")?;
122
123        for _ in 0..self.repeats {
124            
125            for c in self.sequence.chars() {
126                
127                if c == '1' {
128                    send_bit(&lh, true, self.pulse_length, self.protocol.long, self.protocol.short)?;
129                } else if c == '0' {
130                    send_bit(&lh, false, self.pulse_length, self.protocol.long, self.protocol.short)?;
131                } else {
132                    send_sync_bit(&lh, self.pulse_length, self.protocol.sync_gap, self.protocol.sync_bit)?;
133                }
134            
135            }
136        
137        }
138    
139        Ok(())
140    }
141
142    /// Modifies the binary sequence.
143    pub fn sequence(&mut self, seq: &str) {
144        self.sequence = String::from(seq);
145    }
146
147    /// Creates a new instance with default values, e.g. all fields
148    /// with numbers will be zero, and the string will be
149    /// empty.
150    pub fn new() -> Self {
151        Default::default()
152    }
153
154    /// Invokes the builder.
155    pub fn builder() -> TransmissionBuilder {
156        TransmissionBuilder::default()
157    }
158
159    /// Creates a byte vector containing the data from the transmission
160    /// in the form of literal characters in the following comma
161    /// separated pattern:
162    ///
163    /// `SEQUENCE,PULSE_LENGTH,REPEATS,SHORT,LONG,SYNC_BIT,SYNC_GAP`
164    ///
165    /// You can send those bytes e.g. via UART to an appropriately
166    /// programmed microcontroller, which subsequently can parse the values,
167    /// and transmit the binary sequence accordingly via a radio module.
168    /// You can for example connect an Arduino Nano via USB to a regular
169    /// x86 PC and make use of the [serialport](https://crates.io/crates/serialport)
170    /// crate.
171    pub fn csv_as_bytes(&self) -> Vec<u8> {
172        let output = String::from(&format!("{},{},{},{},{},{},{}",
173                                           self.sequence,
174                                           self.pulse_length,
175                                           self.repeats,
176                                           self.protocol.short,
177                                           self.protocol.long,
178                                           self.protocol.sync_bit,
179                                           self.protocol.sync_gap));
180        output.as_bytes().to_vec()
181    }
182}
183
184// Send one single bit
185// Long pulse and short pause results in a binary one.
186// Short pulse and long pause results in a binary zero.
187// The relation between short and long period is defined in the 'ProtocolProperties' struct.
188
189fn send_bit(lh: &LineHandle, bit: bool, pulse_length: u16, factor1: u8, factor2: u8) -> Result<(), gpio_cdev::Error> {
190    if bit {
191        lh.set_value(1)?;
192        thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor1 as u64));
193        lh.set_value(0)?;
194        thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor2 as u64));
195    } else {
196        lh.set_value(1)?;
197        thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor2 as u64));
198        lh.set_value(0)?;
199        thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor1 as u64));
200    }
201    Ok(())
202}
203
204// A so called sync bit must be transmitted before the actual binary sequence.
205// The relation between 'high' and 'low' status is defined in the 'ProtocolProperties' struct.
206
207fn send_sync_bit(lh: &LineHandle, pulse_length: u16, factor1: u8, factor2: u8) -> Result<(), gpio_cdev::Error> {
208    lh.set_value(1)?;
209    thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor2 as u64));
210    lh.set_value(0)?;
211    thread::sleep(time::Duration::from_micros(pulse_length as u64 * factor1 as u64));
212    Ok(())
213}