Используем беспроводной выключатель на 433МГц для управления ПК

Привет geektimes habr.

У меня дома скопилось несколько беспроводных выключателей на 433МГц, стало интересно, можно ли их использовать для каких-либо задач, например для управления компьютером или для интегрирования в систему «умного дома».

Эти выключатели удобны своей дешевизной и стабильной работой, выглядят они примерно так:
upttp4hzw_us_v2bb0kdliszify.jpeg

Как это работает, и что с ними можно сделать (гусары молчать:), подробности под катом.

Теория


Скажу сразу — как работает такой выключатель, я не знаю, хотя и примерно догадываюсь. Значит нужно будет произвести небольшой reverse engineering.

Первым делом сигнал нужно принять, для чего используем многим уже известный RTL-SDR приемник, у радиолюбителей часто называемый просто «свисток». Этот девайс ценой всего в 10$ позволяет принимать радиосигналы в диапазоне примерно от 50 до 1250МГц, для нас то что нужно. Тема старая, но если кто не читал — https://habr.com/post/149698/.

Делаем первый шаг анализа — внимательно смотрим на выключатель. Обнаруживаем что сзади на корпусе у него написано «Made in China» (кто бы мог подумать?) и, что более важно, указана частота 433МГц. Теперь можно подключить SDR-приемник, запустить SDR# и убедиться, что данные действительно передаются.
qdwdhdd9acsjgl1rp5cbobpb-qg.jpeg

Симметрия сигнала на спектре подсказывает про наличие AM-модуляции. Кстати справа виден более слабый «чужой» сигнал — их тоже можно принимать и декодировать, про них будет подробнее сказано отдельно. Впрочем, вернемся к сигналу. Записываем его в формате обычного WAV и нажимаем кнопки на пульте — для примера я нажал кнопки ON и OFF на канале »1».

Открываем звуковой файл в любом аудиоредакторе, и воспользуемся для сравнения сигналов другим профессиональным инструментом аналитиков — программой Paint. Размещаем 2 сигнала c разных кнопок один над другим, чтобы увидеть разницу:
hys58cp3y1f0dujky-3kzjxxb1y.png

Нетрудно видеть, что мы имеем обычную битовую последовательность, отличие в которой как раз в одном бите, соответствующем кнопке ON или OFF. Пока кнопка нажата, выключатель просто циклически повторяет эту последовательность в эфир.

Из этого кстати можно сделать один важный вывод — сигналы таких выключателей (речь о дешевых моделях) передаются в эфир «как есть», без какой-либо аутентификации, защиты или шифрования. Такой выключатель или беспроводную розетку с таким выключателем не стоит использовать для каких-то ответственных функций, например для включения мощных обогревателей или тем более для открытия входной двери или гаража. Дело тут даже не в хакерах (шанс что кто-то будет взламывать мой дом беспроводным способом я оцениваю меньше чем шанс падения на мой дом МКС), а в том, что сосед может случайно купить такой же выключатель, и коды у них могут совпасть (впрочем на выключателе есть возможность выбора между 4 каналами). По моему опыту использования, 2–3 раза за год выключатель таки включался «сам», то ли помеха, то ли действительно принимался далекий сигнал от такой же модели.

Разумеется, это не относится к более сложным системам, таким как Lora или Philips Hue, там с шифрованием все впорядке.

Впрочем, вернемся к нашей задаче. Можно написать декодер таких сигналов самостоятельно, но к счастью, это уже сделали до нас, в проекте называемом «rtl_433». Изначально программа была создана для Linux, Windows-версию можно скачать по адресу https://cognito.me.uk/computers/rtl_433-windows-binary-32-bit/ Linux версию можно скачать с https://github.com/merbanan/rtl_433.

Запускаем программу из командной строки: «rtl_433.exe -F json»
openqnlud5suc071yu4_ja_ro-a.png

