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}