├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── rpi_rf ├── __init__.py └── rpi_rf.py ├── scripts ├── rpi-rf_receive └── rpi-rf_send └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | *.egg-info 4 | *.pyc 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Suat Özgür, Micha LaQua 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the author nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENCE.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | rpi-rf 2 | ====== 3 | 4 | Introduction 5 | ------------ 6 | 7 | Python module for sending and receiving 433/315MHz LPD/SRD signals with generic low-cost GPIO RF modules on a Raspberry Pi. 8 | 9 | Protocol and base logic ported ported from `rc-switch`_. 10 | 11 | Supported hardware 12 | ------------------ 13 | 14 | Most generic 433/315MHz capable modules (cost: ~2€) connected via GPIO to a Raspberry Pi. 15 | 16 | .. figure:: http://i.imgur.com/vG89UP9.jpg 17 | :alt: 433modules 18 | 19 | Compatibility 20 | ------------- 21 | 22 | Generic RF outlets and most 433/315MHz switches (cost: ~15€/3pcs). 23 | 24 | .. figure:: http://i.imgur.com/WVRxvWe.jpg 25 | :alt: rfoutlet 26 | 27 | 28 | Chipsets: 29 | 30 | * SC5262 / SC5272 31 | * HX2262 / HX2272 32 | * PT2262 / PT2272 33 | * EV1527 / RT1527 / FP1527 / HS1527 34 | 35 | For a full list of compatible devices and chipsets see the `rc-switch Wiki`_ 36 | 37 | Dependencies 38 | ------------ 39 | 40 | :: 41 | 42 | RPi.GPIO 43 | 44 | Installation 45 | ------------ 46 | 47 | On your Raspberry Pi, install the *rpi_rf* module via pip. 48 | 49 | Python 3:: 50 | 51 | # apt-get install python3-pip 52 | # pip3 install rpi-rf 53 | 54 | Wiring diagram (example) 55 | ------------------------ 56 | 57 | Raspberry Pi 1/2(B+):: 58 | 59 | RPI GPIO HEADER 60 | ____________ 61 | | ____|__ 62 | | | | | 63 | | 01| . x |02 64 | | | . x__|________ RX 65 | | | . x__|______ | ________ 66 | | | . . | | | | | 67 | TX | ____|__x . | | |__|VCC | 68 | _______ | | __|__x . | | | | 69 | | | | | | | x____|______|____|DATA | 70 | | GND|____|__| | | . . | | | | 71 | | | | | | . . | | |DATA | 72 | | VCC|____| | | . . | | | | 73 | | | | | . . | |____|GND | 74 | | DATA|_________| | . . | |________| 75 | |_______| | . . | 76 | | . . | 77 | | . . | 78 | | . . | 79 | | . . | 80 | | . . | 81 | | . . | 82 | 39| . . |40 83 | |_______| 84 | 85 | TX: 86 | GND > PIN 09 (GND) 87 | VCC > PIN 02 (5V) 88 | DATA > PIN 11 (GPIO17) 89 | 90 | RX: 91 | VCC > PIN 04 (5V) 92 | DATA > PIN 13 (GPIO27) 93 | GND > PIN 06 (GND) 94 | 95 | Usage 96 | ----- 97 | 98 | See `scripts`_ (`rpi-rf_send`_, `rpi-rf_receive`_) which are also shipped as cmdline tools. 99 | 100 | Open Source 101 | ----------- 102 | 103 | * The code is licensed under the `BSD Licence`_ 104 | * The project source code is hosted on `GitHub`_ 105 | * Please use `GitHub issues`_ to submit bugs and report issues 106 | 107 | .. _rc-switch: https://github.com/sui77/rc-switch 108 | .. _rc-switch Wiki: https://github.com/sui77/rc-switch/wiki 109 | .. _BSD Licence: http://www.linfo.org/bsdlicense.html 110 | .. _GitHub: https://github.com/milaq/rpi-rf 111 | .. _GitHub issues: https://github.com/milaq/rpi-rf/issues 112 | .. _scripts: https://github.com/milaq/rpi-rf/blob/master/scripts 113 | .. _rpi-rf_send: https://github.com/milaq/rpi-rf/blob/master/scripts/rpi-rf_send 114 | .. _rpi-rf_receive: https://github.com/milaq/rpi-rf/blob/master/scripts/rpi-rf_receive 115 | -------------------------------------------------------------------------------- /rpi_rf/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .rpi_rf import RFDevice 3 | 4 | 5 | __version__ = '0.9.7' 6 | -------------------------------------------------------------------------------- /rpi_rf/rpi_rf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sending and receiving 433/315Mhz signals with low-cost GPIO RF Modules on a Raspberry Pi. 3 | """ 4 | 5 | import logging 6 | import time 7 | from collections import namedtuple 8 | 9 | from RPi import GPIO 10 | 11 | MAX_CHANGES = 67 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | Protocol = namedtuple('Protocol', 16 | ['pulselength', 17 | 'sync_high', 'sync_low', 18 | 'zero_high', 'zero_low', 19 | 'one_high', 'one_low']) 20 | PROTOCOLS = (None, 21 | Protocol(350, 1, 31, 1, 3, 3, 1), 22 | Protocol(650, 1, 10, 1, 2, 2, 1), 23 | Protocol(100, 30, 71, 4, 11, 9, 6), 24 | Protocol(380, 1, 6, 1, 3, 3, 1), 25 | Protocol(500, 6, 14, 1, 2, 2, 1), 26 | Protocol(200, 1, 10, 1, 5, 1, 1)) 27 | 28 | 29 | class RFDevice: 30 | """Representation of a GPIO RF device.""" 31 | 32 | # pylint: disable=too-many-instance-attributes,too-many-arguments 33 | def __init__(self, gpio, 34 | tx_proto=1, tx_pulselength=None, tx_repeat=10, tx_length=24, rx_tolerance=80): 35 | """Initialize the RF device.""" 36 | self.gpio = gpio 37 | self.tx_enabled = False 38 | self.tx_proto = tx_proto 39 | if tx_pulselength: 40 | self.tx_pulselength = tx_pulselength 41 | else: 42 | self.tx_pulselength = PROTOCOLS[tx_proto].pulselength 43 | self.tx_repeat = tx_repeat 44 | self.tx_length = tx_length 45 | self.rx_enabled = False 46 | self.rx_tolerance = rx_tolerance 47 | # internal values 48 | self._rx_timings = [0] * (MAX_CHANGES + 1) 49 | self._rx_last_timestamp = 0 50 | self._rx_change_count = 0 51 | self._rx_repeat_count = 0 52 | # successful RX values 53 | self.rx_code = None 54 | self.rx_code_timestamp = None 55 | self.rx_proto = None 56 | self.rx_bitlength = None 57 | self.rx_pulselength = None 58 | 59 | GPIO.setmode(GPIO.BCM) 60 | _LOGGER.debug("Using GPIO " + str(gpio)) 61 | 62 | def cleanup(self): 63 | """Disable TX and RX and clean up GPIO.""" 64 | if self.tx_enabled: 65 | self.disable_tx() 66 | if self.rx_enabled: 67 | self.disable_rx() 68 | _LOGGER.debug("Cleanup") 69 | GPIO.cleanup() 70 | 71 | def enable_tx(self): 72 | """Enable TX, set up GPIO.""" 73 | if self.rx_enabled: 74 | _LOGGER.error("RX is enabled, not enabling TX") 75 | return False 76 | if not self.tx_enabled: 77 | self.tx_enabled = True 78 | GPIO.setup(self.gpio, GPIO.OUT) 79 | _LOGGER.debug("TX enabled") 80 | return True 81 | 82 | def disable_tx(self): 83 | """Disable TX, reset GPIO.""" 84 | if self.tx_enabled: 85 | # set up GPIO pin as input for safety 86 | GPIO.setup(self.gpio, GPIO.IN) 87 | self.tx_enabled = False 88 | _LOGGER.debug("TX disabled") 89 | return True 90 | 91 | def tx_code(self, code, tx_proto=None, tx_pulselength=None, tx_length=None): 92 | """ 93 | Send a decimal code. 94 | 95 | Optionally set protocol, pulselength and code length. 96 | When none given reset to default protocol, default pulselength and set code length to 24 bits. 97 | """ 98 | if tx_proto: 99 | self.tx_proto = tx_proto 100 | else: 101 | self.tx_proto = 1 102 | if tx_pulselength: 103 | self.tx_pulselength = tx_pulselength 104 | elif not self.tx_pulselength: 105 | self.tx_pulselength = PROTOCOLS[self.tx_proto].pulselength 106 | if tx_length: 107 | self.tx_length = tx_length 108 | elif self.tx_proto == 6: 109 | self.tx_length = 32 110 | elif (code > 16777216): 111 | self.tx_length = 32 112 | else: 113 | self.tx_length = 24 114 | rawcode = format(code, '#0{}b'.format(self.tx_length + 2))[2:] 115 | if self.tx_proto == 6: 116 | nexacode = "" 117 | for b in rawcode: 118 | if b == '0': 119 | nexacode = nexacode + "01" 120 | if b == '1': 121 | nexacode = nexacode + "10" 122 | rawcode = nexacode 123 | self.tx_length = 64 124 | _LOGGER.debug("TX code: " + str(code)) 125 | return self.tx_bin(rawcode) 126 | 127 | def tx_bin(self, rawcode): 128 | """Send a binary code.""" 129 | _LOGGER.debug("TX bin: " + str(rawcode)) 130 | for _ in range(0, self.tx_repeat): 131 | if self.tx_proto == 6: 132 | if not self.tx_sync(): 133 | return False 134 | for byte in range(0, self.tx_length): 135 | if rawcode[byte] == '0': 136 | if not self.tx_l0(): 137 | return False 138 | else: 139 | if not self.tx_l1(): 140 | return False 141 | if not self.tx_sync(): 142 | return False 143 | 144 | return True 145 | 146 | def tx_l0(self): 147 | """Send a '0' bit.""" 148 | if not 0 < self.tx_proto < len(PROTOCOLS): 149 | _LOGGER.error("Unknown TX protocol") 150 | return False 151 | return self.tx_waveform(PROTOCOLS[self.tx_proto].zero_high, 152 | PROTOCOLS[self.tx_proto].zero_low) 153 | 154 | def tx_l1(self): 155 | """Send a '1' bit.""" 156 | if not 0 < self.tx_proto < len(PROTOCOLS): 157 | _LOGGER.error("Unknown TX protocol") 158 | return False 159 | return self.tx_waveform(PROTOCOLS[self.tx_proto].one_high, 160 | PROTOCOLS[self.tx_proto].one_low) 161 | 162 | def tx_sync(self): 163 | """Send a sync.""" 164 | if not 0 < self.tx_proto < len(PROTOCOLS): 165 | _LOGGER.error("Unknown TX protocol") 166 | return False 167 | return self.tx_waveform(PROTOCOLS[self.tx_proto].sync_high, 168 | PROTOCOLS[self.tx_proto].sync_low) 169 | 170 | def tx_waveform(self, highpulses, lowpulses): 171 | """Send basic waveform.""" 172 | if not self.tx_enabled: 173 | _LOGGER.error("TX is not enabled, not sending data") 174 | return False 175 | GPIO.output(self.gpio, GPIO.HIGH) 176 | self._sleep((highpulses * self.tx_pulselength) / 1000000) 177 | GPIO.output(self.gpio, GPIO.LOW) 178 | self._sleep((lowpulses * self.tx_pulselength) / 1000000) 179 | return True 180 | 181 | def enable_rx(self): 182 | """Enable RX, set up GPIO and add event detection.""" 183 | if self.tx_enabled: 184 | _LOGGER.error("TX is enabled, not enabling RX") 185 | return False 186 | if not self.rx_enabled: 187 | self.rx_enabled = True 188 | GPIO.setup(self.gpio, GPIO.IN) 189 | GPIO.add_event_detect(self.gpio, GPIO.BOTH) 190 | GPIO.add_event_callback(self.gpio, self.rx_callback) 191 | _LOGGER.debug("RX enabled") 192 | return True 193 | 194 | def disable_rx(self): 195 | """Disable RX, remove GPIO event detection.""" 196 | if self.rx_enabled: 197 | GPIO.remove_event_detect(self.gpio) 198 | self.rx_enabled = False 199 | _LOGGER.debug("RX disabled") 200 | return True 201 | 202 | # pylint: disable=unused-argument 203 | def rx_callback(self, gpio): 204 | """RX callback for GPIO event detection. Handle basic signal detection.""" 205 | timestamp = int(time.perf_counter() * 1000000) 206 | duration = timestamp - self._rx_last_timestamp 207 | 208 | if duration > 5000: 209 | if abs(duration - self._rx_timings[0]) < 200: 210 | self._rx_repeat_count += 1 211 | self._rx_change_count -= 1 212 | if self._rx_repeat_count == 2: 213 | for pnum in range(1, len(PROTOCOLS)): 214 | if self._rx_waveform(pnum, self._rx_change_count, timestamp): 215 | _LOGGER.debug("RX code " + str(self.rx_code)) 216 | break 217 | self._rx_repeat_count = 0 218 | self._rx_change_count = 0 219 | 220 | if self._rx_change_count >= MAX_CHANGES: 221 | self._rx_change_count = 0 222 | self._rx_repeat_count = 0 223 | self._rx_timings[self._rx_change_count] = duration 224 | self._rx_change_count += 1 225 | self._rx_last_timestamp = timestamp 226 | 227 | def _rx_waveform(self, pnum, change_count, timestamp): 228 | """Detect waveform and format code.""" 229 | code = 0 230 | delay = int(self._rx_timings[0] / PROTOCOLS[pnum].sync_low) 231 | delay_tolerance = delay * self.rx_tolerance / 100 232 | 233 | for i in range(1, change_count, 2): 234 | if (abs(self._rx_timings[i] - delay * PROTOCOLS[pnum].zero_high) < delay_tolerance and 235 | abs(self._rx_timings[i+1] - delay * PROTOCOLS[pnum].zero_low) < delay_tolerance): 236 | code <<= 1 237 | elif (abs(self._rx_timings[i] - delay * PROTOCOLS[pnum].one_high) < delay_tolerance and 238 | abs(self._rx_timings[i+1] - delay * PROTOCOLS[pnum].one_low) < delay_tolerance): 239 | code <<= 1 240 | code |= 1 241 | else: 242 | return False 243 | 244 | if self._rx_change_count > 6 and code != 0: 245 | self.rx_code = code 246 | self.rx_code_timestamp = timestamp 247 | self.rx_bitlength = int(change_count / 2) 248 | self.rx_pulselength = delay 249 | self.rx_proto = pnum 250 | return True 251 | 252 | return False 253 | 254 | def _sleep(self, delay): 255 | _delay = delay / 100 256 | end = time.time() + delay - _delay 257 | while time.time() < end: 258 | time.sleep(_delay) 259 | -------------------------------------------------------------------------------- /scripts/rpi-rf_receive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import signal 5 | import sys 6 | import time 7 | import logging 8 | 9 | from rpi_rf import RFDevice 10 | 11 | rfdevice = None 12 | 13 | # pylint: disable=unused-argument 14 | def exithandler(signal, frame): 15 | rfdevice.cleanup() 16 | sys.exit(0) 17 | 18 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', 19 | format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s', ) 20 | 21 | parser = argparse.ArgumentParser(description='Receives a decimal code via a 433/315MHz GPIO device') 22 | parser.add_argument('-g', dest='gpio', type=int, default=27, 23 | help="GPIO pin (Default: 27)") 24 | args = parser.parse_args() 25 | 26 | signal.signal(signal.SIGINT, exithandler) 27 | rfdevice = RFDevice(args.gpio) 28 | rfdevice.enable_rx() 29 | timestamp = None 30 | logging.info("Listening for codes on GPIO " + str(args.gpio)) 31 | while True: 32 | if rfdevice.rx_code_timestamp != timestamp: 33 | timestamp = rfdevice.rx_code_timestamp 34 | logging.info(str(rfdevice.rx_code) + 35 | " [pulselength " + str(rfdevice.rx_pulselength) + 36 | ", protocol " + str(rfdevice.rx_proto) + "]") 37 | time.sleep(0.01) 38 | rfdevice.cleanup() 39 | -------------------------------------------------------------------------------- /scripts/rpi-rf_send: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import logging 5 | 6 | from rpi_rf import RFDevice 7 | 8 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', 9 | format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s',) 10 | 11 | parser = argparse.ArgumentParser(description='Sends a decimal code via a 433/315MHz GPIO device') 12 | parser.add_argument('code', metavar='CODE', type=int, 13 | help="Decimal code to send") 14 | parser.add_argument('-g', dest='gpio', type=int, default=17, 15 | help="GPIO pin (Default: 17)") 16 | parser.add_argument('-p', dest='pulselength', type=int, default=None, 17 | help="Pulselength (Default: 350)") 18 | parser.add_argument('-t', dest='protocol', type=int, default=None, 19 | help="Protocol (Default: 1)") 20 | parser.add_argument('-l', dest='length', type=int, default=None, 21 | help="Codelength (Default: 24)") 22 | parser.add_argument('-r', dest='repeat', type=int, default=10, 23 | help="Repeat cycles (Default: 10)") 24 | args = parser.parse_args() 25 | 26 | rfdevice = RFDevice(args.gpio) 27 | rfdevice.enable_tx() 28 | rfdevice.tx_repeat = args.repeat 29 | 30 | if args.protocol: 31 | protocol = args.protocol 32 | else: 33 | protocol = "default" 34 | if args.pulselength: 35 | pulselength = args.pulselength 36 | else: 37 | pulselength = "default" 38 | if args.length: 39 | length = args.length 40 | else: 41 | length = "default" 42 | 43 | logging.info(str(args.code) + 44 | " [protocol: " + str(protocol) + 45 | ", pulselength: " + str(pulselength) + 46 | ", length: " + str(length) + 47 | ", repeat: " + str(rfdevice.tx_repeat) + "]") 48 | 49 | rfdevice.tx_code(args.code, args.protocol, args.pulselength, args.length) 50 | rfdevice.cleanup() 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name='rpi-rf', 11 | version='0.9.7', 12 | author='Micha LaQua', 13 | author_email='micha.laqua@gmail.com', 14 | description='Sending and receiving 433/315MHz signals with low-cost GPIO RF modules on a Raspberry Pi', 15 | long_description=long_description, 16 | url='https://github.com/milaq/rpi-rf', 17 | license='BSD', 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'Topic :: Software Development :: Build Tools', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Programming Language :: Python :: 3', 24 | 'Operating System :: POSIX :: Linux', 25 | 'Topic :: Software Development :: Libraries :: Python Modules' 26 | ], 27 | keywords=[ 28 | 'rpi', 29 | 'raspberry', 30 | 'raspberry pi', 31 | 'rf', 32 | 'gpio', 33 | 'radio', 34 | '433', 35 | '433mhz', 36 | '315', 37 | '315mhz' 38 | ], 39 | install_requires=['RPi.GPIO'], 40 | scripts=['scripts/rpi-rf_send', 'scripts/rpi-rf_receive'], 41 | packages=find_packages(exclude=['contrib', 'docs', 'tests']) 42 | ) 43 | --------------------------------------------------------------------------------