Мы получили данные, осталось написать программу для их обработки.

Raspberry Pi


Первое, что интересно рассмотреть, это Raspberry Pi. Для установки rtl_433 на Raspbian распаковываем архив github.com/merbanan/rtl_433/archive/18.05.tar.gz и выполняем следующие команды:

sudo apt-get install libtool libusb-1.0.0-dev librtlsdr-dev rtl-sdr build-essential autoconf cmake pkg-config
cd rtl_433/
autoreconf --install
./configure
make
make install


Вторым шагом, напишем программу которая будет получать эти данные, и в зависимости от них, выполнять нужные действия. Код на Python весьма несложный:

from __future__ import print_function

import os, sys, io
import json
import subprocess

print("RTLSDR listening started")
transmitter_name = "Waveman Switch Transmitter"
transmitter_channel = 1

proc = subprocess.Popen(["rtl_433 -F json"], stdout=subprocess.PIPE, shell=True)
while True:
    try:
        line = proc.stdout.readline().encode('ascii','ignore')
        proc.poll()
        data = json.loads(line)
        print(data)
        m,st,ch,btn= data['model'],data['state'],data['channel'],data['button']
        if m==transmitter_name and ch==transmitter_channel and btn==1 and st=='on':
            print("ON")
        elif m==transmitter_name and ch==transmitter_channel and btn==1 and st=='off':
            print("OFF")
    except KeyboardInterrupt:
        break
    except:
        pass

print("RTLSDR listening done")


Чтобы запустить код, нужно сохранить его в файле (например rtl_listen.py) и запустить командой «python rtl_listen.py».

Как можно видеть, программа запускает процесс с помощью subprocess.Popen и читает из него данные. Дальше все просто, код вполне читабельный, и внести изменения будет не сложно. В данном примере, при нажатии кнопки »1» выводится сообщение print («ON»), вместо этого можно делать что-то другое, например, активировать пин GPIO, включать реле, посылать данные на сервер и пр.

Кстати, сам RTL-SDR-приемник по сравнению с Raspberry Pi выглядит так:
juypeoflpdtecvmhvtjpc4hrx2y.jpeg

Windows


К сожалению, под Windows 10 вышеприведенный код не заработал. Но как подсказал поиск на github, работает асинхронное чтение данных из отдельного потока. Почему так, выяснять было лень, просто приведу под спойлером работающий код.

Исходный код
from __future__ import print_function

import os, sys
import subprocess
import time
import threading
import Queue
import json

class AsynchronousFileReader(threading.Thread):
    # Helper class to implement asynchronous reading
    def __init__(self, fd, queue):
        assert isinstance(queue, Queue.Queue)
        assert callable(fd.readline)
        threading.Thread.__init__(self)
        self._fd = fd
        self._queue = queue

    def run(self):
        # The body of the tread: read lines and put them on the queue.
        for line in iter(self._fd.readline, ''):
            self._queue.put(line)

    def eof(self):
        # Check whether there is no more content to expect
        return not self.is_alive() and self._queue.empty()

def replace(string):
    while '  ' in string:
        string = string.replace('  ', ' ')
    return string


