├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── acurite.c ├── datum.c ├── datum.h ├── go ├── .gitignore └── src │ ├── ook │ ├── bitstream.go │ ├── files.go │ ├── ook.go │ └── quantify.go │ ├── ookanalyze │ └── ookanalyze.go │ ├── ooklog │ └── ooklog.go │ └── ookplay │ └── ookplay.go ├── img ├── README ├── architecture.png └── architecture.xml ├── man ├── .gitignore ├── ookd.1.md ├── ookdump.1.md └── oregonsci.1.md ├── nexa.c ├── ook.c ├── ook.h ├── ookd.c ├── ookdump.c ├── oregonsci.c ├── rtl.c ├── rtl.h ├── samples ├── samp1.dat └── samp2.dat ├── wh1080.c └── ws2300.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.d 3 | gmon.out 4 | ookd 5 | ookdump 6 | wh1080 7 | oregonsci 8 | ws2300 9 | nexa 10 | acurite 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jim Studt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PREFIX = /usr/local 3 | 4 | # 5 | # See what our hardware machine is and our compiler (gcc or clang) 6 | # 7 | MACHINE = $(shell uname -m) 8 | COMPILER = $(shell $(CC) 2>&1 | ( fgrep -q clang && echo clang || echo gcc ) ) 9 | 10 | DEBUGFLAGS_gcc = -pg 11 | DEBUGFLAGS = -pg -g -O0 $(DEBUGFLAGS_$(COMPILER)) 12 | #DEBUGFLAGS = 13 | 14 | FLOATFLAGS_clang = -ffast-math -O3 15 | FLOATFLAGS_gcc = -ffast-math -O3 16 | FLOATFLAGS_armv7l_gcc = -ftree-vectorize -mfpu=neon 17 | FLOATFLAGS = $(FLOATFLAGS_$(MACHINE)_$(COMPILER)) $(FLOATFLAGS_$(COMPILER)) 18 | 19 | COMPILERFLAGS_gcc = 20 | COMPILERFLAGS_clang = 21 | COMPILERFLAGS = -std=c99 $(COMPILERFLAGS_$(COMPILER)) 22 | 23 | LDLIBS += -lm 24 | 25 | CPPFLAGS = -MMD 26 | 27 | CFLAGS = $(COMPILERFLAGS) -Wall -Werror -D_POSIX_C_SOURCE=200112L -D_BSD_SOURCE=1 -D_DEFAULT_SOURCE=1 -D_DARWIN_C_SOURCE=1 $(DEBUGFLAGS) $(FLOATFLAGS) 28 | DAEMON_LDLIBS = -lrtlsdr 29 | 30 | ifeq ("$(shell uname)", "Darwin") 31 | LINK.c += -L /usr/local/lib 32 | CPPFLAGS += -I /usr/local/include 33 | endif 34 | 35 | MANPAGES = man/ookd.1 man/ookdump.1 man/oregonsci.1 36 | CLIENTS = ookdump wh1080 oregonsci ws2300 nexa acurite 37 | 38 | all : daemon clients go-clients man-pages 39 | 40 | daemon : ookd 41 | 42 | clients : $(CLIENTS) 43 | 44 | go-clients : go/bin/ooklog go/bin/ookanalyze go/bin/ookplay 45 | 46 | go/bin/% : $(wildcard go/src/*/*.go ) 47 | ( cd go ; GOPATH=`pwd` go install $(@:go/bin/%=%) ) 48 | 49 | ookd : ookd.o rtl.o ook.o 50 | $(LINK.c) $^ $(LOADLIBES) $(DAEMON_LDLIBS) $(LDLIBS) -o $@ 51 | 52 | ookdump : ookdump.o ook.o 53 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 54 | 55 | wh1080 : wh1080.o ook.o 56 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 57 | 58 | ws2300 : ws2300.o ook.o 59 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 60 | 61 | acurite : acurite.o ook.o 62 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 63 | 64 | oregonsci : oregonsci.o ook.o datum.o 65 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 66 | 67 | nexa : nexa.o ook.o 68 | $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 69 | 70 | man-pages : $(MANPAGES) 71 | 72 | man/%.1 : man/%.1.md 73 | pandoc -s -t man -o $@ $< 74 | 75 | clean : 76 | rm -f *.o ookd $(CLIENTS) $(MANPAGES) 77 | 78 | install : ookd $(CLIENTS) 79 | install $^ $(PREFIX)/bin 80 | 81 | ookd.o : ook.h rtl.h 82 | 83 | ookdump.o wh1080.o oregonsci.o ws2300.o : ook.h datum.h 84 | 85 | .PHONY : clean all install 86 | 87 | 88 | include $(wildcard %.d) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ook-decoder 2 | =========== 3 | 4 | 5 | Ook-decoder reads On-Off Keying radio data commonly used in the 433MHz ISM bands using a software defined radio (SDR). 6 | 7 | Status - alpha 8 | -------------- 9 | 10 | ON HOLD: My test rig is 500 miles away and unreachable during winter months. The transmitter 11 | has failed, so no more development until the spring thaw comes and I can get a human out there. 12 | 13 | I'm just filling this repository now, you probably don't want to use it yet. 14 | 15 | Installing 16 | -------- 17 | 18 | $ make 19 | $ make install 20 | 21 | Abstract 22 | -------- 23 | 24 | On-Off-Keying (OOK) is a simple radio modulation scheme where the 25 | carrier is pulsed on and off to encode data. Morse code is a well known 26 | historical example. In modern times OOK is commonly used in the 27 | Industrial, Scientific, and Medical (ISM) radio bands (433MHz, 900MHz, and others) by simple 28 | telemetry devices like weather stations, remote thermometers, 29 | alarm systems, and other remote sensors. 30 | 31 | ### Modern OOK in ISM ### 32 | 33 | These devices are generally cobbled together by embedded system 34 | electrical engineers who didn't study advanced encoding techniques, 35 | believe in simplicity, and are using the smallest, cheapest 36 | microcontroller possible. Typically short pulses and long pulses are 37 | used to encode ones and zeros in an arbitrary polarity. Sometimes 38 | distance between the pulses carries the data, or carries bits of its 39 | own. Generally there are one or more start pulses to let the receiver's 40 | automatic gain control (AGC) sort itself out before the data comes 41 | rolling in. 42 | 43 | It seems unlikely to imagine a general solution to decode all of the 44 | protocols imagined by the engineers, to that end, this software is 45 | split into two layers. 46 | 47 | ### Structure of ook-decoder ### 48 | 49 | ![architecture diagram](img/architecture.png) 50 | 51 | **ookd** receives and analyzes the RF data looking for bursts of 52 | pulses. When it finds a pulse burst it multicasts the pulses to an 53 | arbitrary number of listens. ookd doesn't know anything 54 | about any specific protocol. Most of the computation takes place in 55 | ookd. In a typical environment ookd will receive and perform 56 | computation on 2MB/s and pass on less than 100B/s. You should not need 57 | to alter ookd to support a new device. 58 | 59 | Decoding processes receive the multicast pulse bursts and attempt 60 | to decode them. For development simplicity it is envisioned that each 61 | type of device will have its own decoder process. A typical decoder 62 | would look at the length of the pulse stream, the length of individual 63 | pulses, and the plausibility of the data to decide if it has received 64 | a valid message from a device it understands. It can then take 65 | whatever action it wishes. 66 | 67 | **ookdump** is a client which characterizes and dumps received 68 | bursts. You will find it useful for understanding your device's 69 | protocol and if you are lucky how to set the parameters to an already 70 | existing function to convert your pulse stream into an array of bytes. 71 | 72 | **wh1080** decodes weather information from Fine Offset wh1080/wh1081 based weather stations. These are sold under many brand names. Mine is an Ambient Weather. If you get 73 | a burst or two of 88 pulses every 45 seconds, you are probably a wh1080. This saves a json file with the current observations and also writes periodic files of accumulated data. 74 | 75 | **???** Add the LaCrosse reader here. 76 | 77 | **nexa** decodes ON/OFF signals for Nexa wireless units (http://www.nexa.se) of the smart home. This outputs the transmitter code to stdout and can also send statistics to StatsD server. 78 | 79 | The rtl-sdr library and the ook library itself are linked statically to 80 | avoid build complexity. 81 | 82 | 83 | Dependencies 84 | ------------ 85 | 86 | ### Unix-like OS ### 87 | 88 | I build and test on Linux and Mac OS X. Others probably work. If you feel you must send me a Windows patch, make it *very* clean. 89 | 90 | ### rtl-sdr ### 91 | [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr) uses the software defined radio in common USB TV receivers to capture raw RF data and deliver it to your process. Although I have licensed my code freely, this rtl-sdr is GPL2 and propagates its GPL2 requirements to my code if you distribute a binary. If that is a problem for you, then write your own rtl-sdr replacement. 92 | 93 | If you would rather use a hardware 433MHz receiver you could dispense with the SDR section and multicast pulse data directly from your receiver's data line. All of the decoders would still work. 94 | 95 | ### C99 compiler ### 96 | Either gcc or clang works fine. It also needs to support the non-standard ({ }) *statement expression* extension. (That see's limited use and you could patch around it easily if you need to.) Just be happy I refrained from using blocks in clang for the sake of gcc users. 97 | 98 | Notes 99 | ----- 100 | 101 | ### Performance ### 102 | 103 | On an original BeagleBone at 250ksps ookd will consume about 30% of the CPU. This is almost entirely because I am doing floating point math and I suspect it is all soft float because of my Debian variant. The clang code is faster than the gcc code with the compilers in Debian Wheezy. 104 | 105 | In general, ookd matters for performance, but the clients don't. The data reduction between the two is just too large for the clients to matter. 106 | 107 | ### Testing ### 108 | 109 | You can record a raw IQ data stream using something like... 110 | 111 | rtl_sdr -f 433900000 -s 250000 -n 25000000 /tmp/my-filename.iq 112 | 113 | ... and then play that back into `ookd` at high speed using the `-r` flag. 114 | 115 | ### Building on Mac OS X ### 116 | 117 | You will need the rtl-sdr library to build. You can install this with: 118 | 119 | $ brew install librtlsdr 120 | 121 | If you have to build rtl-sdr, you will need to install a bunch of utilities. I made it build with: 122 | 123 | $ sudo chmod 2775 /usr/local/include 124 | $ brew install cmake 125 | $ brew install pkgconfig 126 | $ brew install libusb 127 | $ mkdir build 128 | $ cd build 129 | $ cmake .. 130 | $ make 131 | $ make install 132 | 133 | 134 | Credits 135 | ------- 136 | 137 | [rtl-433](https://github.com/merbanan/rtl_433) was helpful for getting started. It provides a great deal of analysis of the 433MHz signals. 138 | -------------------------------------------------------------------------------- /acurite.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ook.h" 13 | 14 | #define ACURITE_MSGTYPE_5N1_WINDSPEED_WINDDIR_RAINFALL 0x31 15 | #define ACURITE_MSGTYPE_5N1_WINDSPEED_TEMP_HUMIDITY 0x38 16 | 17 | // From draythomp/Desert-home-rtl_433 18 | // matches acu-link internet bridge values 19 | // The mapping isn't circular, it jumps around. 20 | // units are 22.5 deg 21 | int const acurite_5n1_winddirections[] = { 22 | 14, // 0 - NW 23 | 11, // 1 - WSW 24 | 13, // 2 - WNW 25 | 12, // 3 - W 26 | 15, // 4 - NNW 27 | 10, // 5 - SW 28 | 0, // 6 - N 29 | 9, // 7 - SSW 30 | 3, // 8 - ENE 31 | 6, // 9 - SE 32 | 4, // a - E 33 | 5, // b - ESE 34 | 2, // c - NE 35 | 7, // d - SSE 36 | 1, // e - NNE 37 | 8, // f - S 38 | }; 39 | 40 | int verbose=0; 41 | 42 | struct report { 43 | uint32_t valid:1; 44 | uint32_t channel:2; 45 | uint32_t id:14; 46 | uint32_t temperature:12; 47 | uint32_t humidity:8; 48 | uint32_t batteryLow:1; 49 | uint32_t windValid:1; 50 | uint32_t wind10:10; // m/s * 10 51 | uint32_t direction:4; // 22.5 deg increments 52 | uint32_t rainValid:1; 53 | uint32_t rain:14; // mm 54 | }; 55 | static const uint8_t chanMap[] = { 3, 0, 2, 1}; 56 | 57 | 58 | static bool isStart( uint32_t high_ns, uint32_t low_ns) { 59 | return high_ns >= 600000 && high_ns <= 700000 && low_ns >= 500000 && low_ns <= 600000; 60 | } 61 | static bool isOne( uint32_t high_ns, uint32_t low_ns) { 62 | return high_ns >= 400000 && high_ns <= 500000 && low_ns >= 100000 && low_ns <= 220000; 63 | } 64 | static bool isZero( uint32_t high_ns, uint32_t low_ns) { 65 | return high_ns >= 200000 && high_ns <= 300000 && low_ns >= 300000 && low_ns <= 400000; 66 | } 67 | static bool isStop( uint32_t high_ns, uint32_t low_ns) { 68 | return high_ns >= 200000 && high_ns <= 300000 && low_ns >= 500000; 69 | } 70 | 71 | static struct report decode_acurite( const struct ook_burst *burst) { 72 | enum { IDLE=0, STARTS, CONTENT } state = IDLE; 73 | unsigned idleGarbage = 0; 74 | unsigned falseStarts = 0; 75 | 76 | uint8_t data[8]; 77 | uint32_t bits = 0; 78 | 79 | for ( uint32_t p = 0; p < burst->pulses; p++) { 80 | switch( state) { 81 | case IDLE: 82 | if ( isStart( burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds) ) { 83 | state = STARTS; 84 | } else { 85 | idleGarbage++; 86 | } 87 | break; 88 | case STARTS: 89 | if ( isStart(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds)) continue; 90 | if ( !isOne(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds) && 91 | !isZero(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds)) { 92 | state = IDLE; 93 | falseStarts++; 94 | continue; 95 | } 96 | state = CONTENT; 97 | bits = 0; 98 | memset( data, 0, sizeof data); 99 | 100 | // FALLTHROUGH!!!!!!! 101 | case CONTENT: 102 | if ( isOne(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds) ) { 103 | if ( bits < 8*sizeof(data)) { 104 | uint8_t byte = bits/8; 105 | uint8_t bit = 1 << (7 - bits%8); 106 | 107 | data[byte] |= bit; 108 | } 109 | bits++; 110 | } else if ( isZero(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds)) { 111 | bits++; 112 | } else if ( isStop(burst->pulse[p].hiNanoseconds, burst->pulse[p].lowNanoseconds)) { 113 | if (verbose) { 114 | fprintf(stderr, "At stop we have %d bits, ", bits); 115 | for ( uint8_t i = 0; i < bits/8; i++) fprintf(stderr, "%02x", data[i]); 116 | fprintf(stderr, "\n"); 117 | } 118 | 119 | state = IDLE; 120 | 121 | if ( bits % 8 != 0 ) { 122 | if (verbose) fprintf(stderr, "Not an integral number of bytes: %d bits\n", bits); 123 | continue; 124 | } 125 | if ( bits > 8 * sizeof(data)) { 126 | if (verbose) fprintf(stderr, "Bits overran buffer: %d bits\n", bits); 127 | continue; 128 | } 129 | 130 | uint8_t sum = 0; 131 | for ( uint8_t i = 0; i < bits/8 - 1; i++) sum += data[i]; 132 | 133 | if ( sum != data[ bits/8 - 1 ] ) { 134 | if (verbose) fprintf(stderr, "CRC invalid: %02x != %02x\n", sum, data[ bits/8 -1]); 135 | continue; 136 | } 137 | 138 | if ( bits < 56) { 139 | if ( verbose) fprintf(stderr, "Too short to be an acurite message: %d bits\n", bits); 140 | continue; 141 | } 142 | 143 | uint8_t battery = (data[2]>>6) & 1; 144 | uint8_t message = (data[2]) & 0x3f; 145 | uint8_t mParity = (data[2]>>7) & 1; 146 | 147 | if ( mParity != ( (__builtin_popcount( message) + battery) & 1)) { 148 | if ( verbose) fprintf(stderr, "parity error in message code\n"); 149 | continue; 150 | } 151 | 152 | switch( message) { 153 | case 4: // 592TXR 154 | { 155 | if ( bits != 56) { 156 | if ( verbose) fprintf(stderr, "592TXR message is not 56 bits\n"); 157 | continue; 158 | } 159 | 160 | uint8_t channel = chanMap[ (data[0]>>6) & 0x03]; 161 | uint16_t id = (( data[0] & 0x3f)<<8) | data[1]; 162 | 163 | uint8_t humidity = data[3] & 0x7f; 164 | uint8_t hParity = (data[3]>>7) & 1; 165 | if ( hParity != (__builtin_popcount( humidity) & 1)) { 166 | if ( verbose) fprintf(stderr, "parity error in humidity\n"); 167 | continue; 168 | } 169 | 170 | uint8_t tempHigh = data[4] & 0x7f; 171 | uint8_t tempLow = data[5] & 0x7f; 172 | uint16_t temperatureRaw = (tempHigh<<7) | tempLow; 173 | uint16_t temperature10 = temperatureRaw - 1000; 174 | uint8_t tHighParity = (data[4]>>7) & 1; 175 | uint8_t tLowParity = (data[5]>>7) & 1; 176 | 177 | if ( tHighParity != (__builtin_popcount( tempHigh) & 1) || 178 | tLowParity != (__builtin_popcount( tempLow) & 1)) { 179 | if ( verbose) fprintf(stderr, "parity error in temperature\n"); 180 | continue; 181 | } 182 | if ( verbose) fprintf(stderr, " chan=%d id=%d bat=%d msg=%d temperature=%.1f hum=%d\n", channel, id, battery, message, temperature10/10.0, humidity); 183 | struct report good = { .valid = 1, 184 | .channel = channel, 185 | .id = id, 186 | .batteryLow = !battery, 187 | .temperature = temperature10, 188 | .humidity = humidity}; 189 | return good; 190 | } 191 | case ACURITE_MSGTYPE_5N1_WINDSPEED_TEMP_HUMIDITY: 192 | case ACURITE_MSGTYPE_5N1_WINDSPEED_WINDDIR_RAINFALL: 193 | { 194 | if ( bits != 64) { 195 | if ( verbose) fprintf(stderr, "5-n-1 message is not 64 bits\n"); 196 | continue; 197 | } 198 | 199 | uint8_t channel = chanMap[ (data[0]>>6) & 0x03]; 200 | // uint8_t resend = (data[0])>>4 & 0x03; 201 | uint16_t id = (( data[0] & 0x0f)<<8) | data[1]; 202 | 203 | uint16_t pulsesPerFourSeconds = ((data[3]<<3) & 0xf8) | ((data[4]>>4) & 0x07); 204 | float windKmPerHour = pulsesPerFourSeconds * 0.8278 + 1.00; 205 | float windSpeedMetersPerSecond = pulsesPerFourSeconds == 0 ? 0.0 : windKmPerHour * 0.27778; 206 | 207 | static bool stashedTemperatureValid = false; 208 | static float stashedTemperature = 0; 209 | static uint8_t stashedHumidity = 0; 210 | 211 | static bool stashedDirectionValid = false; 212 | static uint8_t stashedDirection = 0; 213 | static uint16_t stashedRain = 0; 214 | 215 | switch (message) { 216 | case ACURITE_MSGTYPE_5N1_WINDSPEED_TEMP_HUMIDITY: 217 | { 218 | uint16_t tempRaw = ((data[4]<<7) & 0x780) | (data[5] & 0x7F); 219 | float temperatureF = (tempRaw - 400) * 0.1; 220 | float temperature = (temperatureF - 32.0)*(100.0/180.0); 221 | 222 | uint8_t humidity = (data[6] & 0x7f); 223 | if ( verbose) fprintf(stderr, " msg=%d chan=%d id=%d wind speed = %.1fm/s temp=%.1f hum=%d\n", message, channel, id, windSpeedMetersPerSecond, temperature, humidity); 224 | 225 | stashedTemperatureValid = true; 226 | stashedTemperature = temperature; 227 | stashedHumidity = humidity; 228 | 229 | if ( stashedDirectionValid) { 230 | struct report good = { .valid = 1, 231 | .channel = channel, 232 | .id = id, 233 | .batteryLow = !battery, 234 | .temperature = temperature*10.0, 235 | .humidity = humidity, 236 | .windValid = true, 237 | .wind10 = windSpeedMetersPerSecond * 10.0, 238 | .direction = stashedDirection, 239 | .rainValid = true, 240 | .rain = stashedRain, 241 | }; 242 | stashedDirectionValid = false; // we used it 243 | return good; 244 | } else { 245 | continue; 246 | } 247 | } 248 | case ACURITE_MSGTYPE_5N1_WINDSPEED_WINDDIR_RAINFALL: 249 | { 250 | uint8_t direction = acurite_5n1_winddirections[ data[4] & 0x0f]; 251 | float rainInches = ( ((data[5]<<7)&0x3f80) & (data[6] & 0x7f) ) / 100.0; 252 | float rainmm = rainInches * 25.4; 253 | 254 | stashedDirectionValid = true; 255 | stashedDirection = direction; 256 | stashedRain = rainmm; 257 | 258 | if ( verbose) fprintf(stderr, " msg=%d chan=%d id=%d wind speed = %.1fm/s windDir=%.1fdeg rain=%.1fmm\n", message, channel, id, windSpeedMetersPerSecond, direction*22.5, rainmm); 259 | if ( stashedTemperatureValid) { 260 | struct report good = { .valid = 1, 261 | .channel = channel, 262 | .id = id, 263 | .batteryLow = !battery, 264 | .temperature = stashedTemperature*10.0, 265 | .humidity = stashedHumidity, 266 | .windValid = true, 267 | .wind10 = windSpeedMetersPerSecond * 10.0, 268 | .direction = direction, 269 | .rainValid = true, 270 | .rain = rainmm, 271 | }; 272 | stashedTemperatureValid = false; // we used it 273 | return good; 274 | } else { 275 | continue; 276 | } 277 | } 278 | default: 279 | fprintf(stderr,"5n1 got stupid message: %d\n", message); 280 | continue; 281 | } 282 | continue; 283 | } 284 | default: 285 | if ( verbose) fprintf(stderr, "Unknown acurite message code: %d\n", message); 286 | continue; 287 | } 288 | } else { 289 | if ( verbose) fprintf(stderr, "Message fell apart in content.\n"); 290 | state = IDLE; 291 | } 292 | } 293 | } 294 | 295 | struct report bad = { .valid = 0 }; 296 | return bad; 297 | } 298 | 299 | static void writeReport( const struct report *report, const char *template) { 300 | char fn[1024]; 301 | char fnTemp[1025]; 302 | 303 | snprintf( fn, sizeof fn, "%s-%d-%d.json", template, report->channel, report->id); 304 | snprintf( fnTemp, sizeof fnTemp, "%s,", fn); 305 | 306 | FILE *f = fopen( fnTemp, "w"); 307 | if ( !f) { 308 | fprintf(stderr,"Failed to make temp file '%s': %s\n", fnTemp, strerror(errno)); 309 | return; 310 | } 311 | 312 | fprintf(f,"{\n"); 313 | fprintf(f,"\t\"channel\":%d,\n", report->channel); 314 | fprintf(f,"\t\"id\":%d,\n", report->id); 315 | fprintf(f,"\t\"temperature\":%.1f,\n", report->temperature / 10.0); 316 | fprintf(f,"\t\"humidity\":%d,\n", report->humidity); 317 | if ( report->windValid) { 318 | fprintf(f,"\t\"windspeed\":%.1f,\n", report->wind10/10.0); 319 | fprintf(f,"\t\"windbearing\":%.1f,\n", report->direction*22.5); 320 | } 321 | if ( report->rainValid) { 322 | fprintf(f,"\t\"rainfall\":%d,\n", report->rain); 323 | } 324 | fprintf(f,"\t\"batteryLow\":%d\n", report->batteryLow); 325 | fprintf(f,"}\n"); 326 | fclose(f); 327 | 328 | if (rename( fnTemp, fn)) { 329 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 330 | unlink(fnTemp); 331 | } 332 | } 333 | 334 | 335 | static void showHelp( FILE *f) 336 | { 337 | fprintf(f, 338 | "Usage: acurite [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface]\n" 339 | " -h | -? | --help display usage and exit\n" 340 | " -v | --verbose verbose logging\n" 341 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 342 | " -p port | --multicast-port port multicast port, default 3636\n" 343 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 344 | " -r path | --recent path path to most recent data, /tmp/current-weather, appends channel, identifier, and .json.\n" 345 | ); 346 | } 347 | 348 | 349 | int main( int argc, char **argv) 350 | { 351 | const char *multicastAddress = "236.0.0.1"; 352 | const char *multicastPort = "3636"; 353 | const char *multicastInterface = "127.0.0.1"; 354 | const char *recentFileName = "/tmp/current-weather"; 355 | 356 | // Handle options 357 | for(;;) { 358 | int optionIndex = 0; 359 | static struct option options[] = { 360 | { "verbose", no_argument, 0, 'v' }, 361 | { "help", no_argument, 0, 'h' }, 362 | { "multicast-address", required_argument, 0, 'a'}, 363 | { "multicast-port", required_argument, 0, 'p' }, 364 | { "multicast-interface", required_argument, 0, 'i' }, 365 | { "recent", required_argument, 0, 'r' }, 366 | { 0,0,0,0} 367 | }; 368 | 369 | int c = getopt_long( argc, argv, "vh?f:a:p:i:r:", options, &optionIndex ); 370 | if ( c == -1) break; 371 | 372 | switch(c) { 373 | case 'h': 374 | case '?': 375 | showHelp(stdout); 376 | return 0; 377 | case 'v': 378 | verbose = 1; 379 | break; 380 | case 'a': 381 | multicastAddress = optarg; 382 | break; 383 | case 'p': 384 | multicastPort = optarg; 385 | break; 386 | case 'i': 387 | multicastInterface = optarg; 388 | break; 389 | case 'r': 390 | recentFileName = optarg; 391 | break; 392 | default: 393 | fprintf(stderr,"Illegal option\n"); 394 | showHelp(stderr); 395 | exit(1); 396 | } 397 | } 398 | 399 | if ( verbose) fprintf(stderr,"Recent file is %s\n", recentFileName); 400 | 401 | // Parse our multicast address 402 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 403 | if ( sock < 0) { 404 | fprintf(stderr,"Failed to open multicast interface\n"); 405 | exit(1); 406 | } 407 | 408 | for (;;) { 409 | struct ook_burst *burst; 410 | struct sockaddr_storage addr; 411 | socklen_t addrLen = sizeof(addr); 412 | 413 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 414 | if ( e < 0) { 415 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 416 | break; 417 | } 418 | if ( e == 0) { 419 | fprintf(stderr,"Corrupt burst\n"); 420 | continue; 421 | } 422 | 423 | if ( verbose) fprintf(stderr, "Considering a %u pulse burst...\n", burst->pulses); 424 | struct report r = decode_acurite( burst); 425 | 426 | if ( r.valid) { 427 | writeReport( &r, recentFileName); 428 | } 429 | 430 | fflush(stdin); 431 | free(burst); 432 | } 433 | 434 | close(sock); 435 | return 0; 436 | } 437 | -------------------------------------------------------------------------------- /datum.c: -------------------------------------------------------------------------------- 1 | #include "datum.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | void resetDatum(struct datum *d) 8 | { 9 | memset(d,0,sizeof(*d)); 10 | } 11 | void resetCDatum(struct cdatum *d) 12 | { 13 | memset(d,0,sizeof(*d)); 14 | } 15 | 16 | 17 | void addSample( struct datum *d, double v) 18 | { 19 | if ( d->n==0) { 20 | d->minimum = v; 21 | d->maximum = v; 22 | } else { 23 | if ( v > d->maximum) d->maximum = v; 24 | if ( v < d->minimum) d->minimum = v; 25 | } 26 | d->n++; 27 | d->sum += v; 28 | d->sumOfSquares += v*v; 29 | } 30 | 31 | void addCSample( struct cdatum *d, double complex v) 32 | { 33 | if ( d->n==0) { 34 | d->minimum = v; 35 | d->maximum = v; 36 | } else { 37 | double mag = cabs(v); 38 | if ( mag > cabs(d->maximum)) d->maximum = v; 39 | if ( mag < cabs(d->minimum)) d->minimum = v; 40 | } 41 | d->n++; 42 | d->sum += v; 43 | d->sumOfSquares += v*v; 44 | } 45 | 46 | void addCSampleMA( struct cdatum *d, double magnitude, double angle) 47 | { 48 | addCSample( d, magnitude*cos(angle) + I*magnitude*sin(angle) ); 49 | } 50 | 51 | 52 | 53 | void dumpDatum( struct datum *d, const char *name, const char *units) 54 | { 55 | if ( d->n == 0) { 56 | fprintf(stderr,"%s no data\n", name); 57 | } else { 58 | fprintf(stderr, "%s %u samples, %5.1f %s min %5.1f max %5.1f\n", 59 | name, d->n, d->sum/d->n, units, d->minimum, d->maximum); 60 | } 61 | } 62 | 63 | void dumpCDatum( struct cdatum *d, const char *name, const char *units) 64 | { 65 | if ( d->n == 0) { 66 | fprintf(stderr,"%s no data\n", name); 67 | } else { 68 | double complex avg = creal(d->sum)/d->n + I*(cimag(d->sum)/d->n); 69 | 70 | fprintf(stderr, "%s %u samples, (%5.1f+%5.1fi)(%5.1f @ %5.1fdeg) %s min (%5.1f+%5.1fi) max (%5.1f+%5.1fi)\n", 71 | name, d->n, 72 | creal(avg),cimag(avg), 73 | cabs(avg), carg(avg), 74 | //cabs(avg), carg(avg)*360.0/M_2_PI, 75 | units, 76 | creal(d->minimum), cimag(d->minimum), 77 | creal(d->maximum), cimag(d->maximum)); 78 | } 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /datum.h: -------------------------------------------------------------------------------- 1 | #ifndef SAMPLES_IS_IN 2 | #define SAMPLES_IS_IN 3 | 4 | #include 5 | 6 | // Datum keeps enough information to sum them up and still compute the standard deviation 7 | struct datum { 8 | unsigned n; 9 | double sum; 10 | double sumOfSquares; 11 | double maximum; 12 | double minimum; 13 | }; 14 | 15 | // Datum keeps enough information to sum them up and still compute the standard deviation 16 | struct cdatum { 17 | unsigned n; 18 | double complex sum; 19 | double complex sumOfSquares; 20 | double complex maximum; 21 | double complex minimum; 22 | }; 23 | 24 | void resetDatum(struct datum *d); 25 | void resetCDatum(struct cdatum *d); 26 | 27 | void addSample( struct datum *d, double v); 28 | void addCSample( struct cdatum *d, double complex v); 29 | void addCSampleMA( struct cdatum *d, double magnitude, double angle); 30 | 31 | void dumpDatum( struct datum *d, const char *name, const char *units); 32 | void dumpCDatum( struct cdatum *d, const char *name, const char *units); 33 | 34 | 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /go/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | pkg 3 | -------------------------------------------------------------------------------- /go/src/ook/bitstream.go: -------------------------------------------------------------------------------- 1 | package ook 2 | 3 | import () 4 | 5 | type BitStream struct { 6 | bits []byte // one bit per byte to make things easy 7 | } 8 | 9 | type BitStreamReader struct { 10 | stream *BitStream 11 | thumb int 12 | } 13 | 14 | func cleanBit(b byte) int { 15 | if b == 0 { 16 | return 0 17 | } else { 18 | return 1 19 | } 20 | } 21 | 22 | func nibbleLSB(b []byte, t int) int { 23 | return cleanBit(b[t]) + 2*cleanBit(b[t+1]) + 4*cleanBit(b[t+2]) + 8*cleanBit(b[t+3]) 24 | } 25 | 26 | func NewBitStream(sizedFor int) *BitStream { 27 | return &BitStream{ 28 | bits: make([]byte, 0, sizedFor), 29 | } 30 | } 31 | 32 | func (bs *BitStream) Add(v int) { 33 | if v != 0 && v != 1 { 34 | panic("illegal bit") 35 | } 36 | bs.bits = append(bs.bits, byte(v)) 37 | } 38 | 39 | func (bs *BitStream) Reader() *BitStreamReader { 40 | return &BitStreamReader{ 41 | stream: bs, 42 | } 43 | } 44 | 45 | func (bs *BitStreamReader) EOF() bool { 46 | return bs.thumb >= len(bs.stream.bits) 47 | } 48 | 49 | func (bs *BitStreamReader) PeekBit() int { 50 | if bs.EOF() { 51 | panic("overread") 52 | } 53 | return cleanBit(bs.stream.bits[bs.thumb]) 54 | } 55 | 56 | func (bs *BitStreamReader) GetBit() int { 57 | if bs.EOF() { 58 | panic("overread") 59 | } 60 | bit := bs.stream.bits[bs.thumb] 61 | bs.thumb++ 62 | return cleanBit(bit) 63 | } 64 | 65 | func (bs *BitStreamReader) UngetNBits(n int) { 66 | p := bs.thumb - n 67 | if p < 0 { 68 | panic("over unget") 69 | } 70 | bs.thumb = p 71 | } 72 | 73 | func (bs *BitStreamReader) UngetBit() { 74 | bs.UngetNBits(1) 75 | } 76 | 77 | func (bs *BitStreamReader) PeekNibbleLSB() (int, bool) { 78 | if bs.thumb+4 > len(bs.stream.bits) { 79 | return 0, false 80 | } 81 | return nibbleLSB(bs.stream.bits, bs.thumb), true 82 | } 83 | func (bs *BitStreamReader) GetNibbleLSB() (int, bool) { 84 | v, ok := bs.PeekNibbleLSB() 85 | if !ok { 86 | return 0, false 87 | } 88 | bs.thumb += 4 89 | return v, true 90 | } 91 | 92 | func (bs *BitStreamReader) UngetNibble() { 93 | bs.UngetNBits(4) 94 | } 95 | 96 | func (bsr *BitStreamReader) RemainingBits() string { 97 | h := "" 98 | for i := 0; !bsr.EOF(); i++ { 99 | b := bsr.GetBit() 100 | if b == 0 { 101 | h += "0" 102 | } else if b == 1 { 103 | h += "1" 104 | } else { 105 | h += "?" 106 | } 107 | if i%4 == 3 { 108 | h += " " 109 | } 110 | } 111 | return h 112 | } 113 | -------------------------------------------------------------------------------- /go/src/ook/files.go: -------------------------------------------------------------------------------- 1 | package ook 2 | 3 | import ( 4 | "archive/tar" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | type Reader interface { 11 | Read() ( *Burst, error) 12 | Close() 13 | } 14 | 15 | type Writer interface { 16 | Write( *Burst) error 17 | Close() 18 | } 19 | 20 | type tarWriter struct { 21 | sequence int 22 | w *tar.Writer 23 | } 24 | 25 | func NewTarWriter( sink io.Writer) Writer { 26 | return &tarWriter{ 27 | w: tar.NewWriter(sink), 28 | sequence: 1, 29 | } 30 | } 31 | 32 | func (tw *tarWriter) Write( burst *Burst) error { 33 | encoded,err := burst.Encode( ) 34 | if err != nil { 35 | return fmt.Errorf("Failed to encode burst: %s", err.Error()) 36 | } 37 | 38 | head := tar.Header{ 39 | Name: fmt.Sprintf("%04d-%#v.burst", tw.sequence, burst.Position), 40 | Mode: 0775, 41 | Size: int64(len(encoded)), 42 | ModTime: time.Now(), 43 | Typeflag: tar.TypeReg, 44 | } 45 | tw.sequence = tw.sequence + 1 46 | if err := tw.w.WriteHeader(&head); err != nil { 47 | return fmt.Errorf("Failed to write tar header: %s", err.Error()) 48 | } 49 | n,err := tw.w.Write( encoded) 50 | if err != nil { 51 | return fmt.Errorf("Failed to write tar contents: %s", err.Error()) 52 | } 53 | if n != len(encoded) { 54 | return fmt.Errorf("Got wrong length written to tar file") 55 | } 56 | return nil 57 | } 58 | 59 | func (tw *tarWriter) Close() { 60 | tw.w.Close() 61 | } 62 | 63 | 64 | type tarReader struct { 65 | r *tar.Reader 66 | } 67 | 68 | func (tr *tarReader) Read () (*Burst, error) { 69 | header,err := tr.r.Next() 70 | if err != nil { 71 | return nil,err 72 | } 73 | 74 | buf := make( []byte, header.Size ) 75 | 76 | n,err := tr.r.Read( buf) 77 | if n != int(header.Size) { 78 | return nil,fmt.Errorf("Short read in tarReader") 79 | } 80 | if err != nil { 81 | return nil,err 82 | } 83 | 84 | burst,used,err := DecodeBurst(buf) 85 | if err != nil { 86 | return nil,err 87 | } 88 | _ = used 89 | 90 | return burst,nil 91 | } 92 | 93 | func (tr *tarReader) Close() { 94 | // odd, there isn't a close on tar.Reader. 95 | // I guess we have to read the whole thing and throw it away. 96 | // worry about that later 97 | } 98 | 99 | func OpenFile( source io.Reader) (Reader,error) { 100 | return &tarReader{ r: tar.NewReader(source) }, nil 101 | } 102 | -------------------------------------------------------------------------------- /go/src/ook/ook.go: -------------------------------------------------------------------------------- 1 | package ook 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "net" 9 | "time" 10 | ) 11 | 12 | type Pulse struct { 13 | High uint32 14 | Low uint32 15 | Frequency int32 16 | } 17 | 18 | type Burst struct { 19 | Position time.Duration 20 | Pulses []Pulse 21 | } 22 | 23 | type BurstHandler func(*Burst) bool 24 | 25 | func (burst *Burst) Encode() ([]byte, error) { 26 | buf := bytes.NewBuffer([]byte{}) 27 | 28 | version := uint32(0x36360001) 29 | position := uint64(burst.Position) 30 | count := uint32(len(burst.Pulses)) 31 | 32 | if err := binary.Write(buf, binary.LittleEndian, &version); err != nil { 33 | return []byte{}, err 34 | } 35 | if err := binary.Write(buf, binary.LittleEndian, &position); err != nil { 36 | return []byte{}, err 37 | } 38 | if err := binary.Write(buf, binary.LittleEndian, &count); err != nil { 39 | return []byte{}, err 40 | } 41 | 42 | for _, p := range burst.Pulses { 43 | hi := uint32(p.High) 44 | lo := uint32(p.Low) 45 | freq := int32(p.Frequency) 46 | 47 | if err := binary.Write(buf, binary.LittleEndian, &hi); err != nil { 48 | return []byte{}, err 49 | } 50 | if err := binary.Write(buf, binary.LittleEndian, &lo); err != nil { 51 | return []byte{}, err 52 | } 53 | if err := binary.Write(buf, binary.LittleEndian, &freq); err != nil { 54 | return []byte{}, err 55 | } 56 | } 57 | 58 | return buf.Bytes(), nil 59 | } 60 | 61 | func DecodeBurst(data []byte) (*Burst, int, error) { 62 | version := uint32(0) 63 | position := uint64(0) 64 | count := uint32(0) 65 | 66 | buf := bytes.NewBuffer(data) 67 | 68 | if err := binary.Read(buf, binary.LittleEndian, &version); err != nil { 69 | return nil, 0, err 70 | } 71 | if version != uint32(0x36360001) { 72 | return nil, 0, fmt.Errorf("Bad version in burst packet") 73 | } 74 | 75 | if err := binary.Read(buf, binary.LittleEndian, &position); err != nil { 76 | return nil, 0, err 77 | } 78 | if err := binary.Read(buf, binary.LittleEndian, &count); err != nil { 79 | return nil, 0, err 80 | } 81 | 82 | pulses := make([]Pulse, 0, count) 83 | for i := 0; i < int(count); i++ { 84 | hi := uint32(0) 85 | lo := uint32(0) 86 | freq := int32(0) 87 | 88 | if err := binary.Read(buf, binary.LittleEndian, &hi); err != nil { 89 | return nil, 0, err 90 | } 91 | if err := binary.Read(buf, binary.LittleEndian, &lo); err != nil { 92 | return nil, 0, err 93 | } 94 | if err := binary.Read(buf, binary.LittleEndian, &freq); err != nil { 95 | return nil, 0, err 96 | } 97 | 98 | pulses = append(pulses, Pulse{High: hi, Low: lo, Frequency: freq}) 99 | } 100 | 101 | // need to say how much we used 102 | return &Burst{Position: time.Duration(position), Pulses: pulses}, 0, nil 103 | } 104 | 105 | func ListenTo(iface *net.Interface, addr *net.UDPAddr, burstChannel chan *Burst) error { 106 | conn, err := net.ListenMulticastUDP("udp", iface, addr) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | body := func() { 112 | defer conn.Close() 113 | 114 | buf := make([]byte, 65536) 115 | 116 | for { 117 | count, err := conn.Read(buf) 118 | if err != nil { 119 | log.Fatalf("Bad read in ListenTo: %s", err.Error()) 120 | } 121 | 122 | burst, used, err := DecodeBurst(buf[0:count]) 123 | if err != nil { 124 | log.Printf("Bad decode in ListenTo: %s", err.Error()) 125 | } 126 | _ = used 127 | 128 | burstChannel <- burst 129 | } 130 | } 131 | 132 | go body() 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /go/src/ook/quantify.go: -------------------------------------------------------------------------------- 1 | package ook 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | ) 8 | 9 | type Symbol int 10 | 11 | const ( 12 | Spurious Symbol = iota 13 | HighShort 14 | HighLong 15 | LowShort 16 | LowLong 17 | EndOfTransmission 18 | ) 19 | 20 | var symbolNames = map[Symbol]string{ 21 | Spurious: "?", 22 | HighShort: "-", 23 | HighLong: "--", 24 | LowShort: "_", 25 | LowLong: "__", 26 | EndOfTransmission: ".", 27 | } 28 | 29 | func (s Symbol) String() string { 30 | v, ok := symbolNames[s] 31 | if ok { 32 | return v 33 | } else { 34 | return "!" 35 | } 36 | } 37 | 38 | type ClusterDescription struct { 39 | Min int 40 | Max int 41 | Count int 42 | Sum int 43 | Sum2 int 44 | Sym Symbol 45 | } 46 | type ClusterDescriptions []*ClusterDescription 47 | 48 | func AssignSymbols(highs, lows ClusterDescriptions) { 49 | if len(highs) == 2 { 50 | highs[0].Sym = HighShort 51 | highs[1].Sym = HighLong 52 | } 53 | if len(lows) == 3 { 54 | lows[0].Sym = LowShort 55 | lows[1].Sym = LowLong 56 | if lows[2].Count == 1 { 57 | lows[2].Sym = EndOfTransmission 58 | } 59 | } else if len(lows) == 2 && lows[1].Count == 1 { 60 | lows[0].Sym = LowShort 61 | lows[1].Sym = EndOfTransmission 62 | } 63 | } 64 | 65 | func (s ClusterDescriptions) Lookup(duration int) *ClusterDescription { 66 | for _, c := range s { 67 | if duration >= c.Min && duration <= c.Max { 68 | return c 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func CombineClusters(a, b *ClusterDescription) *ClusterDescription { 75 | r := ClusterDescription{ 76 | Count: a.Count + b.Count, 77 | Sum: a.Sum + b.Sum, 78 | Sum2: a.Sum2 + b.Sum2, 79 | } 80 | if a.Min < b.Min { 81 | r.Min = a.Min 82 | } else { 83 | r.Min = b.Min 84 | } 85 | 86 | if a.Max > b.Max { 87 | r.Max = a.Max 88 | } else { 89 | r.Max = b.Max 90 | } 91 | 92 | return &r 93 | } 94 | 95 | func FoldLeadingRunt(s ClusterDescriptions, firstLength int) ClusterDescriptions { 96 | if len(s) < 2 { 97 | return s 98 | } 99 | 100 | for i, c := range s { 101 | if c.Min > firstLength { 102 | break 103 | } 104 | if c.Count == 1 && c.Min == firstLength && i+1 < len(s) { 105 | r := []*ClusterDescription{} 106 | if i > 0 { 107 | r = append(r, s[0:i]...) 108 | } 109 | r = append(r, CombineClusters(s[i], s[i+1])) 110 | r = append(r, s[i+2:]...) 111 | return r 112 | } 113 | } 114 | return s 115 | } 116 | 117 | type byMin ClusterDescriptions 118 | 119 | func (a byMin) Len() int { return len(a) } 120 | func (a byMin) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 121 | func (a byMin) Less(i, j int) bool { return a[i].Min < a[j].Min } 122 | 123 | func Quantify(burst *Burst) { 124 | //bins := 4 125 | 126 | highs := make([]int, len(burst.Pulses)) 127 | lows := make([]int, len(burst.Pulses)) 128 | 129 | for i, p := range burst.Pulses { 130 | highs[i] = int(p.High) 131 | lows[i] = int(p.Low) 132 | } 133 | 134 | // LLoyds(highs, bins) 135 | //LLoyds(lows, bins) 136 | 137 | tolerance := 0.2 138 | verbose := false 139 | highClusters := GuessAndGrow(highs, tolerance, verbose) 140 | highClusters = FoldLeadingRunt(highClusters, int(burst.Pulses[0].High)) 141 | lowClusters := GuessAndGrow(lows, tolerance, verbose) 142 | 143 | AssignSymbols(highClusters, lowClusters) 144 | 145 | for n, c := range highClusters { 146 | s := "" 147 | if c.Count == 1 { 148 | s += " single" 149 | if c.Min == highs[0] { 150 | s += " first" 151 | } 152 | if c.Min == highs[len(highs)-1] { 153 | s += " last" 154 | } 155 | } 156 | log.Printf("high %d - %d..%d%s", n, c.Min, c.Max, s) 157 | } 158 | for n, c := range lowClusters { 159 | s := "" 160 | if c.Count == 1 { 161 | s += " single" 162 | if c.Min == lows[0] { 163 | s += " first" 164 | } 165 | if c.Min == lows[len(lows)-1] { 166 | s += " last" 167 | } 168 | } 169 | log.Printf("low %d - %d..%d%s", n, c.Min, c.Max, s) 170 | } 171 | 172 | syms := make([]Symbol, 0, len(burst.Pulses)*2) 173 | 174 | v := "" 175 | for _, p := range burst.Pulses { 176 | ch := highClusters.Lookup(int(p.High)) 177 | cl := lowClusters.Lookup(int(p.Low)) 178 | 179 | syms = append(syms, ch.Sym) 180 | syms = append(syms, cl.Sym) 181 | 182 | v = v + ch.Sym.String() + cl.Sym.String() 183 | } 184 | log.Printf(":: %d pulses %s", len(burst.Pulses), v) 185 | 186 | decoded, err := DecodeManchester(syms) 187 | if err != nil { 188 | log.Printf("Manchester decode failed: %s", err.Error()) 189 | } else { 190 | log.Printf("machester: %s", decoded.Reader().RemainingBits()) 191 | 192 | n := "" 193 | nr := decoded.Reader() 194 | for !nr.EOF() { 195 | nibble, ok := nr.GetNibbleLSB() 196 | if ok { 197 | n = n + fmt.Sprintf("%x", nibble) 198 | } else { 199 | n = n + "+b:" + nr.RemainingBits() 200 | } 201 | } 202 | log.Printf("manchester: %s", n) 203 | } 204 | 205 | } 206 | 207 | func rangeOf(seq []int) (int, int) { 208 | min := seq[0] 209 | max := seq[0] 210 | 211 | for _, v := range seq { 212 | if v > max { 213 | max = v 214 | } 215 | if v < min { 216 | min = v 217 | } 218 | } 219 | return min, max 220 | } 221 | 222 | func summarizeSamples(seq []int) string { 223 | if len(seq) == 0 { 224 | return "no samples" 225 | } 226 | 227 | sum := 0 228 | sum2 := 0 229 | max := seq[0] 230 | min := seq[0] 231 | 232 | for _, v := range seq { 233 | sum += v 234 | sum2 += v * v 235 | if v > max { 236 | max = v 237 | } 238 | if v < min { 239 | min = v 240 | } 241 | } 242 | 243 | return fmt.Sprintf("%d samples center=%d width=%d %d...%d", len(seq), sum/len(seq), max-min+1, min, max) 244 | } 245 | 246 | type action byte 247 | 248 | const ( 249 | errorAction action = iota 250 | emitZeroAction 251 | emitOneAction 252 | noAction 253 | endAction 254 | ) 255 | 256 | type state byte 257 | 258 | const ( 259 | d0 state = 8 * iota 260 | d1 261 | c0 262 | c1 263 | ) 264 | 265 | type actionNext struct { 266 | act action 267 | next state 268 | } 269 | 270 | var machine = [4 * 8]actionNext{ 271 | byte(d1) + byte(LowShort): actionNext{noAction, c0}, 272 | byte(d1) + byte(LowLong): actionNext{emitZeroAction, d0}, 273 | byte(d1) + byte(EndOfTransmission): actionNext{endAction, c0}, 274 | byte(c0) + byte(HighShort): actionNext{emitOneAction, d1}, 275 | 276 | byte(d0) + byte(HighShort): actionNext{noAction, c1}, 277 | byte(d0) + byte(HighLong): actionNext{emitOneAction, d1}, 278 | byte(c1) + byte(LowShort): actionNext{emitZeroAction, d0}, 279 | byte(c1) + byte(EndOfTransmission): actionNext{endAction, c0}, 280 | } 281 | 282 | func DecodeManchester(syms []Symbol) (*BitStream, error) { 283 | bits := NewBitStream(len(syms)) 284 | done := false 285 | 286 | st := c0 287 | for _, sym := range syms { 288 | if done { 289 | return nil, fmt.Errorf("Symbols after EndOfTransmission") 290 | } 291 | idx := byte(st) + byte(sym) 292 | switch machine[idx].act { 293 | case errorAction: 294 | return nil, fmt.Errorf("Invalid manchester encoding: state=%d sym=%d %#v", 295 | st, sym, bits) 296 | case emitZeroAction: 297 | bits.Add(0) 298 | case emitOneAction: 299 | bits.Add(1) 300 | case endAction: 301 | done = true 302 | } 303 | st = machine[idx].next 304 | } 305 | 306 | return bits, nil 307 | } 308 | 309 | func GuessAndGrow(seqUnsorted []int, tolerance float64, verbose bool) ClusterDescriptions { 310 | pool := make([]int, 0, len(seqUnsorted)) 311 | pool = append(pool, seqUnsorted...) 312 | sort.Ints(pool) 313 | 314 | if verbose { 315 | log.Printf("GuessAndGrow, tolerance=%f", tolerance) 316 | } 317 | 318 | clusters := ClusterDescriptions{} 319 | 320 | grow := func(left int, right int, sum int, sum2 int, count int) (int, int, int, int, int) { 321 | min := int(float64(pool[left]) * (1.0 - tolerance)) 322 | max := int(float64(pool[right]) * (1.0 + tolerance)) 323 | 324 | for i := left - 1; i >= 0 && pool[i] >= min; i-- { 325 | sum += pool[i] 326 | sum2 += pool[i] * pool[i] 327 | left = i 328 | } 329 | 330 | for i := right + 1; i < len(pool) && pool[i] <= max; i++ { 331 | sum += pool[i] 332 | sum2 += pool[i] * pool[i] 333 | right = i 334 | } 335 | 336 | return left, right, sum, sum2, right - left + 1 337 | } 338 | 339 | for len(pool) > 0 { 340 | center := len(pool) / 2 341 | cv := pool[center] 342 | 343 | left, right, sum, sum2, count := grow(center, center, cv, cv*cv, 1) 344 | if verbose { 345 | log.Printf(" pass1 %s", summarizeSamples(pool[left:right+1])) 346 | } 347 | 348 | for pass := 2; ; pass++ { 349 | previousCount := count 350 | left, right, sum, sum2, count = grow(left, right, sum, sum2, count) 351 | if count == previousCount { 352 | break 353 | } 354 | if verbose { 355 | log.Printf(" pass(%d) %s", pass, summarizeSamples(pool[left:right+1])) 356 | } 357 | } 358 | 359 | clusters = append(clusters, &ClusterDescription{ 360 | Min: pool[left], 361 | Max: pool[right], 362 | Count: count, 363 | Sum: sum, 364 | Sum2: sum2, 365 | }) 366 | 367 | pool = append(pool[0:left], pool[right+1:]...) 368 | } 369 | 370 | sort.Sort(byMin(clusters)) 371 | return clusters 372 | } 373 | 374 | func LLoyds(seqUnsorted []int, binCount int) { 375 | seq := make([]int, 0, len(seqUnsorted)) 376 | seq = append(seq, seqUnsorted...) 377 | sort.Ints(seq) 378 | 379 | min, max := rangeOf(seq) 380 | max += 1 381 | 382 | log.Printf("Lloyds %d samples into %d bins", len(seq), binCount) 383 | log.Printf("range is %d to %d", min, max) 384 | 385 | type bin struct { 386 | left int // inclusive 387 | right int // exclusive 388 | center int 389 | count int 390 | sum int 391 | badness int 392 | min int 393 | max int 394 | previousCount int 395 | } 396 | 397 | bins := make([]bin, binCount) 398 | 399 | for i, _ := range bins { 400 | bins[i].left = min + (max-min)*i/binCount 401 | bins[i].right = min + (max-min)*(i+1)/binCount 402 | bins[i].center = (bins[i].left + bins[i].right + 1) / 2 403 | } 404 | 405 | iabs := func(i int) int { 406 | if i < 0 { 407 | return -i 408 | } 409 | return i 410 | } 411 | 412 | firstPass := true 413 | 414 | for { 415 | if firstPass { 416 | // put each element of the sequence into its bin. 417 | // We put about an equal number of elements in each bin to start 418 | for nth, s := range seq { 419 | i := len(bins) * nth / len(seq) 420 | bins[i].count += 1 421 | bins[i].sum += s 422 | bins[i].badness += iabs(s - bins[i].center) 423 | } 424 | firstPass = false 425 | } else { 426 | // prepare for new run 427 | for i, _ := range bins { 428 | log.Printf("bin %d --- %7d .. %7d < %7d < %7d .. %7d = %4d", i, 429 | bins[i].left, bins[i].min, bins[i].center, bins[i].max, bins[i].right, bins[i].count) 430 | bins[i].previousCount = bins[i].count 431 | bins[i].count = 0 432 | bins[i].sum = 0 433 | bins[i].badness = 0 434 | } 435 | 436 | // put each element of the sequence into its bin 437 | for _, s := range seq { 438 | for i, _ := range bins { 439 | if s >= bins[i].left && s < bins[i].right { 440 | bins[i].count += 1 441 | bins[i].sum += s 442 | bins[i].badness += iabs(s - bins[i].center) 443 | if s > bins[i].max { 444 | bins[i].max = s 445 | } 446 | if s < bins[i].min { 447 | bins[i].min = s 448 | } 449 | break 450 | } 451 | } 452 | 453 | } 454 | } 455 | 456 | // see how we are 457 | for i, _ := range bins { 458 | log.Printf("bin %d --- %7d .. %7d < %7d < %7d .. %7d = %4d", i, 459 | bins[i].left, bins[i].min, bins[i].center, bins[i].max, bins[i].right, bins[i].count) 460 | } 461 | 462 | // compute new centers 463 | for i, _ := range bins { 464 | if bins[i].count != 0 { 465 | bins[i].center = bins[i].sum / bins[i].count 466 | } else { 467 | bins[i].center = (bins[i].left + bins[i].right) / 2 468 | } 469 | bins[i].min = bins[i].center 470 | bins[i].max = bins[i].center 471 | } 472 | // update the edges (except first and last of course) 473 | for i := 1; i < len(bins); i++ { 474 | e := (bins[i-1].center + bins[i].center) / 2 475 | bins[i].left = e 476 | bins[i-1].right = e 477 | } 478 | 479 | // count how many bins changed population counts 480 | stable := 0 481 | for i, _ := range bins { 482 | if bins[i].count == bins[i].previousCount { 483 | stable += 1 484 | } 485 | } 486 | 487 | // when no bins changed population counts, then no elements 488 | // moved. We are stable. 489 | if stable == len(bins) { 490 | break 491 | } 492 | 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /go/src/ookanalyze/ookanalyze.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "log" 7 | "ook" 8 | "os" 9 | ) 10 | 11 | var verbose = flag.Bool("verbose", false, "be verbose") 12 | var input = flag.String("input", "-", "path to source file") 13 | 14 | func main() { 15 | flag.Parse() 16 | 17 | sourceFile := func() *os.File { 18 | if *input == "-" { 19 | return os.Stdin 20 | } else { 21 | s, err := os.Open(*input) 22 | if err != nil { 23 | log.Fatalf("Failed to open input file (%s): %s", *input, err.Error()) 24 | } 25 | return s 26 | } 27 | return os.Stdin // not really reached, go 1.1 requires this. sad. 28 | }() 29 | 30 | source, err := ook.OpenFile(sourceFile) 31 | if err != nil { 32 | log.Fatalf("Unable to open %s: %s", *input, err.Error()) 33 | } 34 | defer source.Close() 35 | 36 | for { 37 | burst, err := source.Read() 38 | if err == io.EOF { 39 | break 40 | } 41 | if err != nil { 42 | log.Fatalf("Error reading burst from input: %s", err.Error()) 43 | } 44 | if *verbose { 45 | log.Printf("read burst: %d pulses, offset=%dHz", len(burst.Pulses), burst.Pulses[0].Frequency) 46 | } 47 | ook.Quantify(burst) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go/src/ooklog/ooklog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net" 7 | "ook" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | var address = flag.String("address", "236.0.0.1:3636", "listen to this address") 14 | var ifaceName = flag.String("interface", "lo", "interface on which to listen") 15 | var verbose = flag.Bool("verbose", false, "be verbose") 16 | var output = flag.String("output", "-", "path to write output") 17 | 18 | func main() { 19 | flag.Parse() 20 | 21 | terminate := make(chan os.Signal) 22 | signal.Notify(terminate, syscall.SIGINT) 23 | 24 | udpAddr, err := net.ResolveUDPAddr("udp", *address) 25 | if err != nil { 26 | log.Fatalf("Unable to resolve listenting address (%s): %s", *address, err.Error()) 27 | } 28 | if *verbose { 29 | log.Printf("Listening to %s", udpAddr.String()) 30 | } 31 | 32 | iface,err := net.InterfaceByName( *ifaceName) 33 | if err != nil { 34 | log.Fatalf("Unknown interface (%s): %s", *ifaceName, err.Error()) 35 | } 36 | 37 | sink := func() *os.File { 38 | if *output == "-" { 39 | return os.Stdout 40 | } else { 41 | s, err := os.Create(*output) 42 | if err != nil { 43 | log.Fatalf("Failed to create output file (%s): %s", *output, err.Error()) 44 | } 45 | return s 46 | } 47 | return os.Stdout // not really reached. very sad. go 1.1 requires this 48 | }() 49 | 50 | writer := ook.NewTarWriter(sink) 51 | defer writer.Close() 52 | 53 | incoming := make(chan *ook.Burst, 16) 54 | 55 | handler := func(burst *ook.Burst) bool { 56 | if *verbose { 57 | log.Printf("got a burst") 58 | } 59 | 60 | err := writer.Write(burst) 61 | if err != nil { 62 | log.Fatalf("Failed to write burst: %s", err.Error()) 63 | } 64 | return true 65 | } 66 | 67 | if err := ook.ListenTo(iface, udpAddr, incoming); err != nil { 68 | log.Fatalf("Failed to listen to address: %s", err.Error()) 69 | } 70 | 71 | done := false 72 | 73 | for !done { 74 | select { 75 | case burst := <-incoming: 76 | handler(burst) 77 | case _ = <-terminate: 78 | done = true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /go/src/ookplay/ookplay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "log" 7 | "net" 8 | "ook" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | ) 13 | 14 | var address = flag.String("address", "236.0.0.1:3636", "multicast to this address") 15 | var ifaceName = flag.String("interface", "lo", "interface on which to multicast") 16 | var verbose = flag.Bool("verbose", false, "be verbose") 17 | var input = flag.String("input", "-", "path to file of bursts") 18 | 19 | // Get an IPv4 address for a given interface. 20 | func interfaceIP4(iface *net.Interface) net.IP { 21 | addrs, err := iface.Addrs() 22 | if err != nil { 23 | return nil 24 | } 25 | for _, n := range addrs { 26 | ip, _, err := net.ParseCIDR(n.String()) 27 | if err == nil { 28 | if ip4 := ip.To4(); ip4 != nil { 29 | return ip4 30 | } 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func main() { 37 | flag.Parse() 38 | 39 | terminate := make(chan os.Signal) 40 | signal.Notify(terminate, syscall.SIGINT) 41 | 42 | udpAddr, err := net.ResolveUDPAddr("udp", *address) 43 | if err != nil { 44 | log.Fatalf("Unable to resolve multicasting address (%s): %s", *address, err.Error()) 45 | } 46 | if *verbose { 47 | log.Printf("Multicasting to %s", udpAddr.String()) 48 | } 49 | 50 | iface, err := net.InterfaceByName(*ifaceName) 51 | if err != nil { 52 | log.Fatalf("Unknown interface (%s): %s", *ifaceName, err.Error()) 53 | } 54 | 55 | ifaceAddr := interfaceIP4(iface) 56 | if ifaceAddr == nil { 57 | log.Fatalf("Could not find an IPv4 address on the interface") 58 | } 59 | 60 | localAddr := net.UDPAddr{IP: ifaceAddr, Port: 0} 61 | 62 | udpConn, err := net.DialUDP("udp4", &localAddr, udpAddr) 63 | if err != nil { 64 | log.Fatalf("Failed to dial UDP: %s", err.Error()) 65 | } 66 | 67 | source := func() *os.File { 68 | if *input == "-" { 69 | return os.Stdin 70 | } else { 71 | s, err := os.Open(*input) 72 | if err != nil { 73 | log.Fatalf("Failed to open input file (%s): %s", *input, err.Error()) 74 | } 75 | return s 76 | } 77 | }() 78 | 79 | reader, err := ook.OpenFile(source) 80 | if err != nil { 81 | log.Fatalf("Failed to open input source: %s", err.Error()) 82 | } 83 | 84 | for { 85 | burst, err := reader.Read() 86 | if err == io.EOF { 87 | break 88 | } 89 | if err != nil { 90 | log.Fatalf("Error reading burst: %s", err.Error()) 91 | } 92 | log.Printf("Read %d pulses", len(burst.Pulses)) 93 | 94 | bytes, err := burst.Encode() 95 | if err != nil { 96 | log.Fatalf("Failed to encode burst: %s", err.Error()) 97 | } 98 | log.Printf("Encodes to %d bytes", len(bytes)) 99 | 100 | c, err := udpConn.Write(bytes) 101 | if err != nil { 102 | log.Fatalf("Failed to write burst (%s -> %s): %s", localAddr.String(), udpAddr.String(), err.Error()) 103 | } 104 | if c != len(bytes) { 105 | log.Fatalf("Truncated burst %d != %d", c, len(bytes)) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /img/README: -------------------------------------------------------------------------------- 1 | I used http://draw.io to make these diagrams. The XML files 2 | can probably be edited there. 3 | -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimstudt/ook-decoder/HEAD/img/architecture.png -------------------------------------------------------------------------------- /img/architecture.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /man/.gitignore: -------------------------------------------------------------------------------- 1 | *.1 2 | -------------------------------------------------------------------------------- /man/ookd.1.md: -------------------------------------------------------------------------------- 1 | % ookd(1) 2 | % Jim Studt 3 | % June 12, 2015 4 | 5 | # NAME 6 | 7 | ookd - on-off keying decoder 8 | 9 | # SYNOPSIS 10 | 11 | ookd [*-f frequency*]... 12 | 13 | # DESCRIPTION 14 | 15 | Ookd bridges broadcast radio telemetry data to internet multicast 16 | addresses. 17 | 18 | Ookd listen to a software defined radio using librtlsdr, recognizes an 19 | on off keyed signal, and broadcasts a description of the signal onto 20 | an IP network in a defined binary format. 21 | 22 | You will use ookd in conjunction with one or more clients which 23 | receive the pulse data from the internet multicast messages and 24 | interpret them for a specific device, e.g. a weather station, 25 | thermometer, or alarm device. 26 | 27 | # OPTIONS 28 | 29 | -f *FREQUENCY*, \--frequency *FREQUENCY* 30 | : Specify the center frequency of the carrier in hertz. The default is 31 | 433910000. Signals some way to each side are still received, and 32 | the actual frequency of each pulse is recorded along with its 33 | length to facilitate disambiguation of sources. 34 | 35 | -m *NUM*, \--min-packet *NUM* 36 | : The minimum number of pulses required to make a packet. This is used 37 | to avoid sending large numbers of packets for radio noise. The default 38 | is 10. 39 | 40 | -a *ADDRESS*, \--multicast-address *ADDRESS* 41 | : The multicast address where the recognized packets are broadcast. 42 | The default is 236.0.0.1. 43 | 44 | -p *PORT*, \--multicast-port *PORT* 45 | : The port to which recognized packets are sent. The default is 3636. 46 | 47 | -i *ADDRESS*, \--multicast-interface *ADDRESS* 48 | : The address of the interface where recognized packets will be multicast. 49 | The default is 127.0.0.1 which will confine them to the local computer. If 50 | you wish to send them out of the computer you will need to use one of your 51 | external IP addresses. 52 | 53 | -r *FILENAME*, \--read-file *FILENAME* 54 | : The name of a file to read the IQ raw data instead of from a radio. 55 | This is useful for testing and tuning. 56 | 57 | -v, \--verbose 58 | : Print verbose information while working. 59 | 60 | -h, -?, \--help 61 | : Print usage information. 62 | 63 | # SEE ALSO 64 | 65 | `ookdump` (1), `wh1080` (1), `oregonsci` (1), `ws2300` (1), `nexa` (1) 66 | 67 | # WWW 68 | 69 | ookd is maintained at . 70 | -------------------------------------------------------------------------------- /man/ookdump.1.md: -------------------------------------------------------------------------------- 1 | % ookd(1) 2 | % Jim Studt 3 | % June 12, 2015 4 | 5 | # NAME 6 | 7 | ookdump - translate ookd messages into human readable form 8 | 9 | # SYNOPSIS 10 | 11 | ookdump [*-a address*] [*-p port*]... 12 | 13 | # DESCRIPTION 14 | 15 | Ookdump listens for ookd messages on a multicast port and prints a human 16 | readable dump of the messages to standard output. Once started it runs 17 | until interrupted. 18 | 19 | # OPTIONS 20 | 21 | -a *ADDRESS*, \--multicast-address *ADDRESS* 22 | : The multicast address to listen on. 23 | The default is 236.0.0.1. 24 | 25 | -p *PORT*, \--multicast-port *PORT* 26 | : The port to to listen on. The default is 3636. 27 | 28 | -i *ADDRESS*, \--multicast-interface *ADDRESS* 29 | : The address of the interface to listen on. 30 | The default is 127.0.0.1 which will only see them from the local computer. If 31 | you wish to see packets from other computers you will need to use 32 | an external IP addresses. 33 | 34 | -v, \--verbose 35 | : Print verbose information while working. 36 | 37 | -h, -?, \--help 38 | : Print usage information. 39 | 40 | # SEE ALSO 41 | 42 | `ookd` (1) 43 | 44 | # WWW 45 | 46 | ookd is maintained at . 47 | -------------------------------------------------------------------------------- /man/oregonsci.1.md: -------------------------------------------------------------------------------- 1 | % ookd(1) 2 | % Jim Studt 3 | % June 12, 2015 4 | 5 | # NAME 6 | 7 | oregonsci - recieve Oregon Scientific weather station data from ookd 8 | 9 | # SYNOPSIS 10 | 11 | oregonsci [*-a address*] [*-p port*]... 12 | 13 | # DESCRIPTION 14 | 15 | Oregonsci listens for ookd messages on a multicast port, decodes them 16 | as Oregon Scientific weather station data, and stores the data in 17 | JSON files for other programs to use. 18 | 19 | The units are unix time, celcius, meters per second, degrees of angle, 20 | and percent relative humidity. 21 | 22 | It stores the most recent observations in a file with a static name, 23 | the defaults is `/tmp/current-weather.json`. 24 | 25 | An example static file is: 26 | 27 | ```` 28 | $ cat /tmp/current-weather.json 29 | { 30 | "temperature":16.1, 31 | "humidity":67.0, 32 | "avgWindSpeed":2.1, 33 | "gustSpeed":2.6, 34 | "rainfall":0.0000, 35 | "batteryLow":0, 36 | "windDirection":337 37 | } 38 | ```` 39 | 40 | Periodically, it stores a summary of the interval in a timestamped 41 | file, the default is `/tmp/weather-YYYYMMDD-HHMMSS.json` where the 42 | capital letters are replaced with the current timestamp. The default 43 | interval is 5 minutes. 44 | 45 | The periodic file contains for each sample type the number of samples 46 | summed, the sum, the sum of the squares, the minimum and the 47 | maximum. You can use the sum of the squares to calculate a standard 48 | deviation if you like. 49 | 50 | An example periodic file is: 51 | ```` 52 | $ cat /tmp/weather-20150616-150620.json 53 | { 54 | "start":1434467180, 55 | "end":1434467498, 56 | "temperature" : { "n":7, "sum":116, "sum2":1909, "min":16.4,"max":16.6 }, 57 | "humidity" : { "n":7, "sum":468, "sum2":3.13e+04, "min":65, "max":68}, 58 | "avgWindSpeed" : { "n":0, "sum":0, "sum2":0, "min":0, "max":0 }, 59 | "gustSpeed" : { "n":0, "sum":0, "sum2":0, "min":0, "max":0 }, 60 | "rainfall" : { "n":6, "sum":0, "sum2":0, "min":0, "max":0 }, 61 | "batteryLow" : { "n":0, "sum":0, "sum2":0, "min":0, "max":0 }, 62 | "windDirection" : { "n":0, "sum":0, "sum2":0, "min":0, "max":0 } 63 | } 64 | ```` 65 | 66 | # OPTIONS 67 | 68 | -a *ADDRESS*, \--multicast-address *ADDRESS* 69 | : The multicast address to listen on. 70 | The default is 236.0.0.1. 71 | 72 | -p *PORT*, \--multicast-port *PORT* 73 | : The port to to listen on. The default is 3636. 74 | 75 | -i *ADDRESS*, \--multicast-interface *ADDRESS* 76 | : The address of the interface to listen on. 77 | The default is 127.0.0.1 which will only see them from the local computer. If 78 | you wish to see packets from other computers you will need to use 79 | an external IP addresses. 80 | 81 | -v, \--verbose 82 | : Print verbose information while working. 83 | 84 | -h, -?, \--help 85 | : Print usage information. 86 | 87 | # SEE ALSO 88 | 89 | `ookd` (1) 90 | 91 | # WWW 92 | 93 | ookd is maintained at . 94 | -------------------------------------------------------------------------------- /nexa.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ook.h" 14 | 15 | /* Nexa protocol specification was used from http://tech.jolowe.se/home-automation-rf-protocols/ */ 16 | 17 | #define SYNC_BIT_LEN 11 18 | #define PAUSE_BIT_LEN 41 19 | #define PHYSICAL_1_BIT_LEN 2 20 | #define PHYSICAL_0_BIT_LEN 6 21 | #define TRANSMITTER_CODE_LEN 26 22 | 23 | #define UPPER_LIMIT_OF_PHYSICAL_BITS 4096 24 | 25 | #define PULSE_LENGTH_NANOSEC 250000 26 | 27 | #define STATSD_HOST "127.0.0.1" 28 | #define STATSD_PORT 8125 29 | 30 | int verbose = 0; 31 | 32 | struct nexa_p 33 | { 34 | /* transmitter unique code, and it is this code that the reciever "learns" to recognize */ 35 | uint32_t transmitter_code:TRANSMITTER_CODE_LEN; 36 | 37 | /* 1 - on, 0 - off */ 38 | uint8_t group_code:1; 39 | 40 | /* 1 - on, 0 - off */ 41 | uint8_t on_off:1; 42 | 43 | /* Channel bits. Proove/Anslut = 00, Nexa = 11 */ 44 | uint8_t channel_bits:2; 45 | 46 | /* Unit bits. Device to be turned on or off. 47 | Proove/Anslut Unit #1 = 00, #2 = 01, #3 = 10. 48 | Nexa Unit #1 = 11, #2 = 10, #3 = 01 */ 49 | uint8_t unit_bits:2; 50 | }; 51 | 52 | /* 53 | Returns 1 if the data pointer points to a "sync" bit, 0 otherwise 54 | */ 55 | int is_sync_bit(unsigned char** data) 56 | { 57 | if((*data)[0] == 1) { 58 | for(int i = 1; i < SYNC_BIT_LEN; ++i) { 59 | if((*data)[i] != 0) { 60 | return 0; 61 | } 62 | } 63 | } 64 | 65 | *data += SYNC_BIT_LEN; 66 | return 1; 67 | } 68 | 69 | /* 70 | Returns 1 if the data pointer points to a "pause" bit, 0 otherwise 71 | */ 72 | int is_pause_bit(unsigned char** data) 73 | { 74 | if((*data)[0] == 1) { 75 | for(int i = 1; i < PAUSE_BIT_LEN; ++i) { 76 | if((*data)[i] != 0) { 77 | return 0; 78 | } 79 | } 80 | } 81 | 82 | *data += PAUSE_BIT_LEN; 83 | return 1; 84 | } 85 | 86 | /* 87 | Decodes physical bit from the pulses sequence 88 | Returns bit value (1 or 0) 89 | */ 90 | int decode_physical_bit(unsigned char** data) 91 | { 92 | if((*data)[0] == 1) { 93 | for(int i = 1; i < PHYSICAL_0_BIT_LEN; ++i) { 94 | if((*data)[i] != 0) { 95 | *data += PHYSICAL_1_BIT_LEN; 96 | return 1; 97 | } 98 | } 99 | } 100 | 101 | *data += PHYSICAL_0_BIT_LEN; 102 | return 0; 103 | } 104 | 105 | /* 106 | Decodes logical bit from the pair of 2 physical bits 107 | Returns bit value on success (1 or 0), -1 on error 108 | */ 109 | int decode_logical_bit(unsigned char** data) 110 | { 111 | int first_bit = decode_physical_bit(data); 112 | int second_bit = decode_physical_bit(data); 113 | 114 | if(first_bit && !second_bit) { 115 | return 1; 116 | } else if(!first_bit && second_bit) { 117 | return 0; 118 | } else if(verbose) { 119 | fprintf(stderr, "incorrect physical bit sequence %d %d\n", first_bit, second_bit); 120 | } 121 | 122 | return -1; 123 | } 124 | 125 | /* 126 | Decodes set of physical bits to the Nexa control packet structure 127 | Returns a filled struct on success, NULL on error 128 | */ 129 | struct nexa_p* decode_nexa_p(unsigned char* data) 130 | { 131 | if(is_sync_bit(&data)) { 132 | struct nexa_p* packet = (struct nexa_p*)malloc(sizeof(struct nexa_p)); 133 | memset(packet, 0, sizeof(struct nexa_p)); 134 | 135 | for(int i = 0; i < TRANSMITTER_CODE_LEN; ++i) { 136 | uint8_t decoded_bit = decode_logical_bit(&data); 137 | 138 | if(decoded_bit == 1) { 139 | packet->transmitter_code |= (1 << i); 140 | } 141 | } 142 | 143 | if(decode_logical_bit(&data) == 0) { 144 | packet->group_code = 1; // inversed by protocol 145 | } 146 | 147 | if(decode_logical_bit(&data) == 0) { 148 | packet->on_off = 1; // inversed by protocol 149 | } 150 | 151 | if(decode_logical_bit(&data) == 1) { 152 | packet->channel_bits |= 1; 153 | } 154 | if(decode_logical_bit(&data) == 1) { 155 | packet->channel_bits |= (1 << 1); 156 | } 157 | 158 | if(decode_logical_bit(&data) == 1) { 159 | packet->unit_bits |= 1; 160 | } 161 | if(decode_logical_bit(&data) == 1) { 162 | packet->unit_bits |= (1 << 1); 163 | } 164 | 165 | if(is_pause_bit(&data)) { 166 | return packet; 167 | } else if(verbose) { 168 | fprintf(stderr, "no pause bit\n"); 169 | free(packet); 170 | } 171 | } else if(verbose) { 172 | fprintf(stderr, "no sync bit\n"); 173 | } 174 | 175 | return NULL; 176 | } 177 | 178 | /* 179 | Sends "1" to the gauge metric of StatsD server 180 | Returns 0 on success, -1 on error 181 | */ 182 | int send_statsd_gauge(const char* metricName) 183 | { 184 | struct sockaddr_in si_other; 185 | int s, slen = sizeof(si_other); 186 | char buf[128]; 187 | 188 | if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) { 189 | return -1; 190 | } 191 | 192 | memset((char *) &si_other, 0, sizeof(si_other)); 193 | si_other.sin_family = AF_INET; 194 | si_other.sin_port = htons(STATSD_PORT); 195 | if (inet_aton(STATSD_HOST, &si_other.sin_addr) == 0) { 196 | fprintf(stderr, "inet_aton() failed for address '%s'\n", STATSD_HOST); 197 | return -1; 198 | } 199 | 200 | snprintf(buf, sizeof(buf), "%s:1|g", metricName); 201 | if (sendto(s, buf, strlen(buf), 0, (struct sockaddr*)&si_other, slen) == -1) { 202 | return -1; 203 | } 204 | 205 | close(s); 206 | return 0; 207 | } 208 | 209 | static void showHelp( FILE *f) 210 | { 211 | fprintf(f, 212 | "Usage: nexa [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface] [-f transmittercode] [-m metricname]\n" 213 | " -h | -? | --help display usage and exit\n" 214 | " -v | --verbose verbose logging\n" 215 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 216 | " -p port | --multicast-port port multicast port, default 3636\n" 217 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 218 | " -f code | --filter-transmitter-code transmitter code to filter output with, disabled by default\n" 219 | " -m name | --metric-name name of the gauge metric to send to StatsD server, disabled by default\n" 220 | ); 221 | } 222 | 223 | int main( int argc, char **argv) 224 | { 225 | const char *multicastAddress = "236.0.0.1"; 226 | const char *multicastPort = "3636"; 227 | const char *multicastInterface = "127.0.0.1"; 228 | int32_t filterTransmitterCode = -1; 229 | const char *metricName = NULL; 230 | 231 | // Handle options 232 | for(;;) { 233 | int optionIndex = 0; 234 | static struct option options[] = { 235 | { "verbose", no_argument, 0, 'v' }, 236 | { "help", no_argument, 0, 'h' }, 237 | { "multicast-address", required_argument, 0, 'a'}, 238 | { "multicast-port", required_argument, 0, 'p' }, 239 | { "multicast-interface", required_argument, 0, 'i' }, 240 | { "filter-transmitter-code", required_argument, 0, 'f' }, 241 | { "metric-name", required_argument, 0, 'm' }, 242 | { 0,0,0,0} 243 | }; 244 | 245 | int c = getopt_long( argc, argv, "vh?f:a:p:i:m:", options, &optionIndex ); 246 | if ( c == -1) break; 247 | 248 | switch(c) { 249 | case 'h': 250 | case '?': 251 | showHelp(stdout); 252 | return 0; 253 | case 'v': 254 | verbose = 1; 255 | break; 256 | case 'a': 257 | multicastAddress = optarg; 258 | break; 259 | case 'p': 260 | multicastPort = optarg; 261 | break; 262 | case 'i': 263 | multicastInterface = optarg; 264 | break; 265 | case 'f': 266 | filterTransmitterCode = atoi(optarg); 267 | break; 268 | case 'm': 269 | metricName = optarg; 270 | break; 271 | default: 272 | fprintf(stderr,"Illegal option\n"); 273 | showHelp(stderr); 274 | exit(1); 275 | } 276 | } 277 | 278 | // Parse our multicast address 279 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 280 | if ( sock < 0) { 281 | fprintf(stderr,"Failed to open multicast interface\n"); 282 | exit(1); 283 | } 284 | 285 | unsigned char *data = (unsigned char *)malloc(UPPER_LIMIT_OF_PHYSICAL_BITS * sizeof(unsigned char)); 286 | 287 | for (;;) { 288 | struct ook_burst *burst; 289 | struct sockaddr_storage addr; 290 | socklen_t addrLen = sizeof(addr); 291 | 292 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 293 | if ( e < 0) { 294 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 295 | break; 296 | } 297 | if ( e == 0) { 298 | fprintf(stderr,"Corrupt burst\n"); 299 | continue; 300 | } 301 | 302 | size_t bits = 0; 303 | memset(data, 0, UPPER_LIMIT_OF_PHYSICAL_BITS * sizeof(unsigned char)); 304 | 305 | for ( int i = 0; i < burst->pulses; i++) { 306 | data[bits++] = 1; 307 | 308 | uint8_t zeros = burst->pulse[i].lowNanoseconds / PULSE_LENGTH_NANOSEC; 309 | while(zeros--) { 310 | data[bits++] = 0; 311 | } 312 | } 313 | 314 | struct nexa_p* packet = decode_nexa_p(data); 315 | if(packet) { 316 | if(filterTransmitterCode == -1 || packet->transmitter_code == filterTransmitterCode) { 317 | if(verbose) { 318 | fprintf(stderr, "transmitter code: %d: %s\n", packet->transmitter_code, 319 | packet->on_off ? "ON" : "OFF"); 320 | } 321 | 322 | if(metricName) { 323 | send_statsd_gauge(metricName); 324 | } 325 | } 326 | } else { 327 | fprintf(stderr, "decoding error\n"); 328 | } 329 | 330 | fflush(stdin); 331 | free(packet); 332 | free(burst); 333 | } 334 | 335 | free(data); 336 | close(sock); 337 | return 0; 338 | } 339 | -------------------------------------------------------------------------------- /ook.c: -------------------------------------------------------------------------------- 1 | #include "ook.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct ook_burst *ook_allocate_burst( uint32_t maximumPulses) 13 | { 14 | struct ook_burst *r = 0; 15 | size_t need = sizeof(*r)+maximumPulses*sizeof(r->pulse[0]); 16 | 17 | r = malloc(need); 18 | if ( r) { 19 | r->positionNanoseconds = 0; 20 | r->pulses = 0; 21 | r->allocatedPulses = maximumPulses; 22 | } 23 | return r; 24 | } 25 | 26 | 27 | int ook_add_pulse( struct ook_burst *burst, uint32_t hiNs, uint32_t lowNs, int32_t freqOffsetHz) 28 | { 29 | if ( burst->pulses < burst->allocatedPulses) { 30 | burst->pulse[burst->pulses].hiNanoseconds = hiNs; 31 | burst->pulse[burst->pulses].lowNanoseconds = lowNs; 32 | burst->pulse[burst->pulses].frequencyOffsetHz = freqOffsetHz; 33 | burst->pulses++; 34 | return 0; 35 | } 36 | return -1; 37 | } 38 | 39 | int ook_encode( struct ook_burst *burst, void **dataReturn, size_t *sizeReturn) 40 | { 41 | size_t maxSize = sizeof(*burst) + sizeof(burst->pulse[0])*burst->pulses + 64 /* some packet overhead */; 42 | void *data = malloc( maxSize); 43 | if ( data == 0) return -1; 44 | 45 | void *thumb = data; 46 | size_t left = maxSize; 47 | 48 | #define OPUT_U32(V) { if ( left < 4) goto Overflow; memcpy( thumb, &(V), 4); thumb+=4; left-=4; } 49 | #define OPUT_I32(V) { if ( left < 4) goto Overflow; memcpy( thumb, &(V), 4); thumb+=4; left-=4; } 50 | #define OPUT_U64(V) { if ( left < 8) goto Overflow; memcpy( thumb, &(V), 8); thumb+=8; left-=8; } 51 | 52 | uint32_t vers = 0x36360001; 53 | OPUT_U32( vers); // version signature 54 | OPUT_U64( burst->positionNanoseconds); 55 | OPUT_U32( burst->pulses); 56 | for ( int i = 0; i < burst->pulses; i++) { 57 | OPUT_U32( burst->pulse[i].hiNanoseconds); 58 | OPUT_U32( burst->pulse[i].lowNanoseconds); 59 | OPUT_I32( burst->pulse[i].frequencyOffsetHz); 60 | } 61 | 62 | *dataReturn = data; 63 | *sizeReturn = thumb-data; 64 | return 0; 65 | 66 | Overflow: 67 | return -1; 68 | } 69 | 70 | int ook_open( const char *address, const char *port, const char *interface) 71 | { 72 | int sock = -1; 73 | struct addrinfo *multicast_ai = 0; 74 | struct addrinfo *interface_ai = 0; 75 | struct addrinfo hints = { .ai_family = AF_UNSPEC, 76 | .ai_socktype = SOCK_DGRAM, 77 | }; 78 | 79 | int err = getaddrinfo( address, port, &hints, &multicast_ai); 80 | if (err){ 81 | fprintf(stderr,"Illegal multicast address (addr=%s port=%s):%s\n", address, port, gai_strerror(err)); 82 | goto Fail; 83 | } 84 | 85 | err = getaddrinfo( interface, port, &hints, &interface_ai); 86 | if (err){ 87 | fprintf(stderr,"Illegal interface address (addr=%s port=%s):%s\n", interface, port, gai_strerror(err)); 88 | goto Fail; 89 | } 90 | 91 | // add a verbose print here 92 | 93 | sock = socket( interface_ai->ai_family, SOCK_DGRAM, 0); 94 | if ( sock < 0) { 95 | fprintf(stderr,"Failed to create socket: %s\n", strerror(errno)); 96 | goto Fail; 97 | } 98 | 99 | int reuse=1; 100 | if ( setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { 101 | fprintf(stderr, "Failed to reuse socket: %s\n", strerror(errno)); 102 | goto Fail; 103 | } 104 | 105 | switch( multicast_ai->ai_family) { 106 | case AF_INET: 107 | { 108 | struct sockaddr_in anySock = { .sin_family = AF_INET, 109 | .sin_port = ((struct sockaddr_in *)multicast_ai->ai_addr)->sin_port, 110 | .sin_addr.s_addr = INADDR_ANY }; 111 | if ( bind( sock, (struct sockaddr *)&anySock, sizeof(anySock)) < 0) { 112 | fprintf(stderr,"Failed to bind to multicast interface: %s\n", strerror(errno)); 113 | goto Fail; 114 | } 115 | 116 | struct ip_mreq group; 117 | group.imr_multiaddr.s_addr = ((struct sockaddr_in *)multicast_ai->ai_addr)->sin_addr.s_addr; 118 | group.imr_interface.s_addr = ((struct sockaddr_in *)interface_ai->ai_addr)->sin_addr.s_addr; 119 | 120 | int r = setsockopt( sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const void *)&group, sizeof(group)); 121 | if ( r < 0) { 122 | fprintf(stderr,"Failed to join multicast group: %s\n", strerror(errno)); 123 | goto Fail; 124 | } 125 | } 126 | break; 127 | case AF_INET6: 128 | { 129 | struct sockaddr_in6 anySock = { .sin6_family = AF_INET6, 130 | .sin6_port = ((struct sockaddr_in6 *)multicast_ai->ai_addr)->sin6_port, 131 | .sin6_addr = in6addr_any }; 132 | if ( bind( sock, (struct sockaddr *)&anySock, sizeof(anySock)) < 0) { 133 | fprintf(stderr,"Failed to bind to multicast interface: %s\n", strerror(errno)); 134 | goto Fail; 135 | } 136 | 137 | struct ipv6_mreq group; 138 | memcpy( &group.ipv6mr_multiaddr, &((struct sockaddr_in6 *)multicast_ai->ai_addr)->sin6_addr, sizeof(struct in6_addr)); 139 | group.ipv6mr_interface = 0; 140 | 141 | int r = setsockopt( sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const void *)&group, sizeof(group)); 142 | if ( r < 0) { 143 | fprintf(stderr,"Failed to join multicast group: %s\n", strerror(errno)); 144 | goto Fail; 145 | } 146 | } 147 | break; 148 | default: 149 | fprintf(stderr, "Unsupported family for multicast groups: %d\n", multicast_ai->ai_family); 150 | goto Fail; 151 | break; 152 | } 153 | 154 | freeaddrinfo( multicast_ai); 155 | freeaddrinfo( interface_ai); 156 | 157 | return sock; 158 | 159 | Fail: 160 | if ( multicast_ai) freeaddrinfo( multicast_ai); 161 | if ( interface_ai) freeaddrinfo( interface_ai); 162 | if ( sock >= 0) close(sock); 163 | 164 | return -1; 165 | } 166 | 167 | 168 | int ook_decode_from_socket( int sock, struct ook_burst **burstReturn, struct sockaddr *from, socklen_t *fromLen, int verbose) 169 | { 170 | struct ook_burst *burst = 0; 171 | unsigned char buf[65536]; 172 | int e=0; 173 | 174 | do { 175 | e = recvfrom( sock, buf, sizeof(buf), 0, from, fromLen); 176 | if ( e == -1 && (errno == EAGAIN || errno == EINTR)) continue; 177 | if ( e == -1) return -1; 178 | } while(0); 179 | 180 | if ( verbose) fprintf(stderr,"Received %u bytes\n", e); 181 | 182 | uint32_t left = e; 183 | unsigned char *thumb = buf; 184 | 185 | #define OGET_U32() ({ uint32_t v; if ( left<4) goto Fail; memcpy(&v,thumb,4); thumb+=4; left -= 4; v; }) 186 | #define OGET_I32() ({ int32_t v; if ( left<4) goto Fail; memcpy(&v,thumb,4); thumb+=4; left -= 4; v; }) 187 | #define OGET_U64() ({ uint64_t v; if ( left<8) goto Fail; memcpy(&v,thumb,8); thumb+=8; left -= 8; v; }) 188 | 189 | uint32_t vers = OGET_U32(); 190 | if ( vers != 0x36360001) goto Fail; 191 | 192 | uint64_t pos = OGET_U64(); 193 | uint32_t pulses = OGET_U32(); 194 | 195 | burst = ook_allocate_burst( pulses); 196 | if ( !burst) goto Fail; 197 | 198 | burst->positionNanoseconds = pos; 199 | for ( int i = 0; i < pulses; i++) { 200 | uint32_t hi = OGET_U32(); 201 | uint32_t low = OGET_U32(); 202 | int32_t freq = OGET_I32(); 203 | 204 | if ( ook_add_pulse( burst, hi, low, freq) < 0) goto Fail; 205 | } 206 | 207 | if ( left > 0) goto Fail; 208 | 209 | *burstReturn = burst; 210 | 211 | return 1; 212 | 213 | Fail: 214 | *burstReturn = 0; 215 | if (burst) free(burst); 216 | 217 | return 0; 218 | } 219 | 220 | int ook_decode_pulse_width( struct ook_burst *burst, 221 | uint32_t minZeroHi, uint32_t maxZeroHi, 222 | uint32_t minOneHi, uint32_t maxOneHi, 223 | uint32_t minLow, uint32_t maxLow, 224 | unsigned char **dataReturn, size_t *dataLenReturn, 225 | int verbose) 226 | { 227 | size_t dataLen = (burst->pulses + 7)/8; 228 | unsigned char *data = (unsigned char *)malloc( dataLen); 229 | if ( data == 0) goto Fail; 230 | 231 | unsigned char accum = 0; 232 | unsigned char *thumb = data; 233 | unsigned char bitsInAccum = 0; 234 | unsigned bits = 0; 235 | for ( int i = 0; i < burst->pulses; i++) { 236 | uint32_t hi = burst->pulse[i].hiNanoseconds; 237 | uint32_t low = burst->pulse[i].lowNanoseconds; 238 | 239 | if ( low < minLow || low > maxLow) { 240 | if ( verbose) fprintf(stderr,"low of %u was %u, not between %u and %u\n", i, low, minLow, maxLow); 241 | goto Fail; 242 | } 243 | 244 | if ( hi >= minZeroHi && hi <= maxZeroHi) { 245 | accum = (accum<<1); 246 | bitsInAccum++; 247 | bits++; 248 | } else if ( hi >= minOneHi && hi <= maxOneHi) { 249 | accum = ((accum<<1)|1); 250 | bitsInAccum++; 251 | bits++; 252 | } else { 253 | if ( verbose) fprintf(stderr,"high of %u was %u, not between %u and %u or %u and %u\n", 254 | i, hi, minZeroHi, maxZeroHi, minOneHi, maxOneHi); 255 | goto Fail; 256 | } 257 | 258 | if ( bitsInAccum == 8) { 259 | *thumb++ = accum; 260 | bitsInAccum = 0; 261 | } 262 | } 263 | if ( bitsInAccum != 0) { 264 | *thumb++ = accum; 265 | bitsInAccum = 0; 266 | } 267 | 268 | *dataReturn = data; 269 | *dataLenReturn = dataLen; 270 | return bits; 271 | 272 | Fail: 273 | if ( data) free(data); 274 | return -1; 275 | } 276 | 277 | 278 | int ook_decode_manchester( struct ook_burst *burst, 279 | uint32_t minShortHi, uint32_t maxShortHi, 280 | uint32_t minLongHi, uint32_t maxLongHi, 281 | uint32_t minShortLow, uint32_t maxShortLow, 282 | uint32_t minLongLow, uint32_t maxLongLow, 283 | unsigned char **dataReturn, size_t *dataLenReturn, 284 | int verbose) 285 | { 286 | size_t dataLen = burst->pulses*2; // this is an upper limit 287 | unsigned char *data = (unsigned char *)malloc( dataLen); 288 | if ( data == 0) goto Fail; 289 | 290 | enum signal { shortHi=0, longHi, shortLow, longLow, endLow, indeterminate}; 291 | #define NUMSIG 6 292 | 293 | #if 0 294 | static const char *signalName[] = { 295 | [shortHi]="-", 296 | [longHi]="--", 297 | [shortLow]="_", 298 | [longLow]="__", 299 | [endLow]=".__.", 300 | [indeterminate]="=" 301 | }; 302 | #endif 303 | 304 | /* 305 | var machine = [4 * 8]actionNext{ 306 | byte(d1) + byte(LowShort): actionNext{noAction, c0}, 307 | byte(d1) + byte(LowLong): actionNext{emitZeroAction, d0}, 308 | byte(d1) + byte(EndOfTransmission): actionNext{endAction, c0}, 309 | byte(c0) + byte(HighShort): actionNext{emitOneAction, d1}, 310 | 311 | byte(d0) + byte(HighShort): actionNext{noAction, c1}, 312 | byte(d0) + byte(HighLong): actionNext{emitOneAction, d1}, 313 | byte(c1) + byte(LowShort): actionNext{emitZeroAction, d0}, 314 | byte(c1) + byte(EndOfTransmission): actionNext{endAction, c0}, 315 | } 316 | */ 317 | 318 | enum state { c0=NUMSIG*0, c1=NUMSIG*1, d0=NUMSIG*2, d1=NUMSIG*3}; 319 | #define NUMSTATE 4 320 | enum action { errorAction=0, noAction, emitZero, emitOne, endAction }; 321 | 322 | enum state currentState = c0; 323 | 324 | #if 0 325 | static const char *stateName[] = { 326 | [c0] = "c0", 327 | [c1] = "c1", 328 | [d0] = "d0", 329 | [d1] = "d1", 330 | }; 331 | #endif 332 | 333 | static const struct { 334 | unsigned char action; // enum action, but I want it one byte 335 | unsigned char next; // enum state, but I want it one byte 336 | } state[NUMSTATE*NUMSIG] = { 337 | // clang detects overlaps and out of bounds entries so this is safe 338 | // uninitialzed entries are going to get a zero and be an errorAction 339 | [d1 + shortLow] = { noAction, c0 }, 340 | [d1 + longLow] = { emitZero, d0 }, 341 | [d1 + endLow] = { endAction, c0 }, 342 | [c0 + shortHi] = { emitOne, d1 }, 343 | 344 | [d0 + shortHi] = { noAction, c1 }, 345 | [d0 + longHi] = { emitOne, d1 }, 346 | [c1 + shortLow] = { emitZero, d0 }, 347 | [c1 + endLow] = { endAction, c0}, 348 | }; 349 | 350 | 351 | unsigned bits = 0; 352 | 353 | for ( int i = 0; i < burst->pulses; i++) { 354 | enum signal signal[2]; 355 | 356 | // turn the high pulse into a signal symbol 357 | uint32_t hi = burst->pulse[i].hiNanoseconds; 358 | if ( hi >= minShortHi && hi <= maxShortHi) { 359 | signal[0] = shortHi; 360 | } else if ( hi >= minLongHi && hi <= maxLongHi) { 361 | signal[0] = longHi; 362 | } else { 363 | signal[0] = indeterminate; 364 | } 365 | 366 | // turn the low pulse into a signal symbol 367 | uint32_t low = burst->pulse[i].lowNanoseconds; 368 | if ( low >= minShortLow && low <= maxShortLow) { 369 | signal[1] = shortLow; 370 | } else if ( low >= minLongLow && low <= maxLongLow) { 371 | signal[1] = longLow; 372 | } else if ( i == burst->pulses - 1) { 373 | signal[1] = endLow; 374 | } else { 375 | signal[1] = indeterminate; 376 | } 377 | 378 | // run the signal symbols through the state machine 379 | for ( unsigned b = 0; b <= 1; b++) { 380 | switch ( state[ currentState + signal[b] ].action) { 381 | case noAction: 382 | break; 383 | case emitZero: 384 | if ( bits >= dataLen) goto Fail; 385 | data[bits++] = 1; 386 | break; 387 | case emitOne: 388 | if ( bits >= dataLen) goto Fail; 389 | data[bits++] = 0; 390 | break; 391 | case endAction: 392 | goto Finish; 393 | case errorAction: 394 | goto Fail; 395 | } 396 | 397 | currentState = state[ currentState + signal[b] ].next; 398 | } 399 | } 400 | 401 | Finish: 402 | *dataReturn = data; // don't bother to realloc to right length, it goes away fast 403 | *dataLenReturn = dataLen; 404 | return bits; 405 | 406 | Fail: 407 | if ( data) free(data); 408 | return -1; 409 | } 410 | -------------------------------------------------------------------------------- /ook.h: -------------------------------------------------------------------------------- 1 | #ifndef OOK_IS_IN 2 | #define OOK_IS_IN 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct ook_pulse { 9 | uint32_t hiNanoseconds; 10 | uint32_t lowNanoseconds; 11 | int32_t frequencyOffsetHz; 12 | }; 13 | 14 | struct ook_burst { 15 | uint64_t positionNanoseconds; // relative to when the daemon started 16 | uint32_t pulses; 17 | uint32_t allocatedPulses; // how many pulses can be stored in here 18 | struct ook_pulse pulse[]; 19 | }; 20 | 21 | // Free with free(), return NULL on error 22 | struct ook_burst *ook_allocate_burst( uint32_t maximumPulses); 23 | 24 | // -1 if tried to overflow or bad burst, 0 if ok 25 | int ook_add_pulse( struct ook_burst *burst, uint32_t hiNs, uint32_t lowNs, int32_t freqOffsetHz); 26 | 27 | // Serialize an ook_pulse into a sequence of bytes. 28 | // return 0 if ok 29 | // dataReturn should be free()d if it is set. 30 | int ook_encode( struct ook_burst *burst, void **dataReturn, size_t *sizeReturn); 31 | 32 | // Get a socket bound for listening for pulse bursts, -1 on error 33 | // This handles the rather tedious UDP multicast jiggery 34 | int ook_open( const char *address, const char *port, const char *interface); 35 | 36 | // -1 socket error, 0 bad packet (from/fromLen valid), >0 good burst (burstReturn/from/fromLen valid) 37 | // Will block awaiting data. You should use select() if that isn't for you. 38 | // If burstReturn is set, it must be free()ed. 39 | int ook_decode_from_socket( int sock, struct ook_burst **burstReturn, struct sockaddr *from, socklen_t *fromLen, int verbose); 40 | 41 | // This is for decoding pulse width encoding. The bits are determined by the length of the high part 42 | // of the pulse, the lows are important for timing, but not data bits. 43 | // -1 illegal pulse in there, otherwise number bits!! read that again, bits, in data. datLen is in bytes. 44 | // The last byte may not be full, live bits will be in the least significant bits 45 | // pulse time limits are in nanoseconds 46 | // if data is non-NULL it must be free()ed. 47 | // If verbose is set, then it will print diagnostics to stderr. 48 | int ook_decode_pulse_width( struct ook_burst *burst, 49 | uint32_t minZeroHi, uint32_t maxZeroHi, 50 | uint32_t minOneHi, uint32_t maxOneHi, 51 | uint32_t minLow, uint32_t maxLow, 52 | unsigned char **dataReturn, size_t *dataLenReturn, 53 | int verbose); 54 | 55 | // This is for decoding machester encoding. Manchester coding appears as two different lengths between 56 | // transitions. We call these short and long. Because the receiver may stretch or trim the radio pulses 57 | // we have separate bounds for the hi and low portions of the signal. 58 | // 59 | // The returned data is one bit per byte. 60 | // 61 | // It is possible to analyze an 62 | // arbitrary pulse stream to determine these bounds, but that is not part of this function. That logic 63 | // is present in the golang analysis tools. 64 | int ook_decode_manchester( struct ook_burst *burst, 65 | uint32_t minShortHi, uint32_t maxShortHi, 66 | uint32_t minLongHi, uint32_t maxLongHi, 67 | uint32_t minShortLow, uint32_t maxShortLow, 68 | uint32_t minLongLow, uint32_t maxLongLow, 69 | unsigned char **dataReturn, size_t *dataLenReturn, 70 | int verbose); 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /ookd.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "ook.h" 15 | #include "rtl.h" 16 | 17 | int verbose=0; 18 | static uint32_t centerFrequency = 433910000; 19 | static uint32_t sampleRate = 250000; 20 | 21 | static uint64_t sampleCounter = 0; 22 | 23 | static struct rtldev *rtlToStop = 0; // used by signal handlers to stop cleanly. 24 | 25 | static int multicastSocket = -1; 26 | static struct sockaddr *multicastSockaddr = 0; 27 | static size_t multicastSockaddrLen = 0; 28 | 29 | static int minPacket = 16; 30 | 31 | static const char *inputFileName = 0; 32 | 33 | static int showHistogram = 0; 34 | static int showModes = 0; 35 | 36 | static void showHelp( FILE *f) 37 | { 38 | fprintf(f, 39 | "Usage: ookd [-h] [-?] [-v] [-f frequency] [-a mcastaddr] [-p mcastport] [-i mcastinterface] [-m minpacket]\n" 40 | " -h | -? | --help display usage and exit\n" 41 | " -v | --verbose verbose logging\n" 42 | " -f nnnn | --frequency nnn set center frequency, default 433910000\n" 43 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 44 | " -p port | --multicast-port port multicast port, default 3636\n" 45 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 46 | " -m nnnn | --min-packet nnnn minimum number of pulses for a packet, default 10\n" 47 | " -r filename | --read-file filename read from input file instead of radio, for testing\n" 48 | " -H | --histogram show diagnostic histogram\n" 49 | " -M | --modes show diagnostic modes\n" 50 | ); 51 | } 52 | 53 | static uint64_t samplesToNs( uint64_t s) 54 | { 55 | return s*(1000000000/sampleRate); // that math could be better, but for 250000 is is exactly 4000, so ok. 56 | } 57 | 58 | static void recordPulse( unsigned n, unsigned rise, unsigned drop, unsigned end, 59 | unsigned cw, unsigned ccw, unsigned crazy, unsigned terminal) 60 | { 61 | static struct ook_burst *burst = 0; 62 | unsigned hiLen = drop-rise; 63 | unsigned lowLen = end-drop; 64 | 65 | // The frequency calculation could be a lot better. There is a lot of noise 66 | // in there which leads to misinterpretations of cw and ccw. There is a significant 67 | // variance in the pulse to pulse results of the same transmitter. 68 | float cycles = ((int)cw-(int)ccw)/4.0; 69 | if ( cycles > 0) cycles += crazy/2.0; // figure we are going fast enough to sometimes skip 70 | if ( cycles < 0) cycles -= crazy/2.0; // .. might ought to check that. 71 | float frequency = cycles/(hiLen/(float)sampleRate); 72 | 73 | if ( !burst) { 74 | burst = ook_allocate_burst(512*8); // first pulse of new burst 75 | if ( !burst) { 76 | fprintf(stderr,"Failed to allocate burst\n"); 77 | exit(-1); 78 | } else { 79 | burst->positionNanoseconds = samplesToNs( sampleCounter + rise); 80 | } 81 | } 82 | 83 | if ( ook_add_pulse(burst, samplesToNs(hiLen), samplesToNs(lowLen), lrint(frequency))) { 84 | fprintf(stderr,"Failed to add pulse to burst! Too long?\n"); 85 | } 86 | 87 | if ( terminal) { 88 | if ( burst) { 89 | if ( burst->pulses > minPacket) { 90 | void *data=0; 91 | size_t len; 92 | if ( ook_encode( burst, &data, &len) != 0 || data == 0) { 93 | fprintf(stderr, "Failed to encode a pulse burst.\n"); 94 | } else { 95 | int e = sendto( multicastSocket, data, len, 0, multicastSockaddr, multicastSockaddrLen); 96 | if ( e < 0) { 97 | fprintf(stderr, "Failed to multicast pulse (%zu bytes): %s\n", len, strerror(errno)); 98 | } 99 | if ( verbose) fprintf(stderr,"Multicast %u pulse, %zu bytes\n", burst->pulses, len); 100 | 101 | free(data); 102 | } 103 | } else { 104 | if ( verbose) fprintf(stderr,"Skipped run burst of %d pulses\n", burst->pulses); 105 | } 106 | 107 | free(burst); 108 | burst = 0; 109 | } 110 | } 111 | } 112 | 113 | static void findPulses( const unsigned char *data, uint32_t len, const float alpha) 114 | { 115 | enum motionType { NONE, CRAZY, CW, CCW }; 116 | static const unsigned char motion[16] = { // indexed by 4*oldquadrant+newquadrant 117 | NONE, CCW, CRAZY, CW, 118 | CW, NONE, CCW, CRAZY, 119 | CRAZY, CW, NONE, CCW, 120 | CCW, CRAZY, CW, NONE 121 | }; 122 | 123 | const int showAll = 0; 124 | static float lowPassPowerSquared = 0; 125 | 126 | static double totalPowerSquared = 0; 127 | static int powerSamples = 0; 128 | 129 | /* 130 | ** All of this static data is so we keep our state across 131 | ** invocations, it comes from being called in callbacks 132 | */ 133 | static enum { IDLE, HIGH, LOW} state = IDLE; 134 | static int quadrant = 0; // range 0-3 135 | 136 | static unsigned crazyMotion = 0; // these three are signal rotation during pulse high period 137 | static unsigned cwMotion = 0; 138 | static unsigned ccwMotion = 0; 139 | 140 | const float riseThreshold = 0.250; 141 | const float dropThreshold = 0.100; 142 | const unsigned lowLengthLimit = 2000; 143 | 144 | static int riseSample = 0; 145 | static int dropSample = 0; 146 | static unsigned pulseNumber = 0; 147 | 148 | for ( int i = 0; i < len; i += 2) { 149 | float I = (data[i]-128)/128.0; 150 | float Q = (data[i+1]-128)/128.0; 151 | float powerSquared = I*I+Q*Q; 152 | 153 | totalPowerSquared += powerSquared; 154 | powerSamples++; 155 | 156 | if ( verbose && powerSamples >= 100000) { 157 | fprintf(stderr,"average power is %5.2f\n", sqrt(totalPowerSquared/powerSamples)); 158 | powerSamples = 0; 159 | totalPowerSquared = 0; 160 | } 161 | 162 | unsigned newQuadrant =0; 163 | 164 | if ( I >= 0) { 165 | if ( Q >= 0) newQuadrant = 0; 166 | else newQuadrant = 3; 167 | } else { 168 | if ( Q >= 0) newQuadrant = 1; 169 | else newQuadrant = 2; 170 | } 171 | if ( state==HIGH) { 172 | if (showAll) fprintf(stderr,"%u->%u %5.2f %5.2f ", quadrant, newQuadrant, I, Q); 173 | switch( motion[4*quadrant + newQuadrant]) { 174 | case CRAZY: 175 | if ( showAll) fprintf(stderr," crazy\n"); 176 | crazyMotion++; 177 | break; 178 | case CW: 179 | if ( showAll) fprintf(stderr," cw\n"); 180 | cwMotion++; 181 | break; 182 | case CCW: 183 | if ( showAll) fprintf(stderr," ccw\n"); 184 | ccwMotion++; 185 | break; 186 | default: 187 | if ( showAll) fprintf(stderr,"\n"); 188 | break; 189 | } 190 | } 191 | quadrant = newQuadrant; 192 | 193 | lowPassPowerSquared = alpha*powerSquared + (1.0-alpha)*lowPassPowerSquared; 194 | 195 | if ( state==HIGH && lowPassPowerSquared < dropThreshold) { 196 | dropSample = i/2; 197 | state = LOW; 198 | } else if ( (state==IDLE || state==LOW) && lowPassPowerSquared > riseThreshold) { 199 | if ( state==LOW) { // if IDLE, the pulse was already pushed 200 | recordPulse( pulseNumber++, riseSample, dropSample, i/2, cwMotion, ccwMotion, crazyMotion, 0); 201 | } 202 | state = HIGH; 203 | riseSample = i/2; 204 | dropSample = 0; 205 | cwMotion = 0; 206 | ccwMotion = 0; 207 | crazyMotion = 0; 208 | } else if ( state==LOW && i/2 - dropSample > lowLengthLimit ) { 209 | state = IDLE; 210 | recordPulse( pulseNumber, riseSample, dropSample, i/2, cwMotion, ccwMotion, crazyMotion, 1); 211 | pulseNumber = 0; 212 | // ok to leave counters and timers, they get set on transition to HIGH 213 | } 214 | } 215 | 216 | if ( state != IDLE) { // shift them so they work on next invocation 217 | riseSample -= len/2; 218 | if ( state == LOW) dropSample -= len/2; 219 | } 220 | } 221 | 222 | static void debugHistogram( const unsigned char *data, uint32_t len, uint8_t bins, const float alpha) 223 | { 224 | unsigned bin[bins]; 225 | unsigned sbin[bins]; 226 | int havePower = 0; 227 | 228 | memset( bin, 0, sizeof(*bin)*bins); 229 | memset( sbin, 0, sizeof(*sbin)*bins); 230 | 231 | float s = 0; 232 | 233 | for ( int i = 0; i < len-1; i += 2) { 234 | float I = (data[i]-128)/128.0; 235 | float Q = (data[i+1]-128)/128.0; 236 | float powerSquared = I*I+Q*Q; 237 | 238 | s = alpha*powerSquared + (1.0-alpha)*s; 239 | 240 | float lowPassPowerSquared = s; 241 | 242 | unsigned b = powerSquared*(bins-1); 243 | if (b > bins-1) b = bins-1; 244 | bin[b]++; 245 | 246 | if ( b >= bins/4) havePower++; 247 | 248 | unsigned sb = lowPassPowerSquared*(bins-1); 249 | if (sb > bins-1) sb = bins-1; 250 | sbin[sb]++; 251 | } 252 | if ( havePower > len/128) { 253 | for ( int i = 0; i < bins; i++) { 254 | fprintf(stderr,"%5.3f %7u %7u\n", i/(float)bins, bin[i], sbin[i]); 255 | } 256 | } 257 | } 258 | 259 | static void debugModes( const unsigned char *data, uint32_t len) 260 | { 261 | if ( len < 8) return; // sort of hopeless for small bins 262 | 263 | unsigned samples = len/2; 264 | 265 | unsigned char power[samples]; 266 | 267 | for (unsigned s = 0; s < samples; s++) { 268 | float I = (data[s]-128)/128.0; // range -1 to 1 269 | float Q = (data[s+1]-128)/128.0; 270 | 271 | power[s] = floorf( hypotf(I,Q) / M_SQRT2 * 255.0); 272 | } 273 | 274 | // My initial means are guessed at 1/4 and 3/4 of the range 275 | unsigned char m1 = 256/4; 276 | unsigned char m2 = 3*256/4; 277 | 278 | for(unsigned iterations = 1; ; iterations++) { 279 | unsigned sum1 = 0, count1 = 0; 280 | unsigned sum2 = 0, count2 = 0; 281 | 282 | 283 | // Assign each sample to its most likely mode 284 | for ( unsigned s = 0; s < samples; s++) { 285 | int p = power[s]; 286 | unsigned char d1 = abs( p - m1); // distance from mode centers 287 | unsigned char d2 = abs( p - m2); 288 | 289 | if ( d1 < d2) { 290 | sum1 += p; 291 | count1++; 292 | } else { 293 | sum2 += p; 294 | count2++; 295 | } 296 | } 297 | 298 | // Calculate new centers for the modes 299 | unsigned char newM1 = count1 ? sum1/count1 : m1; // If no samples, leave it be 300 | unsigned char newM2 = count2 ? sum2/count2 : m2; 301 | 302 | if ( count1 == 0) newM1 = (m1 + newM2)/2; // Move toward other mode if we didn't get any 303 | if ( count2 == 0) newM2 = (m2 + newM1)/2; 304 | 305 | if ( (newM1 == m1 && newM2 == m2) || iterations >= 16) { // We converged or overiterated. 306 | fprintf(stderr, "LLOYD %5.3f[%5d] %5.3f[%5d] in %d\n", m1/255.0, count1, m2/255.0, count2, iterations); 307 | break; 308 | } 309 | 310 | m1 = newM1; 311 | m2 = newM2; 312 | } 313 | 314 | } 315 | 316 | static void iqHandler(const unsigned char *data, uint32_t len, void *ctx, struct rtldev *rtl) 317 | { 318 | if ( showHistogram) debugHistogram( data, len, 16, 0.2); 319 | if ( showModes) debugModes( data, len); 320 | 321 | findPulses( data, len, 0.2); 322 | sampleCounter += len/2; 323 | } 324 | 325 | static void exitNicely(int signum) 326 | { 327 | if ( rtlToStop) { 328 | rtlStop( rtlToStop); 329 | rtlToStop = 0; 330 | } else { 331 | exit(0); // we are stuck on something else 332 | } 333 | } 334 | 335 | static const char *humanName( struct sockaddr *addr, size_t len) 336 | { 337 | static char buf[INET6_ADDRSTRLEN]; 338 | int e = getnameinfo( addr, len, buf, sizeof(buf),0,0,NI_NUMERICHOST); 339 | if (e) return gai_strerror(e); 340 | return buf; 341 | } 342 | 343 | // contain OS specific nonsense here 344 | static int setMulticastIF( int sock, const struct sockaddr *addr, size_t len) 345 | { 346 | #if __APPLE__ 347 | switch ( addr->sa_family) { 348 | case AF_INET: 349 | return setsockopt( sock, IPPROTO_IP, IP_MULTICAST_IF, 350 | (char *)&(((struct sockaddr_in *)addr)->sin_addr), sizeof(struct in_addr)); 351 | default: 352 | errno = EINVAL; 353 | return -1; 354 | } 355 | #elif __linux__ 356 | // Shouldn't this be a ip_mreqn or ip_mreq structure?? 357 | return setsockopt( sock, IPPROTO_IP, IP_MULTICAST_IF, (char *)addr, len); 358 | #else 359 | #error Unsupported OS in setMulticastIF 360 | #endif 361 | } 362 | 363 | // exit() on error 364 | static void setupNetworking( const char *address, const char *port, const char *interface) 365 | { 366 | // Parse our multicast address 367 | { 368 | struct addrinfo *ai = 0; 369 | struct addrinfo hints = { .ai_family = AF_UNSPEC, 370 | .ai_socktype = SOCK_DGRAM, 371 | }; 372 | 373 | int err = getaddrinfo( address, port, &hints, &ai); 374 | if (err){ 375 | fprintf(stderr,"Illegal multicast address (addr=%s port=%s):%s\n", address, port, gai_strerror(err)); 376 | exit(1); 377 | } 378 | // add a verbose print here 379 | 380 | multicastSockaddr = (struct sockaddr *)malloc( ai->ai_addrlen); 381 | memcpy( multicastSockaddr, ai->ai_addr, ai->ai_addrlen); 382 | multicastSockaddrLen = ai->ai_addrlen; 383 | 384 | freeaddrinfo(ai); 385 | } 386 | 387 | // create our socket 388 | int sock = socket( multicastSockaddr->sa_family, SOCK_DGRAM, 0); 389 | if ( sock < 0) { 390 | fprintf(stderr,"Failed to create socket: %s\n", strerror(errno)); 391 | exit(1); 392 | } 393 | 394 | // Set our multicast interface 395 | { 396 | struct addrinfo *ai = 0; 397 | struct addrinfo hints = { .ai_family = AF_UNSPEC, 398 | .ai_socktype = SOCK_DGRAM, 399 | }; 400 | 401 | int err = getaddrinfo( interface, "0", &hints, &ai); 402 | if (err){ 403 | fprintf(stderr,"Illegal interface address (addr=%s):%s\n", address, gai_strerror(err)); 404 | exit(1); 405 | } 406 | // add a verbose print here 407 | 408 | if ( setMulticastIF( sock, ai->ai_addr, ai->ai_addrlen) < 0) { 409 | fprintf(stderr, "Failed to set multicast interface to %s (%s): %s\n", 410 | interface, humanName(ai->ai_addr, ai->ai_addrlen), strerror(errno)); 411 | exit(1); 412 | } 413 | 414 | // enable loopback so clients can be on this host 415 | uint8_t loop=1; 416 | setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)); 417 | 418 | freeaddrinfo(ai); 419 | } 420 | 421 | // Leave TTL defaulted to 1 for now, stay on subnet. 422 | 423 | 424 | multicastSocket = sock; 425 | } 426 | 427 | int main( int argc, char **argv) 428 | { 429 | const char *multicastAddress = "236.0.0.1"; 430 | const char *multicastPort = "3636"; 431 | const char *multicastInterface = "127.0.0.1"; 432 | 433 | // Handle options 434 | for(;;) { 435 | int optionIndex = 0; 436 | static struct option options[] = { 437 | { "verbose", no_argument, 0, 'v' }, 438 | { "help", no_argument, 0, 'h' }, 439 | { "frequency", required_argument, 0, 'f' }, 440 | { "multicast-address", required_argument, 0, 'a'}, 441 | { "multicast-port", required_argument, 0, 'p' }, 442 | { "multicast-interface", required_argument, 0, 'i' }, 443 | { "min-packet", required_argument, 0, 'm' }, 444 | { "read-file", required_argument, 0, 'r' }, 445 | { "histogram", no_argument, 0, 'H' }, 446 | { "modes", no_argument, 0, 'M' }, 447 | { 0,0,0,0} 448 | }; 449 | 450 | int c = getopt_long( argc, argv, "vh?HMf:a:p:i:m:r:", options, &optionIndex ); 451 | if ( c == -1) break; 452 | 453 | switch(c) { 454 | case 'h': 455 | case '?': 456 | showHelp(stdout); 457 | return 0; 458 | case 'v': 459 | verbose = 1; 460 | break; 461 | case 'H': 462 | showHistogram = 1; 463 | break; 464 | case 'M': 465 | showModes = 1; 466 | break; 467 | case 'f': 468 | // set frequency to optarg 469 | { 470 | unsigned f = atoi( optarg); 471 | if (f==0) { 472 | fprintf(stderr,"Bad frequency: %s\n", optarg); 473 | exit(1); 474 | } 475 | centerFrequency = f; 476 | } 477 | break; 478 | case 'm': 479 | minPacket = atoi(optarg); 480 | break; 481 | case 'a': 482 | multicastAddress = optarg; 483 | break; 484 | case 'i': 485 | multicastInterface = optarg; 486 | break; 487 | case 'p': 488 | multicastPort = optarg; 489 | break; 490 | case 'r': 491 | inputFileName = optarg; 492 | break; 493 | default: 494 | fprintf(stderr,"Illegal option\n"); 495 | showHelp(stderr); 496 | exit(1); 497 | } 498 | } 499 | 500 | setupNetworking(multicastAddress, multicastPort, multicastInterface); 501 | 502 | signal(SIGINT, exitNicely); 503 | 504 | if ( inputFileName == 0) { 505 | struct rtldev *rtl = rtlOpen(NULL,0); 506 | if ( !rtl) { 507 | fprintf(stderr,"Failed to open RTL SDR device\n"); 508 | exit(1); 509 | } 510 | 511 | if ( rtlSetup( rtl, centerFrequency, sampleRate) < 0) { 512 | fprintf(stderr,"Failed to setup RTL SDR for %uHz %usamp/sec\n", centerFrequency, sampleRate); 513 | } 514 | 515 | // something must call rtlStop(rtl) to kill this, to this end we stash in a global, ick 516 | rtlToStop = rtl; 517 | if ( rtlRun( rtl, iqHandler, 0)) { 518 | fprintf(stderr, "Failed to run iqHandler\n"); 519 | } 520 | rtlToStop = 0; 521 | 522 | rtlClose(rtl); 523 | } else { 524 | FILE *in = fopen( inputFileName, "r"); 525 | if ( !in) { 526 | fprintf(stderr, "Failed to open input file '%s': %s\n", inputFileName, strerror(errno)); 527 | exit(1); 528 | } 529 | 530 | for(;;) { 531 | unsigned char buf[16384]; 532 | size_t got = fread( buf, 1, sizeof(buf), in); 533 | if ( got == 0 && feof(in)) break; 534 | if ( got == 0) { 535 | fprintf(stderr, "Error while reading input file: %s\n", strerror(errno)); 536 | exit(1); 537 | } 538 | iqHandler( buf, got, 0, 0); 539 | } 540 | 541 | fclose(in); 542 | } 543 | 544 | // 545 | // the rest of this is just in case someone is running a leak detector on us. 546 | // 547 | if ( multicastSocket != -1) { 548 | close(multicastSocket); 549 | multicastSocket = -1; 550 | } 551 | if ( multicastSockaddr) { 552 | free(multicastSockaddr); 553 | multicastSockaddr = 0; 554 | multicastSockaddrLen = 0; 555 | } 556 | 557 | return 0; 558 | } 559 | -------------------------------------------------------------------------------- /ookdump.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ook.h" 9 | 10 | int verbose=0; 11 | 12 | static void showHelp( FILE *f) 13 | { 14 | fprintf(f, 15 | "Usage: ookdump [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface]\n" 16 | " -h | -? | --help display usage and exit\n" 17 | " -v | --verbose verbose logging\n" 18 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 19 | " -p port | --multicast-port port multicast port, default 3636\n" 20 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 21 | ); 22 | } 23 | 24 | 25 | int main( int argc, char **argv) 26 | { 27 | const char *multicastAddress = "236.0.0.1"; 28 | const char *multicastPort = "3636"; 29 | const char *multicastInterface = "127.0.0.1"; 30 | 31 | // Handle options 32 | for(;;) { 33 | int optionIndex = 0; 34 | static struct option options[] = { 35 | { "verbose", no_argument, 0, 'v' }, 36 | { "help", no_argument, 0, 'h' }, 37 | { "multicast-address", required_argument, 0, 'a'}, 38 | { "multicast-port", required_argument, 0, 'p' }, 39 | { "multicast-interface", required_argument, 0, 'i' }, 40 | { 0,0,0,0} 41 | }; 42 | 43 | int c = getopt_long( argc, argv, "vh?f:a:p:i:", options, &optionIndex ); 44 | if ( c == -1) break; 45 | 46 | switch(c) { 47 | case 'h': 48 | case '?': 49 | showHelp(stdout); 50 | return 0; 51 | case 'v': 52 | verbose = 1; 53 | break; 54 | case 'a': 55 | multicastAddress = optarg; 56 | break; 57 | case 'p': 58 | multicastPort = optarg; 59 | break; 60 | case 'i': 61 | multicastInterface = optarg; 62 | break; 63 | default: 64 | fprintf(stderr,"Illegal option\n"); 65 | showHelp(stderr); 66 | exit(1); 67 | } 68 | } 69 | 70 | 71 | // Parse our multicast address 72 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 73 | if ( sock < 0) { 74 | fprintf(stderr,"Failed to open multicast interface\n"); 75 | exit(1); 76 | } 77 | 78 | for (;;) { 79 | struct ook_burst *burst; 80 | struct sockaddr_storage addr; 81 | socklen_t addrLen = sizeof(addr); 82 | 83 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 84 | if ( e < 0) { 85 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 86 | break; 87 | } 88 | if ( e == 0) { 89 | fprintf(stderr,"Corrupt burst\n"); 90 | continue; 91 | } 92 | 93 | printf("%014.6fs ### %3u pulses\n", burst->positionNanoseconds/1000000000.0, burst->pulses); 94 | printf("num high low freq\n"); 95 | for ( int i = 0; i < burst->pulses; i++) { 96 | printf( "%3u %4uuS %6uuS %8.3fkHz\n", i+1, burst->pulse[i].hiNanoseconds/1000, 97 | burst->pulse[i].lowNanoseconds/1000, burst->pulse[i].frequencyOffsetHz/1000.0); 98 | } 99 | 100 | fflush(stdin); 101 | free(burst); 102 | } 103 | 104 | close(sock); 105 | return 0; 106 | } 107 | -------------------------------------------------------------------------------- /oregonsci.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ook.h" 13 | #include "datum.h" 14 | 15 | int verbose=0; 16 | 17 | static time_t oldestDatum = 0; 18 | static struct datum temperature[3]; 19 | static struct datum humidity[3]; 20 | static struct datum averageWindSpeed; 21 | static struct datum gustWindSpeed; 22 | static struct datum rainfall; 23 | static struct datum batteryLow; 24 | static struct datum windDirection; 25 | static struct cdatum windVector; 26 | 27 | static void dumpWeather( void) 28 | { 29 | dumpDatum( &temperature[0], "temperature0", "C"); 30 | dumpDatum( &temperature[1], "temperature1", "C"); 31 | dumpDatum( &temperature[2], "temperature2", "C"); 32 | dumpDatum( &humidity[0], "humidity0", "%"); 33 | dumpDatum( &humidity[1], "humidity1", "%"); 34 | dumpDatum( &humidity[2], "humidity2", "%"); 35 | dumpDatum( &averageWindSpeed, "wind", "m/s"); 36 | dumpDatum( &gustWindSpeed, "gust", "m/s"); 37 | dumpDatum( &rainfall, "rainfall", "m"); 38 | dumpDatum( &batteryLow, "batt", "??"); 39 | dumpDatum( &windDirection, "dir", "NEWS"); 40 | dumpCDatum( &windVector, "wind", "m/s"); 41 | } 42 | 43 | static void recordRecent( const char *file, 44 | double temp0, double temp1, double temp2, 45 | double hum0, double hum1, double hum2, 46 | double avgWind, double gustWind, 47 | double rain, 48 | int batteryLowBits, int windDirectionBits) 49 | { 50 | char name[256]; 51 | strcpy(name,"/tmp/oregonsci.XXXXXX"); 52 | 53 | int fd = mkstemp(name); 54 | if ( fd < 0) { 55 | fprintf(stderr,"Failed to create temp file for recent: %s\n", strerror(errno)); 56 | return; 57 | } 58 | 59 | if ( fchmod( fd, 0664) != 0) { 60 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 61 | unlink(name); 62 | close(fd); 63 | return; 64 | } 65 | 66 | FILE *f = fdopen(fd,"w"); 67 | fprintf(f,"{\n"); 68 | fprintf(f,"\t\"temperature0\":%.1f,\n", temp0); 69 | fprintf(f,"\t\"temperature1\":%.1f,\n", temp1); 70 | fprintf(f,"\t\"temperature2\":%.1f,\n", temp2); 71 | fprintf(f,"\t\"humidity0\":%.1f,\n", hum0); 72 | fprintf(f,"\t\"humidity1\":%.1f,\n", hum1); 73 | fprintf(f,"\t\"humidity2\":%.1f,\n", hum2); 74 | fprintf(f,"\t\"avgWindSpeed\":%.1f,\n", avgWind); 75 | fprintf(f,"\t\"gustSpeed\":%.1f,\n", gustWind); 76 | fprintf(f,"\t\"rainfall\":%.4f,\n", rain); 77 | fprintf(f,"\t\"batteryLow\":%d,\n", batteryLowBits); 78 | fprintf(f,"\t\"windDirection\":%d\n", windDirectionBits); 79 | fprintf(f,"}\n"); 80 | fclose(f); 81 | 82 | if (rename( name, file)) { 83 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 84 | } 85 | } 86 | 87 | static void recordDatum( FILE *f, struct datum *d, const char *name, int comma) 88 | { 89 | 90 | fprintf(f, "\t\"%s\" : { \"n\":%d, \"sum\":%.3g, \"sum2\":%.4g, \"min\":%.3g, \"max\":%.3g }%s\n", 91 | name, d->n, d->sum, d->sumOfSquares, d->minimum, d->maximum, (comma ? ",":"")); 92 | resetDatum( d); 93 | } 94 | 95 | 96 | static void recordCDatum( FILE *f, struct cdatum *d, const char *name, int comma) 97 | { 98 | 99 | fprintf(f, "\t\"%s\" : { \"n\":%d, \"sum\":[%.3g,%3.g], \"sum2\":[%.4g,%.4g], \"min\":[%.3g,%.3g], \"max\":[%.3g,%.3g] }%s\n", 100 | name, d->n, 101 | creal(d->sum), cimag(d->sum), 102 | creal(d->sumOfSquares), cimag(d->sumOfSquares), 103 | creal(d->minimum), cimag(d->minimum), 104 | creal(d->maximum), cimag(d->maximum), 105 | (comma ? ",":"")); 106 | resetCDatum( d); 107 | } 108 | 109 | 110 | static void recordPeriodic( const char *file) 111 | { 112 | char name[256]; 113 | strcpy(name,"/tmp/oregonsci.XXXXXX"); 114 | 115 | int fd = mkstemp(name); 116 | if ( fd < 0) { 117 | fprintf(stderr,"Failed to create temp file for periodic: %s\n", strerror(errno)); 118 | return; 119 | } 120 | 121 | if ( fchmod( fd, 0664) != 0) { 122 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 123 | unlink(name); 124 | close(fd); 125 | return; 126 | } 127 | 128 | FILE *f = fdopen(fd,"w"); 129 | fprintf(f,"{\n"); 130 | fprintf(f,"\t\"start\":%lld,\n", (long long)oldestDatum); 131 | fprintf(f,"\t\"end\":%lld,\n", (long long)time(0)); 132 | recordDatum( f, &temperature[0], "temperature0", 1); 133 | recordDatum( f, &temperature[1], "temperature1", 1); 134 | recordDatum( f, &temperature[2], "temperature2", 1); 135 | recordDatum( f, &humidity[0], "humidity0", 1); 136 | recordDatum( f, &humidity[1], "humidity1", 1); 137 | recordDatum( f, &humidity[2], "humidity2", 1); 138 | recordDatum( f, &averageWindSpeed, "avgWindSpeed", 1); 139 | recordDatum( f, &gustWindSpeed, "gustSpeed", 1); 140 | recordDatum( f, &rainfall, "rainfall", 1); 141 | recordDatum( f, &batteryLow, "batteryLow", 1); 142 | recordDatum( f, &windDirection, "windDirection", 1); 143 | recordCDatum( f, &windVector, "windVector", 0); 144 | fprintf(f,"}\n"); 145 | fclose(f); 146 | 147 | char stamp[32]={0}; 148 | strftime( stamp, sizeof(stamp), "%Y%m%d-%H%M%S", gmtime(&oldestDatum)); 149 | char finalName[1024]="/ERROR"; 150 | snprintf(finalName, sizeof(finalName)-1, "%s-%s.json", file, stamp); 151 | 152 | if (rename( name, finalName)) { 153 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 154 | } 155 | 156 | oldestDatum = 0; 157 | 158 | } 159 | 160 | static void showHelp( FILE *f) 161 | { 162 | fprintf(f, 163 | "Usage: oregonsci [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface]\n" 164 | " -h | -? | --help display usage and exit\n" 165 | " -v | --verbose verbose logging\n" 166 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 167 | " -p port | --multicast-port port multicast port, default 3636\n" 168 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 169 | " -r path | --recent path path to most recent data, /tmp/current-weather.json\n" 170 | " -P path | --periodic path path to the periodic data, /tmp/weather\n" 171 | " timestamp.json gets appended.\n" 172 | " -m period | --minutes period number of minutes between periodic data files.\n" 173 | ); 174 | } 175 | 176 | static int okChecksum( unsigned char *nibbles, unsigned int csumLocation) { 177 | unsigned int sum = 0; 178 | for ( int n = 7; n < csumLocation; n++) { // skips sync and preamble 179 | sum += nibbles[n]; 180 | } 181 | unsigned int csum = nibbles[csumLocation+1]*16 + nibbles[csumLocation]; 182 | return sum == csum; 183 | } 184 | 185 | int main( int argc, char **argv) 186 | { 187 | const char *multicastAddress = "236.0.0.1"; 188 | const char *multicastPort = "3636"; 189 | const char *multicastInterface = "127.0.0.1"; 190 | const char *recentFileName = "/tmp/current-weather.json"; 191 | const char *periodicFileName = "/tmp/weather"; 192 | int minutes = 5; 193 | 194 | double recentTemp[3] = {-500.0, -500.0, -500.0}; 195 | double recentHum[3] = {-1,-1,-1}; 196 | double recentWind = -1; 197 | double recentGust = -1; 198 | double recentRain = -1; 199 | int recentBattery = 0; 200 | int recentDirection = -1; 201 | 202 | int lastRainCounter = -1; 203 | 204 | // Handle options 205 | for(;;) { 206 | int optionIndex = 0; 207 | static struct option options[] = { 208 | { "verbose", no_argument, 0, 'v' }, 209 | { "help", no_argument, 0, 'h' }, 210 | { "multicast-address", required_argument, 0, 'a'}, 211 | { "multicast-port", required_argument, 0, 'p' }, 212 | { "multicast-interface", required_argument, 0, 'i' }, 213 | { "recent", required_argument, 0, 'r' }, 214 | { "periodic", required_argument, 0, 'P' }, 215 | { "minutes", required_argument, 0, 'm' }, 216 | { 0,0,0,0} 217 | }; 218 | 219 | int c = getopt_long( argc, argv, "vh?f:a:p:i:m:r:P:", options, &optionIndex ); 220 | if ( c == -1) break; 221 | 222 | switch(c) { 223 | case 'h': 224 | case '?': 225 | showHelp(stdout); 226 | return 0; 227 | case 'v': 228 | verbose = 1; 229 | break; 230 | case 'a': 231 | multicastAddress = optarg; 232 | break; 233 | case 'p': 234 | multicastPort = optarg; 235 | break; 236 | case 'i': 237 | multicastInterface = optarg; 238 | break; 239 | case 'r': 240 | recentFileName = optarg; 241 | break; 242 | case 'P': 243 | periodicFileName = optarg; 244 | break; 245 | case 'm': 246 | { 247 | int m = atoi(optarg); 248 | if (m<1) { 249 | fprintf(stderr,"Illegal minutes, less than 1\n"); 250 | exit(1); 251 | } 252 | minutes = m; 253 | } 254 | break; 255 | default: 256 | fprintf(stderr,"Illegal option\n"); 257 | showHelp(stderr); 258 | exit(1); 259 | } 260 | } 261 | 262 | if ( verbose) fprintf(stderr,"Periodic file is %s\n", periodicFileName); 263 | 264 | // Parse our multicast address 265 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 266 | if ( sock < 0) { 267 | fprintf(stderr,"Failed to open multicast interface\n"); 268 | exit(1); 269 | } 270 | 271 | for (;;) { 272 | struct ook_burst *burst; 273 | struct sockaddr_storage addr; 274 | socklen_t addrLen = sizeof(addr); 275 | 276 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 277 | if ( e < 0) { 278 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 279 | break; 280 | } 281 | if ( e == 0) { 282 | fprintf(stderr,"Corrupt burst\n"); 283 | continue; 284 | } 285 | 286 | // Data never comes faster than 5 seconds, we are looking at the second of a pair 287 | // of redundant transmissions 288 | //if ( oldestDatum && time(0)-oldestDatum < 5) continue; 289 | 290 | { 291 | unsigned char *data = 0; 292 | size_t dataLen = 0; 293 | int bits = ook_decode_manchester( burst, 294 | 200000, 715000, // on short 295 | 715000, 1200000, // on long 296 | 200000, 650000, // off short 297 | 650000, 1200000, //off long 298 | &data, &dataLen, 299 | verbose); 300 | 301 | if ( bits > 0) { 302 | unsigned nibbles = (bits+3)/4; 303 | unsigned char nibble[ nibbles]; 304 | memset( nibble, 0, nibbles); 305 | for ( int i = 0; i < bits; i++) { 306 | if (data[i]==0) { 307 | nibble[ i/4] += (1<<(i%4)); // least significant first 308 | } 309 | } 310 | if ( verbose) { 311 | fprintf(stderr, "Decoded manchester %d bits, %d nibbles ", bits, nibbles); 312 | for ( int n = 0; n < nibbles; n++) { 313 | fprintf(stderr,"%x", nibble[n]); 314 | } 315 | fprintf(stderr,"\n"); 316 | } 317 | 318 | if ( nibbles < 16) { 319 | if ( verbose) fprintf(stderr,"too short to be valid data\n"); 320 | continue; 321 | } 322 | 323 | for ( int i = 0; i < 6; i++) { 324 | if ( nibble[i] != 0x0f) { 325 | if ( verbose) fprintf(stderr,"sync bits were not all 0xf\n"); 326 | continue; 327 | } 328 | } 329 | 330 | if ( nibble[6] != 0xa) { 331 | if ( verbose) fprintf(stderr,"preamble was not 0xa\n"); 332 | continue; 333 | } 334 | 335 | unsigned int sensorId = (nibble[7]<<12) + (nibble[8]<<8) + (nibble[9]<<4) + nibble[10]; 336 | unsigned int channel = nibble[11]; 337 | unsigned int rollingCode = (nibble[12]<<4)+nibble[13]; 338 | unsigned int flags = nibble[14]; 339 | if ( verbose) fprintf(stderr,"sensor=%04x channel=%d rollingcode=%d flags=0x%x\n", sensorId, channel, rollingCode, flags); 340 | 341 | // Sensor specific data begins at 15. 342 | switch( sensorId) { 343 | case 0xf824: 344 | case 0x1220: 345 | case 0xf8b4: 346 | { 347 | if ( nibbles != 26) { 348 | if ( verbose) fprintf(stderr,"Temperature/Humidity sensor data is wrong length, sensorid=%04x, lenght=%d needed 26\n", sensorId, nibbles); 349 | break; 350 | } 351 | if ( okChecksum( nibble, 22)) { 352 | int tempTenthsC = nibble[17]*100+nibble[16]*10+nibble[15]; 353 | if (nibble[18] != 0) tempTenthsC *= -1; 354 | int relativeHum = nibble[20]*10 + nibble[19]; 355 | if ( verbose) fprintf(stderr,"Temp=%4.1fC Hum=%02d%% %d\n", tempTenthsC/10.0, relativeHum, nibbles); 356 | if ( channel <= 2) { 357 | addSample( &temperature[channel], tempTenthsC/10.0); 358 | addSample( &humidity[channel], relativeHum); 359 | recentTemp[channel] = tempTenthsC/10.0; 360 | recentHum[channel] = relativeHum; 361 | } else { 362 | fprintf(stderr,"Bad channel on sensor %04x, channel %d\n", sensorId, channel); 363 | } 364 | if (oldestDatum == 0) oldestDatum = time(0); 365 | 366 | } else { 367 | fprintf(stderr,"Bad checksum on sensor %04x\n", sensorId); 368 | } 369 | 370 | } 371 | break; 372 | case 0x2914: 373 | { 374 | if ( nibbles != 29) { 375 | if ( verbose) fprintf(stderr,"Rain sensor data is wrong length, sensorid=%04x, lenght=%d needed 29\n", sensorId, nibbles); 376 | break; 377 | } 378 | if ( okChecksum( nibble, 25)) { 379 | const int inchesPerMeter = 1000.0/25.4; 380 | int rainHundrethsPerHour = nibble[18]*1000+nibble[17]*100+nibble[16]*10+nibble[15]; // inch/100 381 | int rainCount = nibble[24]*100000 + nibble[23]*10000 + nibble[22]*1000 + 382 | nibble[21]*100 + nibble[20]*10 + nibble[19]; // inch/1000 383 | if ( verbose) fprintf(stderr,"Rain=%4.1fin/hr Tot=%6d thousandths\n", rainHundrethsPerHour/100.0, rainCount); 384 | if ( lastRainCounter < 0 || lastRainCounter > rainCount ) { // if first or wrapped, just set for later 385 | lastRainCounter = rainCount; 386 | } else { 387 | int r = (rainCount - lastRainCounter) * inchesPerMeter; 388 | lastRainCounter = rainCount; 389 | recentRain = r; 390 | addSample(&rainfall, r); 391 | if (oldestDatum == 0) oldestDatum = time(0); 392 | } 393 | } else { 394 | fprintf(stderr,"Bad checksum on sensor %04x\n", sensorId); 395 | } 396 | 397 | } 398 | break; 399 | case 0x1984: 400 | case 0x1994: 401 | { 402 | if ( nibbles != 28) { 403 | if ( verbose) fprintf(stderr,"Wind sensor data is wrong length, sensorid=%04x, lenght=%d needed 28\n", sensorId, nibbles); 404 | break; 405 | } 406 | if ( okChecksum( nibble, 24)) { 407 | int direction = nibble[15]; 408 | int directionDegrees = (int)(direction*22.5); 409 | 410 | // 16 and 17 are unknown 411 | int currentSpeed = nibble[20]*100 + nibble[19]*10 + nibble[18]; 412 | int averageSpeed = nibble[23]*100 + nibble[22]*10 + nibble[21]; 413 | 414 | if ( verbose) fprintf(stderr,"Wind=%4.1fm/s avg=%4.1fm/s dir=%ds\n", currentSpeed/10.0, averageSpeed/10.0, directionDegrees); 415 | 416 | addSample( &averageWindSpeed, averageSpeed/10.0); 417 | addSample( &gustWindSpeed, currentSpeed/10.0); 418 | addSample( &windDirection, directionDegrees); 419 | addCSampleMA( &windVector, averageSpeed/10.0, directionDegrees/360.0*M_2_PI); 420 | 421 | recentWind = averageSpeed/10.0; 422 | recentGust = currentSpeed/10.0; 423 | recentDirection = directionDegrees; 424 | 425 | if (oldestDatum == 0) oldestDatum = time(0); 426 | } else { 427 | fprintf(stderr,"Bad checksum on sensor %04x\n", sensorId); 428 | } 429 | 430 | } 431 | break; 432 | break; 433 | default: 434 | if (verbose) fprintf(stderr,"Unknown sensor: %04x\n", sensorId); 435 | } 436 | 437 | if ( oldestDatum && time(0)-oldestDatum > minutes*60) { 438 | recordPeriodic( periodicFileName); 439 | } 440 | 441 | if ( verbose) dumpWeather(); 442 | 443 | recordRecent( recentFileName, 444 | recentTemp[0], recentTemp[1], recentTemp[2], 445 | recentHum[0], recentHum[1], recentHum[2], 446 | recentWind, recentGust, recentRain, recentBattery, recentDirection); 447 | } else { 448 | if ( verbose) fprintf(stderr,"ignored %d pulse burst\n", burst->pulses); 449 | } 450 | 451 | if (data) free(data); 452 | } 453 | 454 | fflush(stdin); 455 | free(burst); 456 | } 457 | 458 | close(sock); 459 | return 0; 460 | } 461 | -------------------------------------------------------------------------------- /rtl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "rtl.h" 6 | 7 | const int magic = 0x471005D4; 8 | 9 | static int rtlOk( const struct rtldev *r) 10 | { 11 | if ( !r || r->magic != magic) return 0; 12 | return 1; 13 | } 14 | 15 | struct rtldev *rtlOpen( const char *serial, int32_t ind) 16 | { 17 | if ( ind < 0) { 18 | if ( serial) { 19 | ind = rtlsdr_get_index_by_serial( serial); 20 | if ( ind < 0) return 0; 21 | } else { 22 | ind = 0; 23 | } 24 | } 25 | 26 | rtlsdr_dev_t *dev = 0; 27 | int e = rtlsdr_open( &dev, (uint32_t)ind); 28 | 29 | if ( e == 0 && dev != 0) { 30 | struct rtldev *r = calloc( sizeof(*r), 1); 31 | r->magic = magic; 32 | r->dev = dev; 33 | return r; 34 | } else { 35 | return 0; 36 | } 37 | } 38 | 39 | void rtlClose( struct rtldev *r) 40 | { 41 | if (!r) return; 42 | if (!rtlOk(r)) { 43 | fprintf(stderr,"RTL SDR passed to rtlClose is corrupted\n"); 44 | return; 45 | } 46 | 47 | rtlsdr_cancel_async( r->dev); 48 | // don't check error, looks like it gives -2 if you weren't streaming. 49 | 50 | int e = rtlsdr_close( r->dev); 51 | if ( e) { 52 | fprintf(stderr,"Failed to close RTL SDR device\n"); 53 | } 54 | 55 | memset(r, 0, sizeof(*r)); // make invalid 56 | free(r); 57 | } 58 | 59 | int rtlSetup( struct rtldev *r, uint32_t frequency, uint32_t sampleRate) 60 | { 61 | if ( !rtlOk(r)) { 62 | return -1; 63 | } 64 | 65 | int e = rtlsdr_set_center_freq( r->dev, frequency); 66 | if (e) { 67 | fprintf(stderr,"Failed to set center frequency to %uHz\n", frequency); 68 | return -1; 69 | } 70 | 71 | int s = rtlsdr_set_sample_rate( r->dev, sampleRate); 72 | if (s) { 73 | fprintf(stderr,"Failed to set sample rate to %usamples/sec\n", sampleRate); 74 | return -1; 75 | } 76 | 77 | int a = rtlsdr_set_tuner_gain_mode( r->dev, 0); 78 | if ( a) { 79 | fprintf(stderr,"Failed to set tuner automatic gain mode\n"); 80 | return -1; 81 | } 82 | 83 | return 0; 84 | } 85 | 86 | struct rtlHandlerState { 87 | struct rtldev *rtl; 88 | sdr_handler handler; 89 | void *ctx; 90 | }; 91 | 92 | static void rtlHandler(unsigned char *buf, uint32_t len, void *ctx) 93 | { 94 | struct rtlHandlerState *state = (struct rtlHandlerState *)ctx; 95 | 96 | state->handler( buf, len, state->ctx, state->rtl); 97 | } 98 | 99 | int rtlRun( struct rtldev *r, sdr_handler handler, void *ctx) 100 | { 101 | if ( !rtlOk(r)) { 102 | return -1; 103 | } 104 | 105 | int re = rtlsdr_reset_buffer( r->dev); // this is important 106 | if ( re) return 0; 107 | 108 | struct rtlHandlerState state = { r, handler, ctx }; 109 | 110 | int e = rtlsdr_read_async( r->dev, rtlHandler, (void *)&state, 0,0); 111 | 112 | if ( e != 0) return 0; 113 | 114 | return 0; 115 | } 116 | 117 | int rtlStop( struct rtldev *r) 118 | { 119 | if ( !rtlOk(r)) { 120 | return -1; 121 | } 122 | return rtlsdr_cancel_async(r->dev); 123 | } 124 | 125 | -------------------------------------------------------------------------------- /rtl.h: -------------------------------------------------------------------------------- 1 | #ifndef RTL_IS_IN 2 | #define RTL_IS_IN 3 | 4 | /* 5 | ** This is vaguely similar to the rtl-sdr API, but is slightly easier to use for simple use cases, 6 | ** and safer. 7 | ** 8 | ** It will print errors to stderr for programmer misuse or misconfiguration. 9 | ** 10 | */ 11 | 12 | #include 13 | 14 | struct rtldev { 15 | int magic; // set to 0x471005D4 if this is live, 16 | rtlsdr_dev_t *dev; 17 | }; 18 | 19 | typedef void (*sdr_handler)(const unsigned char *data, uint32_t len, void *ctx, struct rtldev *rtl); 20 | 21 | 22 | /* 23 | ** Open by name or index. 24 | ** 25 | ** If index is not -1, then it will be used to select the device 26 | ** If serial is not NULL then it will be used to select the device 27 | ** Otherwise the first device is selected 28 | ** 29 | ** NULL is returned for failure 30 | */ 31 | struct rtldev *rtlOpen( const char *serial, int32_t index); 32 | 33 | /* 34 | ** Close and free an open device, it is ok to pass in NULL 35 | */ 36 | void rtlClose( struct rtldev *rtl); 37 | 38 | /* 39 | ** Tune the center frequency and sample rate. 40 | ** 41 | ** Returns <0 on failure. 42 | */ 43 | int rtlSetup( struct rtldev *r, uint32_t centerFrequency, uint32_t sampleRate); 44 | 45 | /* 46 | ** Begin processing the signal. It will repeatedly 47 | ** invoke your handler and pass it buffers. 48 | ** 49 | ** This will continue until an rtlStop() is used. This might happen 50 | ** in your handler, or perhaps in a signal() handler. 51 | */ 52 | int rtlRun( struct rtldev *rtl, sdr_handler handler, void *ctx); 53 | 54 | /* 55 | ** Stop processing the signal, if running 56 | ** Safe to call from signal() handlers. 57 | */ 58 | int rtlStop( struct rtldev *rtl); 59 | 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /samples/samp1.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimstudt/ook-decoder/HEAD/samples/samp1.dat -------------------------------------------------------------------------------- /samples/samp2.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimstudt/ook-decoder/HEAD/samples/samp2.dat -------------------------------------------------------------------------------- /wh1080.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "ook.h" 12 | 13 | int verbose=0; 14 | 15 | // Datum keeps enough information to sum them up and still compute the standard deviation 16 | struct datum { 17 | unsigned n; 18 | double sum; 19 | double sumOfSquares; 20 | double maximum; 21 | double minimum; 22 | }; 23 | 24 | static time_t oldestDatum = 0; 25 | static struct datum temperature; 26 | static struct datum humidity; 27 | static struct datum averageWindSpeed; 28 | static struct datum gustWindSpeed; 29 | static struct datum rainfall; 30 | static struct datum batteryLow; 31 | static struct datum windDirection; 32 | 33 | static void resetDatum(struct datum *d) 34 | { 35 | memset(d,0,sizeof(*d)); 36 | } 37 | 38 | static void addSample( struct datum *d, double v) 39 | { 40 | if ( oldestDatum == 0) oldestDatum = time(0); 41 | 42 | if ( d->n==0) { 43 | d->minimum = v; 44 | d->maximum = v; 45 | } else { 46 | if ( v > d->maximum) d->maximum = v; 47 | if ( v < d->minimum) d->minimum = v; 48 | } 49 | d->n++; 50 | d->sum += v; 51 | d->sumOfSquares += v*v; 52 | } 53 | 54 | static void dumpDatum( struct datum *d, const char *name, const char *units) 55 | { 56 | if ( d->n == 0) { 57 | fprintf(stderr,"%s no data\n", name); 58 | } else { 59 | fprintf(stderr, "%s %u samples, %5.1f %s min %5.1f max %5.1f\n", 60 | name, d->n, d->sum/d->n, units, d->minimum, d->maximum); 61 | } 62 | } 63 | 64 | static void dumpWeather( void) 65 | { 66 | dumpDatum( &temperature, "temperature", "C"); 67 | dumpDatum( &humidity, "humidity", "%"); 68 | dumpDatum( &averageWindSpeed, "wind", "m/s"); 69 | dumpDatum( &gustWindSpeed, "gust", "m/s"); 70 | dumpDatum( &rainfall, "rain", "mm"); 71 | dumpDatum( &batteryLow, "batt", "??"); 72 | dumpDatum( &windDirection, "dir", "NEWS"); 73 | } 74 | 75 | static void recordRecent( const char *file, double temp, double hum, double avgWind, double gustWind, double rain, 76 | int batteryLowBits, int windDirectionBits) 77 | { 78 | char name[256]; 79 | strcpy(name,"/tmp/wh1080.XXXXXX"); 80 | 81 | int fd = mkstemp(name); 82 | if ( fd < 0) { 83 | fprintf(stderr,"Failed to create temp file for recent: %s\n", strerror(errno)); 84 | return; 85 | } 86 | 87 | if ( fchmod( fd, 0664) != 0) { 88 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 89 | unlink(name); 90 | close(fd); 91 | return; 92 | } 93 | 94 | FILE *f = fdopen(fd,"w"); 95 | fprintf(f,"{\n"); 96 | fprintf(f,"\t\"temperature\":%.1f,\n", temp); 97 | fprintf(f,"\t\"humidity\":%.1f,\n", hum); 98 | fprintf(f,"\t\"avgWindSpeed\":%.1f,\n", avgWind); 99 | fprintf(f,"\t\"gustSpeed\":%.1f,\n", gustWind); 100 | fprintf(f,"\t\"rain\":%.1f,\n", rain); 101 | fprintf(f,"\t\"batteryLow\":%d,\n", batteryLowBits); 102 | fprintf(f,"\t\"windDirection\":%d\n", windDirectionBits*45); 103 | fprintf(f,"}\n"); 104 | fclose(f); 105 | 106 | if (rename( name, file)) { 107 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 108 | } 109 | } 110 | 111 | static void recordDatum( FILE *f, struct datum *d, const char *name, int comma) 112 | { 113 | fprintf(f, "\t\"%s\" : { \"n\":%d, \"sum\":%.1f, \"sum2\":%.1f, \"min\":%.1f, \"max\":%.1f }%s\n", 114 | name, d->n, d->sum, d->sumOfSquares, d->minimum, d->maximum, (comma ? ",":"")); 115 | resetDatum( d); 116 | } 117 | 118 | 119 | static void recordPeriodic( const char *file) 120 | { 121 | char name[256]; 122 | strcpy(name,"/tmp/wh1080.XXXXXX"); 123 | 124 | int fd = mkstemp(name); 125 | if ( fd < 0) { 126 | fprintf(stderr,"Failed to create temp file for periodic: %s\n", strerror(errno)); 127 | return; 128 | } 129 | 130 | if ( fchmod( fd, 0664) != 0) { 131 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 132 | unlink(name); 133 | close(fd); 134 | return; 135 | } 136 | 137 | FILE *f = fdopen(fd,"w"); 138 | fprintf(f,"{\n"); 139 | fprintf(f,"\t\"start\":%lld,\n", (long long)oldestDatum); 140 | fprintf(f,"\t\"end\":%lld,\n", (long long)time(0)); 141 | recordDatum( f, &temperature, "temperature", 1); 142 | recordDatum( f, &humidity, "humidity", 1); 143 | recordDatum( f, &averageWindSpeed, "avgWindSpeed", 1); 144 | recordDatum( f, &gustWindSpeed, "gustSpeed", 1); 145 | recordDatum( f, &rainfall, "rainfall", 1); 146 | recordDatum( f, &batteryLow, "batteryLow", 1); 147 | recordDatum( f, &windDirection, "windDirection", 0); 148 | fprintf(f,"}\n"); 149 | fclose(f); 150 | 151 | char stamp[32]={0}; 152 | strftime( stamp, sizeof(stamp), "%Y%m%d-%H%M%S", gmtime(&oldestDatum)); 153 | char finalName[1024]="/ERROR"; 154 | snprintf(finalName, sizeof(finalName)-1, "%s-%s.json", file, stamp); 155 | 156 | if (rename( name, finalName)) { 157 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 158 | } 159 | 160 | oldestDatum = 0; 161 | 162 | } 163 | 164 | /* 165 | * Function taken from Luc Small (http://lucsmall.com), itself 166 | * derived from the OneWire Arduino library. Modifications to 167 | * the polynomial according to Fine Offset's CRC8 calulations. 168 | * Oddly, that may have ultimately come from me. I wrote maybe the 169 | * original OneWire Arduino code back in the day including the CRC. 170 | */ 171 | static uint8_t wh1080_crc8( uint8_t *addr, uint8_t len) 172 | { 173 | uint8_t crc = 0; 174 | 175 | // Indicated changes are from reference CRC-8 function in OneWire library 176 | while (len--) { 177 | uint8_t inbyte = *addr++; 178 | uint8_t i; 179 | for (i = 8; i; i--) { 180 | uint8_t mix = (crc ^ inbyte) & 0x80; // changed from & 0x01 181 | crc <<= 1; // changed from right shift 182 | if (mix) crc ^= 0x31;// changed from 0x8C; 183 | inbyte <<= 1; // changed from right shift 184 | } 185 | } 186 | return crc; 187 | } 188 | 189 | 190 | static void showHelp( FILE *f) 191 | { 192 | fprintf(f, 193 | "Usage: wh1080 [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface]\n" 194 | " -h | -? | --help display usage and exit\n" 195 | " -v | --verbose verbose logging\n" 196 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 197 | " -p port | --multicast-port port multicast port, default 3636\n" 198 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 199 | " -r path | --recent path path to most recent data, /tmp/current-weather.json\n" 200 | " -P path | --periodic path path to the periodic data, /tmp/weather\n" 201 | " timestamp.json gets appended.\n" 202 | " -m period | --minutes period number of minutes between periodic data files.\n" 203 | ); 204 | } 205 | 206 | 207 | int main( int argc, char **argv) 208 | { 209 | const char *multicastAddress = "236.0.0.1"; 210 | const char *multicastPort = "3636"; 211 | const char *multicastInterface = "127.0.0.1"; 212 | const char *recentFileName = "/tmp/current-weather.json"; 213 | const char *periodicFileName = "/tmp/weather"; 214 | int minutes = 5; 215 | 216 | // Handle options 217 | for(;;) { 218 | int optionIndex = 0; 219 | static struct option options[] = { 220 | { "verbose", no_argument, 0, 'v' }, 221 | { "help", no_argument, 0, 'h' }, 222 | { "multicast-address", required_argument, 0, 'a'}, 223 | { "multicast-port", required_argument, 0, 'p' }, 224 | { "multicast-interface", required_argument, 0, 'i' }, 225 | { "recent", required_argument, 0, 'r' }, 226 | { "periodic", required_argument, 0, 'P' }, 227 | { "minutes", required_argument, 0, 'm' }, 228 | { 0,0,0,0} 229 | }; 230 | 231 | int c = getopt_long( argc, argv, "vh?f:a:p:i:m:", options, &optionIndex ); 232 | if ( c == -1) break; 233 | 234 | switch(c) { 235 | case 'h': 236 | case '?': 237 | showHelp(stdout); 238 | return 0; 239 | case 'v': 240 | verbose = 1; 241 | break; 242 | case 'a': 243 | multicastAddress = optarg; 244 | break; 245 | case 'p': 246 | multicastPort = optarg; 247 | break; 248 | case 'i': 249 | multicastInterface = optarg; 250 | break; 251 | case 'r': 252 | recentFileName = optarg; 253 | break; 254 | case 'P': 255 | periodicFileName = optarg; 256 | break; 257 | case 'm': 258 | { 259 | int m = atoi(optarg); 260 | if (m<1) { 261 | fprintf(stderr,"Illegal minutes, less than 1\n"); 262 | exit(1); 263 | } 264 | minutes = m; 265 | } 266 | break; 267 | default: 268 | fprintf(stderr,"Illegal option\n"); 269 | showHelp(stderr); 270 | exit(1); 271 | } 272 | } 273 | 274 | if ( verbose) fprintf(stderr,"Periodic file is %s\n", periodicFileName); 275 | 276 | // Parse our multicast address 277 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 278 | if ( sock < 0) { 279 | fprintf(stderr,"Failed to open multicast interface\n"); 280 | exit(1); 281 | } 282 | 283 | for (;;) { 284 | struct ook_burst *burst; 285 | struct sockaddr_storage addr; 286 | socklen_t addrLen = sizeof(addr); 287 | 288 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 289 | if ( e < 0) { 290 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 291 | break; 292 | } 293 | if ( e == 0) { 294 | fprintf(stderr,"Corrupt burst\n"); 295 | continue; 296 | } 297 | 298 | // Data never comes faster than 5 seconds, we are looking at the second of a pair 299 | // of redundant transmissions 300 | if ( oldestDatum && time(0)-oldestDatum < 5) continue; 301 | 302 | { 303 | unsigned char *data = 0; 304 | size_t dataLen = 0; 305 | int bits = ook_decode_pulse_width( burst, 306 | 1400000,1600000, 400000,600000, 900000,UINT_MAX, 307 | &data, &dataLen, 308 | verbose); 309 | 310 | if ( bits == 88) { 311 | if ( verbose) { 312 | for (int i = 0; i < dataLen; i++) fprintf(stderr,"%02x ", data[i]); 313 | fprintf(stderr,"\n"); 314 | } 315 | if (data[0] != 0xff) { 316 | if ( verbose) fprintf(stderr,"Did not begin 0xff\n"); 317 | goto NotGood; 318 | } 319 | if (wh1080_crc8(data+1,9) != data[10]) { 320 | if ( verbose) fprintf(stderr,"Bad CRC\n"); 321 | goto NotGood; 322 | } 323 | 324 | //unsigned short deviceId = ( (data[1]<<4) | (data[2]>>4) ); 325 | unsigned short temperatureBits = (((data[2]&0xf)<<8) | data[3]); 326 | double temp = (temperatureBits-400)/10.0; // degrees C 327 | unsigned short humidityBits = data[4]; 328 | double hum = humidityBits; // %rh 329 | unsigned short averageWindSpeedBits = data[5]; 330 | double avgWind = averageWindSpeedBits*0.34; // meters per second 331 | unsigned short gustWindSpeedBits = data[6]; 332 | double gustWind = gustWindSpeedBits*0.34; // meters per second 333 | unsigned short rainfallBits = (((data[7]&0x0f)<<8) | data[8]); 334 | double rain = rainfallBits*0.3; // millimeters 335 | unsigned short batteryLowBits = (data[9]>>4); 336 | unsigned short windDirectionBits = (data[9]&0x0f); 337 | 338 | if ( oldestDatum && time(0)-oldestDatum > minutes*60) { 339 | recordPeriodic( periodicFileName); 340 | } 341 | 342 | addSample( &temperature, temp); 343 | addSample( &humidity, hum); 344 | addSample( &averageWindSpeed, avgWind); 345 | addSample( &gustWindSpeed, gustWind); 346 | addSample( &rainfall, rain); 347 | addSample( &batteryLow, batteryLowBits); 348 | addSample( &windDirection, windDirectionBits); 349 | 350 | if ( verbose) dumpWeather(); 351 | 352 | recordRecent( recentFileName, temp, hum, avgWind, gustWind, rain, batteryLowBits, windDirectionBits); 353 | } else { 354 | if ( verbose) fprintf(stderr,"ignored %d pulse burst\n", burst->pulses); 355 | } 356 | 357 | NotGood: 358 | if (data) free(data); 359 | } 360 | 361 | fflush(stdin); 362 | free(burst); 363 | } 364 | 365 | close(sock); 366 | return 0; 367 | } 368 | -------------------------------------------------------------------------------- /ws2300.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ook.h" 13 | 14 | // 15 | // Data format comes from http://makin-things.com/articles/decoding-lacrosse-weather-sensor-rf-transmissions/ 16 | // And http://www.practicalarduino.com/projects/weather-station-receiver 17 | // 18 | 19 | int verbose=0; 20 | 21 | // Datum keeps enough information to sum them up and still compute the standard deviation 22 | struct datum { 23 | unsigned n; 24 | double sum; 25 | double sumOfSquares; 26 | double maximum; 27 | double minimum; 28 | }; 29 | 30 | static time_t oldestDatum = 0; 31 | static struct datum temperature; 32 | static struct datum humidity; 33 | static struct datum averageWindSpeed; 34 | static struct datum gustWindSpeed; 35 | static struct datum rainfall; 36 | static struct datum batteryLow; 37 | static struct datum windDirection; 38 | 39 | static double currentTemperature = NAN; 40 | static double currentHumidity = NAN; 41 | static double currentWindSpeed = NAN; 42 | static double currentGustSpeed = NAN; 43 | static double currentWindDirection = NAN; 44 | 45 | static void resetDatum(struct datum *d) 46 | { 47 | memset(d,0,sizeof(*d)); 48 | } 49 | 50 | static void addSample( struct datum *d, double v) 51 | { 52 | if ( oldestDatum == 0) oldestDatum = time(0); 53 | 54 | if ( d->n==0) { 55 | d->minimum = v; 56 | d->maximum = v; 57 | } else { 58 | if ( v > d->maximum) d->maximum = v; 59 | if ( v < d->minimum) d->minimum = v; 60 | } 61 | d->n++; 62 | d->sum += v; 63 | d->sumOfSquares += v*v; 64 | } 65 | 66 | static void dumpDatum( struct datum *d, const char *name, const char *units) 67 | { 68 | if ( d->n == 0) { 69 | fprintf(stderr,"%s no data\n", name); 70 | } else { 71 | fprintf(stderr, "%s %u samples, %5.1f %s min %5.1f max %5.1f\n", 72 | name, d->n, d->sum/d->n, units, d->minimum, d->maximum); 73 | } 74 | } 75 | 76 | static void dumpWeather( void) 77 | { 78 | dumpDatum( &temperature, "temperature", "C"); 79 | dumpDatum( &humidity, "humidity", "%"); 80 | dumpDatum( &averageWindSpeed, "wind", "m/s"); 81 | dumpDatum( &gustWindSpeed, "gust", "m/s"); 82 | dumpDatum( &rainfall, "rain", "mm"); 83 | dumpDatum( &batteryLow, "batt", "??"); 84 | dumpDatum( &windDirection, "dir", "NEWS"); 85 | } 86 | 87 | static void reportRecent( const char *file) 88 | { 89 | char name[256]; 90 | strcpy(name,"/tmp/ws2300.XXXXXX"); 91 | 92 | int fd = mkstemp(name); 93 | if ( fd < 0) { 94 | fprintf(stderr,"Failed to create temp file for recent: %s\n", strerror(errno)); 95 | return; 96 | } 97 | 98 | if ( fchmod( fd, 0664) != 0) { 99 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 100 | unlink(name); 101 | close(fd); 102 | return; 103 | } 104 | 105 | FILE *f = fdopen(fd,"w"); 106 | fprintf(f,"{\n"); 107 | 108 | if ( !isnan(currentTemperature)) fprintf(f,"\t\"temperature\":%.1f,\n", currentTemperature); 109 | if ( !isnan(currentHumidity)) fprintf(f,"\t\"humidity\":%.1f,\n", currentHumidity); 110 | if ( !isnan(currentWindSpeed)) fprintf(f,"\t\"avgWindSpeed\":%.1f,\n", currentWindSpeed); 111 | if ( !isnan(currentGustSpeed)) fprintf(f,"\t\"gustSpeed\":%.1f,\n", currentGustSpeed); 112 | if ( !isnan(currentWindDirection)) fprintf(f,"\t\"windDirection\":%d,\n", (int)(currentWindDirection*45)); 113 | fprintf(f,"\t\"timestamp\":%ld\n", time(0)); 114 | fprintf(f,"}\n"); 115 | fclose(f); 116 | 117 | if (rename( name, file)) { 118 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 119 | } 120 | } 121 | 122 | static void recordDatum( FILE *f, struct datum *d, const char *name, int comma) 123 | { 124 | fprintf(f, "\t\"%s\" : { \"n\":%d, \"sum\":%.1f, \"sum2\":%.1f, \"min\":%.1f, \"max\":%.1f }%s\n", 125 | name, d->n, d->sum, d->sumOfSquares, d->minimum, d->maximum, (comma ? ",":"")); 126 | resetDatum( d); 127 | } 128 | 129 | 130 | static void recordPeriodic( const char *file) 131 | { 132 | char name[256]; 133 | strcpy(name,"/tmp/ws2300.XXXXXX"); 134 | 135 | int fd = mkstemp(name); 136 | if ( fd < 0) { 137 | fprintf(stderr,"Failed to create temp file for periodic: %s\n", strerror(errno)); 138 | return; 139 | } 140 | 141 | if ( fchmod( fd, 0664) != 0) { 142 | fprintf(stderr, "Failed to chmod temp file: %s\n", strerror(errno)); 143 | unlink(name); 144 | close(fd); 145 | return; 146 | } 147 | 148 | FILE *f = fdopen(fd,"w"); 149 | fprintf(f,"{\n"); 150 | fprintf(f,"\t\"start\":%lld,\n", (long long)oldestDatum); 151 | fprintf(f,"\t\"end\":%lld,\n", (long long)time(0)); 152 | recordDatum( f, &temperature, "temperature", 1); 153 | recordDatum( f, &humidity, "humidity", 1); 154 | recordDatum( f, &averageWindSpeed, "avgWindSpeed", 1); 155 | recordDatum( f, &gustWindSpeed, "gustSpeed", 1); 156 | recordDatum( f, &rainfall, "rainfall", 1); 157 | recordDatum( f, &batteryLow, "batteryLow", 1); 158 | recordDatum( f, &windDirection, "windDirection", 0); 159 | fprintf(f,"}\n"); 160 | fclose(f); 161 | 162 | char stamp[32]={0}; 163 | strftime( stamp, sizeof(stamp), "%Y%m%d-%H%M%S", gmtime(&oldestDatum)); 164 | char finalName[1024]="/ERROR"; 165 | snprintf(finalName, sizeof(finalName)-1, "%s-%s.json", file, stamp); 166 | 167 | if (rename( name, finalName)) { 168 | fprintf(stderr,"Failed to rename temp file: %s\n", strerror(errno)); 169 | } 170 | 171 | oldestDatum = 0; 172 | 173 | } 174 | 175 | 176 | static void showHelp( FILE *f) 177 | { 178 | fprintf(f, 179 | "Usage: ws2300 [-h] [-?] [-v] [-a mcastaddr] [-p mcastport] [-i mcastinterface]\n" 180 | " -h | -? | --help display usage and exit\n" 181 | " -v | --verbose verbose logging\n" 182 | " -a addr | --multicast-address addr multicast address, default 236.0.0.1\n" 183 | " -p port | --multicast-port port multicast port, default 3636\n" 184 | " -i addr | --multicast-interface addr address of the multicast interface, default 127.0.0.1\n" 185 | " -r path | --recent path path to most recent data, /tmp/current-weather.json\n" 186 | " -P path | --periodic path path to the periodic data, /tmp/weather\n" 187 | " timestamp.json gets appended.\n" 188 | " -m period | --minutes period number of minutes between periodic data files.\n" 189 | ); 190 | } 191 | 192 | 193 | int main( int argc, char **argv) 194 | { 195 | const char *multicastAddress = "236.0.0.1"; 196 | const char *multicastPort = "3636"; 197 | const char *multicastInterface = "127.0.0.1"; 198 | const char *recentFileName = "/tmp/current-weather.json"; 199 | const char *periodicFileName = "/tmp/weather"; 200 | int minutes = 5; 201 | 202 | // Handle options 203 | for(;;) { 204 | int optionIndex = 0; 205 | static struct option options[] = { 206 | { "verbose", no_argument, 0, 'v' }, 207 | { "help", no_argument, 0, 'h' }, 208 | { "multicast-address", required_argument, 0, 'a'}, 209 | { "multicast-port", required_argument, 0, 'p' }, 210 | { "multicast-interface", required_argument, 0, 'i' }, 211 | { "recent", required_argument, 0, 'r' }, 212 | { "periodic", required_argument, 0, 'P' }, 213 | { "minutes", required_argument, 0, 'm' }, 214 | { 0,0,0,0} 215 | }; 216 | 217 | int c = getopt_long( argc, argv, "vh?f:a:p:i:m:r:P:", options, &optionIndex ); 218 | if ( c == -1) break; 219 | 220 | switch(c) { 221 | case 'h': 222 | case '?': 223 | showHelp(stdout); 224 | return 0; 225 | case 'v': 226 | verbose = 1; 227 | break; 228 | case 'a': 229 | multicastAddress = optarg; 230 | break; 231 | case 'p': 232 | multicastPort = optarg; 233 | break; 234 | case 'i': 235 | multicastInterface = optarg; 236 | break; 237 | case 'r': 238 | recentFileName = optarg; 239 | break; 240 | case 'P': 241 | periodicFileName = optarg; 242 | break; 243 | case 'm': 244 | { 245 | int m = atoi(optarg); 246 | if (m<1) { 247 | fprintf(stderr,"Illegal minutes, less than 1\n"); 248 | exit(1); 249 | } 250 | minutes = m; 251 | } 252 | break; 253 | default: 254 | fprintf(stderr,"Illegal option\n"); 255 | showHelp(stderr); 256 | exit(1); 257 | } 258 | } 259 | 260 | if ( verbose) fprintf(stderr,"Periodic file is %s\n", periodicFileName); 261 | 262 | // Parse our multicast address 263 | int sock = ook_open( multicastAddress, multicastPort, multicastInterface); 264 | if ( sock < 0) { 265 | fprintf(stderr,"Failed to open multicast interface\n"); 266 | exit(1); 267 | } 268 | 269 | for (;;) { 270 | struct ook_burst *burst; 271 | struct sockaddr_storage addr; 272 | socklen_t addrLen = sizeof(addr); 273 | 274 | int e = ook_decode_from_socket( sock, &burst, (struct sockaddr *)&addr, &addrLen, verbose); 275 | if ( e < 0) { 276 | fprintf(stderr,"Failed to decode from socket: %s\n", strerror(errno)); 277 | break; 278 | } 279 | if ( e == 0) { 280 | fprintf(stderr,"Corrupt burst\n"); 281 | continue; 282 | } 283 | 284 | { 285 | unsigned char *data = 0; 286 | size_t dataLen = 0; 287 | int bits = ook_decode_pulse_width( burst, 288 | 1300000,1500000, 250000,400000, 900000,UINT_MAX, 289 | &data, &dataLen, 290 | verbose); 291 | const int tx13_id = 0x06; 292 | const int ws2300_id = 0x09; 293 | 294 | if ( bits == 52 && (data[0] == tx13_id || data[0] == ws2300_id) ) { 295 | unsigned csum = 0; 296 | for ( int i = 0; i < 6; i++) { 297 | unsigned highNibble = (data[i]>>4); 298 | unsigned lowNibble = (data[i]&0x0f); 299 | csum += highNibble + lowNibble; 300 | } 301 | unsigned csumNibble = (csum & 0x0f); 302 | unsigned pcsumNibble = (data[6] & 0x0f); 303 | 304 | if ( csumNibble != pcsumNibble) { 305 | fprintf(stderr,"Invalid checksum computed=0x%02x - packet says 0x%02x\n", csumNibble, pcsumNibble); 306 | goto NotGood; 307 | } 308 | 309 | if ( verbose) { 310 | for (int i = 0; i < dataLen; i++) fprintf(stderr,"%02x ", data[i]); 311 | fprintf(stderr,"\n"); 312 | } 313 | 314 | if ( oldestDatum && time(0)-oldestDatum > minutes*60) { 315 | recordPeriodic( periodicFileName); 316 | } 317 | 318 | int packetId = ((data[1]>>4)&0x03); 319 | int stationId = ((data[1]&0x0f)<<4)+((data[2]&0xf0)>>4); 320 | 321 | if ( verbose) fprintf(stderr,"packetid=%d station=%d\n", packetId, stationId); 322 | 323 | switch( packetId) { 324 | case 0: // temp 325 | { 326 | double temp = (data[3]&0x0f)*10 + ((data[4]&0xf0)>>4) + (data[4]&0x0f)*0.1 - 30.0; // TX13 is -40 327 | addSample( &temperature, temp); 328 | currentTemperature = temp; 329 | } 330 | break; 331 | case 1: // humidity 332 | { 333 | int hum = (data[3]&0x0f)*10 + ((data[4]&0xf0)>>4); 334 | addSample( &humidity, hum); 335 | currentHumidity = hum; 336 | } 337 | break; 338 | case 2: // rainfall 339 | { 340 | int rain = ((data[3]&0x0f)<<8) + data[4]; 341 | addSample( &rainfall, rain); 342 | } 343 | break; 344 | case 3: // wind 345 | { 346 | double wind = (((data[3]&0x1f)<<4) + ((data[4]&0xf0)>>4) ) / 10.0; 347 | int windDir = (data[4] & 0x0f); 348 | if ( data[1] & 0x80) { 349 | if ( wind != 51.0) { 350 | addSample( &gustWindSpeed, wind); 351 | currentGustSpeed = wind; 352 | } 353 | } else { 354 | if ( wind != 51.0) { 355 | addSample( &averageWindSpeed, wind); 356 | addSample( &windDirection, windDir); 357 | currentWindSpeed = wind; 358 | currentWindDirection = windDir; 359 | } 360 | } 361 | } 362 | break; 363 | } 364 | 365 | if ( verbose) dumpWeather(); 366 | 367 | reportRecent(recentFileName); 368 | } else { 369 | if ( verbose) fprintf(stderr,"ignored %d pulse burst\n", burst->pulses); 370 | } 371 | 372 | NotGood: 373 | if (data) free(data); 374 | } 375 | 376 | fflush(stdin); 377 | free(burst); 378 | } 379 | 380 | close(sock); 381 | return 0; 382 | } 383 | --------------------------------------------------------------------------------