Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
milaq committed Apr 20, 2016
0 parents commit f1bab03
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
.idea
dist
*.pyc
24 changes: 24 additions & 0 deletions LICENSE.txt
@@ -0,0 +1,24 @@
Copyright (c) 2016 Micha LaQua
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,2 @@
include README.rst
include LICENCE.txt
59 changes: 59 additions & 0 deletions README.rst
@@ -0,0 +1,59 @@
rpi-rf
======

Introduction
------------

Python module for sending and receiving 433MHz LPD/SRD signals with generic low-cost GPIO RF modules on a Raspberry Pi.

Base logic ported from `NinjaBlocks 433Utils`_.

Supported hardware
------------------

All generic 433MHz capable modules (cost: ~2€) connected via GPIO to a Raspberry Pi.

.. figure:: http://i.imgur.com/vG89UP9.jpg
:alt: 433modules

Interoperability
----------------

Generic RF outlets and most 433MHz switches (cost: ~15€/3pcs).

.. figure:: http://i.imgur.com/WVRxvWe.jpg
:alt: rfoutlet

Dependencies
------------

::

RPi.GPIO

Installation
------------

On your Raspberry Pi, install the *rpi_rf* module via pip.

Python 3::

# apt-get install python3-pip
# pip3 install rpi-rf

Usage
-----

See examples (*send.py*, *receive.py*).

Open Source
-----------

* The code is licensed under the `BSD Licence`_
* The project source code is hosted on `GitHub`_
* Please use `GitHub issues`_ to submit bugs and report issues

.. _NinjaBlocks 433Utils: https://github.com/ninjablocks/433Utils
.. _BSD Licence: http://www.linfo.org/bsdlicense.html
.. _GitHub: https://github.com/milaq/rpi-rf
.. _GitHub issues: https://github.com/milaq/rpi-rf/issues
36 changes: 36 additions & 0 deletions examples/receive.py
@@ -0,0 +1,36 @@
#!/usr/bin/env python3

import argparse
import signal
import sys
import time
import logging

from rpi_rf import RFDevice

rfdevice = None

# pylint: disable=unused-argument
def exithandler(signal, frame):
rfdevice.cleanup()
sys.exit(0)

logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s', )

parser = argparse.ArgumentParser(description='Receives a decimal code via a 433MHz GPIO device')
parser.add_argument('-g', dest='gpio', type=int, default=27,
help="GPIO pin (Default: 27)")
args = parser.parse_args()

signal.signal(signal.SIGINT, exithandler)
rfdevice = RFDevice(args.gpio)
rfdevice.enable_rx()
timestamp = None
logging.info("Listening for codes on GPIO " + str(args.gpio))
while True:
if rfdevice.rx_code_timestamp != timestamp:
timestamp = rfdevice.rx_code_timestamp
logging.info(rfdevice.rx_code)
time.sleep(0.01)
rfdevice.cleanup()
25 changes: 25 additions & 0 deletions examples/send.py
@@ -0,0 +1,25 @@
#!/usr/bin/env python3

import argparse
import logging

from rpi_rf import RFDevice

logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s',)

parser = argparse.ArgumentParser(description='Sends a decimal code via a 433MHz GPIO device')
parser.add_argument('code', metavar='CODE', type=int,
help="Decimal code to send")
parser.add_argument('-g', dest='gpio', type=int, default=17,
help="GPIO pin (Default: 17)")
parser.add_argument('-p', dest='pulselength', type=int, default=350,
help="Pulselength (Default: 350)")
args = parser.parse_args()

rfdevice = RFDevice(args.gpio)
rfdevice.enable_tx()
rfdevice.tx_pulselength = args.pulselength
logging.info("Sending code " + str(args.code) + " with a pulselength of " + str(args.pulselength))
rfdevice.tx_code(args.code)
rfdevice.cleanup()
5 changes: 5 additions & 0 deletions rpi_rf/__init__.py
@@ -0,0 +1,5 @@
from __future__ import absolute_import
from .rpi_rf import RFDevice


__version__ = '0.9.3'
188 changes: 188 additions & 0 deletions rpi_rf/rpi_rf.py
@@ -0,0 +1,188 @@
"""
Sending and receiving 433Mhz signals with cheap GPIO RF Modules on Raspberry Pi.
"""

import logging
import time

from RPi import GPIO

MAX_CHANGES = 67

_LOGGER = logging.getLogger(__name__)


class RFDevice:
"""Representation of a GPIO RF device."""

# pylint: disable=too-many-instance-attributes,too-many-arguments
def __init__(self, gpio, tx_pulselength=350, tx_repeat=10, tx_length=24, rx_tolerance=120):
"""Initialize the RF device."""
self.gpio = gpio
self.proto = 1 # unused atm
self.tx_enabled = False
self.tx_pulselength = tx_pulselength
self.tx_repeat = tx_repeat
self.tx_length = tx_length
self.rx_enabled = False
self.rx_tolerance = rx_tolerance
# internal values
self._rx_timings = [0] * (MAX_CHANGES + 1)
self._rx_last_timestamp = 0
self._rx_change_count = 0
self._rx_repeat_count = 0
# successful RX values
self.rx_code = None
self.rx_code_timestamp = None
self.rx_bitlength = None
self.rx_delay = None

GPIO.setmode(GPIO.BCM)
_LOGGER.debug("Using GPIO " + str(gpio))

def cleanup(self):
"""Disable TX and RX and clean up GPIO."""
if self.tx_enabled:
self.disable_tx()
if self.rx_enabled:
self.disable_rx()
_LOGGER.debug("Cleanup")
GPIO.cleanup()