def read_rtl_data():
    process = subprocess.Popen(["rtl_433.exe", "-F", "json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # Launch the asynchronous readers of stdout and stderr.
    stdout_queue = Queue.Queue()
    stdout_reader = AsynchronousFileReader(process.stdout, stdout_queue)
    stdout_reader.start()
    stderr_queue = Queue.Queue()
    stderr_reader = AsynchronousFileReader(process.stderr, stderr_queue)
    stderr_reader.start()

    transmitter_name = "Waveman Switch Transmitter"
    transmitter_channel = 1

    # Check the queues if we received some output
    while not stdout_reader.eof() or not stderr_reader.eof():
        # Show what we received from standard output.
        while not stdout_queue.empty():
           line = stdout_queue.get()
           print("Line1:", repr(line))
           data = json.loads(line)
           # print("Data:", repr(line))
           m,st,ch,btn= data['model'],data['state'],data['channel'],data['button']
           if m== transmitter_name and ch== transmitter_channel and btn==1 and st=='on':
               print("ON")
           elif m== transmitter_name and ch==transmitter_channel and btn==1 and st=='off':
               print("OFF")

        # Show what we received from standard error.
        while not stderr_queue.empty():
            line = replace(stderr_queue.get())
            print("Line2:", line)

        # Sleep a bit before asking the readers again.
        time.sleep(0.1)

    stdout_reader.join()
    stderr_reader.join()

    # Close subprocess' file descriptors.
    process.stdout.close()
    process.stderr.close()

if __name__ == '__main__':
    print("RTLSDR listening started")
    
    read_rtl_data()
    
    print("RTLSDR listening done")


С этим кодом мы можем использовать любые действия в обработчике, логика такая же как и в коде на Raspberry Pi.

Пример: допустим, у нас есть компьютер, выделенный под домашний кинотеатр, и мы хотим выключать его нажатием кнопки с пульта. Заменяем код 'print («OFF»)' на

               os.system('shutdown -s')
               sys.exit(0)


После чего, компьютер будет выключаться по нажатию соответствующей кнопки.

Заключение


Как можно видеть, все довольно-таки просто, и есть место для экспериментов. Наконец, небольшой бонус для тех кто дочитал до сюда. На 433МГц работает большое количество разных устройств, которые rtl_433 может декодировать, можно просто оставить программу работать несколько часов, и посмотреть что «поймается». Под спойлером пример такого лога, записанного ранее:

Лог
2018-01-10 21:15:17 : Prologue sensor : 5 : 15
Channel: 1
Battery: OK
Button: 0
Temperature: 6.00 C
Humidity: 11 %

2018-01-10 21:15:28 : inFactory sensor
ID: 71
Temperature: 6.67 °C
Humidity: 99 %

2018-01-10 21:16:07 : Toyota : TPMS : 61511475 : 60e5006b : CRC

2018-01-10 21:20:33 : Prologue sensor : 5 : 15
Channel: 1
Battery: OK
Button: 0
Temperature: 6.00 C
Humidity: 11 %
: Waveman Switch Transmitter
id: A
channel: 2
button: 1
state: on
: Waveman Switch Transmitter
id: A
channel: 2
button: 1
state: on
: Waveman Switch Transmitter
id: A
channel: 2
button: 1
state: on

2018-01-10 21:21:21 : Akhan 100F14 remote keyless entry
ID (20bit): 0x41
Data (4bit): 0x4 (Mute)
: Waveman Switch Transmitter
id: A
channel: 2
button: 1
state: off

2018-01-10 21:32:31 : Ford : TPMS : 00268b1f : a34a0e : CHECKSUM
2018-01-10 21:32:32 : Ford : TPMS : 00268a5c : 9c440e : CHECKSUM
2018-01-10 21:32:37 : Ford : TPMS : 016dbfce : 99430e : CHECKSUM
2018-01-10 21:32:39 : Ford : TPMS : 002671a0 : 9c4a0e : CHECKSUM



Есть интересные данные, например давление в шинах у соседского автомобиля (TPMS, tire-pressure monitoring system), или наружняя температура +6 с чьего-то датчика. Это позволяет например, выводить наружнюю температуру, если у соседей случайно окажется совместимая с этим протоколом метеостанция.

Всем удачных экспериментов.

Disclaimer: Я понимаю, что использование SDR и цифровой обработки для чтения сигналов OOK-модуляции — это по сути, стрельба из пушки по воробьям. Возможно, на aliexpress существуют готовые приемники за 1–2$, которые делают то же самое, с меньшей ценой и меньшим энергопотреблением. Если кто знает такие модели, напишите в комментариях.

© Habrahabr.ru