├── .gitignore ├── FM-Radio ├── FM-Radio-WX.grc ├── FM-Radio.grc └── README.md ├── LICENSE ├── README.md ├── TFA ├── README.md ├── analyzer.grc ├── analyzer2.grc ├── decoder.grc ├── docs │ ├── Decoding Description.odg │ ├── Decoding_Description.png │ ├── Demodulation_Description.png │ ├── plot_spectrum.py │ ├── rf_signals.png │ ├── spectrum.png │ └── spectrum.txt ├── live_decoder.grc ├── recorder.grc └── stream_decoder.py ├── TS33C ├── README.md ├── decoder.grc ├── docs │ ├── Decoding Description 1.png │ ├── Decoding Description 2.png │ ├── Decoding Description.odg │ ├── RXB8.fzpz │ ├── RXB8_Raspi.fzz │ ├── RXB8_Raspi_bb.png │ ├── RXB8_Raspi_schem.png │ ├── fft.png │ ├── frames.png │ ├── single_frame.png │ ├── single_frame_zoom.png │ ├── single_frame_zoom2.png │ ├── spectrum_waveform.png │ ├── spectrum_waveform_zoom.png │ └── spectrum_waveform_zoom2.png ├── external_decoder.py ├── live_decoder.grc ├── recorder.grc ├── rxb8 │ ├── decoder.py │ ├── sniff.py │ ├── test.py │ └── trigger.py └── stream_decoder.py ├── baldr ├── README.md ├── baldr-decoder.service ├── decoder.py ├── doc │ ├── Baldr Decoding Rules.pptx │ ├── baldr_bits_raw_annotation.jpg │ ├── baldr_bits_raw_annotation.svg │ ├── baldr_decoding_rules.png │ └── baldr_sensor.png └── lib │ ├── __init__.py │ └── rfm69.py ├── basics ├── GFSK.grc └── OOK.grc ├── berner ├── README.md ├── decoder.grc ├── docs │ └── spectrum_magnitude.png ├── lib │ ├── __init__.py │ └── rfm69.py ├── recorder.grc ├── sniff.py └── transmitter.py ├── blocks └── README.md ├── kwmobile ├── README.md ├── decoder.py ├── decoder.service ├── doc │ ├── Decoding Rules.odp │ ├── bits_raw.png │ ├── bits_raw_annotation.jpg │ ├── decoding_rules.png │ ├── frame_raw.png │ ├── kwmobile_datasheet.jpg │ ├── kwmobile_sensor.png │ └── sample.png ├── lib │ ├── __init__.py │ └── rfm69.py └── sniff.py ├── mumbi ├── FS300.grc ├── README.md ├── docs │ ├── frames.png │ ├── histogram.png │ └── single_frame.png ├── lib │ ├── __init__.py │ └── rfm69.py ├── sniff.py └── transmitter.py ├── somfy ├── README.md ├── Telis1RTS.grc ├── decoder.py ├── docs │ ├── data_pin.png │ ├── spectrum_magnitude.png │ └── start_of_frame.png ├── epy_block_1.py ├── lib │ ├── __init__.py │ └── rfm69.py ├── live_decoder.grc ├── sniff.py └── transmitter.py └── velux ├── README.md ├── decoder.py ├── iohomecontrol.ksy ├── iohomecontrol.py ├── kaitai-decoder.py ├── transmitter.py └── wiring.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | top_block.py 2 | 3 | *.wav 4 | *.dat 5 | *.raw 6 | *.pyc 7 | *.cfile 8 | *.cu8 9 | *.fc32 10 | *.pyc 11 | *.bin 12 | 13 | blocks/gr-bitmap/build/ 14 | 15 | TS33C/\.vscode/ 16 | 17 | 18 | TS33C/rxb8/trace\.py 19 | berner/sample.cu8 20 | 21 | \.vscode/ 22 | somfy/config.json 23 | -------------------------------------------------------------------------------- /FM-Radio/README.md: -------------------------------------------------------------------------------- 1 | # FM-Radio 2 | This GNU Radio Companion project does: 3 | * receive&process FM radio 4 | * play FM radio on your audio device 5 | 6 | # References 7 | * https://re-ws.pl/tag/rtl-sdr/ 8 | * http://www.instructables.com/id/RTL-SDR-FM-radio-receiver-with-GNU-Radio-Companion/ 9 | * https://www.youtube.com/watch?v=KWeY2yqwVA0 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDR-Intro 2 | 3 | Collection of software defined radio (SDR) stuff 4 | 5 | ## Projects 6 | 7 | * [FM-Radio](./FM-Radio) 8 | * [Live Decoding of Hideki TS33C Wireless Temperature Sensor](./TS33C) 9 | * [Live Decoding of TFA 30.3180.IT Wireless Temperature Sensor](./TFA) 10 | * [Berner garage door opener remote control](./berner) 11 | * [somfy sunshade remote control](./somfy) 12 | * [mumbi remote controlled power outlets](./mumbi) 13 | 14 | ## Getting Started 15 | 16 | 1. plug in the USB-Receiver 17 | 2. Install drivers as described in [Quick Start Guide](https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/) 18 | ## Hardware 19 | 20 | I use a RTL2832U-based USB-receiver [RTL_SDR by Radioddity](https://www.radioddity.com/radioddity-100khz-1766mhz-0-1mhz-1-7ghz-full-band-uhf-vhf-hf-rtl-sdr-usb-tuner-receiver.html). It is shipped with a monopole antenna which works o.k. for most use-cases (e.g. FM-Radio, 433MHz, 868MHz). To increase the mobility of the whole setup you might want to exchange the stock antenna for a [smaller version](https://de.aliexpress.com/item/GSM-868M-900M-915MHz-antenna-2dbi-SMA-male-connector-5cm-long-RC-Receive-transmit-aerial-2/32511895558.html). 21 | 22 | If you want to spend more money you might go for a [HackRF One](https://greatscottgadgets.com/hackrf/) 23 | 24 | ## Software (Linux) 25 | 26 | * [SDR++](https://github.com/AlexandreRouma/SDRPlusPlus) 27 | * [inspectrum](https://github.com/miek/inspectrum) 28 | * [baudline](http://baudline.com/index.html) 29 | 30 | ## References 31 | 32 | * [I/Q Data for Dummies](http://whiteboard.ping.se/SDR/IQ) 33 | * [RTL-SDR (RTL2832U) and software defined radio news and projects](https://www.rtl-sdr.com/) 34 | -------------------------------------------------------------------------------- /TFA/README.md: -------------------------------------------------------------------------------- 1 | # TFA Dostmann Thermo-Hygro Sensor 30.3180.IT 2 | 3 | 4 | ## Technical Specifications 5 | Item | Value | Description 6 | -------------: | ------------- | :------------- 7 | Model | 30.3180.IT 8 | Moulation | QAM 9 | Frequency | 868.25MHz | 10 | Bit-timing | ? | 11 | Baudrate | 38400 | 12 | Coding | RZ | 13 | 14 | ## Signal Charactersistics 15 | 16 | RF-Signals: 17 | ![RF-Signal](docs/rf_signals.png) 18 | 19 | Demodulation: 20 | ![Demodulation](docs/Demodulation_Description.png) 21 | 22 | Decoding: 23 | ![Decoding](docs/Decoding_Description.png) 24 | 25 | ## Decoding Tool 26 | Use https://github.com/baycom/tfrec as reference for decoding: 27 | 28 | ``` 29 | $ ./tfrec -D -S ~/sdr/sdr/TFA/sample.raw | grep 71c9 30 | Found Rafael Micro R820T tuner 31 | #000 1512819632 2d d4 71 c9 86 14 36 60 00 56 5e ID 71c9 +21.4 54% seq 0 lowbat 0 RSSI 81 32 | ``` 33 | 34 | 0x71, 0xc9, 0x86, 0x14, 0x36, 0x60, 0x00, 0x56 35 | 36 | 37 | ## References 38 | * https://nccgroup.github.io/RFTM/fsk_receiver.html 39 | * https://www.reaktor.com/blog/radio-waves-packets-software-defined-radio/ 40 | * http://tfa-dostmann.de/index.php?id=129&L=0 41 | * https://github.com/sum-sum/rtl_868 42 | * http://www.linux-magazine.com/Online/Features/Reading-Weather-Data-with-Software-Defined-Radio 43 | * https://github.com/ChristopheJacquet/Pydemod 44 | * http://www.sunshine2k.de/coding/javascript/crc/crc_js.html 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /TFA/docs/Decoding Description.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TFA/docs/Decoding Description.odg -------------------------------------------------------------------------------- /TFA/docs/Decoding_Description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TFA/docs/Decoding_Description.png -------------------------------------------------------------------------------- /TFA/docs/Demodulation_Description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TFA/docs/Demodulation_Description.png -------------------------------------------------------------------------------- /TFA/docs/plot_spectrum.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | #with plt.xkcd(): 7 | data = np.genfromtxt('spectrum.txt', delimiter='\t', skip_header=1, names=['x', 'y']) 8 | 9 | x = data["x"] 10 | y = data["y"] 11 | 12 | 13 | fig, ax = plt.subplots() 14 | 15 | 16 | ax.set(xlabel='f (Hz)', ylabel='magnitude (dB)', 17 | title='Spectrum of Preamble') 18 | 19 | ax.annotate('17.1kHz', xy=(17100, -15), xytext=(1500, -25), 20 | arrowprops=dict(facecolor='black', connectionstyle="arc3,rad=-0.2", arrowstyle='->'), 21 | ) 22 | 23 | ax.annotate('21.7kHz', xy=(21700, -25), xytext=(50000, -25), 24 | arrowprops=dict(facecolor='black', connectionstyle="arc3,rad=-0.2", arrowstyle='->'), 25 | ) 26 | 27 | ax.semilogx(x, y) 28 | ax.grid(True, zorder=5) 29 | 30 | 31 | plt.show() 32 | -------------------------------------------------------------------------------- /TFA/docs/rf_signals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TFA/docs/rf_signals.png -------------------------------------------------------------------------------- /TFA/docs/spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TFA/docs/spectrum.png -------------------------------------------------------------------------------- /TFA/stream_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Live decoder for TFA Dostmann Thermo-Hygro Sensor 30.3180.IT""" 4 | 5 | import numpy as np 6 | 7 | FILENAME = "303180IT_bits.dat" 8 | 9 | ERROR = 1 10 | INFO = 2 11 | TRACE = 3 12 | 13 | class StreamDecoder(object): 14 | """30.3180.IT Decoder Class""" 15 | # pylint: disable=too-many-instance-attributes, C0301, C0103 16 | 17 | def __init__(self, sample_rate=20000, debug_level=0): 18 | self.debug_level = debug_level 19 | self.sample_rate = sample_rate 20 | self.state = "idle" 21 | 22 | self.chunk_id = 0 23 | self.pulse_short_limit = 30 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 24 | self.gap_short_limit = 30 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 25 | self.reset_limit = 500 26 | 27 | self.currentSymbols = np.empty(0, dtype=np.uint8) 28 | self.rawBuffer = np.empty(0, dtype=np.uint8) 29 | self.currentSymbolType = 0 30 | self.previousSymbolType = 0 31 | 32 | # sensor data 33 | self.identifier = 0 34 | self.temperature = 0 35 | self.humidity = 0 36 | self.battery_ok = False 37 | 38 | 39 | def debug(self, message, level=0): 40 | """Debug output depending on debug level.""" 41 | if self.debug_level >= level: 42 | print message 43 | 44 | @classmethod 45 | def crc8(cls, data): 46 | """Simple CRC-8 calculator""" 47 | generator = 0x31 # x^8 + x^5 + x^4 + 1 48 | crc = np.uint8(0) # use uint8 49 | 50 | for currByte in data: 51 | crc ^= currByte 52 | for i in range(8): 53 | if (crc & 0x80) != 0: 54 | crc = np.uint8((crc << 1) ^ generator) 55 | else: 56 | crc <<= 1 57 | return crc 58 | 59 | def work(self, input_items): 60 | """The actual Decoder.""" 61 | # pylint: disable=too-many-nested-blocks, too-many-statements, R0914, R0912, C0301 62 | 63 | rawChunk = input_items[0] 64 | self.rawBuffer = np.append(self.rawBuffer, rawChunk) 65 | 66 | # we need at least as many samples as is defined by reset_limit 67 | if self.rawBuffer.size*1e6/self.sample_rate > self.reset_limit: 68 | 69 | # skip empty or zero-only chunks 70 | if (self.rawBuffer.size > 0) and ((np.sum(self.rawBuffer) > 0) or (self.currentSymbols.size > 0)): 71 | 72 | self.currentSymbolType = self.rawBuffer[0] 73 | self.debug(("first raw sample", self.rawBuffer[0]), TRACE) 74 | self.debug(("last raw sample", self.rawBuffer[-1]), TRACE) 75 | 76 | # force edge at beginning an end of rawBuffer 77 | self.rawBuffer = np.insert(self.rawBuffer, 0, [1 - self.rawBuffer[0]]) 78 | self.rawBuffer = np.append(self.rawBuffer, [1 - self.rawBuffer[-1]]) 79 | 80 | self.chunk_id += 1 81 | self.debug(("Chunk", self.chunk_id), TRACE) 82 | #self.debug("rawBuffer", rawBuffer.size, rawBuffer) 83 | 84 | # calculate edges from raw data; insert one at beginning for sync 85 | edges = np.insert(np.diff(self.rawBuffer), 0, [0]) 86 | # position (offset) of edges 87 | edge_positions = np.ravel(np.nonzero(edges)) 88 | self.debug(("edge_positions:", edge_positions), TRACE) 89 | 90 | self.debug(("edges", edges[np.nonzero(edges)]), TRACE) 91 | 92 | if edge_positions.size > 0: 93 | # lengths of pulses and pauses are our symbols, convert to µs 94 | symbols = np.diff(edge_positions)*1e6/self.sample_rate 95 | 96 | # split symbols at packet boundary 97 | symbols = np.array(np.split(symbols, np.ravel(np.where(symbols > self.reset_limit))+1)) 98 | self.debug(("Symbols:", symbols.size, symbols), TRACE) 99 | self.debug(("symbols.shape", symbols.shape[0]), TRACE) 100 | self.debug(("state", self.state), TRACE) 101 | 102 | # if symbols are split into 2+ arrays at packet boundary we 103 | # have a valid frame 104 | if symbols.shape[0] > 1: 105 | self.state = "frame" 106 | 107 | # process all parts separately 108 | for symbol_chunk in symbols: 109 | self.debug(("state", self.state), TRACE) 110 | if (symbols.size == 1) or self.state == "idle": 111 | # skip leading inter-frame gap 112 | symbol_chunk = symbol_chunk[1::] 113 | if symbol_chunk.size > 0: 114 | self.debug(("symbol_chunk:", symbol_chunk.size, symbol_chunk), TRACE) 115 | self.state = "frame" 116 | 117 | # add first symbol to last from previous to reassemble separated pulses/gaps 118 | if (self.currentSymbols.size > 0) and (self.previousSymbolType == self.currentSymbolType): 119 | self.currentSymbols[-1] += symbol_chunk[0] 120 | symbol_chunk = symbol_chunk[1::] 121 | 122 | self.currentSymbols = np.append(self.currentSymbols, symbol_chunk) 123 | 124 | if self.currentSymbols[0] > self.reset_limit: 125 | self.currentSymbols = self.currentSymbols[1::] 126 | 127 | self.debug(("currentSymbols:", self.currentSymbols.size, self.currentSymbols), TRACE) 128 | 129 | # complete packet is determined by long gap at the end 130 | if (self.currentSymbols.size > 1) and (self.currentSymbols[-1] > self.reset_limit): 131 | 132 | # remove first few symbols 133 | self.currentSymbols = self.currentSymbols[6:] 134 | 135 | # sync pattern - about 75 bits 136 | sync = self.currentSymbols[:2*75] 137 | symbol_length = np.mean(np.sum(np.reshape(sync, (-1, 2)), axis=1)) 138 | baudrate = int(1e6/symbol_length) 139 | self.debug(("symbol_length:", symbol_length), TRACE) 140 | self.debug(("baudrate:", baudrate), TRACE) 141 | 142 | # determine start of data packet 143 | data_start = np.where(self.currentSymbols > symbol_length*1.5)[0][0] 144 | self.debug(("data_start:", data_start), TRACE) 145 | 146 | # extract pulses 147 | pulses = self.currentSymbols[data_start:-1:2] 148 | 149 | # remove short pulses (they are just part of zeroes) 150 | pulses = np.where(pulses < symbol_length, 0, pulses) 151 | 152 | # extract gaps 153 | gaps = self.currentSymbols[data_start+1:-1:2] 154 | 155 | self.debug(("Pulses:", pulses.size, pulses), TRACE) 156 | self.debug(("Gaps:", gaps.size, gaps), TRACE) 157 | 158 | # interleave pulses and gaps into one array like this [p, g, p, g, p, g] 159 | combined = np.empty(pulses.size + gaps.size, dtype=pulses.dtype) 160 | combined[0::2] = np.floor(pulses/symbol_length) 161 | combined[1::2] = np.ceil(gaps/symbol_length) 162 | 163 | self.debug(("combined:", combined.size, combined), TRACE) 164 | 165 | # FIXME - there must be a more elegant solution... 166 | rawBits = np.empty(0, dtype=np.uint8) 167 | for index, symbol in np.ndenumerate(combined): 168 | if symbol: 169 | if not index[0] % 2: 170 | rawBits = np.append(rawBits, np.ones(int(symbol), dtype=np.uint8)) 171 | else: 172 | rawBits = np.append(rawBits, np.zeros(int(symbol), dtype=np.uint8)) 173 | 174 | self.debug(("rawBits:", rawBits.size, rawBits), TRACE) 175 | 176 | # make sure we have at least 11 bytes 177 | if rawBits.size >= 11*8: 178 | # use the first eleven bytes only, swap MSB and LSB, pack 8 bits into one byte, reverse order to compensate for 1st flip 179 | packet = np.packbits(rawBits[:11*8][::-1])[::-1] 180 | 181 | # check CRC8 182 | if self.crc8(packet[2:10]) == packet[10]: 183 | np.set_printoptions(formatter={'int':hex}) 184 | self.debug(("packet:", packet.size, packet), INFO) 185 | np.set_printoptions(formatter=None) 186 | 187 | self.identifier = "{:02X}".format((packet[2]<<8) + packet[3]) 188 | self.temperature = (packet[4] & 0x0f) * 10 + ((packet[5]>>4) & 0x0f) * 1 + (packet[5] & 0x0f) * 0.1 - 40 189 | self.humidity = (packet[6] & 0x7f) 190 | self.battery_ok = (packet[7]>>7) & 0x01 == 0 191 | 192 | self.debug(("Identifier:", self.identifier), INFO) 193 | self.debug(("Battery ok:", self.battery_ok), INFO) 194 | print "Temperature: %02.1f°C" % self.temperature 195 | print "Humidity: %02i%%" % self.humidity 196 | else: 197 | self.debug("CRC-Check failed", ERROR) 198 | else: 199 | self.debug("Invalid packet length", ERROR) 200 | 201 | # done with this packet 202 | self.currentSymbols = np.empty(0, dtype=np.uint8) 203 | else: 204 | self.debug("Waiting for more data", TRACE) 205 | else: 206 | self.debug("skipping empty chunk", INFO) 207 | self.state = "idle" 208 | else: 209 | self.debug("no edges in chunk", INFO) 210 | self.previousSymbolType = self.rawBuffer[-2] 211 | else: 212 | #self.debug("Chunk is empty or contains no edges") 213 | self.state = "idle" 214 | # all done, delete current rawBuffer 215 | self.rawBuffer = np.empty(0, dtype=np.uint8) 216 | else: 217 | # wait for more samples 218 | self.debug("filling rawBuffer", TRACE) 219 | 220 | def main(): 221 | """ main function """ 222 | # set up decoder 223 | decoder = StreamDecoder(sample_rate=2000000, debug_level=TRACE) 224 | 225 | # load raw binary data from file 226 | raw_data = np.ravel(np.fromfile(FILENAME, dtype=np.int8)) 227 | 228 | # simulate streaming of input data 229 | raw_stream = np.array_split(raw_data, 200) 230 | #raw_stream = [raw_data] 231 | 232 | for chunk in raw_stream: 233 | # feed chunks to decoder 234 | decoder.work([chunk]) 235 | 236 | if __name__ == "__main__": 237 | main() 238 | -------------------------------------------------------------------------------- /TS33C/README.md: -------------------------------------------------------------------------------- 1 | # Hideki TS33C Wireless Temperature/Humidity Sensor 2 | 3 | Analysis and decoding of temperatures and humidity values sent by this wireless sensor. 4 | 5 | ## Technical Specifications 6 | 7 | Item | Value | Description 8 | -----------|------------- | ------------- 9 | Frequency | 433.964 MHz | 10 | Wavelength | 69 cm | λ=c/f 11 | Modulation | On-off keying (OOK) | 12 | Symbols | Pulse Width Modulation | 13 | 14 | ## Decoding Rules 15 | 16 | Pulse lengths Tp are decoded as follows: 17 | 18 | Timing | Tp <= 390µs | Tp > 390µs 19 | --- | --- | --- 20 | Symbol | `Short` | `Long` 21 | 22 | Gaps lengths Tg are decoded as follows: 23 | 24 | Timing | Tg <= 1140µs | Tg > 1140µs | Tg > 1550µs 25 | --- | --- | --- | --- 26 | Symbol | `Short` | `Long` | `Reset` 27 | 28 | The resulting symbols are decoded as follows: 29 | 30 | Symbol | `Short` | `Long` | `Reset` 31 | --- | --- | --- | --- 32 | Pulse | `1` | `0` | *Error* 33 | Gap | *(skip)* | `0` | *Reset* 34 | 35 | Please note that the stream-decoder adapts the values for short/long symbols for each packet by creating and evaluating a histogram of all pulse and gap lengths. 36 | 37 | After decoding all symbols to bits, the resulting bitstream contains 90 bits and must be further processed as follows: 38 | 39 | 1. Extract parity bits and data bytes 40 | 2. Reverse data bytes (swap MSB and LSB) 41 | 3. Invert each bit of the data bytes (xor 0xff) 42 | 4. Check parity bits and frame length 43 | 5. Extract sensor signals 44 | 45 | ## Details and Examples 46 | 47 | ### Frames are transmitted in groups of three every 42s 48 | ![frame group](docs/frames.png) 49 | Time-Axis in ms 50 | 51 | ### Decoding of raw radio signal to bits 52 | 53 | ![Decoding of raw radio signal to bits](docs/Decoding%20Description%201.png) 54 | 55 | ### Decoding of bits to sensor information 56 | 57 | ![Decoding of bits to sensor information](docs/Decoding%20Description%202.png) 58 | 59 | ## Stream decoding with Gnu Radio 60 | GNU radio block process the raw data in chunks. So the decoder must be able to handle these as well. 61 | 62 | ### Files: 63 | * decoder.grc - GNU Radio file 64 | * stream_decoder.py - stand-alone decoder for raw binary (demodulated) data 65 | 66 | ## Decoding on Raspberry Pi with RXB8-Receiver 67 | 68 | The signal can also be decoded with a cheap transceiver on a Raspberry Pi. 69 | 70 | ### Hardware 71 | * [RXB8 433 Mhz Superheterodyne Receiver](https://de.aliexpress.com/item/RXB8-433-Mhz-Superheterodyne-Funkempf-nger-Perfekte-f-r-Arduino-AVR/32673931505.html) and [Datasheet](http://p.globalsources.com/IMAGES/PDT/SPEC/508/K1045318508.pdf) 72 | * [433 MHz Coil Loaded Antenna](http://www.instructables.com/id/433-MHz-Coil-loaded-antenna/) 73 | * 2 Resistors (1kΩ and 2.2kΩ) 74 | * some wiring and connectors 75 | 76 | Raspi GPIO have a maximum rating of 3.5V. The output level on DATA is 4.5V so you **MUST** use a voltage divider to connect the data-pin to the Raspi: 77 | Schematic| Breadboard 78 | --- | --- 79 | ![schematic View](docs/RXB8_Raspi_schem.png) | ![Breadboard View](docs/RXB8_Raspi_bb.png) 80 | 81 | You can use any two resistors that have a ratio of 1:2 for that divider, but keep in mind, that the input impendance of a GPIO port is around 50k. So better stay well below 10k with your resistors (see [GPIO Electrical Specifications](http://www.mosaic-industries.com/embedded-systems/microcontroller-projects/raspberry-pi/gpio-pin-electrical-specifications)). 82 | 83 | ### Software 84 | * install [pigpio library](http://abyz.me.uk/rpi/pigpio/index.html) 85 | * see [rxb8/decoder.py](rxb8/decoder.py) 86 | 87 | ## Decoding with rtl_433 88 | 89 | Use the [rtl_433-tool](https://github.com/merbanan/rtl_433) to decode the values for reference or analyze the radio data. 90 | 91 | Please note that the output shown here is generated by a modified version of rtl_433. 92 | 93 | ``` 94 | $ rtl_433 -f 433964000 -R 42 -DD 95 | Registering protocol [1] "HIDEKI TS04 Temperature, Humidity, Wind and Rain Sensor" 96 | Registered 1 out of 95 device decoding protocols 97 | Found 1 device(s) 98 | 99 | trying device 0: Realtek, RTL2838UHIDIR, SN: 00000001 100 | Found Rafael Micro R820T tuner 101 | Using device 0: Generic RTL2832U OEM 102 | Exact sample rate is: 250000.000414 Hz 103 | [R82XX] PLL not locked! 104 | Sample rate set to 250000. 105 | Bit detection level set to 0 (Auto). 106 | Tuner gain set to Auto. 107 | Reading samples in async mode... 108 | Tuned to 433964000 Hz. 109 | 110 | Pulses: 241, 231, 226, 102, 105, 104, 226, 106, 99, 104, 110, 104, 103, 103, 105, 228, 230, 105, 104, 106, 103, 103, 226, 225, 107, 226, 108, 106, 227, 106, 233, 107, 109, 104, 106, 229, 104, 108, 109, 108, 226, 226, 104, 107, 228, 104, 108, 109, 229, 104, 229, 231, 230, 229, 227, 105, 107, 226, 102, 227, 104, 105, 107, 104, 109, 107, 105, 111 | Gaps: 257, 261, 143, 140, 261, 140, 139, 145, 139, 137, 259, 143, 141, 138, 140, 258, 138, 139, 141, 139, 265, 139, 264, 260, 139, 138, 258, 141, 259, 134, 136, 137, 139, 260, 137, 140, 138, 134, 137, 260, 264, 140, 137, 261, 260, 138, 135, 137, 261, 139, 257, 258, 259, 262, 138, 138, 262, 142, 263, 139, 138, 139, 261, 136, 136, 138, 2501, 112 | device->name: HIDEKI TS04 Temperature, Humidity, Wind and Rain Sensor 113 | device->short_limit: 130.000000 114 | device->long_limit: 260.000000 115 | n: 0 bitbuffer:: Number of rows: 1 116 | [00] {1} 00 : 0 117 | n: 1 bitbuffer:: Number of rows: 1 118 | [00] {2} 00 : 00 119 | n: 2 bitbuffer:: Number of rows: 1 120 | [00] {3} 00 : 000 121 | n: 3 bitbuffer:: Number of rows: 1 122 | [00] {4} 00 : 0000 123 | n: 4 bitbuffer:: Number of rows: 1 124 | [00] {5} 00 : 00000 125 | n: 6 bitbuffer:: Number of rows: 1 126 | [00] {6} 04 : 000001 127 | n: 8 bitbuffer:: Number of rows: 1 128 | [00] {7} 06 : 0000011 129 | n: 9 bitbuffer:: Number of rows: 1 130 | [00] {8} 06 : 00000110 131 | n: 11 bitbuffer:: Number of rows: 1 132 | [00] {9} 06 80 : 00000110 1 133 | n: 12 bitbuffer:: Number of rows: 1 134 | [00] {10} 06 80 : 00000110 10 135 | n: 14 bitbuffer:: Number of rows: 1 136 | [00] {11} 06 a0 : 00000110 101 137 | n: 16 bitbuffer:: Number of rows: 1 138 | [00] {12} 06 b0 : 00000110 1011 139 | n: 18 bitbuffer:: Number of rows: 1 140 | [00] {13} 06 b8 : 00000110 10111 141 | n: 20 bitbuffer:: Number of rows: 1 142 | [00] {14} 06 bc : 00000110 101111 143 | n: 21 bitbuffer:: Number of rows: 1 144 | [00] {15} 06 bc : 00000110 1011110 145 | n: 23 bitbuffer:: Number of rows: 1 146 | [00] {16} 06 bd : 00000110 10111101 147 | n: 25 bitbuffer:: Number of rows: 1 148 | [00] {17} 06 bd 80 : 00000110 10111101 1 149 | n: 27 bitbuffer:: Number of rows: 1 150 | [00] {18} 06 bd c0 : 00000110 10111101 11 151 | n: 29 bitbuffer:: Number of rows: 1 152 | [00] {19} 06 bd e0 : 00000110 10111101 111 153 | n: 30 bitbuffer:: Number of rows: 1 154 | [00] {20} 06 bd e0 : 00000110 10111101 1110 155 | n: 31 bitbuffer:: Number of rows: 1 156 | [00] {21} 06 bd e0 : 00000110 10111101 11100 157 | n: 32 bitbuffer:: Number of rows: 1 158 | [00] {22} 06 bd e0 : 00000110 10111101 111000 159 | n: 34 bitbuffer:: Number of rows: 1 160 | [00] {23} 06 bd e2 : 00000110 10111101 1110001 161 | n: 36 bitbuffer:: Number of rows: 1 162 | [00] {24} 06 bd e3 : 00000110 10111101 11100011 163 | n: 38 bitbuffer:: Number of rows: 1 164 | [00] {25} 06 bd e3 80 : 00000110 10111101 11100011 1 165 | n: 40 bitbuffer:: Number of rows: 1 166 | [00] {26} 06 bd e3 c0 : 00000110 10111101 11100011 11 167 | n: 41 bitbuffer:: Number of rows: 1 168 | [00] {27} 06 bd e3 c0 : 00000110 10111101 11100011 110 169 | n: 43 bitbuffer:: Number of rows: 1 170 | [00] {28} 06 bd e3 d0 : 00000110 10111101 11100011 1101 171 | n: 44 bitbuffer:: Number of rows: 1 172 | [00] {29} 06 bd e3 d0 : 00000110 10111101 11100011 11010 173 | n: 45 bitbuffer:: Number of rows: 1 174 | [00] {30} 06 bd e3 d0 : 00000110 10111101 11100011 110100 175 | n: 46 bitbuffer:: Number of rows: 1 176 | [00] {31} 06 bd e3 d0 : 00000110 10111101 11100011 1101000 177 | n: 47 bitbuffer:: Number of rows: 1 178 | [00] {32} 06 bd e3 d0 : 00000110 10111101 11100011 11010000 179 | n: 49 bitbuffer:: Number of rows: 1 180 | [00] {33} 06 bd e3 d0 80 : 00000110 10111101 11100011 11010000 1 181 | n: 50 bitbuffer:: Number of rows: 1 182 | [00] {34} 06 bd e3 d0 80 : 00000110 10111101 11100011 11010000 10 183 | n: 52 bitbuffer:: Number of rows: 1 184 | [00] {35} 06 bd e3 d0 a0 : 00000110 10111101 11100011 11010000 101 185 | n: 53 bitbuffer:: Number of rows: 1 186 | [00] {36} 06 bd e3 d0 a0 : 00000110 10111101 11100011 11010000 1010 187 | n: 55 bitbuffer:: Number of rows: 1 188 | [00] {37} 06 bd e3 d0 a8 : 00000110 10111101 11100011 11010000 10101 189 | n: 56 bitbuffer:: Number of rows: 1 190 | [00] {38} 06 bd e3 d0 a8 : 00000110 10111101 11100011 11010000 101010 191 | n: 57 bitbuffer:: Number of rows: 1 192 | [00] {39} 06 bd e3 d0 a8 : 00000110 10111101 11100011 11010000 1010100 193 | n: 59 bitbuffer:: Number of rows: 1 194 | [00] {40} 06 bd e3 d0 a9 : 00000110 10111101 11100011 11010000 10101001 195 | n: 60 bitbuffer:: Number of rows: 1 196 | [00] {41} 06 bd e3 d0 a9 00 : 00000110 10111101 11100011 11010000 10101001 0 197 | n: 62 bitbuffer:: Number of rows: 1 198 | [00] {42} 06 bd e3 d0 a9 40 : 00000110 10111101 11100011 11010000 10101001 01 199 | n: 64 bitbuffer:: Number of rows: 1 200 | [00] {43} 06 bd e3 d0 a9 60 : 00000110 10111101 11100011 11010000 10101001 011 201 | n: 66 bitbuffer:: Number of rows: 1 202 | [00] {44} 06 bd e3 d0 a9 70 : 00000110 10111101 11100011 11010000 10101001 0111 203 | n: 67 bitbuffer:: Number of rows: 1 204 | [00] {45} 06 bd e3 d0 a9 70 : 00000110 10111101 11100011 11010000 10101001 01110 205 | n: 69 bitbuffer:: Number of rows: 1 206 | [00] {46} 06 bd e3 d0 a9 74 : 00000110 10111101 11100011 11010000 10101001 011101 207 | n: 70 bitbuffer:: Number of rows: 1 208 | [00] {47} 06 bd e3 d0 a9 74 : 00000110 10111101 11100011 11010000 10101001 0111010 209 | n: 72 bitbuffer:: Number of rows: 1 210 | [00] {48} 06 bd e3 d0 a9 75 : 00000110 10111101 11100011 11010000 10101001 01110101 211 | n: 74 bitbuffer:: Number of rows: 1 212 | [00] {49} 06 bd e3 d0 a9 75 80 : 00000110 10111101 11100011 11010000 10101001 01110101 1 213 | n: 76 bitbuffer:: Number of rows: 1 214 | [00] {50} 06 bd e3 d0 a9 75 c0 : 00000110 10111101 11100011 11010000 10101001 01110101 11 215 | n: 78 bitbuffer:: Number of rows: 1 216 | [00] {51} 06 bd e3 d0 a9 75 e0 217 | n: 79 bitbuffer:: Number of rows: 1 218 | [00] {52} 06 bd e3 d0 a9 75 e0 219 | n: 80 bitbuffer:: Number of rows: 1 220 | [00] {53} 06 bd e3 d0 a9 75 e0 221 | n: 81 bitbuffer:: Number of rows: 1 222 | [00] {54} 06 bd e3 d0 a9 75 e0 223 | n: 82 bitbuffer:: Number of rows: 1 224 | [00] {55} 06 bd e3 d0 a9 75 e0 225 | n: 84 bitbuffer:: Number of rows: 1 226 | [00] {56} 06 bd e3 d0 a9 75 e1 227 | n: 86 bitbuffer:: Number of rows: 1 228 | [00] {57} 06 bd e3 d0 a9 75 e1 80 229 | n: 87 bitbuffer:: Number of rows: 1 230 | [00] {58} 06 bd e3 d0 a9 75 e1 80 231 | n: 88 bitbuffer:: Number of rows: 1 232 | [00] {59} 06 bd e3 d0 a9 75 e1 80 233 | n: 89 bitbuffer:: Number of rows: 1 234 | [00] {60} 06 bd e3 d0 a9 75 e1 80 235 | n: 91 bitbuffer:: Number of rows: 1 236 | [00] {61} 06 bd e3 d0 a9 75 e1 88 237 | n: 93 bitbuffer:: Number of rows: 1 238 | [00] {62} 06 bd e3 d0 a9 75 e1 8c 239 | n: 95 bitbuffer:: Number of rows: 1 240 | [00] {63} 06 bd e3 d0 a9 75 e1 8e 241 | n: 96 bitbuffer:: Number of rows: 1 242 | [00] {64} 06 bd e3 d0 a9 75 e1 8e 243 | n: 97 bitbuffer:: Number of rows: 1 244 | [00] {65} 06 bd e3 d0 a9 75 e1 8e 00 245 | n: 99 bitbuffer:: Number of rows: 1 246 | [00] {66} 06 bd e3 d0 a9 75 e1 8e 40 247 | n: 100 bitbuffer:: Number of rows: 1 248 | [00] {67} 06 bd e3 d0 a9 75 e1 8e 40 249 | n: 101 bitbuffer:: Number of rows: 1 250 | [00] {68} 06 bd e3 d0 a9 75 e1 8e 40 251 | n: 102 bitbuffer:: Number of rows: 1 252 | [00] {69} 06 bd e3 d0 a9 75 e1 8e 40 253 | n: 103 bitbuffer:: Number of rows: 1 254 | [00] {70} 06 bd e3 d0 a9 75 e1 8e 40 255 | n: 104 bitbuffer:: Number of rows: 1 256 | [00] {71} 06 bd e3 d0 a9 75 e1 8e 40 257 | n: 105 bitbuffer:: Number of rows: 1 258 | [00] {72} 06 bd e3 d0 a9 75 e1 8e 40 259 | n: 106 bitbuffer:: Number of rows: 1 260 | [00] {73} 06 bd e3 d0 a9 75 e1 8e 40 00 261 | n: 107 bitbuffer:: Number of rows: 1 262 | [00] {74} 06 bd e3 d0 a9 75 e1 8e 40 00 263 | n: 108 bitbuffer:: Number of rows: 1 264 | [00] {75} 06 bd e3 d0 a9 75 e1 8e 40 00 265 | n: 110 bitbuffer:: Number of rows: 1 266 | [00] {76} 06 bd e3 d0 a9 75 e1 8e 40 10 267 | n: 112 bitbuffer:: Number of rows: 1 268 | [00] {77} 06 bd e3 d0 a9 75 e1 8e 40 18 269 | n: 113 bitbuffer:: Number of rows: 1 270 | [00] {78} 06 bd e3 d0 a9 75 e1 8e 40 18 271 | n: 114 bitbuffer:: Number of rows: 1 272 | [00] {79} 06 bd e3 d0 a9 75 e1 8e 40 18 273 | n: 116 bitbuffer:: Number of rows: 1 274 | [00] {80} 06 bd e3 d0 a9 75 e1 8e 40 19 275 | n: 117 bitbuffer:: Number of rows: 1 276 | [00] {81} 06 bd e3 d0 a9 75 e1 8e 40 19 00 277 | n: 118 bitbuffer:: Number of rows: 1 278 | [00] {82} 06 bd e3 d0 a9 75 e1 8e 40 19 00 279 | n: 120 bitbuffer:: Number of rows: 1 280 | [00] {83} 06 bd e3 d0 a9 75 e1 8e 40 19 20 281 | n: 122 bitbuffer:: Number of rows: 1 282 | [00] {84} 06 bd e3 d0 a9 75 e1 8e 40 19 30 283 | n: 124 bitbuffer:: Number of rows: 1 284 | [00] {85} 06 bd e3 d0 a9 75 e1 8e 40 19 38 285 | n: 125 bitbuffer:: Number of rows: 1 286 | [00] {86} 06 bd e3 d0 a9 75 e1 8e 40 19 38 287 | n: 127 bitbuffer:: Number of rows: 1 288 | [00] {87} 06 bd e3 d0 a9 75 e1 8e 40 19 3a 289 | n: 129 bitbuffer:: Number of rows: 1 290 | [00] {88} 06 bd e3 d0 a9 75 e1 8e 40 19 3b 291 | n: 131 bitbuffer:: Number of rows: 1 292 | [00] {89} 06 bd e3 d0 a9 75 e1 8e 40 19 3b 80 293 | n: 132 bitbuffer:: Number of rows: 1 294 | [00] {90} 06 bd e3 d0 a9 75 e1 8e 40 19 3b c0 295 | bitbuffer:: Number of rows: 1 296 | [00] {90} 06 bd e3 d0 a9 75 e1 8e 40 19 3b c0 297 | Packet: 9f 21 0e 5e 16 c2 39 fb 67 11 298 | 2017-11-22 10:27:04 : HIDEKI TS04 sensor 299 | Rolling Code: 1 300 | Channel: 1 301 | Battery: OK 302 | Temperature: 21.6 C 303 | Humidity: 39 % 304 | pulse_demod_clock_bits(): HIDEKI TS04 Temperature, Humidity, Wind and Rain Sensor 305 | bitbuffer:: Number of rows: 1 306 | [00] {90} 06 bd e3 d0 a9 75 e1 8e 40 19 3b c0 307 | n: 133 bitbuffer:: Number of rows: 0 308 | Pulse data: 67 pulses 309 | [ 0] Pulse: 241, Gap: 257, Period: 498 -> 00 310 | [ 1] Pulse: 231, Gap: 261, Period: 492 -> 00 311 | [ 2] Pulse: 226, Gap: 143, Period: 369 -> 0 312 | [ 3] Pulse: 102, Gap: 140, Period: 242 -> 1 313 | [ 4] Pulse: 105, Gap: 261, Period: 366 -> 10 314 | [ 5] Pulse: 104, Gap: 140, Period: 244 -> 1 315 | [ 6] Pulse: 226, Gap: 139, Period: 365 -> 0 316 | [ 7] Pulse: 106, Gap: 145, Period: 251 -> 1 317 | [ 8] Pulse: 99, Gap: 139, Period: 238 -> 1 318 | [ 9] Pulse: 104, Gap: 137, Period: 241 -> 1 319 | [ 10] Pulse: 110, Gap: 259, Period: 369 -> 10 320 | [ 11] Pulse: 104, Gap: 143, Period: 247 -> 1 (0000.0110,1011.1101) 321 | [ 12] Pulse: 103, Gap: 141, Period: 244 322 | [ 13] Pulse: 103, Gap: 138, Period: 241 323 | [ 14] Pulse: 105, Gap: 140, Period: 245 324 | [ 15] Pulse: 228, Gap: 258, Period: 486 325 | [ 16] Pulse: 230, Gap: 138, Period: 368 326 | [ 17] Pulse: 105, Gap: 139, Period: 244 327 | [ 18] Pulse: 104, Gap: 141, Period: 245 328 | [ 19] Pulse: 106, Gap: 139, Period: 245 329 | [ 20] Pulse: 103, Gap: 265, Period: 368 330 | [ 21] Pulse: 103, Gap: 139, Period: 242 331 | [ 22] Pulse: 226, Gap: 264, Period: 490 332 | [ 23] Pulse: 225, Gap: 260, Period: 485 333 | [ 24] Pulse: 107, Gap: 139, Period: 246 334 | [ 25] Pulse: 226, Gap: 138, Period: 364 335 | [ 26] Pulse: 108, Gap: 258, Period: 366 336 | [ 27] Pulse: 106, Gap: 141, Period: 247 337 | [ 28] Pulse: 227, Gap: 259, Period: 486 338 | [ 29] Pulse: 106, Gap: 134, Period: 240 339 | [ 30] Pulse: 233, Gap: 136, Period: 369 340 | [ 31] Pulse: 107, Gap: 137, Period: 244 341 | [ 32] Pulse: 109, Gap: 139, Period: 248 342 | [ 33] Pulse: 104, Gap: 260, Period: 364 343 | [ 34] Pulse: 106, Gap: 137, Period: 243 344 | [ 35] Pulse: 229, Gap: 140, Period: 369 345 | [ 36] Pulse: 104, Gap: 138, Period: 242 346 | [ 37] Pulse: 108, Gap: 134, Period: 242 347 | [ 38] Pulse: 109, Gap: 137, Period: 246 348 | [ 39] Pulse: 108, Gap: 260, Period: 368 349 | [ 40] Pulse: 226, Gap: 264, Period: 490 350 | [ 41] Pulse: 226, Gap: 140, Period: 366 351 | [ 42] Pulse: 104, Gap: 137, Period: 241 352 | [ 43] Pulse: 107, Gap: 261, Period: 368 353 | [ 44] Pulse: 228, Gap: 260, Period: 488 354 | [ 45] Pulse: 104, Gap: 138, Period: 242 355 | [ 46] Pulse: 108, Gap: 135, Period: 243 356 | [ 47] Pulse: 109, Gap: 137, Period: 246 357 | [ 48] Pulse: 229, Gap: 261, Period: 490 358 | [ 49] Pulse: 104, Gap: 139, Period: 243 359 | [ 50] Pulse: 229, Gap: 257, Period: 486 360 | [ 51] Pulse: 231, Gap: 258, Period: 489 361 | [ 52] Pulse: 230, Gap: 259, Period: 489 362 | [ 53] Pulse: 229, Gap: 262, Period: 491 363 | [ 54] Pulse: 227, Gap: 138, Period: 365 364 | [ 55] Pulse: 105, Gap: 138, Period: 243 365 | [ 56] Pulse: 107, Gap: 262, Period: 369 366 | [ 57] Pulse: 226, Gap: 142, Period: 368 367 | [ 58] Pulse: 102, Gap: 263, Period: 365 368 | [ 59] Pulse: 227, Gap: 139, Period: 366 369 | [ 60] Pulse: 104, Gap: 138, Period: 242 370 | [ 61] Pulse: 105, Gap: 139, Period: 244 371 | [ 62] Pulse: 107, Gap: 261, Period: 368 372 | [ 63] Pulse: 104, Gap: 136, Period: 240 373 | [ 64] Pulse: 109, Gap: 136, Period: 245 374 | [ 65] Pulse: 107, Gap: 138, Period: 245 375 | [ 66] Pulse: 105, Gap: 2501, Period: 2606 376 | 377 | ``` 378 | 379 | ### decoding sequence rtl_433 380 | 381 | 1. rtlsdr_callback() 382 | 2. Convert to magnitude and filter 383 | 3. pulse_detect_package() 384 | 4. pulse_demod_clock_bits() 385 | 5. hideki_ts04_callback() 386 | 387 | ## References 388 | 389 | * https://github.com/pimatic/rfcontroljs/issues/68 390 | * https://github.com/merbanan/rtl_433 391 | * http://www.hidekielectronics.com/?m=99 392 | * https://github.com/kevinmehall/rtlsdr-433m-sensor 393 | * https://stackoverflow.com/questions/13439718/how-to-interpret-numpy-correlate-and-numpy-corrcoef-values 394 | -------------------------------------------------------------------------------- /TS33C/docs/Decoding Description 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/Decoding Description 1.png -------------------------------------------------------------------------------- /TS33C/docs/Decoding Description 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/Decoding Description 2.png -------------------------------------------------------------------------------- /TS33C/docs/Decoding Description.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/Decoding Description.odg -------------------------------------------------------------------------------- /TS33C/docs/RXB8.fzpz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/RXB8.fzpz -------------------------------------------------------------------------------- /TS33C/docs/RXB8_Raspi.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/RXB8_Raspi.fzz -------------------------------------------------------------------------------- /TS33C/docs/RXB8_Raspi_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/RXB8_Raspi_bb.png -------------------------------------------------------------------------------- /TS33C/docs/RXB8_Raspi_schem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/RXB8_Raspi_schem.png -------------------------------------------------------------------------------- /TS33C/docs/fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/fft.png -------------------------------------------------------------------------------- /TS33C/docs/frames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/frames.png -------------------------------------------------------------------------------- /TS33C/docs/single_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/single_frame.png -------------------------------------------------------------------------------- /TS33C/docs/single_frame_zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/single_frame_zoom.png -------------------------------------------------------------------------------- /TS33C/docs/single_frame_zoom2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/single_frame_zoom2.png -------------------------------------------------------------------------------- /TS33C/docs/spectrum_waveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/spectrum_waveform.png -------------------------------------------------------------------------------- /TS33C/docs/spectrum_waveform_zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/spectrum_waveform_zoom.png -------------------------------------------------------------------------------- /TS33C/docs/spectrum_waveform_zoom2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/TS33C/docs/spectrum_waveform_zoom2.png -------------------------------------------------------------------------------- /TS33C/external_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import struct 6 | import timeit 7 | 8 | FILENAME = "TS33C_samples.dat" 9 | SAMPLERATE = 20000 # in Hz 10 | 11 | short_limit = 775 # maximum length of a short bit in µs 12 | reset_limit = 2*short_limit 13 | 14 | #long_limit = 200 15 | #short_limit = 130 16 | 17 | if __name__ == "__main__": 18 | rawData = [np.fromfile(FILENAME, dtype=np.byte)] 19 | if rawData[0].size > 0: 20 | # FIXME: handle high-levels at beginning of rawData 21 | 22 | # calculate edges from raw data; insert one at beginning for sync 23 | edges = np.insert(np.diff(rawData[0]),0,[0]) 24 | 25 | # position (offset) of edges 26 | edge_positions = np.ravel(np.nonzero(edges)) 27 | #print("edge_positions:", edge_positions) 28 | 29 | # lengths of pulses and pauses are our symbols, convert to µs 30 | symbols = np.diff(edge_positions)*1e6/SAMPLERATE 31 | print("symbols", symbols.size, symbols) 32 | 33 | # extract pulses 34 | pulses = symbols[0::2] 35 | print(np.histogram(pulses, bins=[0,short_limit,2*short_limit,100000])) 36 | 37 | # extract gaps; add long gap at the end for symmetry with pulses 38 | gaps = np.append(symbols[1::2],np.amax(symbols))*1e6/SAMPLERATE 39 | print(np.histogram(gaps, bins=[0,short_limit,2*short_limit,100000])) 40 | # set short gaps to zero as per decoding rule to remove them later 41 | # convert to µs 42 | gaps = np.where(gaps>short_limit,gaps,0) 43 | 44 | print("Pulses:", pulses.size,pulses) 45 | print("Gaps:", gaps.size, gaps) 46 | 47 | # test data 48 | #pulses = np.array([241, 231, 226, 102, 105, 104, 226, 106, 99, 104, 110, 104, 103, 103, 105, 228, 230, 105, 104, 106, 103, 103, 226, 225, 107, 226, 108, 106, 227, 106, 233, 107, 109, 104, 106, 229, 104, 108, 109, 108, 226, 226, 104, 107, 228, 104, 108, 109, 229, 104, 229, 231, 230, 229, 227, 105, 107, 226, 102, 227, 104, 105, 107, 104, 109, 107, 105]) 49 | #gaps = np.array([257, 261, 143, 140, 261, 140, 139, 145, 139, 137, 259, 143, 141, 138, 140, 258, 138, 139, 141, 139, 265, 139, 264, 260, 139, 138, 258, 141, 259, 134, 136, 137, 139, 260, 137, 140, 138, 134, 137, 260, 264, 140, 137, 261, 260, 138, 135, 137, 261, 139, 257, 258, 259, 262, 138, 138, 262, 142, 263, 139, 138, 139, 261, 136, 136, 138, 2501]) 50 | 51 | # combine pulses and gaps into one array like this [p, g, p, g, p, g] 52 | combined = np.ravel(np.dstack((pulses,gaps))) 53 | # remove short gaps 54 | combined = combined[np.where(combined>0)] 55 | print("combined:", combined.size,combined) 56 | 57 | # convert pulse/gap-width to bits as per decoding rule 58 | raw_bits = np.where(combined>short_limit,0,1) 59 | 60 | # pack raw bits into bytes 61 | packet_raw = np.packbits(raw_bits) 62 | 63 | # prepare final packet 64 | packet = np.empty(0, dtype=np.uint8) 65 | 66 | # for element in packet_raw.flat: 67 | #print("0x{:02x}".format(int('{:08b}'.format(element)[::-1], 2)^0xff)) 68 | #print(int('{:08b}'.format(element)[::-1],2)^0xff) 69 | # packet = np.append(packet, np.uint8(int('{:08b}'.format(element)[::-1],2)^0xff)) 70 | #print(i) 71 | #print(int('{:08b}'.format(0x06)[::-1], 2)^0xff) 72 | 73 | #start_time = timeit.default_timer() 74 | 75 | # FIXME: decode multiple packets as well 76 | for i in range(0, 10): 77 | # do some magic 78 | item = np.uint8(packet_raw[i+i/8] << (i%8)) 79 | item |= np.uint8(packet_raw[i+i/8+1] >> (8 - i%8)) 80 | # reverse and invert bits; add to packet 81 | packet = np.append(packet, np.uint8(int('{:08b}'.format(item)[::-1],2)^0xff)) 82 | #packet = np.append(packet, item) 83 | 84 | # switch MSB and LSB 85 | #packet = np.packbits(np.ravel(np.asarray(np.hsplit(np.unpackbits(packet), np.arange(8,np.unpackbits(packet).size,8)))[:,::-1])) 86 | # invert bits 87 | #packet ^= 0xFF 88 | #elapsed = timeit.default_timer() - start_time 89 | #print(elapsed) 90 | 91 | np.set_printoptions(formatter={'int':hex}) 92 | print("raw packet:", packet_raw.size, packet_raw) 93 | print("packet:", packet.size, packet) 94 | 95 | # check for correct header-byte 96 | if packet[0] == 0x9f: 97 | channel = (packet[1] >> 5) & 0x0F 98 | if channel >= 5: 99 | channel -= 1 100 | rc = packet[1] & 0x0F 101 | temp = (packet[5] & 0x0F) * 100 + ((packet[4] & 0xF0) >> 4) * 10 + (packet[4] & 0x0F) 102 | if ((packet[5]>>7) & 0x01) == 0: 103 | temp = -temp 104 | battery_ok = (packet[5]>>6) & 0x01 105 | humidity = ((packet[6] & 0xF0) >> 4) * 10 + (packet[6] & 0x0F) 106 | 107 | print("Channel:", channel) 108 | print("Rolling Code:", rc) 109 | print("Battery:", battery_ok) 110 | print("Temperature:", temp/10.) 111 | print("Humidity:", humidity) 112 | else: 113 | print("Unknown Header") 114 | -------------------------------------------------------------------------------- /TS33C/rxb8/decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ Decoder for Hideki TS33C Wireless Temperature/Humidity Sensor 4 | for Raspberry Pi with RXB8 Receiver 5 | """ 6 | import matplotlib 7 | # Force matplotlib to not use any Xwindows backend. 8 | matplotlib.use('Agg') 9 | 10 | import os 11 | import pigpio as gpio 12 | import numpy as np 13 | 14 | from time import sleep, time 15 | from datetime import datetime 16 | 17 | # performance and profiling 18 | import timeit 19 | 20 | # used for further processing of received data 21 | import matplotlib.pyplot as plt 22 | import paho.mqtt.client as mqtt 23 | import json 24 | 25 | SILENT = 0 26 | ERROR = 1 27 | INFO = 2 28 | TRACE = 3 29 | 30 | class RXB8_Decoder(object): 31 | """Decoder-Class for external Receiver""" 32 | def __init__(self, host="localhost", port=8888, debug_level=SILENT): 33 | self.debug_level = debug_level 34 | self.host = host 35 | self.port = port 36 | self.onDecode = None 37 | 38 | # set up pigpio connection 39 | self.pi = gpio.pi(self.host, self.port) 40 | self.pi.set_mode(27, gpio.OUTPUT) 41 | if not self.pi.connected: 42 | self.debug("Could not connected to " + self.host) 43 | exit() 44 | self.callback = None 45 | 46 | # initialize timestamp for further use 47 | self.start_tick = self.pi.get_current_tick() 48 | 49 | # control variables 50 | self.active = False # enables the decoder 51 | 52 | # symbol parameters 53 | self.pulse_short_limit = 775 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 54 | self.gap_short_limit = 775 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 55 | self.frame_gap = 2*self.gap_short_limit 56 | 57 | self.pulse_short = 460 58 | self.pulse_long = 989 59 | self.gap_short = 492 60 | self.gap_long = 998 61 | 62 | # receiver data 63 | self.edges = np.empty(0, dtype=np.uint8) 64 | self.edge_positions = np.empty(0, dtype=np.uint32) 65 | 66 | # decoding variables 67 | self.state = "idle" 68 | self.currentSymbols = np.empty(0, dtype=np.uint8) 69 | self.min_edges = 120 70 | 71 | # sensor data 72 | self.temperature = 0 73 | self.humidity = 0 74 | 75 | def __enter__(self): 76 | """Class can be used in with-statement""" 77 | return self 78 | 79 | def __exit__(self, exc_type, exc_value, traceback): 80 | """clean up stuff""" 81 | if self.callback: 82 | self.callback.cancel() 83 | self.pi.stop() 84 | 85 | def debug(self, message, level=0): 86 | """Debug output depending on debug level.""" 87 | if self.debug_level >= level: 88 | print message 89 | 90 | def write_png(self, filename, data, title=None): 91 | fig, ax = plt.subplots( nrows=1, ncols=1 ) # create figure & 1 axis 92 | fig.set_size_inches(16, 3) 93 | ax.set_title(title, fontsize=10) 94 | fig.text(0.5, 0, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 95 | fontsize=8, color='black', 96 | ha='center', va='bottom', alpha=0.4) 97 | ax.step(data[0], data[1], linewidth=1, where='post') 98 | ax.grid(True) 99 | xmin, xmax = plt.xlim() 100 | plt.xlim(0, xmax) 101 | plt.tight_layout() 102 | fig.savefig(filename) 103 | plt.close(fig) # close the figure 104 | 105 | def cbf(self, pin, level, tick): 106 | if level < 2: 107 | if self.start_tick < tick: 108 | self.edge_positions = np.append(self.edge_positions, gpio.tickDiff(self.start_tick, tick)) 109 | self.edges = np.append(self.edges, level) 110 | # watchdog event 111 | else: 112 | # self.debug("reset gap detected") 113 | self.edge_positions = np.append(self.edge_positions, gpio.tickDiff(self.start_tick, tick)) 114 | self.edges = np.append(self.edges, 4) # 4 = marker for watchdog 115 | self.decode() 116 | 117 | def decode(self): 118 | """Actual decoder""" 119 | # ignore packets with less than min_edges 120 | if self.edge_positions.size > self.min_edges and self.active: 121 | self.debug(("edge_positions:", self.edge_positions.size, self.edge_positions), TRACE) 122 | self.debug(("edges", self.edges.size, self.edges), TRACE) 123 | self.debug(("frame_gap:", self.frame_gap), TRACE) 124 | 125 | # lengths of pulses and pauses are our symbols 126 | symbols = np.diff(self.edge_positions) 127 | 128 | 129 | # split symbols at packet boundary 130 | #symbols = np.array(np.split(symbols, np.ravel(np.where(symbols > self.frame_gap))+1)) 131 | 132 | # self.start_pattern = 133 | 134 | self.debug(("Symbols:", symbols.size, symbols), TRACE) 135 | self.debug(("symbols.shape", symbols.shape[0]), TRACE) 136 | self.debug(("state", self.state), TRACE) 137 | 138 | pattern = np.array([ self.pulse_long, self.gap_long, self.pulse_long, self.gap_long, 139 | self.pulse_long, self.gap_short, self.pulse_short, self.gap_short, self.pulse_short, 140 | self.gap_long, self.pulse_short]) 141 | 142 | pattern = np.interp(np.linspace(0, np.sum(pattern), 250), np.cumsum(pattern), [1,0,1,0,1,0,1,0,1,0,1]) 143 | self.debug(("Pattern:", pattern.size, pattern), TRACE) 144 | 145 | #symbols = symbols[:-1:] 146 | symbols = symbols[np.where(symbols <= self.frame_gap)] 147 | mean = np.mean(symbols) 148 | symbols_normalised = symbols - mean 149 | pattern_normalised = pattern - mean 150 | res = np.correlate(symbols_normalised, pattern_normalised) 151 | index = np.argmax(res) 152 | print("max=", index) 153 | 154 | self.write_png('raw.png', [self.edge_positions/1000., self.edges]) 155 | self.write_png("correlate.png", [range(0,res.size), res]) 156 | self.write_png("symbols.png", [range(0,symbols.size), symbols]) 157 | # self.write_png("pattern.png", [range(0,pattern.size), pattern]) 158 | self.write_png("pattern.png", [np.cumsum(pattern), [1,0,1,0,1,0,1,0,1,0,1]]) 159 | self.write_png('filtered.png', [self.edge_positions[index::]/1000., self.edges[index::]]) 160 | 161 | # if symbols are split into 2+ arrays at packet boundary we 162 | # *might* have a valid frame 163 | if symbols.shape[0] > 1: 164 | self.state = "frame" 165 | 166 | # we process the symbols as one chunk 167 | for symbol_chunk in symbols: 168 | self.debug(("state", self.state), TRACE) 169 | if (symbols.size == 1) or self.state == "idle": 170 | # skip leading inter-frame gap 171 | symbol_chunk = symbol_chunk[1::] 172 | if symbol_chunk.size > 0: 173 | self.debug(("symbol_chunk:", symbol_chunk.size, symbol_chunk), TRACE) 174 | self.state = "frame" 175 | 176 | # add first symbol to last from previous to reassemble separated pulses/gaps 177 | 178 | """ 179 | if (self.currentSymbols.size > 0) and (self.previousSymbolType == self.currentSymbolType): 180 | self.currentSymbols[-1] += symbol_chunk[0] 181 | symbol_chunk = symbol_chunk[1::] 182 | """ 183 | 184 | self.currentSymbols = np.append(self.currentSymbols, symbol_chunk) 185 | 186 | if self.currentSymbols[0] > self.frame_gap: 187 | self.currentSymbols = self.currentSymbols[1::] 188 | 189 | self.debug(("currentSymbols:", self.currentSymbols.size, self.currentSymbols), TRACE) 190 | 191 | # complete packet is determined by long gap at the end 192 | if (self.currentSymbols.size > 1) and (self.currentSymbols[-1] > self.frame_gap): 193 | 194 | # extract pulses 195 | pulses = self.currentSymbols[0::2] 196 | 197 | # extract gaps 198 | gaps = self.currentSymbols[1::2] 199 | 200 | # remove packet pause 201 | gaps = np.where(gaps > self.frame_gap, 0, gaps) 202 | 203 | # calculate histogram to determine length of short and long pulses 204 | pulse_histogram = np.histogram(pulses, "auto") 205 | 206 | # calculate histogram to determine length of short and long gaps; leave out packet pause 207 | gap_histogram = np.histogram(gaps[np.where(gaps > 0)], "auto") 208 | 209 | # derive limits for short pulses/gaps from histogram. Use value in the middle between short and long 210 | self.pulse_short_limit = pulse_histogram[1][pulse_histogram[1].size/2] 211 | self.gap_short_limit = gap_histogram[1][gap_histogram[1].size/2] 212 | 213 | self.debug(("Pulses:", pulses.size, pulses), TRACE) 214 | self.debug(("Pulse Histogram", pulse_histogram), TRACE) 215 | self.debug(("pulse_short_limit", self.pulse_short_limit), TRACE) 216 | self.debug(("Gaps:", gaps.size, gaps), TRACE) 217 | self.debug(("Gaps Histogram", gap_histogram), TRACE) 218 | self.debug(("gap_short_limit", self.gap_short_limit), TRACE) 219 | 220 | # interleave pulses and gaps into one array like this [p, g, p, g, p, g] 221 | combined = np.empty(pulses.size + gaps.size, dtype=pulses.dtype) 222 | combined[0::2] = pulses 223 | combined[1::2] = gaps 224 | 225 | self.debug(("combined:", combined.size, combined), TRACE) 226 | 227 | # convert pulse/gap-width to bits as per decoding rule 228 | rawBits = np.empty(combined.size, dtype=np.uint8) 229 | self.debug(("rawBits.size", rawBits.size), TRACE) 230 | rawBits[0::2] = np.where(combined[0::2] < self.pulse_short_limit, 1, 0) 231 | rawBits[1::2] = np.where(combined[1::2] < self.gap_short_limit, 255, 0) 232 | 233 | # remove short gaps marked as 255 above 234 | rawBits = rawBits[np.where(rawBits <= 1)] 235 | self.debug(("rawBits:", rawBits.size, rawBits), TRACE) 236 | 237 | # prepare boolean mask for parity bits (every 9th bit) 238 | parityMask = np.mod(np.arange(rawBits.size)+1, 9) == 0 239 | 240 | # extract parity bits as boolean array 241 | parityBits = np.extract(parityMask, rawBits) == 1 242 | # extract packet bits as boolean array 243 | packetBits = np.extract(np.invert(parityMask), rawBits) == 1 244 | 245 | # make sure we have whole bytes (8 bit) 246 | if (packetBits.size % 8) == 0: 247 | # swap MSB and LSB, invert Bits, pack 8 bits into one byte, reverse order to compensate for 1st flip 248 | packet = np.packbits(np.invert(packetBits[::-1]))[::-1] 249 | 250 | np.set_printoptions(formatter={'int':hex}) 251 | self.debug(("packet:", packet.size, packet), INFO) 252 | # calculate parity of each byte by splitting the bits into reversed and inverted 8-bit parts and 253 | # calculate the sum of set bits; 254 | packetParity = (np.sum(np.split(np.invert(packetBits[::-1]), np.arange(8, packetBits.size, 8)), axis=1)[::-1] % 2) == 0 255 | 256 | self.debug(("packetParity:", packetParity.size, packetParity), TRACE) 257 | np.set_printoptions(formatter=None) 258 | 259 | # check parity of packet 260 | if np.array_equal(packetParity, parityBits): 261 | # check for correct sensor type and header-byte 262 | if (packet.size == 10) and (packet[0] == 0x9f): 263 | #extract values from packet 264 | channel = (packet[1] >> 5) & 0x0F 265 | if channel >= 5: 266 | channel -= 1 267 | rollingCode = packet[1] & 0x0F 268 | temp = (packet[5] & 0x0F) * 100 + ((packet[4] & 0xF0) >> 4) * 10 + (packet[4] & 0x0F) 269 | sign = (((packet[5]>>7) & 0x01) * 2) - 1 270 | self.temperature = temp*sign/10. 271 | battery_ok = (packet[5]>>6) & 0x01 == 1 272 | self.humidity = ((packet[6] & 0xF0) >> 4) * 10 + (packet[6] & 0x0F) 273 | 274 | self.debug(("Channel:", channel), INFO) 275 | self.debug(("Rolling Code:", rollingCode), INFO) 276 | self.debug(("Battery ok:", battery_ok), INFO) 277 | self.debug("Temperature: %02.1f°C" % self.temperature, INFO) 278 | self.debug("Humidity: %02i%%" % self.humidity, INFO) 279 | 280 | self.pi.write(27, 1) 281 | sleep(.001) 282 | self.pi.write(27, 0) 283 | 284 | # create plot of current packet 285 | self.write_png('{}_pass.png'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), [self.edge_positions/1000., self.edges], u"Temp={}, Hum={}".format(self.temperature, self.humidity)) 286 | 287 | if self.onDecode: 288 | self.onDecode(json.dumps({'timestamp': int(time()), 'value': self.temperature, 'unit': '°C'})) 289 | #self.active = False 290 | else: 291 | self.debug("Unknown Sensor or Header", ERROR) 292 | else: 293 | self.debug("Parity check failed", ERROR) 294 | self.write_png('{}_parity_error.png'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), [self.edge_positions/1000., self.edges], u"parity error") 295 | 296 | else: 297 | self.debug("Invalid packet length", ERROR) 298 | #self.write_png('{}_packet_length.png'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), [self.edge_positions/1000., self.edges], u"Invalid packet length") 299 | # done with this packet 300 | self.currentSymbols = np.empty(0, dtype=np.uint8) 301 | else: 302 | self.debug("Waiting for more data", TRACE) 303 | else: 304 | self.debug("skipping empty chunk", INFO) 305 | self.state = "idle" 306 | else: 307 | #self.debug("discarding invalid rf data", TRACE) 308 | pass 309 | 310 | # reset buffer 311 | self.start_tick = self.pi.get_current_tick() 312 | self.edges = np.empty(0, dtype=np.uint8) 313 | self.edge_positions = np.empty(0, dtype=np.uint32) 314 | 315 | def run(self, pin=17, glitch_filter=150, frame_gap=3100, onDecode=None): 316 | # callback after successful decode 317 | self.onDecode=onDecode 318 | 319 | # filter high frequency noise 320 | self.pi.set_glitch_filter(pin, glitch_filter) 321 | 322 | # set timespan (in µs) between frames 323 | self.frame_gap = frame_gap 324 | 325 | # detect frame gap to try decoding of received data 326 | self.pi.set_watchdog(pin, int(self.frame_gap/1000)) 327 | # watch pin 328 | self.callback = self.pi.callback(pin, gpio.EITHER_EDGE, self.cbf) 329 | 330 | # wait for something to happen, forever... 331 | self.active = True 332 | while self.active: 333 | sleep(.1) 334 | 335 | class Mqtt(object): 336 | def __init__(self, host="localhost", debug_level=SILENT): 337 | self.debug_level = debug_level 338 | self.host = host 339 | self.connected = False 340 | 341 | self.client = mqtt.Client('raspi-%s' % os.getpid()) 342 | self.client.on_connect = self.on_connect 343 | 344 | self.client.connect(self.host) 345 | self.client.loop_start() 346 | 347 | def __enter__(self): 348 | """Class can be used in with-statement""" 349 | return self 350 | 351 | def __exit__(self, exc_type, exc_value, traceback): 352 | self.client.loop_stop() 353 | self.client.disconnect() 354 | 355 | def debug(self, message, level=0): 356 | """Debug output depending on debug level.""" 357 | if self.debug_level >= level: 358 | print message 359 | 360 | def publish(self, json_data): 361 | if self.connected: 362 | self.client.publish("home/test/rxb8", json_data, retain=False) 363 | 364 | def on_connect(self, client, userdata, flags, rc): 365 | self.debug(("Connected to mqtt broker:", self.host), TRACE) 366 | self.connected = True 367 | 368 | def main(): 369 | """ main function """ 370 | # set up decoder and mqtt-connection 371 | with RXB8_Decoder(host="rfpi", debug_level=TRACE) as decoder: 372 | with Mqtt(host="osmc", debug_level=SILENT) as mqtt_client: 373 | try: 374 | decoder.run(pin=17, glitch_filter=150, frame_gap=20000, onDecode=mqtt_client.publish) 375 | except KeyboardInterrupt: 376 | print "cancel" 377 | 378 | if __name__ == "__main__": 379 | main() -------------------------------------------------------------------------------- /TS33C/rxb8/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """OOK-Decoder""" 4 | 5 | from time import sleep 6 | import pigpio as gpio 7 | from lib.rfm69 import Rfm69 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from mpl_toolkits.axes_grid1 import make_axes_locatable 11 | 12 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 13 | RESET = 24 14 | DATA = 25 15 | 16 | # define pigpio-host 17 | HOST = "rfpi" 18 | 19 | start_tick = 0 20 | state = 0 # 0=Idle, 1=Frame 21 | bits = np.empty(0, dtype=np.uint8) 22 | hw_sync = np.empty(0) 23 | tolerance = 100 #µs 24 | clock = 640 25 | 26 | histogram = [0, np.zeros(56)] 27 | 28 | def cbf(pin, level, tick): 29 | global start_tick 30 | global state 31 | global bits 32 | global hw_sync 33 | global clock 34 | global histogram 35 | 36 | # End of Gap 37 | if level == 1: 38 | delta = gpio.tickDiff(start_tick, tick) 39 | start_tick = tick 40 | # HW-Sync 41 | if (0 <= state <=2) and (delta in range(2500-2*tolerance, 2500+2*tolerance)): 42 | if state < 2: 43 | hw_sync = np.empty(0) 44 | hw_sync = np.append(hw_sync, [delta]) 45 | state = 2 46 | # long gap 47 | elif (state == 3) and (delta in range(2*clock-tolerance, 2*clock+tolerance)): 48 | bits = np.append(bits, [0, 0]) 49 | # short gap 50 | elif (state == 3) and (delta in range(clock-tolerance, clock+tolerance)): 51 | bits = np.append(bits, [0]) 52 | else: 53 | pass 54 | 55 | # End of Pulse 56 | elif level == 0: 57 | delta = gpio.tickDiff(start_tick, tick) 58 | start_tick = tick 59 | # wake-up pulse 60 | if (state == 0) and (delta in range(10050-tolerance, 10050+tolerance)): 61 | state = 1 62 | # HW-Sync 63 | elif (0 <= state <=2) and (delta in range(2500-2*tolerance, 2500+2*tolerance)): 64 | if state < 2: 65 | hw_sync = np.empty(0) 66 | hw_sync = np.append(hw_sync, [delta]) 67 | state = 2 68 | # start of frame mark 69 | elif (state == 2) and (delta in range(4850-2*tolerance, 4850+2*tolerance)): 70 | clock = int(np.average(hw_sync)/4) 71 | print "Clock Sync:", hw_sync, clock 72 | bits = np.empty(0, dtype=np.uint8) 73 | state = 3 74 | # long pulse 75 | elif (state == 3) and (delta in range(2*clock-tolerance, 2*clock+tolerance)): 76 | bits = np.append(bits, [1, 1]) 77 | # short pulse 78 | elif (state == 3) and (delta in range(clock-tolerance, clock+tolerance)): 79 | bits = np.append(bits, [1]) 80 | else: 81 | pass 82 | 83 | # Watchdog timeout 84 | elif (level == 2) and (state > 0): 85 | # skip first bit, because it is part of the start of frame mark 86 | bits = bits[1::] 87 | 88 | # append one zero-bit, in case the last bit was a one and the last zero-bit can't be detected, because the frame is over 89 | if bits.size < 112: 90 | bits = np.append(bits, [0]) 91 | if bits.size == 112: 92 | # decode manchester (rising edge = 1, falling edge = 0) 93 | decoded = np.ravel(np.where(np.reshape(bits, (-1, 2)) == [0, 1], 1, 0))[::2] 94 | 95 | histogram[1] = histogram[1] + decoded 96 | histogram[0] += 1 97 | 98 | frame = np.packbits(decoded) 99 | print "Raw: "+''.join('0x{:02X} '.format(x) for x in frame) 100 | 101 | for i in range(frame.size-1, 0, -1): 102 | frame[i] = frame[i] ^ frame[i-1] 103 | 104 | cksum = frame[0] ^ (frame[0] >> 4) 105 | for i in range(1,7): 106 | cksum = cksum ^ frame[i] ^ (frame[i] >> 4) 107 | cksum = cksum & 0x0f 108 | 109 | print "Frame: "+''.join('0x{:02X} '.format(x) for x in frame) 110 | print " Control: 0x{:02X}".format((frame[1] >> 4) & 0x0f) 111 | print " Checksum: {}".format("ok" if cksum==0 else "error") 112 | print " Address: "+''.join('{:02X} '.format(x) for x in frame[4:7]) 113 | print " Rolling Code: "+''.join('{:02X} '.format(x) for x in frame[2:4]) 114 | else: 115 | pass 116 | bits = np.empty(0, dtype=np.uint8) 117 | state = 0 118 | else: 119 | pass 120 | 121 | def main(): 122 | """ main function """ 123 | pi = gpio.pi(host=HOST) 124 | if not pi.connected: 125 | exit() 126 | pi.set_mode(RESET, gpio.OUTPUT) 127 | pi.set_mode(DATA, gpio.OUTPUT) 128 | pi.set_pull_up_down(DATA, gpio.PUD_DOWN) 129 | pi.write(DATA, 0) 130 | 131 | pi.write(RESET, 1) 132 | pi.write(RESET, 0) 133 | 134 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 135 | 136 | # just to make sure SPI is working 137 | rx_data = rf.read_single(0x5A) 138 | if rx_data != 0x55: 139 | print "SPI Error" 140 | 141 | # configure 142 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 143 | 144 | rf.write_burst(0x07, [0x6C, 0x9A, 0x00]) # Frf: Carrier Frequency 434.42MHz/61.035 145 | 146 | # rf.write_single(0x18, 0b00000000) # Lna: 50 Ohm, auto gain 147 | 148 | # rf.write_single(0x19, 0b01001001) # RxBw: 4% DCC, BW=100kHz 149 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 150 | 151 | # make sure we conserve the thesold setting in between WUP and HW-Sync 152 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 153 | # rf.write_single(0x1D, 50) # OokFix 154 | 155 | # Receive 156 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 157 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 158 | 159 | # wait until RFM-Module is ready 160 | while (rf.read_single(0x27) & 0x80) == 0: 161 | print "waiting..." 162 | 163 | # filter high frequency noise 164 | pi.set_glitch_filter(DATA, 150) 165 | 166 | # watchdog to detect end of frame (20ms) 167 | pi.set_watchdog(DATA, 20) 168 | 169 | # watch DATA pin 170 | callback = pi.callback(DATA, gpio.EITHER_EDGE, cbf) 171 | 172 | print "Scanning... Press Ctrl-C to abort" 173 | while 1: 174 | sleep(1) 175 | 176 | pi.stop() 177 | 178 | def show_histogram(matrix, normalize=1): 179 | fig, ax = plt.subplots() 180 | matrix = matrix/normalize 181 | img1 = ax.imshow(matrix) 182 | divider = make_axes_locatable(ax) 183 | cax = divider.append_axes("right", size="5%", pad=0.1) 184 | fig.colorbar(img1, cax=cax) 185 | plt.show() 186 | 187 | if __name__ == "__main__": 188 | try: 189 | main() 190 | except KeyboardInterrupt: 191 | print "" 192 | finally: 193 | print "done" 194 | if histogram[0] > 0: 195 | show_histogram(np.reshape(histogram[1], (-1, 8)), normalize=histogram[0]) 196 | -------------------------------------------------------------------------------- /TS33C/rxb8/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ Decoder for Hideki TS33C Wireless Temperature/Humidity Sensor 4 | for Raspberry Pi with RXB8 Receiver 5 | """ 6 | import matplotlib 7 | # Force matplotlib to not use any Xwindows backend. 8 | #matplotlib.use('Agg') 9 | 10 | import os 11 | import numpy as np 12 | 13 | from time import sleep, time 14 | from datetime import datetime 15 | 16 | # used for further processing of received data 17 | import matplotlib.pyplot as plt 18 | 19 | SILENT = 0 20 | ERROR = 1 21 | INFO = 2 22 | TRACE = 3 23 | 24 | 25 | def write_png(filename, data, title=None): 26 | fig, ax = plt.subplots( nrows=1, ncols=1 ) # create figure & 1 axis 27 | fig.set_size_inches(16, 3) 28 | ax.set_title(title, fontsize=10) 29 | fig.text(0.5, 0, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 30 | fontsize=8, color='black', 31 | ha='center', va='bottom', alpha=0.4) 32 | ax.step(data[0], data[1], linewidth=1, where='post') 33 | # ax.plot(data[0], data[1], linewidth=1) 34 | ax.grid(True) 35 | plt.tight_layout() 36 | # plt.show() 37 | fig.savefig(filename) 38 | #plt.close(fig) # close the figure 39 | 40 | def main(): 41 | """ main function """ 42 | edge_positions = np.array([ 229, 419, 19990, 20189, 23975, 24229, 30154, 30374, 43 | 34669, 34834, 54310, 55490, 56295, 57345, 58250, 59285, 44 | 59700, 60280, 60695, 61250, 62160, 62700, 63135, 64220, 45 | 64605, 65120, 65575, 66135, 66555, 67110, 67530, 68100, 46 | 69005, 69525, 69975, 70500, 70955, 71465, 71925, 72435, 47 | 72905, 73905, 74875, 75845, 76325, 76825, 77305, 77805, 48 | 78275, 78795, 79255, 79740, 80720, 81225, 81705, 82695, 49 | 83645, 84645, 85605, 86080, 86585, 87560, 88050, 88530, 50 | 89520, 90485, 90980, 91460, 91965, 92435, 93420, 94395, 51 | 94890, 95375, 95875, 96330, 96840, 97315, 98310, 98780, 52 | 99275, 100240, 100740, 101220, 101720, 102190, 102695, 103185, 53 | 103680, 104140, 105145, 106095, 107095, 108065, 108560, 109040, 54 | 109545, 110000, 110515, 110985, 111980, 112935, 113440, 113905, 55 | 114420, 114885, 115885, 116835, 117835, 118295, 118820, 119765, 56 | 120770, 121720, 122730, 123665, 124680, 125125, 125655, 126105, 57 | 126645, 127080, 127620, 128070, 128585, 129055, 129560, 130495, 58 | 131520, 131965, 132490, 132945, 133465, 134405, 134935, 135385, 59 | 135905, 136370, 136890, 137335, 138350, 139285, 139815, 140260, 60 | 141280, 141720, 162020]) 61 | edges = np.array([1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 62 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 63 | 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 64 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 65 | 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 66 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 67 | 1, 0, 1, 0, 1, 0, 1, 0, 4]) 68 | symbols = np.array([ 190, 19571, 199, 3786, 254, 5925, 220, 4295, 165, 69 | 19476, 1180, 805, 1050, 905, 1035, 415, 580, 415, 70 | 555, 910, 540, 435, 1085, 385, 515, 455, 560, 71 | 420, 555, 420, 570, 905, 520, 450, 525, 455, 72 | 510, 460, 510, 470, 1000, 970, 970, 480, 500, 73 | 480, 500, 470, 520, 460, 485, 980, 505, 480, 74 | 990, 950, 1000, 960, 475, 505, 975, 490, 480, 75 | 990, 965, 495, 480, 505, 470, 985, 975, 495, 76 | 485, 500, 455, 510, 475, 995, 470, 495, 965, 77 | 500, 480, 500, 470, 505, 490, 495, 460, 1005, 78 | 950, 1000, 970, 495, 480, 505, 455, 515, 470, 79 | 995, 955, 505, 465, 515, 465, 1000, 950, 1000, 80 | 460, 525, 945, 1005, 950, 1010, 935, 1015, 445, 81 | 530, 450, 540, 435, 540, 450, 515, 470, 505, 82 | 935, 1025, 445, 525, 455, 520, 940, 530, 450, 83 | 520, 465, 520, 445, 1015, 935, 530, 445, 1020, 84 | 440, 20300]) 85 | """ 86 | array([ 190, 19571, 199, 3786, 254, 5925, 220, 4295, 165, 87 | 19476, 1180, 805, 1050, 905, 1035, 415, 580, 415, 88 | 555, 910, 540, 435, 1085, 385, 515, 455, 560, 89 | 420, 555, 420, 570, 905, 520, 450, 525, 455, 90 | 510, 460, 510, 470, 1000, 970, 970, 480, 500, 91 | 480, 500, 470, 520, 460, 485, 980, 505, 480, 92 | 990, 950, 1000, 960, 475, 505, 975, 490, 480, 93 | 990, 965, 495, 480, 505, 470, 985, 975, 495, 94 | 485, 500, 455, 510, 475, 995, 470, 495, 965, 95 | 500, 480, 500, 470, 505, 490, 495, 460, 1005, 96 | 950, 1000, 970, 495, 480, 505, 455, 515, 470, 97 | 995, 955, 505, 465, 515, 465, 1000, 950, 1000, 98 | 460, 525, 945, 1005, 950, 1010, 935, 1015, 445, 99 | 530, 450, 540, 435, 540, 450, 515, 470, 505, 100 | 935, 1025, 445, 525, 455, 520, 940, 530, 450, 101 | 520, 465, 520, 445, 1015, 935, 530, 445, 1020, 102 | 440, 20300]) 103 | """ 104 | pulse_short = 460 105 | pulse_long = 989 106 | gap_short = 492 107 | gap_long = 998 108 | """ 109 | pattern = np.array([ 500, 500, 500, 500, 500, 500, 500, 500, 500]) 110 | pattern2 = np.array([ 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500]) 111 | pattern = np.array([ 1028, 936, 1074, 862, 1122, 366, 510, 464, 522]) 112 | 113 | symbols = np.array([1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,]) 114 | zeros = np.array([0, 0, 0, 0, 0, 0]) 115 | ones = np.array([1, 1, 1, 1, 1]) 116 | pattern = np.array([0, 0, 0, 1, 0, 0]) 117 | """ 118 | 119 | #random = np.random.uniform(0,6000,size=100) 120 | #symbols = np.concatenate( (random, symbols, 6000-random) ) 121 | 122 | #pattern = symbols[40:60] 123 | pattern = np.array([ pulse_long, gap_long, pulse_long, gap_long, pulse_long, gap_short, pulse_short, gap_short, pulse_short, gap_long, pulse_short]) 124 | write_png("pattern.png", [np.cumsum(pattern), [1,0,1,0,1,0,1,0,1,0,1]]) 125 | 126 | interp = np.interp(np.linspace(0, np.sum(pattern), 250), np.cumsum(pattern), [1,0,1,0,1,0,1,0,1,0,1]) 127 | write_png("interp.png", [range(0,interp.size), interp]) 128 | 129 | symbols_normalised = symbols - np.mean(symbols) 130 | pattern_normalised = pattern - np.mean(symbols) 131 | res = np.correlate(symbols_normalised, pattern_normalised) 132 | index = np.argmax(res) 133 | print("max=", index) 134 | 135 | write_png("correlate.png", [range(0,res.size), res], title=None) 136 | write_png("symbols.png", [range(0,symbols.size), symbols], title=None) 137 | 138 | if __name__ == "__main__": 139 | main() -------------------------------------------------------------------------------- /TS33C/rxb8/trigger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pigpio as gpio 5 | from time import sleep 6 | 7 | pi = None 8 | 9 | def cbf(pin, level, tick): 10 | global pi 11 | if level < 2: 12 | pass 13 | else: 14 | pi.write(27, 1) 15 | sleep(.01) 16 | pi.write(27, 0) 17 | 18 | def main(): 19 | """ main function """ 20 | global pi 21 | pi = gpio.pi(host="rfpi", port=8888) 22 | if not pi.connected: 23 | exit() 24 | 25 | pin = 17 26 | 27 | pi.set_mode(27, gpio.OUTPUT) 28 | 29 | # initialize timestamp for further use 30 | start_tick = pi.get_current_tick() 31 | 32 | pi.set_glitch_filter(pin, 300) 33 | 34 | # detect frame gap to try decoding of received data 35 | pi.set_watchdog(pin, 52) 36 | 37 | # watch pin 38 | callback = pi.callback(pin, gpio.EITHER_EDGE, cbf) 39 | 40 | # wait for something to happen, forever... 41 | active = True 42 | 43 | try: 44 | while active: 45 | sleep(1) 46 | except KeyboardInterrupt: 47 | print "cancel" 48 | 49 | if __name__ == "__main__": 50 | main() -------------------------------------------------------------------------------- /TS33C/stream_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Live decoder for Hideki TS33C Wireless Temperature/Humidity Sensor""" 4 | 5 | import numpy as np 6 | 7 | FILENAME = "TS33C_samples.dat" 8 | 9 | ERROR = 1 10 | INFO = 2 11 | TRACE = 3 12 | 13 | class StreamDecoder(object): 14 | """TS33C Decoder Class""" 15 | # pylint: disable=too-many-instance-attributes, C0301, C0103 16 | 17 | def __init__(self, sample_rate=20000, debug_level=0): 18 | self.debug_level = debug_level 19 | self.sample_rate = sample_rate 20 | self.state = "idle" 21 | 22 | self.chunk_id = 0 23 | self.pulse_short_limit = 775 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 24 | self.gap_short_limit = 775 # default value for maximum length of a short bit in µs (valid for optimal rx quality) 25 | self.reset_limit = 2*self.gap_short_limit 26 | 27 | self.currentSymbols = np.empty(0, dtype=np.uint8) 28 | self.rawBuffer = np.empty(0, dtype=np.uint8) 29 | self.currentSymbolType = 0 30 | self.previousSymbolType = 0 31 | self.temperature = 0 32 | self.humidity = 0 33 | 34 | def debug(self, message, level=0): 35 | """Debug output depending on debug level.""" 36 | if self.debug_level >= level: 37 | print message 38 | 39 | def work(self, input_items): 40 | """The actual Decoder.""" 41 | # pylint: disable=too-many-nested-blocks, too-many-statements, R0914, R0912, C0301 42 | 43 | rawChunk = input_items[0] 44 | self.rawBuffer = np.append(self.rawBuffer, rawChunk) 45 | 46 | # we need at least as many samples as is defined by reset_limit 47 | if self.rawBuffer.size*1e6/self.sample_rate > self.reset_limit: 48 | 49 | # skip empty or zero-only chunks 50 | if (self.rawBuffer.size > 0) and ((np.sum(self.rawBuffer) > 0) or (self.currentSymbols.size > 0)): 51 | 52 | self.currentSymbolType = self.rawBuffer[0] 53 | self.debug(("first raw sample", self.rawBuffer[0]), TRACE) 54 | self.debug(("last raw sample", self.rawBuffer[-1]), TRACE) 55 | 56 | # force edge at beginning an end of rawBuffer 57 | self.rawBuffer = np.insert(self.rawBuffer, 0, [1 - self.rawBuffer[0]]) 58 | self.rawBuffer = np.append(self.rawBuffer, [1 - self.rawBuffer[-1]]) 59 | 60 | self.chunk_id += 1 61 | self.debug(("Chunk", self.chunk_id), TRACE) 62 | #self.debug("rawBuffer", rawBuffer.size, rawBuffer) 63 | 64 | # calculate edges from raw data; insert one at beginning for sync 65 | edges = np.insert(np.diff(self.rawBuffer), 0, [0]) 66 | # position (offset) of edges 67 | edge_positions = np.ravel(np.nonzero(edges)) 68 | self.debug(("edge_positions:", edge_positions), TRACE) 69 | 70 | self.debug(("edges", edges[np.nonzero(edges)]), TRACE) 71 | 72 | if edge_positions.size > 0: 73 | # lengths of pulses and pauses are our symbols, convert to µs 74 | symbols = np.diff(edge_positions)*1e6/self.sample_rate 75 | 76 | # split symbols at packet boundary 77 | symbols = np.array(np.split(symbols, np.ravel(np.where(symbols > self.reset_limit))+1)) 78 | self.debug(("Symbols:", symbols.size, symbols), TRACE) 79 | self.debug(("symbols.shape", symbols.shape[0]), TRACE) 80 | self.debug(("state", self.state), TRACE) 81 | 82 | # if symbols are split into 2+ arrays at packet boundary we 83 | # have a valid frame 84 | if symbols.shape[0] > 1: 85 | self.state = "frame" 86 | 87 | # process all parts separately 88 | for symbol_chunk in symbols: 89 | self.debug(("state", self.state), TRACE) 90 | if (symbols.size == 1) or self.state == "idle": 91 | # skip leading inter-frame gap 92 | symbol_chunk = symbol_chunk[1::] 93 | if symbol_chunk.size > 0: 94 | self.debug(("symbol_chunk:", symbol_chunk.size, symbol_chunk), TRACE) 95 | self.state = "frame" 96 | 97 | # add first symbol to last from previous to reassemble separated pulses/gaps 98 | if (self.currentSymbols.size > 0) and (self.previousSymbolType == self.currentSymbolType): 99 | self.currentSymbols[-1] += symbol_chunk[0] 100 | symbol_chunk = symbol_chunk[1::] 101 | 102 | self.currentSymbols = np.append(self.currentSymbols, symbol_chunk) 103 | 104 | if self.currentSymbols[0] > self.reset_limit: 105 | self.currentSymbols = self.currentSymbols[1::] 106 | 107 | self.debug(("currentSymbols:", self.currentSymbols.size, self.currentSymbols), TRACE) 108 | 109 | # complete packet is determined by long gap at the end 110 | if (self.currentSymbols.size > 1) and (self.currentSymbols[-1] > self.reset_limit): 111 | 112 | # extract pulses 113 | pulses = self.currentSymbols[0::2] 114 | 115 | # extract gaps 116 | gaps = self.currentSymbols[1::2] 117 | 118 | # remove packet pause 119 | gaps = np.where(gaps > self.reset_limit, 0, gaps) 120 | 121 | # calculate histogram to determine length of short and long pulses 122 | pulse_histogram = np.histogram(pulses, "auto") 123 | 124 | # calculate histogram to determine length of short and long gaps; leave out packet pause 125 | gap_histogram = np.histogram(gaps[np.where(gaps > 0)], "auto") 126 | 127 | # derive limits for short pulses/gaps from histogram. Use value in the middle between short and long 128 | self.pulse_short_limit = pulse_histogram[1][pulse_histogram[1].size/2] 129 | self.gap_short_limit = gap_histogram[1][gap_histogram[1].size/2] 130 | 131 | self.debug(("Pulses:", pulses.size, pulses), TRACE) 132 | self.debug(("Pulse Histogram", pulse_histogram), TRACE) 133 | self.debug(("pulse_short_limit", self.pulse_short_limit), TRACE) 134 | self.debug(("Gaps:", gaps.size, gaps), TRACE) 135 | self.debug(("Gaps Histogram", gap_histogram), TRACE) 136 | self.debug(("gap_short_limit", self.gap_short_limit), TRACE) 137 | 138 | # interleave pulses and gaps into one array like this [p, g, p, g, p, g] 139 | combined = np.empty(pulses.size + gaps.size, dtype=pulses.dtype) 140 | combined[0::2] = pulses 141 | combined[1::2] = gaps 142 | 143 | self.debug(("combined:", combined.size, combined), TRACE) 144 | 145 | # convert pulse/gap-width to bits as per decoding rule 146 | rawBits = np.empty(combined.size, dtype=np.uint8) 147 | self.debug(("rawBits.size", rawBits.size), TRACE) 148 | rawBits[0::2] = np.where(combined[0::2] < self.pulse_short_limit, 1, 0) 149 | rawBits[1::2] = np.where(combined[1::2] < self.gap_short_limit, 255, 0) 150 | 151 | # remove short gaps marked as 255 above 152 | rawBits = rawBits[np.where(rawBits <= 1)] 153 | self.debug(("rawBits:", rawBits.size, rawBits), TRACE) 154 | 155 | # prepare boolean mask for parity bits (every 9th bit) 156 | parityMask = np.mod(np.arange(rawBits.size)+1, 9) == 0 157 | 158 | # extract parity bits as boolean array 159 | parityBits = np.extract(parityMask, rawBits) == 1 160 | # extract packet bits as boolean array 161 | packetBits = np.extract(np.invert(parityMask), rawBits) == 1 162 | 163 | # make sure we have whole bytes (8 bit) 164 | if (packetBits.size % 8) == 0: 165 | # swap MSB and LSB, invert Bits, pack 8 bits into one byte, reverse order to compensate for 1st flip 166 | packet = np.packbits(np.invert(packetBits[::-1]))[::-1] 167 | 168 | np.set_printoptions(formatter={'int':hex}) 169 | self.debug(("packet:", packet.size, packet), INFO) 170 | # calculate parity of each byte by splitting the bits into reversed and inverted 8-bit parts and 171 | # calculate the sum of set bits; 172 | packetParity = (np.sum(np.split(np.invert(packetBits[::-1]), np.arange(8, packetBits.size, 8)), axis=1)[::-1] % 2) == 0 173 | 174 | self.debug(("packetParity:", packetParity.size, packetParity), TRACE) 175 | np.set_printoptions(formatter=None) 176 | 177 | # check parity of packet 178 | if np.array_equal(packetParity, parityBits): 179 | # check for correct sensor type and header-byte 180 | if (packet.size == 10) and (packet[0] == 0x9f): 181 | #extract values from packet 182 | channel = (packet[1] >> 5) & 0x0F 183 | if channel >= 5: 184 | channel -= 1 185 | rollingCode = packet[1] & 0x0F 186 | temp = (packet[5] & 0x0F) * 100 + ((packet[4] & 0xF0) >> 4) * 10 + (packet[4] & 0x0F) 187 | sign = (((packet[5]>>7) & 0x01) * 2) - 1 188 | self.temperature = temp*sign/10. 189 | battery_ok = (packet[5]>>6) & 0x01 == 1 190 | self.humidity = ((packet[6] & 0xF0) >> 4) * 10 + (packet[6] & 0x0F) 191 | 192 | self.debug(("Channel:", channel), INFO) 193 | self.debug(("Rolling Code:", rollingCode), INFO) 194 | self.debug(("Battery ok:", battery_ok), INFO) 195 | print "Temperature: %02.1f°C" % self.temperature 196 | print "Humidity: %02i%%" % self.humidity 197 | else: 198 | self.debug("Unknown Sensor or Header", ERROR) 199 | else: 200 | self.debug("Parity check failed", ERROR) 201 | else: 202 | self.debug("Invalid packet length", ERROR) 203 | # done with this packet 204 | self.currentSymbols = np.empty(0, dtype=np.uint8) 205 | else: 206 | self.debug("Waiting for more data", TRACE) 207 | else: 208 | self.debug("skipping empty chunk", INFO) 209 | self.state = "idle" 210 | else: 211 | self.debug("no edges in chunk", INFO) 212 | self.previousSymbolType = self.rawBuffer[-2] 213 | else: 214 | #self.debug("Chunk is empty or contains no edges") 215 | self.state = "idle" 216 | # all done, delete current rawBuffer 217 | self.rawBuffer = np.empty(0, dtype=np.uint8) 218 | else: 219 | # wait for more samples 220 | self.debug("filling rawBuffer", TRACE) 221 | 222 | def main(): 223 | """ main function """ 224 | # set up decoder 225 | decoder = StreamDecoder(debug_level=TRACE) 226 | 227 | # load raw binary data from file 228 | raw_data = np.ravel(np.fromfile(FILENAME, dtype=np.int8)) 229 | 230 | # simulate streaming of input data 231 | raw_stream = np.array_split(raw_data, 100) 232 | 233 | for chunk in raw_stream: 234 | # feed chunks to decoder 235 | decoder.work([chunk]) 236 | 237 | if __name__ == "__main__": 238 | main() 239 | -------------------------------------------------------------------------------- /baldr/README.md: -------------------------------------------------------------------------------- 1 | # baldr 433Mhz temperature and humidity sensor 2 | 3 | The [Baldr Outdoor Sensor](https://baldr.com/products/touch-buttons-lcd-weather-station-with-moon-phase-1) looks just like the [kwmobile sensor](../kwmobile/README.md) but the encoding is a bit different. 4 | 5 | ![Sensor](doc/baldr_sensor.png) 6 | 7 | ## Technical Specifications 8 | Item | Value | Description 9 | -------------: | ------------- | :------------- 10 | Channels | 3 | 11 | Frequency | 433.89MHz | 12 | Modulation | [PPM](https://en.wikipedia.org/wiki/Pulse-position_modulation) | 13 | Symbol-Rate | variable | 14 | Symbol-Encoding | Pulse-Width | 15 | Transmission Interval | 50s | 16 | 17 | ## Analyze samples 18 | 19 | Sniff raw IQ-Data with `rtl_sdr -f 434000000 -s 2048000 sample.cu8` 20 | 21 | Analyze with [inspectrum](https://github.com/miek/inspectrum): `$ inspectrum sample.cu8 -r 2048000` 22 | 23 | ## Decoding Rules 24 | 25 | ![Sample Annotation](doc/baldr_bits_raw_annotation.jpg) 26 | 27 | Type | Timing 28 | --- | --- 29 | `very short` | 50µs 30 | `short` | 585µs 31 | `medium` | 2000µs 32 | `long` | 4000µs 33 | `very long` | 9000µs 34 | 35 | Symbol | Meaning | Comment 36 | --- | --- | --- 37 | `very short` pulse followed by `very long` gap | start of transmission | 38 | `short` pulse followed by `medium` gap | `0` | 39 | `short` pulse followed by `long` gap | `1` | 40 | `short` pulse followed by `very long` gap | inter-frame gap | 41 | `short` pulse followed by `very long` gap and a `short` pulse | end of transmission | 42 | 43 | Each transmission contains multiple (7) consecutive frames with the same content. Each frame contains 37 bits that can be decoded as follows: 44 | 45 | ![Decoding Rules](doc/baldr_decoding_rules.png) 46 | 47 | ## Decoding with RFM89-Module 48 | 49 | tbd -------------------------------------------------------------------------------- /baldr/baldr-decoder.service: -------------------------------------------------------------------------------- 1 | # copy this file to /etc/systemd/system/ 2 | # enable with: 3 | # sudo systemctl enable baldr-decoder.service 4 | # sudo systemctl start baldr-decoder.service 5 | 6 | [Unit] 7 | Description=rfm69Decoder 8 | After=network.target 9 | 10 | [Service] 11 | Type=simple 12 | ExecStart=/usr/bin/python3 /home/henry/sdr/baldr/decoder.py 13 | RestartSec=30 14 | Restart=always 15 | User=henry 16 | Group=henry 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /baldr/decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from time import sleep, time 6 | from datetime import datetime 7 | 8 | import pigpio as gpio 9 | import numpy as np 10 | 11 | from lib.rfm69 import Rfm69 12 | 13 | import paho.mqtt.client as mqtt 14 | 15 | SILENT = 0 16 | ERROR = 1 17 | INFO = 2 18 | TRACE = 3 19 | 20 | 21 | class Decoder(object): 22 | """Decoder-Class for external Receiver""" 23 | def __init__(self, host="localhost", port=8888, data_pin=25, reset_pin=24, pi=None, debug_level=SILENT): 24 | self.debug_level = debug_level 25 | self.host = host 26 | self.port = port 27 | self.reset_pin = reset_pin 28 | self.data_pin = data_pin 29 | self.onDecode = None 30 | 31 | # set up pigpio connection 32 | if pi: 33 | self.pi = pi 34 | else: 35 | self.pi = gpio.pi(self.host, self.port) 36 | 37 | self.pi.set_mode(data_pin, gpio.OUTPUT) 38 | self.pi.set_pull_up_down(data_pin, gpio.PUD_OFF) 39 | self.pi.write(data_pin, 0) 40 | 41 | if not self.pi.connected: 42 | self.debug("Could not connected to " + self.host) 43 | exit() 44 | self.callback = None 45 | 46 | 47 | # initialize timestamp for further use 48 | self.start_tick = self.pi.get_current_tick() 49 | 50 | # control variables 51 | self.active = False # enables the decoder 52 | 53 | # symbol parameters 54 | self.pulse_medium_gap_µs = 2650 #µs 55 | self.pulse_long_gap_µs = 4700 #µs 56 | self.pulse_verylong_gap_µs = 9600 #µs 57 | self.symbol_tolerance_µs = 200 #µs 58 | 59 | # decoding variables 60 | self.state = 0 # 0=idle; 1=frame; 61 | self.symbols = np.empty(0, dtype=np.uint8) 62 | 63 | # sensor data 64 | self.sensor_id = 0 65 | self.temperature = 0 66 | self.humidity = 0 67 | self.channel = 0 68 | self.battery_ok = False 69 | self.txMode = 0 70 | self.newData = False 71 | 72 | def __enter__(self): 73 | """Class can be used in with-statement""" 74 | return self 75 | 76 | def __exit__(self, exc_type, exc_value, traceback): 77 | """clean up stuff""" 78 | if self.callback: 79 | self.callback.cancel() 80 | self.pi.stop() 81 | 82 | def debug(self, message, level=0): 83 | """Debug output depending on debug level.""" 84 | if self.debug_level >= level: 85 | print(message) 86 | 87 | def cbf(self, pin, level, tick): 88 | # End of Pulse 89 | if level == 0: 90 | pass 91 | # End of Gap 92 | if level == 1: 93 | delta = gpio.tickDiff(self.start_tick, tick) 94 | self.start_tick = tick 95 | # use frame-gap after 1st frame as trigger to scan the next frames; pulse + very long gap 96 | if self.state == 0 and delta in range(self.pulse_verylong_gap_µs - self.symbol_tolerance_µs, self.pulse_verylong_gap_µs + self.symbol_tolerance_µs): 97 | self.debug("Start of frame detected", TRACE) 98 | self.state = 1 99 | # pulse + long gap => 1 100 | elif (self.state == 1) and delta in range(self.pulse_long_gap_µs - self.symbol_tolerance_µs, self.pulse_long_gap_µs + self.symbol_tolerance_µs): 101 | self.symbols = np.append(self.symbols, [1]) 102 | # pulse + medium gap => 0 103 | elif (self.state == 1) and delta in range(self.pulse_medium_gap_µs - self.symbol_tolerance_µs, self.pulse_medium_gap_µs + self.symbol_tolerance_µs): 104 | self.symbols = np.append(self.symbols, [0]) 105 | elif (self.state == 1) and delta in range(self.pulse_verylong_gap_µs - self.symbol_tolerance_µs, self.pulse_verylong_gap_µs + self.symbol_tolerance_µs): 106 | self.debug("End of Frame : " + str(self.symbols.size)+ " Bits received", TRACE) 107 | if self.symbols.size == 37: 108 | self.state = 2 # all good 109 | else: 110 | self.symbols = np.empty(0, dtype=np.uint8) 111 | self.state = 0 112 | else: 113 | pass 114 | # Watchdog timeout 115 | elif (level == 2) and (self.state > 0): 116 | self.debug("End of Transmission: " + str(self.symbols.size) + " Bits received", TRACE) 117 | if self.symbols.size == 37: 118 | self.decode() 119 | else: 120 | pass 121 | self.symbols = np.empty(0, dtype=np.uint8) 122 | self.state = 0 123 | else: 124 | pass 125 | 126 | def decode(self): 127 | """Actual decoder""" 128 | frame = np.packbits(self.symbols) 129 | 130 | temp_raw = (frame[2] << 4) | (frame[3] >> 4) 131 | # handle negative numbers 132 | if temp_raw > 0x7ff: 133 | temp_raw -= 2**12 134 | 135 | # convert to actual temperature 136 | self.temperature = float(temp_raw)/10. 137 | self.humidity = int(((frame[3] & 0x07) << 4) | (frame[4] >> 4)) 138 | self.channel = int(frame[1] & 0x03) 139 | self.txMode = int(frame[1] & 0x04) 140 | self.sensor_id = int(((frame[0] & 0x0f) << 4) | (frame[1] >> 4)) 141 | self.newData = True 142 | self.debug("{} Frame: ".format(datetime.now().isoformat(timespec='seconds')) + ''.join('{:02X} '.format(x) for x in frame) + " - ID={} Channel={} txMode={} {:.1f}°C {:.0f}% rH".format(self.sensor_id, self.channel, self.txMode, self.temperature, self.humidity), INFO) 143 | 144 | def run(self, glitch_filter=150, onDecode=None): 145 | # callback after successful decode 146 | self.onDecode=onDecode 147 | 148 | # filter high frequency noise 149 | self.pi.set_glitch_filter(self.data_pin, glitch_filter) 150 | 151 | # watchdog to detect end of frame 152 | self.pi.set_watchdog(self.data_pin, 18) # 18ms=18000µs 153 | 154 | # watch pin 155 | self.callback = self.pi.callback(self.data_pin, gpio.EITHER_EDGE, self.cbf) 156 | 157 | while 1: 158 | sleep(60) 159 | if self.newData: 160 | # publish values into MQTT topics 161 | if self.onDecode: 162 | self.onDecode("home/greenhouse/temp", '{0:0.1f}'.format(self.temperature)) 163 | self.onDecode("home/greenhouse/hum", '{0:0.0f}'.format(self.humidity)) 164 | self.newData = False 165 | 166 | 167 | class Mqtt(object): 168 | def __init__(self, host="localhost", debug_level=SILENT): 169 | self.debug_level = debug_level 170 | self.host = host 171 | self.connected = False 172 | 173 | self.client = mqtt.Client('raspi-%s' % os.getpid()) 174 | self.client.on_connect = self.on_connect 175 | 176 | self.client.connect(self.host) 177 | self.client.loop_start() 178 | 179 | def __enter__(self): 180 | """Class can be used in with-statement""" 181 | return self 182 | 183 | def __exit__(self, exc_type, exc_value, traceback): 184 | self.client.loop_stop() 185 | self.client.disconnect() 186 | 187 | def debug(self, message, level=0): 188 | """Debug output depending on debug level.""" 189 | if self.debug_level >= level: 190 | print(message) 191 | 192 | def publish(self, topic, data, retain=False): 193 | if self.connected: 194 | self.client.publish(topic, data, retain) 195 | 196 | def on_connect(self, client, userdata, flags, rc): 197 | self.debug(("Connected to mqtt broker:", self.host), TRACE) 198 | self.connected = True 199 | 200 | 201 | def main(): 202 | """ main function """ 203 | 204 | # set up decoder and mqtt-connection 205 | with Rfm69(host="localhost", channel=0, baudrate=32000) as rf: 206 | # just to make sure SPI is working 207 | rx_data = rf.read_single(0x5A) 208 | if rx_data != 0x55: 209 | print("SPI Error") 210 | exit() 211 | 212 | # Configure 213 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 214 | rf.write_burst(0x07, [0x6C, 0x7A, 0xE1]) # Frf: Carrier Frequency 434MHz 215 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 216 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 217 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 218 | # Receive mode 219 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 220 | 221 | # wait until RFM-Module is ready 222 | counter = 0 223 | while (rf.read_single(0x27) & 0x80) == 0: 224 | counter = counter + 1 225 | if counter > 100: 226 | raise Exception("ERROR - Could not initialize RFM-Module") 227 | 228 | with Decoder(host="localhost", debug_level=INFO) as decoder: 229 | with Mqtt(host="omv4", debug_level=SILENT) as mqtt_client: 230 | try: 231 | decoder.run(glitch_filter=400, onDecode=mqtt_client.publish) 232 | except KeyboardInterrupt: 233 | print("cancel") 234 | 235 | if __name__ == "__main__": 236 | main() -------------------------------------------------------------------------------- /baldr/doc/Baldr Decoding Rules.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/baldr/doc/Baldr Decoding Rules.pptx -------------------------------------------------------------------------------- /baldr/doc/baldr_bits_raw_annotation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/baldr/doc/baldr_bits_raw_annotation.jpg -------------------------------------------------------------------------------- /baldr/doc/baldr_decoding_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/baldr/doc/baldr_decoding_rules.png -------------------------------------------------------------------------------- /baldr/doc/baldr_sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/baldr/doc/baldr_sensor.png -------------------------------------------------------------------------------- /baldr/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/baldr/lib/__init__.py -------------------------------------------------------------------------------- /baldr/lib/rfm69.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """RFM69-Class""" 4 | 5 | import pigpio as gpio 6 | 7 | # global defines 8 | ERROR = 1 9 | INFO = 2 10 | TRACE = 3 11 | 12 | class Rfm69(object): 13 | """RFM69-Class""" 14 | # pylint: disable=too-many-instance-attributes, C0301, C0103 15 | 16 | def __init__(self, host="localhost", port=8888, channel=0, baudrate=10000000, debug_level=0): 17 | # general variables 18 | self.debug_level = debug_level 19 | 20 | # RFM69-specific variables 21 | self.pi = gpio.pi(host, port) 22 | if not self.pi.connected: 23 | raise ValueError('Could not connect to pigpio-device at {}:{}'.format(host, port)) 24 | 25 | self.handle = self.pi.spi_open(channel, baudrate, 0) # Flags: CPOL=0 and CPHA=0 26 | 27 | def __enter__(self): 28 | return self 29 | 30 | def __exit__(self, exc_type, exc_value, traceback): 31 | """clean up stuff""" 32 | self.pi.spi_close(self.handle) 33 | self.pi.stop() 34 | 35 | def debug(self, message, level=0): 36 | """Debug output depending on debug level.""" 37 | if self.debug_level >= level: 38 | print(message) 39 | 40 | def read_single(self, address): 41 | """Read single register via spi""" 42 | (count, data) = self.pi.spi_xfer(self.handle, [address & 0x7F, 0x00]) 43 | return data[1] 44 | 45 | def write_single(self, address, value): 46 | """Write single register via spi""" 47 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80, value]) 48 | return count == 2 49 | 50 | def write_burst(self, address, data): 51 | """Write bytearray of data beginning at address""" 52 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80] + data) 53 | return count == (len(data)+1) 54 | 55 | def write_config(self, cfg): 56 | """Write cfg-tuble like this: ((register1, value1), (register2, value2), ...)""" 57 | for i in range(0, len(cfg)): 58 | reg, val = cfg[i] 59 | self.write_single(reg, val) 60 | -------------------------------------------------------------------------------- /berner/README.md: -------------------------------------------------------------------------------- 1 | # berner remote control 2 | 3 | 4 | # Technical Specifications 5 | Item | Value | Description 6 | -------------: | ------------- | :------------- 7 | Model | BHS140 | 8 | Encoding | fixed, unidirectional | 9 | Channels | 4 | 10 | Frequency | 868.2MHz | 11 | Modulation | On-Off-Keying (OOK) | 12 | Symbol-Rate | 1 kHz | 13 | Symbol-Encoding | Pulse-Width | 14 | Antenna-Length (λ/4) | 86mm 15 | 16 | ## analyze samples 17 | 18 | First step is to gather and analyze the data sent by the hand-held transmitter. Start rtl_sdr (or whatever tool you are using) and press a few buttons on the transmitter. 19 | 20 | ``` 21 | $ rtl_sdr -f 868000000 -s 2048000 sample.cu8 22 | Found 1 device(s): 23 | 0: Realtek, RTL2838UHIDIR, SN: 00000001 24 | 25 | Using device 0: Generic RTL2832U OEM 26 | Found Rafael Micro R820T tuner 27 | [R82XX] PLL not locked! 28 | Sampling at 2048000 S/s. 29 | Tuned to 868000000 Hz. 30 | Tuner gain set to automatic. 31 | Reading samples in async mode... 32 | ^CSignal caught, exiting! 33 | 34 | User cancel, exiting... 35 | ``` 36 | 37 | Analyze the data with inspectrum: `$ inspectrum sample.cu8` 38 | 39 | ![Raw Data](docs/spectrum_magnitude.png) 40 | 41 | 42 | 43 | ## Decoding Rules 44 | 45 | Each frame consists of a preamble and 32-bits of data (e.g. `0x02 0xEA 0xAB 0xBF`): 46 | 47 | Symbol | Meaning | Comment 48 | --- | --- | --- 49 | `very long` Pulse followed by `short` gap| *Preamble* | the first frame actually has a shorter preamble of 4667µs 50 | `short` pulse followed by `long` gap | `0` | (duty factor < 0.5) 51 | `long` pulse followed by `short` gap | `1` | (duty factor > 0.5) 52 | 53 | Type | Timing 54 | --- | --- 55 | `very long` | 5440µs 56 | `long` | 333µs 57 | `short` | 667µs 58 | 59 | Upon pressing the button on the hand-held, several frames are transmitted successively. 60 | 61 | # Tools 62 | 63 | * `sniff.py` - a command-line tool to read and decode hand-held transmitter data using my [python RFM69-library](https://github.com/henrythasler/rfm69) on a Raspberry Pi. 64 | * `transmitter.py` - a command-line tool to previously gathered codes via the RFM69-module. 65 | 66 | # References 67 | * http://berner-torantriebe.eu/files/berner_handsender_090516.pdf 68 | -------------------------------------------------------------------------------- /berner/docs/spectrum_magnitude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/berner/docs/spectrum_magnitude.png -------------------------------------------------------------------------------- /berner/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/berner/lib/__init__.py -------------------------------------------------------------------------------- /berner/lib/rfm69.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """RFM69-Class""" 4 | 5 | import pigpio as gpio 6 | 7 | # global defines 8 | ERROR = 1 9 | INFO = 2 10 | TRACE = 3 11 | 12 | class Rfm69(object): 13 | """RFM69-Class""" 14 | # pylint: disable=too-many-instance-attributes, C0301, C0103 15 | 16 | def __init__(self, host="localhost", port=8888, channel=0, baudrate=10000000, debug_level=0): 17 | # general variables 18 | self.debug_level = debug_level 19 | 20 | # RFM69-specific variables 21 | self.pi = gpio.pi(host, port) 22 | self.handle = self.pi.spi_open(channel, baudrate, 0) # Flags: CPOL=0 and CPHA=0 23 | 24 | def __enter__(self): 25 | return self 26 | 27 | def __exit__(self, exc_type, exc_value, traceback): 28 | """clean up stuff""" 29 | self.pi.spi_close(self.handle) 30 | self.pi.stop() 31 | 32 | def debug(self, message, level=0): 33 | """Debug output depending on debug level.""" 34 | if self.debug_level >= level: 35 | print(message) 36 | 37 | def read_single(self, address): 38 | """Read single register via spi""" 39 | (count, data) = self.pi.spi_xfer(self.handle, [address & 0x7F, 0x00]) 40 | return data[1] 41 | 42 | def write_single(self, address, value): 43 | """Write single register via spi""" 44 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80, value]) 45 | return count == 2 46 | 47 | def write_burst(self, address, data): 48 | """Write bytearray of data beginning at address""" 49 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80] + data) 50 | return count == (len(data)+1) 51 | 52 | def write_config(self, cfg): 53 | """Write cfg-tuble like this: ((register1, value1), (register2, value2), ...)""" 54 | for i in range(0, len(cfg)): 55 | reg, val = cfg[i] 56 | self.write_single(reg, val) 57 | -------------------------------------------------------------------------------- /berner/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """OOK-Decoder""" 4 | 5 | from time import sleep 6 | import pigpio as gpio 7 | from lib.rfm69 import Rfm69 8 | import numpy as np 9 | 10 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 11 | RESET = 24 12 | DATA = 25 13 | 14 | # define pigpio-host 15 | HOST = "rfpi" 16 | 17 | start_tick = 0 18 | state = 0 # 0=Idle, 1=Frame 19 | bits = None 20 | 21 | def cbf(pin, level, tick): 22 | global start_tick 23 | global state 24 | global bits 25 | if level == 1: 26 | start_tick = tick 27 | elif level == 0: 28 | delta = gpio.tickDiff(start_tick, tick) 29 | if (state == 0) and (delta > 5300) and (delta < 5700): 30 | bits = np.empty(0, dtype=np.uint8) 31 | state = 1 32 | elif (state == 1) and (delta < 500): 33 | bits = np.append(bits, [0]) 34 | elif (state == 1) and (delta > 500) and (delta < 1000): 35 | bits = np.append(bits, [1]) 36 | elif state == 1: 37 | # print 38 | if bits.size >= 32: 39 | print "Rx: "+''.join('0x{:02X} '.format(x) for x in np.packbits(bits)[:4]) 40 | state = 0 41 | else: 42 | pass 43 | else: 44 | pass 45 | 46 | def main(): 47 | """ main function """ 48 | pi = gpio.pi(host=HOST) 49 | if not pi.connected: 50 | exit() 51 | pi.set_mode(RESET, gpio.OUTPUT) 52 | pi.set_mode(DATA, gpio.OUTPUT) 53 | pi.set_pull_up_down(DATA, gpio.PUD_OFF) 54 | pi.write(DATA, 0) 55 | 56 | pi.write(RESET, 1) 57 | pi.write(RESET, 0) 58 | 59 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 60 | 61 | # just to make sure SPI is working 62 | rx_data = rf.read_single(0x5A) 63 | if rx_data != 0x55: 64 | print "SPI Error" 65 | 66 | # configure 67 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 68 | 69 | rf.write_burst(0x07, [0xD9, 0x12, 0x00]) # Carrier Frequency 868.25MHz 70 | 71 | rf.write_single(0x18, 0b00000000) # Lna: 50 Ohm, auto gain 72 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 73 | 74 | # Receive 75 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 76 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 77 | 78 | # wait until RFM-Module is ready 79 | while (rf.read_single(0x27) & 0x80) == 0: 80 | print "waiting..." 81 | 82 | # filter high frequency noise 83 | pi.set_glitch_filter(DATA, 150) 84 | 85 | # watch pin 86 | callback = pi.callback(DATA, gpio.EITHER_EDGE, cbf) 87 | 88 | print "Scanning... Press Ctrl-C to abort" 89 | while 1: 90 | sleep(1) 91 | 92 | pi.stop() 93 | 94 | if __name__ == "__main__": 95 | try: 96 | main() 97 | except KeyboardInterrupt: 98 | print "" 99 | finally: 100 | print "done" 101 | -------------------------------------------------------------------------------- /berner/transmitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Transmit Berner garage door opener codes""" 4 | 5 | import sys 6 | from time import sleep 7 | import pigpio as gpio 8 | from lib.rfm69 import Rfm69 9 | import numpy as np 10 | 11 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 12 | RESET = 24 13 | DATA = 25 14 | 15 | # define pigpio-host 16 | HOST = "localhost" 17 | 18 | def main(code): 19 | """ main function """ 20 | pi = gpio.pi(host=HOST) 21 | if not pi.connected: 22 | exit() 23 | 24 | # prepare GPIO-Pins 25 | pi.set_mode(RESET, gpio.OUTPUT) 26 | pi.set_mode(DATA, gpio.OUTPUT) 27 | pi.set_pull_up_down(DATA, gpio.PUD_OFF) 28 | pi.write(DATA, 0) 29 | 30 | # reset transmitter before use 31 | pi.write(RESET, 1) 32 | pi.write(RESET, 0) 33 | sleep(.005) 34 | 35 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 36 | # just to make sure SPI is working 37 | rx_data = rf.read_single(0x5A) 38 | if rx_data != 0x55: 39 | print("SPI Error") 40 | 41 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 42 | 43 | """ 44 | F_RF = 868.279 MHz 45 | F_Osc = 32MHz 46 | F_Step = F_Osc/2^19 = 32e6/2^19 = 61.035 47 | Frf = F_RF/F_Step = 14225408 = 0xD91200 48 | """ 49 | # rf.write_burst(0x07, [0x6C, 0x7A, 0x00]) # Frf: Carrier Frequency 434MHz 50 | rf.write_burst(0x07, [0xD9, 0x12, 0x00]) # Carrier Frequency 868.279MHz 51 | 52 | # Use PA_BOOST 53 | rf.write_single(0x13, 0x0F) 54 | rf.write_single(0x5A, 0x5D) 55 | rf.write_single(0x5C, 0x7C) 56 | rf.write_single(0x11, 0b01111111) # Use PA_BOOST 57 | 58 | rf.write_single(0x18, 0b00000110) # Lna: 50 Ohm, highest gain 59 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 60 | 61 | # Transmit Mode 62 | rf.write_single(0x02, 0b01101000) # DataModul: continuous w/o bit sync, OOK, no shaping 63 | rf.write_single(0x01, 0b00001100) # OpMode: SequencerOn, TX 64 | 65 | # wait for ready 66 | while (rf.read_single(0x27) & 0x80) == 0: 67 | pass 68 | #print "waiting..." 69 | 70 | # delete existing waveforms 71 | pi.wave_clear() 72 | 73 | # calculate frame-data from command-line arguments 74 | data = np.empty(0, dtype=np.uint8) 75 | for item in code: 76 | data = np.append(data, np.array(int(item, 16), dtype=np.uint8)) 77 | 78 | # how many consecutive frame repetitions 79 | repetitions = 5 80 | 81 | # create preamble pulse waveform 82 | pi.wave_add_generic([gpio.pulse(1<= level: 93 | print(message) 94 | 95 | def cbf(self, pin, level, tick): 96 | # End of Pulse 97 | if level == 0: 98 | pass 99 | # End of Gap 100 | if level == 1: 101 | delta = gpio.tickDiff(self.start_tick, tick) 102 | self.start_tick = tick 103 | # use frame-gap after 1st frame as trigger to scan the next frames; pulse + very long gap 104 | if self.state == 0 and delta in range(4500-4*self.symbol_tolerance_µs, 4500+4*self.symbol_tolerance_µs): 105 | self.state = 1 106 | # pulse + long gap 107 | elif (self.state == 1) and delta in range(2500-2*self.symbol_tolerance_µs, 2500+2*self.symbol_tolerance_µs): 108 | self.symbols = np.append(self.symbols, [1]) 109 | # pulse + short gap 110 | elif (self.state == 1) and delta in range(1500-2*self.symbol_tolerance_µs, 1500+2*self.symbol_tolerance_µs): 111 | self.symbols = np.append(self.symbols, [0]) 112 | else: 113 | pass 114 | 115 | # Watchdog timeout 116 | elif (level == 2) and (self.state > 0): 117 | if self.symbols.size == 36: 118 | self.decode() 119 | else: 120 | pass 121 | self.symbols = np.empty(0, dtype=np.uint8) 122 | self.state = 0 123 | else: 124 | pass 125 | 126 | def decode(self): 127 | """Actual decoder""" 128 | frame = np.packbits(self.symbols) 129 | self.temperature = float(((frame[1]&0x0f) << 8 | frame[2])/10.) 130 | self.humidity = int(((frame[3]&0x0f) << 4) + (frame[4]>>4)) 131 | self.channel = int((frame[1]&0x30) >> 4) 132 | self.battery_ok = int(frame[1]&0x80) == 0x80 133 | self.sensor_id = int(frame[0]) 134 | self.newData = True 135 | #print("Frame: "+''.join('{:02X} '.format(x) for x in frame) + " - ID={} Channel={} Battery={} {:.1f}°C {:.0f}% rH".format(id, channel, battery, temperature, humidity)) 136 | 137 | def run(self, glitch_filter=150, onDecode=None): 138 | # callback after successful decode 139 | self.onDecode=onDecode 140 | 141 | # filter high frequency noise 142 | self.pi.set_glitch_filter(self.data_pin, 150) 143 | 144 | # watchdog to detect end of frame 145 | self.pi.set_watchdog(self.data_pin, 3) # 3ms=3000µs 146 | 147 | # watch pin 148 | self.callback = self.pi.callback(self.data_pin, gpio.EITHER_EDGE, self.cbf) 149 | 150 | while 1: 151 | sleep(60) 152 | if self.newData: 153 | # save to database every 60s 154 | self.pg_cur.execute("INSERT INTO greenhouse(timestamp, temperature, humidity, battery) VALUES(%s, %s, %s, %s)", (datetime.utcnow(), self.temperature, self.humidity, self.battery_ok)) 155 | self.pg_con.commit() 156 | 157 | # publish values into MQTT topics 158 | if self.onDecode: 159 | self.onDecode("home/greenhouse/temp", '{0:0.1f}'.format(self.temperature)) 160 | self.onDecode("home/greenhouse/hum", '{0:0.0f}'.format(self.humidity)) 161 | self.newData = False 162 | 163 | 164 | class Mqtt(object): 165 | def __init__(self, host="localhost", debug_level=SILENT): 166 | self.debug_level = debug_level 167 | self.host = host 168 | self.connected = False 169 | 170 | self.client = mqtt.Client('raspi-%s' % os.getpid()) 171 | self.client.on_connect = self.on_connect 172 | 173 | self.client.connect(self.host) 174 | self.client.loop_start() 175 | 176 | def __enter__(self): 177 | """Class can be used in with-statement""" 178 | return self 179 | 180 | def __exit__(self, exc_type, exc_value, traceback): 181 | self.client.loop_stop() 182 | self.client.disconnect() 183 | 184 | def debug(self, message, level=0): 185 | """Debug output depending on debug level.""" 186 | if self.debug_level >= level: 187 | print(message) 188 | 189 | def publish(self, topic, data, retain=False): 190 | if self.connected: 191 | self.client.publish(topic, data, retain) 192 | 193 | def on_connect(self, client, userdata, flags, rc): 194 | self.debug(("Connected to mqtt broker:", self.host), TRACE) 195 | self.connected = True 196 | 197 | 198 | def main(): 199 | """ main function """ 200 | 201 | # set up decoder and mqtt-connection 202 | with Rfm69(host="raspberrypi", channel=0, baudrate=32000) as rf: 203 | # just to make sure SPI is working 204 | rx_data = rf.read_single(0x5A) 205 | if rx_data != 0x55: 206 | print("SPI Error") 207 | exit() 208 | 209 | # Configure 210 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 211 | rf.write_burst(0x07, [0x6C, 0x7A, 0xE1]) # Frf: Carrier Frequency 434MHz 212 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 213 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 214 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 215 | # Receive mode 216 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 217 | 218 | # wait until RFM-Module is ready 219 | counter = 0 220 | while (rf.read_single(0x27) & 0x80) == 0: 221 | counter = counter + 1 222 | if counter > 100: 223 | raise Exception("ERROR - Could not initialize RFM-Module") 224 | 225 | with Decoder(host="raspberrypi", debug_level=SILENT) as decoder: 226 | with Mqtt(host="osmc", debug_level=SILENT) as mqtt_client: 227 | try: 228 | decoder.run(glitch_filter=150, onDecode=mqtt_client.publish) 229 | except KeyboardInterrupt: 230 | print("cancel") 231 | 232 | if __name__ == "__main__": 233 | main() -------------------------------------------------------------------------------- /kwmobile/decoder.service: -------------------------------------------------------------------------------- 1 | # copy this file to /etc/systemd/system/ 2 | # enable with: 3 | # sudo systemctl enable decoder.service 4 | # sudo systemctl start decoder.service 5 | 6 | [Unit] 7 | Description=rfm69Decoder 8 | After=network.target 9 | 10 | [Service] 11 | Type=simple 12 | ExecStart=/usr/bin/python3 /home/henry/sdr/kwmobile/decoder.py 13 | RestartSec=30 14 | Restart=always 15 | User=henry 16 | Group=henry 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /kwmobile/doc/Decoding Rules.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/Decoding Rules.odp -------------------------------------------------------------------------------- /kwmobile/doc/bits_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/bits_raw.png -------------------------------------------------------------------------------- /kwmobile/doc/bits_raw_annotation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/bits_raw_annotation.jpg -------------------------------------------------------------------------------- /kwmobile/doc/decoding_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/decoding_rules.png -------------------------------------------------------------------------------- /kwmobile/doc/frame_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/frame_raw.png -------------------------------------------------------------------------------- /kwmobile/doc/kwmobile_datasheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/kwmobile_datasheet.jpg -------------------------------------------------------------------------------- /kwmobile/doc/kwmobile_sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/kwmobile_sensor.png -------------------------------------------------------------------------------- /kwmobile/doc/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/doc/sample.png -------------------------------------------------------------------------------- /kwmobile/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/kwmobile/lib/__init__.py -------------------------------------------------------------------------------- /kwmobile/lib/rfm69.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """RFM69-Class""" 4 | 5 | import pigpio as gpio 6 | 7 | # global defines 8 | ERROR = 1 9 | INFO = 2 10 | TRACE = 3 11 | 12 | class Rfm69(object): 13 | """RFM69-Class""" 14 | # pylint: disable=too-many-instance-attributes, C0301, C0103 15 | 16 | def __init__(self, host="localhost", port=8888, channel=0, baudrate=10000000, debug_level=0): 17 | # general variables 18 | self.debug_level = debug_level 19 | 20 | # RFM69-specific variables 21 | self.pi = gpio.pi(host, port) 22 | if not self.pi.connected: 23 | raise ValueError('Could not connect to pigpio-device at {}:{}'.format(host, port)) 24 | 25 | self.handle = self.pi.spi_open(channel, baudrate, 0) # Flags: CPOL=0 and CPHA=0 26 | 27 | def __enter__(self): 28 | return self 29 | 30 | def __exit__(self, exc_type, exc_value, traceback): 31 | """clean up stuff""" 32 | self.pi.spi_close(self.handle) 33 | self.pi.stop() 34 | 35 | def debug(self, message, level=0): 36 | """Debug output depending on debug level.""" 37 | if self.debug_level >= level: 38 | print(message) 39 | 40 | def read_single(self, address): 41 | """Read single register via spi""" 42 | (count, data) = self.pi.spi_xfer(self.handle, [address & 0x7F, 0x00]) 43 | return data[1] 44 | 45 | def write_single(self, address, value): 46 | """Write single register via spi""" 47 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80, value]) 48 | return count == 2 49 | 50 | def write_burst(self, address, data): 51 | """Write bytearray of data beginning at address""" 52 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80] + data) 53 | return count == (len(data)+1) 54 | 55 | def write_config(self, cfg): 56 | """Write cfg-tuble like this: ((register1, value1), (register2, value2), ...)""" 57 | for i in range(0, len(cfg)): 58 | reg, val = cfg[i] 59 | self.write_single(reg, val) 60 | -------------------------------------------------------------------------------- /kwmobile/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """OOK-Decoder""" 4 | 5 | from time import sleep 6 | import pigpio as gpio 7 | from lib.rfm69 import Rfm69 8 | import numpy as np 9 | 10 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 11 | RESET = 24 12 | DATA = 25 13 | 14 | # define pigpio-host 15 | HOST = "raspberrypi" 16 | 17 | start_tick = 0 18 | state = 0 # 0=Idle, 1=Frame 19 | bits = np.empty(0, dtype=np.uint8) 20 | tolerance = 50 #µs 21 | 22 | def cbf(pin, level, tick): 23 | global start_tick 24 | global state 25 | global bits 26 | global tolerance 27 | 28 | # End of Pulse 29 | if level == 0: 30 | pass 31 | # End of Gap 32 | if level == 1: 33 | delta = gpio.tickDiff(start_tick, tick) 34 | start_tick = tick 35 | # use frame-gap after 1st frame as trigger to scan the next frames; pulse + very long gap 36 | if state == 0 and delta in range(4500-4*tolerance, 4500+4*tolerance): 37 | state = 1 38 | # pulse + long gap 39 | elif (state == 1) and delta in range(2500-2*tolerance, 2500+2*tolerance): 40 | bits = np.append(bits, [1]) 41 | # pulse + short gap 42 | elif (state == 1) and delta in range(1500-2*tolerance, 1500+2*tolerance): 43 | bits = np.append(bits, [0]) 44 | else: 45 | pass 46 | 47 | # Watchdog timeout 48 | elif (level == 2) and (state > 0): 49 | if bits.size == 36: 50 | frame = np.packbits(bits) 51 | temperature = ((frame[1]&0x0f) << 8 | frame[2])/10. 52 | humidity = (((frame[3]&0x0f) << 4) + (frame[4]>>4)) 53 | channel = (frame[1]&0x30) >> 4 54 | battery = (frame[1]&0x80) == 0x80 55 | id = frame[0] 56 | print "Frame: "+''.join('{:02X} '.format(x) for x in frame) + " ".join('{:08b} '.format(x) for x in frame) + " - ID={} Channel={} Battery={} {:.1f}°C {:.0f}% rH".format(id, channel, battery, temperature, humidity) 57 | else: 58 | pass 59 | bits = np.empty(0, dtype=np.uint8) 60 | state = 0 61 | else: 62 | pass 63 | 64 | def main(): 65 | """ main function """ 66 | pi = gpio.pi(host=HOST) 67 | if not pi.connected: 68 | exit() 69 | pi.set_mode(RESET, gpio.OUTPUT) 70 | pi.set_mode(DATA, gpio.OUTPUT) 71 | pi.set_pull_up_down(DATA, gpio.PUD_OFF) 72 | pi.write(DATA, 0) 73 | 74 | pi.write(RESET, 1) 75 | pi.write(RESET, 0) 76 | 77 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 78 | 79 | # just to make sure SPI is working 80 | rx_data = rf.read_single(0x5A) 81 | if rx_data != 0x55: 82 | print "SPI Error" 83 | 84 | # configure 85 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 86 | 87 | rf.write_burst(0x07, [0x6C, 0x7A, 0xE1]) # Frf: Carrier Frequency 434MHz 88 | 89 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 90 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 91 | 92 | # Receive 93 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 94 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 95 | 96 | # wait until RFM-Module is ready 97 | while (rf.read_single(0x27) & 0x80) == 0: 98 | print "waiting..." 99 | 100 | # filter high frequency noise 101 | pi.set_glitch_filter(DATA, 150) 102 | 103 | # watchdog to detect end of frame 104 | pi.set_watchdog(DATA, 3) # 3ms=3000µs 105 | 106 | # watch pin 107 | callback = pi.callback(DATA, gpio.EITHER_EDGE, cbf) 108 | 109 | print "Scanning... Press Ctrl-C to abort" 110 | while 1: 111 | sleep(60) 112 | print "sending..." 113 | 114 | callback.cancel() 115 | pi.stop() 116 | 117 | if __name__ == "__main__": 118 | try: 119 | main() 120 | except KeyboardInterrupt: 121 | print "" 122 | finally: 123 | print "done" 124 | -------------------------------------------------------------------------------- /mumbi/README.md: -------------------------------------------------------------------------------- 1 | # Mumbi FS300 2 | 3 | 4 | # Technical Specifications 5 | Item | Value | Description 6 | -------------: | ------------- | :------------- 7 | Model | m-FS300 | 8 | Encoding | | 9 | Channels | | 10 | Frequency | 434.92 MHz | 11 | Modulation | On-Off-Keying (OOK) | 12 | Symbol-Rate | 865 Hz | 13 | Symbol-Encoding | pulse-width-encoding | 14 | 15 | ## analyze samples 16 | 17 | First step is to gather and analyze the data sent by the hand-held transmitter. Start rtl_sdr (or whatever tool you are using) and press a few buttons on the transmitter. 18 | 19 | ``` 20 | $ rtl_sdr -f 434000000 -s 2048000 sample.cu8 21 | Found 1 device(s): 22 | 0: Realtek, RTL2838UHIDIR, SN: 00000001 23 | 24 | Using device 0: Generic RTL2832U OEM 25 | Found Rafael Micro R820T tuner 26 | [R82XX] PLL not locked! 27 | Sampling at 2048000 S/s. 28 | Tuned to 434000000 Hz. 29 | Tuner gain set to automatic. 30 | Reading samples in async mode... 31 | ^CSignal caught, exiting! 32 | 33 | User cancel, exiting... 34 | ``` 35 | 36 | Analyze the data with inspectrum: 37 | 38 | `$ inspectrum sample.cu8` 39 | Upon pressing a button, 8 consecutive frames are sent within 350ms: 40 | ![Frame Groups](docs/frames.png) 41 | 42 | Closer inspection reveals, that the modulation is On-Off-Keying (OOK) with pulse-width encoding: 43 | ![Frame Groups](docs/single_frame.png) 44 | 45 | ## Decoding Rules 46 | 47 | Each frame consists of a preamble and 34-bits of data (e.g. `0x02 0xEA 0xAB 0xBF`). Let's assume the following rules: 48 | 49 | Symbol | Meaning | Comment 50 | --- | --- | --- 51 | `short` pulse followed by `long` gap | `0` | (duty factor < 0.5) 52 | `long` pulse followed by `short` gap | `1` | (duty factor > 0.5) 53 | 54 | Type | Timing 55 | --- | --- 56 | `long` | 820 µs 57 | `short` | 300 µs 58 | 59 | Upon pressing the button on the hand-held, at least eight frames are transmitted successively. 60 | 61 | ## Tools 62 | 63 | * `sniff.py` - a command-line tool to read and decode hand-held transmitter data using my [python RFM69-library](https://github.com/henrythasler/rfm69) on a Raspberry Pi. Also used to analyze the protocol and generate the charts shown below. 64 | * `transmitter.py` - (coming soon) 65 | 66 | ## Protocol analysis 67 | 68 | The hand-held remote transmits several frames with 34 pulses for each key resulting in 34 bits according to the decoding rules above. Visualizing the bits as a 2D-histogram can support a preliminary analysis. The color in this chart represents the average value for each bit: 69 | ![histogram](docs/histogram.png) 70 | 71 | Obviously there are a lot of zeros involved. Nonetheless do we figure, that there is something like a fixed pattern and a variable part. 72 | 73 | The fixed part could be a uniqe ID for each hand-held transmitter. 74 | 75 | The variable part (8 bits) is more interesting so we look at these some more: 76 | 77 | Group | Action | Value | Value (swapped) 78 | ---|---|---|--- 79 | A | On | `1111 1001` | `1001 1111` 80 | A | Off | `1110 1000`| `1000 1110` 81 | B | On | `1101 1010` | `1010 1101` 82 | B | Off | `1100 1011`| `1011 1100` 83 | C | On | `1011 1110` | `1110 1011` 84 | C | Off | `1010 1111`| `1111 1010` 85 | D | On | `0111 0001` | `0001 0111` 86 | D | Off | `0110 0000`| `0000 0110` 87 | All | On | `0100 0011` | `0011 0100` 88 | All | Off | `1000 1101`| `1101 1000` 89 | 90 | No great insights, except the 4th bit of the value representing on/off except with the All-Button. 91 | 92 | ## Transmitting 93 | 94 | No suprise here. We just send out the previously sniffed 34-bit values and be done with it. 95 | 96 | Except that we can use the more general timing of 1kHz for pulses and pause durations: 97 | 98 | Type | Timing | Comment 99 | --- | --- | --- 100 | `long` | 667 µs 101 | `short` | 333 µs 102 | `pause` | 10000 µs | between repetitions 103 | 104 | ## References 105 | 106 | * ??? 107 | -------------------------------------------------------------------------------- /mumbi/docs/frames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/mumbi/docs/frames.png -------------------------------------------------------------------------------- /mumbi/docs/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/mumbi/docs/histogram.png -------------------------------------------------------------------------------- /mumbi/docs/single_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/mumbi/docs/single_frame.png -------------------------------------------------------------------------------- /mumbi/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/mumbi/lib/__init__.py -------------------------------------------------------------------------------- /mumbi/lib/rfm69.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """RFM69-Class""" 4 | 5 | import pigpio as gpio 6 | 7 | # global defines 8 | ERROR = 1 9 | INFO = 2 10 | TRACE = 3 11 | 12 | class Rfm69(object): 13 | """RFM69-Class""" 14 | # pylint: disable=too-many-instance-attributes, C0301, C0103 15 | 16 | def __init__(self, host="localhost", port=8888, channel=0, baudrate=10000000, debug_level=0): 17 | # general variables 18 | self.debug_level = debug_level 19 | 20 | # RFM69-specific variables 21 | self.pi = gpio.pi(host, port) 22 | self.handle = self.pi.spi_open(channel, baudrate, 0) # Flags: CPOL=0 and CPHA=0 23 | 24 | def __enter__(self): 25 | return self 26 | 27 | def __exit__(self, exc_type, exc_value, traceback): 28 | """clean up stuff""" 29 | self.pi.spi_close(self.handle) 30 | self.pi.stop() 31 | 32 | def debug(self, message, level=0): 33 | """Debug output depending on debug level.""" 34 | if self.debug_level >= level: 35 | print(message) 36 | 37 | def read_single(self, address): 38 | """Read single register via spi""" 39 | (count, data) = self.pi.spi_xfer(self.handle, [address & 0x7F, 0x00]) 40 | return data[1] 41 | 42 | def write_single(self, address, value): 43 | """Write single register via spi""" 44 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80, value]) 45 | return count == 2 46 | 47 | def write_burst(self, address, data): 48 | """Write bytearray of data beginning at address""" 49 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80] + data) 50 | return count == (len(data)+1) 51 | 52 | def write_config(self, cfg): 53 | """Write cfg-tuble like this: ((register1, value1), (register2, value2), ...)""" 54 | for i in range(0, len(cfg)): 55 | reg, val = cfg[i] 56 | self.write_single(reg, val) 57 | -------------------------------------------------------------------------------- /mumbi/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """OOK-Decoder""" 4 | 5 | from time import sleep 6 | import pigpio as gpio 7 | from lib.rfm69 import Rfm69 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from mpl_toolkits.axes_grid1 import make_axes_locatable 11 | 12 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 13 | RESET = 24 14 | DATA = 25 15 | 16 | # define pigpio-host 17 | HOST = "rfpi" 18 | 19 | start_tick = 0 20 | state = 0 # 0=Idle, 1=Frame 21 | bits = np.empty(0, dtype=np.uint8) 22 | tolerance = 50 #µs 23 | 24 | histogram = [0, np.zeros(34)] 25 | 26 | def cbf(pin, level, tick): 27 | global start_tick 28 | global state 29 | global bits 30 | global tolerance 31 | global histogram 32 | 33 | # End of Gap 34 | if level == 1: 35 | start_tick = tick 36 | 37 | # End of Pulse 38 | elif level == 0: 39 | delta = gpio.tickDiff(start_tick, tick) 40 | # start_tick = tick 41 | # long pulse 42 | if delta in range(820-2*tolerance, 820+2*tolerance): 43 | bits = np.append(bits, [1]) 44 | state = 1 45 | # short pulse 46 | elif delta in range(300-tolerance, 300+tolerance): 47 | bits = np.append(bits, [0]) 48 | else: 49 | pass 50 | 51 | # Watchdog timeout 52 | elif (level == 2) and (state > 0): 53 | if bits.size == 34: 54 | histogram[1] = histogram[1] + bits 55 | histogram[0] += 1 56 | frame = np.packbits(bits) 57 | print "Frame: "+''.join('0x{:02X} '.format(x) for x in frame) 58 | #print "Command: 0b{:08b}".format(((frame[2]&0x0f) << 4) + (frame[3]&0x0f)) 59 | 60 | else: 61 | pass 62 | bits = np.empty(0, dtype=np.uint8) 63 | state = 0 64 | else: 65 | pass 66 | 67 | def main(): 68 | """ main function """ 69 | pi = gpio.pi(host=HOST) 70 | if not pi.connected: 71 | exit() 72 | pi.set_mode(RESET, gpio.OUTPUT) 73 | pi.set_mode(DATA, gpio.OUTPUT) 74 | pi.set_pull_up_down(DATA, gpio.PUD_DOWN) 75 | pi.write(DATA, 0) 76 | 77 | pi.write(RESET, 1) 78 | pi.write(RESET, 0) 79 | 80 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 81 | 82 | # just to make sure SPI is working 83 | rx_data = rf.read_single(0x5A) 84 | if rx_data != 0x55: 85 | print "SPI Error" 86 | 87 | # configure 88 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 89 | 90 | rf.write_burst(0x07, [0x6C, 0x9A, 0x00]) # Frf: Carrier Frequency 434.42MHz/61.035 91 | 92 | # rf.write_single(0x18, 0b00000000) # Lna: 50 Ohm, auto gain 93 | 94 | # rf.write_single(0x19, 0b01001001) # RxBw: 4% DCC, BW=100kHz 95 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 96 | 97 | # make sure we conserve the thesold setting in between WUP and HW-Sync 98 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 99 | 100 | # Receive 101 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 102 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 103 | 104 | # wait until RFM-Module is ready 105 | while (rf.read_single(0x27) & 0x80) == 0: 106 | print "waiting..." 107 | 108 | # filter high frequency noise 109 | pi.set_glitch_filter(DATA, 150) 110 | 111 | # watchdog to detect end of frame (20ms) 112 | pi.set_watchdog(DATA, 8) 113 | 114 | # watch DATA pin 115 | callback = pi.callback(DATA, gpio.EITHER_EDGE, cbf) 116 | 117 | print "Scanning... Press Ctrl-C to abort" 118 | try: 119 | while 1: 120 | sleep(1) 121 | except KeyboardInterrupt: 122 | print "" 123 | finally: 124 | print "done" 125 | 126 | 127 | pi.stop() 128 | 129 | def show_histogram(matrix, normalize=False): 130 | fig, ax = plt.subplots() 131 | if normalize: 132 | matrix = matrix/np.amax(matrix) 133 | #print matrix.size, matrix 134 | img1 = ax.imshow(matrix) 135 | divider = make_axes_locatable(ax) 136 | cax = divider.append_axes("right", size="5%", pad=0.1) 137 | fig.colorbar(img1, cax=cax) 138 | plt.show() 139 | 140 | if __name__ == "__main__": 141 | main() 142 | 143 | if histogram[0] > 0: 144 | show_histogram(np.reshape(np.append(histogram[1], np.zeros(6)), (-1, 8)), normalize=True) 145 | -------------------------------------------------------------------------------- /mumbi/transmitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Transmit mumbi power outlet codes""" 4 | 5 | import sys 6 | from time import sleep 7 | import pigpio as gpio 8 | from lib.rfm69 import Rfm69 9 | import numpy as np 10 | 11 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 12 | RESET = 24 13 | DATA = 25 14 | 15 | # define pigpio-host 16 | HOST = "localhost" 17 | 18 | COMMANDS={ 19 | 'a_on': [0x19, 0x10, 0x0F, 0x09, 0x00], 20 | 'a_off': [0x19, 0x10, 0x0E, 0x08, 0x00], 21 | 'b_on': [0x19, 0x10, 0x0D, 0x0A, 0x00], 22 | 'b_off': [0x19, 0x10, 0x0C, 0x0B, 0x00], 23 | 'c_on': [0x19, 0x10, 0x0B, 0x0E, 0x00], 24 | 'c_off': [0x19, 0x10, 0x0A, 0x0F, 0x00], 25 | 'd_on': [0x19, 0x10, 0x07, 0x01, 0x00], 26 | 'd_off': [0x19, 0x10, 0x06, 0x00, 0x00], 27 | 'all_on': [0x19, 0x10, 0x04, 0x03, 0x00], 28 | 'all_off': [0x19, 0x10, 0x08, 0x0D, 0x00] 29 | } 30 | 31 | def main(code): 32 | """ main function """ 33 | pi = gpio.pi(host=HOST) 34 | if not pi.connected: 35 | exit() 36 | 37 | # prepare GPIO-Pins 38 | pi.set_mode(RESET, gpio.OUTPUT) 39 | pi.set_mode(DATA, gpio.OUTPUT) 40 | pi.set_pull_up_down(DATA, gpio.PUD_OFF) 41 | pi.write(DATA, 0) 42 | 43 | # reset transmitter before use 44 | pi.write(RESET, 1) 45 | pi.write(RESET, 0) 46 | sleep(.005) 47 | 48 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 49 | # just to make sure SPI is working 50 | rx_data = rf.read_single(0x5A) 51 | if rx_data != 0x55: 52 | print("SPI Error") 53 | 54 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 55 | 56 | rf.write_burst(0x07, [0x6C, 0x80, 0x12]) # Frf: Carrier Frequency 433.93MHz 57 | 58 | # Use PA_BOOST 59 | rf.write_single(0x13, 0x0F) 60 | rf.write_single(0x5A, 0x5D) 61 | rf.write_single(0x5C, 0x7C) 62 | rf.write_single(0x11, 0b01111111) # Use PA_BOOST 63 | 64 | rf.write_single(0x18, 0b00000110) # Lna: 50 Ohm, highest gain 65 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 66 | 67 | # Transmit Mode 68 | rf.write_single(0x02, 0b01101000) # DataModul: continuous w/o bit sync, OOK, no shaping 69 | rf.write_single(0x01, 0b00001100) # OpMode: SequencerOn, TX 70 | 71 | # wait for ready 72 | while (rf.read_single(0x27) & 0x80) == 0: 73 | pass 74 | #print "waiting..." 75 | 76 | # delete existing waveforms 77 | pi.wave_clear() 78 | 79 | # calculate frame-data from command-line arguments 80 | data = np.empty(0, dtype=np.uint8) 81 | for item in code: 82 | data = np.append(data, np.array(int(item), dtype=np.uint8)) 83 | # how many consecutive frame repetitions 84 | repetitions = 8 85 | 86 | # create "0" pulse waveform 87 | pi.wave_add_generic([gpio.pulse(1< 0 else 0) 13 | manchester = convert_symbols(symbols) 14 | 15 | 16 | """ 17 | stop-command with custom transmitter 18 | 19 | $ sudo python3 transmitter.py stop 20 | key: 0xA0, ctrl: 0x01, rolling code: 0x05, address: 0x365240 21 | cksum: 13 22 | Data: 0xA5 0x1D 0x00 0x05 0x40 0x52 0x36 23 | Frame: 0xA5 0xB8 0xB8 0xBD 0xFD 0xAF 0x99 24 | 25 | extract 112 manchester encoded half-symbols via inspectrum 26 | """ 27 | manchester = np.array([0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1]) 28 | bits = np.ravel(np.where(np.reshape(manchester, (-1, 2)) == [0, 1], 1, 0))[::2] 29 | 30 | print("Bits: {} ({})".format(len(bits), bits)) 31 | payload = np.packbits(bits) 32 | print("Payload: "+''.join('0x{:02X} '.format(x) for x in payload)) 33 | 34 | frame = np.zeros(7, dtype=np.uint8) 35 | # de-obfuscation 36 | frame[0] = payload[0] 37 | for i in range(1, 7): 38 | frame[i] = payload[i] ^ payload[i-1] 39 | 40 | #print("Frame: "+''.join('0x{:02X} '.format(x) for x in frame)) 41 | 42 | # checksum calculation 43 | cksum = 0 44 | for i in range(0,7): 45 | cksum = cksum ^ frame[i] ^ (frame[i] >> 4) 46 | cksum = cksum & 0xf 47 | 48 | COMMANDS={ 49 | 0x00: 'null', 50 | 0x01: 'stop', 51 | 0x02: 'up', 52 | 0x04: 'down', 53 | 0x08: 'prog', 54 | } 55 | 56 | rolling_code = unpack(">H", frame[2:4])[0] 57 | address = unpack("> 4) & 0x0f 59 | 60 | print("Frame: "+''.join('0x{:02X} '.format(x) for x in frame)) 61 | print(" Checksum: {}".format("ok" if cksum==0 else "error")) 62 | print(" Key: 0x{:02X}".format(frame[0])) 63 | print(" Control: {} (0x{:02X})".format(COMMANDS[control], control)) 64 | print(" Rolling Code: {} (0x{})".format(rolling_code, ''.join('{:02X}'.format(x) for x in frame[2:4]))) 65 | print(" Address: {} (0x{})".format(address, ''.join('{:02X}'.format(x) for x in frame[7:3:-1]))) 66 | -------------------------------------------------------------------------------- /somfy/docs/data_pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/somfy/docs/data_pin.png -------------------------------------------------------------------------------- /somfy/docs/spectrum_magnitude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/somfy/docs/spectrum_magnitude.png -------------------------------------------------------------------------------- /somfy/docs/start_of_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/somfy/docs/start_of_frame.png -------------------------------------------------------------------------------- /somfy/epy_block_1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedded Python Blocks: 3 | 4 | Each time this file is saved, GRC will instantiate the first class it finds 5 | to get ports and parameters of your block. The arguments to __init__ will 6 | be the parameters. All of them are required to have default values! 7 | """ 8 | 9 | import numpy as np 10 | import scipy as sci 11 | from gnuradio import gr 12 | 13 | 14 | class blk(gr.sync_block): # other base classes are basic_block, decim_block, interp_block 15 | """Embedded Python Block example - a simple multiply const""" 16 | 17 | def __init__(self, bit_length=1e-6): # only default arguments here 18 | """arguments to this function show up as parameters in GRC""" 19 | gr.sync_block.__init__( 20 | self, 21 | name='Embedded Python Block', # will show up in GRC 22 | in_sig=[np.byte], 23 | out_sig=[np.byte] 24 | ) 25 | # if an attribute with the same name as a parameter is found, 26 | # a callback is registered (properties work, too). 27 | self.bit_length = bit_length 28 | self.state = 0 29 | self.preamble = 0 30 | self.position = 0 31 | print("length of one sample", bit_length) 32 | 33 | 34 | def work(self, input_items, output_items): 35 | """example: multiply with constant""" 36 | self.position = self.position + len(input_items[0]) 37 | if self.state == 0: 38 | if sum(input_items[0]) > 0: 39 | self.preamble = sum(input_items[0]) 40 | print("position of preamble",self.position-self.preamble) 41 | #print(len(input_items[0]), sum(input_items[0])) 42 | #print(input_items[0]) 43 | self.state = 1 44 | zeros = np.zeros(len(input_items[0])) 45 | zeros[np.nonzero(input_items[0])[0][0]]=1. 46 | output_items[0][:] = zeros 47 | #output_items[0][:] = np.gradient(input_items[0],0.5) 48 | else: 49 | output_items[0][:] = [0] 50 | elif self.state == 1: 51 | print(len(input_items[0]), sum(input_items[0])) 52 | if sum(input_items[0]) < len(input_items[0]): 53 | self.preamble = self.preamble + sum(input_items[0]) 54 | self.state = 2 55 | print("length of preamble",self.preamble) 56 | output_items[0][:] = np.gradient(input_items[0],0.5) 57 | else: 58 | self.preamble = self.preamble + sum(input_items[0]) 59 | output_items[0][:] = [1] 60 | else: 61 | output_items[0][:] = [1] 62 | 63 | # output_items[0][:] = input_items[0] * self.example_param 64 | # else: 65 | # output_items[0][:] = input_items[0] * self.example_param 66 | #output_items[0][:] = [2] 67 | return len(output_items[0]) 68 | -------------------------------------------------------------------------------- /somfy/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/somfy/lib/__init__.py -------------------------------------------------------------------------------- /somfy/lib/rfm69.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """RFM69-Class""" 4 | 5 | import pigpio as gpio 6 | 7 | # global defines 8 | ERROR = 1 9 | INFO = 2 10 | TRACE = 3 11 | 12 | class Rfm69(object): 13 | """RFM69-Class""" 14 | # pylint: disable=too-many-instance-attributes, C0301, C0103 15 | 16 | def __init__(self, host="localhost", port=8888, channel=0, baudrate=10000000, debug_level=0): 17 | # general variables 18 | self.debug_level = debug_level 19 | 20 | # RFM69-specific variables 21 | self.pi = gpio.pi(host, port) 22 | self.handle = self.pi.spi_open(channel, baudrate, 0) # Flags: CPOL=0 and CPHA=0 23 | 24 | def __enter__(self): 25 | return self 26 | 27 | def __exit__(self, exc_type, exc_value, traceback): 28 | """clean up stuff""" 29 | self.pi.spi_close(self.handle) 30 | self.pi.stop() 31 | 32 | def debug(self, message, level=0): 33 | """Debug output depending on debug level.""" 34 | if self.debug_level >= level: 35 | print(message) 36 | 37 | def read_single(self, address): 38 | """Read single register via spi""" 39 | (count, data) = self.pi.spi_xfer(self.handle, [address & 0x7F, 0x00]) 40 | return data[1] 41 | 42 | def write_single(self, address, value): 43 | """Write single register via spi""" 44 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80, value]) 45 | return count == 2 46 | 47 | def write_burst(self, address, data): 48 | """Write bytearray of data beginning at address""" 49 | (count, data) = self.pi.spi_xfer(self.handle, [address | 0x80] + data) 50 | return count == (len(data)+1) 51 | 52 | def write_config(self, cfg): 53 | """Write cfg-tuble like this: ((register1, value1), (register2, value2), ...)""" 54 | for i in range(0, len(cfg)): 55 | reg, val = cfg[i] 56 | self.write_single(reg, val) 57 | -------------------------------------------------------------------------------- /somfy/sniff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """OOK-Decoder""" 4 | 5 | from time import sleep 6 | import pigpio as gpio 7 | from lib.rfm69 import Rfm69 8 | import numpy as np 9 | #import matplotlib.pyplot as plt 10 | #from mpl_toolkits.axes_grid1 import make_axes_locatable 11 | 12 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 13 | RESET = 24 14 | DATA = 25 15 | 16 | # define pigpio-host 17 | HOST = "localhost" 18 | 19 | start_tick = 0 20 | state = 0 # 0=Idle, 1=Frame 21 | bits = np.empty(0, dtype=np.uint8) 22 | hw_sync = np.empty(0) 23 | tolerance = 100 #µs 24 | clock = 640 25 | 26 | histogram = [0, np.zeros(56)] 27 | 28 | def cbf(pin, level, tick): 29 | global start_tick 30 | global state 31 | global bits 32 | global hw_sync 33 | global clock 34 | global histogram 35 | 36 | # End of Gap 37 | if level == 1: 38 | delta = gpio.tickDiff(start_tick, tick) 39 | start_tick = tick 40 | # HW-Sync 41 | if (0 <= state <=2) and (delta in range(2500-2*tolerance, 2500+2*tolerance)): 42 | if state < 2: 43 | hw_sync = np.empty(0) 44 | hw_sync = np.append(hw_sync, [delta]) 45 | state = 2 46 | # long gap 47 | elif (state == 3) and (delta in range(2*clock-tolerance, 2*clock+tolerance)): 48 | bits = np.append(bits, [0, 0]) 49 | # short gap 50 | elif (state == 3) and (delta in range(clock-tolerance, clock+tolerance)): 51 | bits = np.append(bits, [0]) 52 | else: 53 | pass 54 | 55 | # End of Pulse 56 | elif level == 0: 57 | delta = gpio.tickDiff(start_tick, tick) 58 | start_tick = tick 59 | # wake-up pulse 60 | if (state == 0) and (delta in range(10050-tolerance, 10050+tolerance)): 61 | state = 1 62 | # HW-Sync 63 | elif (0 <= state <=2) and (delta in range(2500-2*tolerance, 2500+2*tolerance)): 64 | if state < 2: 65 | hw_sync = np.empty(0) 66 | hw_sync = np.append(hw_sync, [delta]) 67 | state = 2 68 | # start of frame mark 69 | elif (state == 2) and (delta in range(4850-2*tolerance, 4850+2*tolerance)): 70 | clock = int(np.average(hw_sync)/4) 71 | print("Clock Sync:", hw_sync, clock) 72 | bits = np.empty(0, dtype=np.uint8) 73 | state = 3 74 | # long pulse 75 | elif (state == 3) and (delta in range(2*clock-tolerance, 2*clock+tolerance)): 76 | bits = np.append(bits, [1, 1]) 77 | # short pulse 78 | elif (state == 3) and (delta in range(clock-tolerance, clock+tolerance)): 79 | bits = np.append(bits, [1]) 80 | else: 81 | pass 82 | 83 | # Watchdog timeout 84 | elif (level == 2) and (state > 0): 85 | # skip first bit, because it is part of the start of frame mark 86 | bits = bits[1::] 87 | 88 | # append one zero-bit, in case the last bit was a one and the last zero-bit can't be detected, because the frame is over 89 | if bits.size < 112: 90 | bits = np.append(bits, [0]) 91 | if bits.size == 112: 92 | # decode manchester (rising edge = 1, falling edge = 0) 93 | decoded = np.ravel(np.where(np.reshape(bits, (-1, 2)) == [0, 1], 1, 0))[::2] 94 | 95 | histogram[1] = histogram[1] + decoded 96 | histogram[0] += 1 97 | 98 | frame = np.packbits(decoded) 99 | print("Raw: "+''.join('0x{:02X} '.format(x) for x in frame)) 100 | 101 | for i in range(frame.size-1, 0, -1): 102 | frame[i] = frame[i] ^ frame[i-1] 103 | 104 | cksum = frame[0] ^ (frame[0] >> 4) 105 | for i in range(1,7): 106 | cksum = cksum ^ frame[i] ^ (frame[i] >> 4) 107 | cksum = cksum & 0x0f 108 | 109 | print("Frame: "+''.join('0x{:02X} '.format(x) for x in frame)) 110 | print(" Control: 0x{:02X}".format((frame[1] >> 4) & 0x0f)) 111 | print(" Checksum: {}".format("ok" if cksum==0 else "error")) 112 | print(" Address: "+''.join('{:02X} '.format(x) for x in frame[4:7])) 113 | print(" Rolling Code: "+''.join('{:02X} '.format(x) for x in frame[2:4])) 114 | else: 115 | pass 116 | bits = np.empty(0, dtype=np.uint8) 117 | state = 0 118 | else: 119 | pass 120 | 121 | def main(): 122 | """ main function """ 123 | pi = gpio.pi(host=HOST) 124 | if not pi.connected: 125 | exit() 126 | pi.set_mode(RESET, gpio.OUTPUT) 127 | pi.set_mode(DATA, gpio.OUTPUT) 128 | pi.set_pull_up_down(DATA, gpio.PUD_DOWN) 129 | pi.write(DATA, 0) 130 | 131 | pi.write(RESET, 1) 132 | pi.write(RESET, 0) 133 | 134 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 135 | 136 | # just to make sure SPI is working 137 | rx_data = rf.read_single(0x5A) 138 | if rx_data != 0x55: 139 | print("SPI Error") 140 | 141 | # configure 142 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 143 | 144 | rf.write_burst(0x07, [0x6C, 0x5A, 0xE1]) # Frf: Carrier Frequency 433.42MHz/61.035 145 | 146 | # rf.write_single(0x18, 0b00000000) # Lna: 50 Ohm, auto gain 147 | 148 | # rf.write_single(0x19, 0b01001001) # RxBw: 4% DCC, BW=100kHz 149 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 150 | 151 | # make sure we conserve the thesold setting in between WUP and HW-Sync 152 | rf.write_single(0x1B, 0b01000011) # ThresType: Peak, Decrement RSSI thresold once every 8 chips (max) 153 | # rf.write_single(0x1D, 50) # OokFix 154 | 155 | # Receive 156 | rf.write_single(0x02, 0b01101000) # DataModul: OOK, continuous w/o bit sync 157 | rf.write_single(0x01, 0b00010000) # OpMode: SequencerOn, RX 158 | 159 | # wait until RFM-Module is ready 160 | while (rf.read_single(0x27) & 0x80) == 0: 161 | print("waiting...") 162 | 163 | # filter high frequency noise 164 | pi.set_glitch_filter(DATA, 150) 165 | 166 | # watchdog to detect end of frame (20ms) 167 | pi.set_watchdog(DATA, 20) 168 | 169 | # watch DATA pin 170 | callback = pi.callback(DATA, gpio.EITHER_EDGE, cbf) 171 | 172 | print("Scanning... Press Ctrl-C to abort") 173 | while 1: 174 | sleep(1) 175 | 176 | pi.stop() 177 | 178 | def show_histogram(matrix, normalize=1): 179 | fig, ax = plt.subplots() 180 | matrix = matrix/normalize 181 | img1 = ax.imshow(matrix) 182 | divider = make_axes_locatable(ax) 183 | cax = divider.append_axes("right", size="5%", pad=0.1) 184 | fig.colorbar(img1, cax=cax) 185 | plt.show() 186 | 187 | if __name__ == "__main__": 188 | try: 189 | main() 190 | except KeyboardInterrupt: 191 | print("") 192 | finally: 193 | print("done") 194 | # if histogram[0] > 0: 195 | # show_histogram(np.reshape(histogram[1], (-1, 8)), normalize=histogram[0]) 196 | -------------------------------------------------------------------------------- /somfy/transmitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Transmit mumbi power outlet codes""" 4 | 5 | import sys 6 | from struct import pack 7 | from time import sleep 8 | import pigpio as gpio 9 | from lib.rfm69 import Rfm69 10 | import numpy as np 11 | import json 12 | import os 13 | 14 | # define pigpio GPIO-pins where RESET- and DATA-Pin of RFM69-Transceiver are connected 15 | RESET = 24 16 | DATA = 25 17 | 18 | # define pigpio-host 19 | HOST = "localhost" 20 | 21 | COMMANDS={ 22 | 'null': 0x00, 23 | 'stop': 0x01, 24 | 'STOP': 0x01, # alias for home-automation 25 | 'up': 0x02, 26 | 'CLOSE': 0x02, # alias for home-automation 27 | 'down': 0x04, 28 | 'OPEN': 0x04, # alias for home-automation 29 | 'prog': 0x08, # USE WITH CARE 30 | } 31 | 32 | config=None 33 | 34 | clock = 640 35 | 36 | config_file = os.path.join(os.path.dirname(__file__), "config.json") 37 | 38 | def main(code): 39 | """ main function """ 40 | pi = gpio.pi(host=HOST) 41 | if not pi.connected: 42 | exit() 43 | 44 | # prepare GPIO-Pins 45 | pi.set_mode(RESET, gpio.OUTPUT) 46 | pi.set_mode(DATA, gpio.OUTPUT) 47 | pi.set_pull_up_down(DATA, gpio.PUD_OFF) 48 | pi.write(DATA, 0) 49 | 50 | # reset transmitter before use 51 | pi.write(RESET, 1) 52 | pi.write(RESET, 0) 53 | sleep(.005) 54 | 55 | # load current config 56 | try: 57 | with open(config_file) as f: 58 | config = json.load(f) 59 | except: 60 | config = {"rolling_code": 1, "key": 160, "address": 1} 61 | 62 | # update config 63 | config["rolling_code"] += 1 64 | 65 | with Rfm69(host=HOST, channel=0, baudrate=32000, debug_level=0) as rf: 66 | # just to make sure SPI is working 67 | rx_data = rf.read_single(0x5A) 68 | if rx_data != 0x55: 69 | print("SPI Error") 70 | 71 | rf.write_single(0x01, 0b00000100) # OpMode: STDBY 72 | 73 | freq = 433_420_000 # 433.42 MHz 74 | f_step = 32_000_000 / 2**19 # 32 MHz XO 75 | reg_frf = int(freq / f_step) 76 | rf.write_burst(0x07, [(reg_frf>>16) & 0xff, (reg_frf>>8) & 0xff, (reg_frf) & 0xff]) # Frf: Carrier Frequency 77 | 78 | # Use PA_BOOST 79 | rf.write_single(0x13, 0x0F) 80 | rf.write_single(0x5A, 0x5D) 81 | rf.write_single(0x5C, 0x7C) 82 | rf.write_single(0x11, 0b01111111) # Use PA_BOOST 83 | 84 | rf.write_single(0x18, 0b00000110) # Lna: 50 Ohm, highest gain 85 | rf.write_single(0x19, 0b01000000) # RxBw: 4% DCC, BW=250kHz 86 | 87 | # Transmit Mode 88 | rf.write_single(0x02, 0b01101000) # DataModul: continuous w/o bit sync, OOK, no shaping 89 | rf.write_single(0x01, 0b00001100) # OpMode: SequencerOn, TX 90 | 91 | # wait for ready 92 | while (rf.read_single(0x27) & 0x80) == 0: 93 | pass 94 | #print "waiting..." 95 | 96 | # delete existing waveforms 97 | pi.wave_clear() 98 | 99 | # calculate frame-data from command-line arguments 100 | print("key: 0x{:02X}, ctrl: 0x{:02X}, rolling code: 0x{:02X}, address: 0x{:02X}".format(config["key"], code, config["rolling_code"], config["address"])) 101 | 102 | data = pack(">BBH", config["key"] | (config["rolling_code"] & 0x0f), code << 4, config["rolling_code"]) 103 | data += pack("> 4) 110 | cksum = cksum & 0xf 111 | print("cksum: {}".format(cksum)) 112 | 113 | frame[1] = frame[1] | cksum 114 | print("Data: "+''.join('0x{:02X} '.format(x) for x in frame)) 115 | 116 | # data whitening/obfuscation 117 | for i in range(1, frame.size): 118 | frame[i] = frame[i] ^ frame[i-1] 119 | 120 | print("Frame: "+''.join('0x{:02X} '.format(x) for x in frame)) 121 | 122 | # how many consecutive frame repetitions (besides the one that is transmitted anyway); set to 0 for no repetitions; increase for robustness 123 | repetitions = 16 if code == COMMANDS["prog"] else 0 124 | 125 | # create wakeup pulse waveform 126 | pi.wave_add_generic([gpio.pulse(1<= 2: 43 | cmd = sys.argv[1].lower() 44 | if cmd in COMMANDS: 45 | main(COMMANDS[cmd]) 46 | else: 47 | print("Unknown command:", cmd) 48 | else: 49 | print("argument missing: open|close|down") 50 | except KeyboardInterrupt: 51 | print("KeyboardInterrupt") 52 | # just make sure we don't transmit forever 53 | pi = gpio.pi(host=HOST) 54 | pi.stop() 55 | finally: 56 | #print("done") 57 | pass 58 | -------------------------------------------------------------------------------- /velux/wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/sdr/1cc6ced2d3f9a4234b975265c0321b9ce1612cbd/velux/wiring.jpg --------------------------------------------------------------------------------