def enable_tx(self):
"""Enable TX, set up GPIO."""
if self.rx_enabled:
_LOGGER.error("RX is enabled, not enabling TX")
return False
if not self.tx_enabled:
self.tx_enabled = True
GPIO.setup(self.gpio, GPIO.OUT)
_LOGGER.debug("TX enabled")
return True

def disable_tx(self):
"""Disable TX, reset GPIO."""
if self.tx_enabled:
# set up GPIO pin as input for safety
GPIO.setup(self.gpio, GPIO.IN)
self.tx_enabled = False
_LOGGER.debug("TX disabled")
return True

def tx_code(self, code):
"""Send a decimal code id."""
rawcode = format(code, '#0{}b'.format(self.tx_length + 2))[2:]
_LOGGER.debug("TX code: " + str(code))
return self.tx_bin(rawcode)

def tx_bin(self, rawcode):
"""Send a binary code id."""
_LOGGER.debug("TX bin: " + str(rawcode))

for _ in range(0, self.tx_repeat):
for byte in range(0, self.tx_length):
if rawcode[byte] == '0':
if not self.tx_l0():
return False
else:
if not self.tx_l1():
return False
if not self.tx_sync():
return False

return True

def tx_l0(self):
"""Send a locic 0."""
return self.tx_waveform(1, 3)

def tx_l1(self):
"""Send a locic 1."""
return self.tx_waveform(3, 1)

def tx_sync(self):
"""Send a sync signal."""
return self.tx_waveform(1, 31)

def tx_waveform(self, highpulses, lowpulses):
"""Send basic waveform."""
if not self.tx_enabled:
_LOGGER.error("TX is not enabled, not sending")
return False
GPIO.output(self.gpio, GPIO.HIGH)
time.sleep((highpulses * self.tx_pulselength) / 1000000)
GPIO.output(self.gpio, GPIO.LOW)
time.sleep((lowpulses * self.tx_pulselength) / 1000000)
return True

def enable_rx(self):
"""Enable RX, set up GPIO and add event detection."""
if self.tx_enabled:
_LOGGER.error("TX is enabled, not enabling RX")
return False
if not self.rx_enabled:
self.rx_enabled = True
GPIO.setup(self.gpio, GPIO.IN)
GPIO.add_event_detect(self.gpio, GPIO.BOTH)
GPIO.add_event_callback(self.gpio, self.rx_callback)
_LOGGER.debug("RX enabled")
return True

def disable_rx(self):
"""Disable RX, remove GPIO event detection."""
if self.rx_enabled:
GPIO.remove_event_detect(self.gpio)
self.rx_enabled = False
_LOGGER.debug("RX disabled")
return True

# pylint: disable=unused-argument
def rx_callback(self, gpio):
"""RX callback for GPIO event detection. Handle basic signal detection."""
timestamp = int(time.perf_counter() * 1000000)
duration = timestamp - self._rx_last_timestamp

if duration > 5000:
if self._rx_timings[0] + 200 > duration > self._rx_timings[0] - 200:
self._rx_repeat_count += 1
self._rx_change_count -= 1
if self._rx_repeat_count == 2:
if self.rx_proto1(self._rx_change_count, timestamp):
_LOGGER.debug("RX code: " + str(self.rx_code))
self._rx_repeat_count = 0
self._rx_change_count = 0

if self._rx_change_count >= MAX_CHANGES:
self._rx_change_count = 0
self._rx_repeat_count = 0
self._rx_timings[self._rx_change_count] = duration
self._rx_change_count += 1
self._rx_last_timestamp = timestamp

def rx_proto1(self, change_count, timestamp):
"""Detect timings and waveform (Protocol 1)."""
code = 0
delay = self._rx_timings[0] / 31
delay_tolerance = delay * self.rx_tolerance * 0.01

for i in range(1, change_count, 2):
if (delay + delay_tolerance > self._rx_timings[i] > delay - delay_tolerance and
delay*3 + delay_tolerance > self._rx_timings[i+1] > delay*3 - delay_tolerance):
code <<= 1
elif (delay*3 + delay_tolerance > self._rx_timings[i] > delay*3 - delay_tolerance and
delay + delay_tolerance > self._rx_timings[i+1] > delay - delay_tolerance):
code += 1
code <<= 1
else:
code = 0
code >>= 1

if change_count > 6 and code != 0:
self.rx_code = code
self.rx_code_timestamp = timestamp
self.rx_bitlength = change_count / 2
self.rx_delay = delay
self.proto = 1
return True

return False
39 changes: 39 additions & 0 deletions setup.py
@@ -0,0 +1,39 @@
from setuptools import setup, find_packages
from os import path

here = path.abspath(path.dirname(__file__))

with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()

setup(
name='rpi-rf',
version='0.9.3',
author='Micha LaQua',
author_email='micha.laqua@gmail.com',
description='Sending and receiving 433MHz signals with cheap GPIO RF modules on a Raspberry Pi',
long_description=long_description,
url='https://github.com/milaq/rpi-rf',
license='BSD',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Operating System :: POSIX :: Linux',
'Topic :: Software Development :: Libraries :: Python Modules'
],
keywords=[
'rpi',
'raspberry',
'raspberry pi',
'rf',
'gpio',
'radio',
'433',
'433mhz'
],
install_requires=['RPi.GPIO'],
packages=find_packages(exclude=['contrib', 'docs', 'tests'])
)

0 comments on commit f1bab03

Please sign in to comment.