Compare commits

...

66 commits
fhss ... master

Author SHA1 Message Date
Jeff Epler
77a2c1361b add an option to receive both scm+idm
Signed-off-by: Jeff Epler <jepler@unpythonic.net>
2015-12-07 21:56:08 -06:00
bemasher
40470d4488 Wait for a message from each meter given in -filterid when combined with -single. Closes #38 2015-11-08 00:05:24 -07:00
bemasher
1e03a514f8 Clarify Pack description. 2015-11-05 23:06:17 -07:00
bemasher
c45fdef076 Remove old travis-ci badge. 2015-07-12 22:31:21 -06:00
bemasher
1364a85f48 Remove flag debug print. 2015-07-12 04:47:11 -06:00
bemasher
5555d22393 Fix travis-ci badge. 2015-07-12 04:46:50 -06:00
bemasher
d453c86711 Remove filter chain debug print. 2015-07-12 02:47:39 -06:00
bemasher
42b1b3d07f Merge branch 'msghandler' 2015-07-12 02:40:18 -06:00
bemasher
6b376a3c26 Write plain text encoder. 2015-07-12 02:37:44 -06:00
bemasher
f8797da412 Update travis-ci config to use new infrastructure. 2015-07-12 02:09:58 -06:00
bemasher
938607e052 Oops, Filter is the method, MessageFilter is the interface. 2015-07-12 02:07:45 -06:00
bemasher
7314e7a38b Simplify filter flag handling. Rename Filter interface. 2015-07-12 02:06:39 -06:00
bemasher
548dac1a11 Refactor message filtering. 2015-07-11 03:45:27 -06:00
bemasher
767fea762f Remove -fastmag from usage. 2015-07-11 01:37:36 -06:00
bemasher
42ceb2cce7 Remove FastMag
Simulation shows that FastMag is of roughly equivalent sensitivity to
MagLUT while not providing much better performance.
2015-07-11 01:36:07 -06:00
bemasher
30ee13eec9 Remove unnecessary benchmark. 2015-07-11 01:11:47 -06:00
bemasher
de00b1d77c Update usage and unique flag description. 2015-07-08 23:29:40 -06:00
bemasher
c4340c3263 Fix symbol index for checksum copy. 2015-07-08 22:23:11 -06:00
bemasher
34b9eb6bb3 Refactor to check against checksum for uniqueness. 2015-07-08 22:23:11 -06:00
John
dbdbcae476 minor fixes
code review feedback
2015-07-08 22:23:11 -06:00
John
b9240f925b add -unique
Add option to suppress duplicate meter readings
2015-07-08 22:23:11 -06:00
bemasher
ae0364d9a0 Merge branch 'decimation' 2015-06-16 19:58:34 -06:00
bemasher
07607af197 Refactor message unpacking in SCM and IDM. 2015-06-14 04:35:58 -06:00
bemasher
7f05214d2b Add decimation factor checking. Clarify warning. 2015-06-07 01:38:21 -06:00
bemasher
8f3cb5205e Apply gofmt to everything. 2015-06-07 01:24:38 -06:00
bemasher
04d73e42a7 Update logging to include decimated configuration. 2015-06-07 00:53:12 -06:00
bemasher
de4bffe446 Fix csum length. 2015-06-07 00:15:48 -06:00
bemasher
953f371bb6 Move R900 message statement. 2015-05-31 04:21:05 -06:00
bemasher
22906a5949 Update link in README to point to gh-pages documentation. 2015-05-28 23:18:51 -06:00
bemasher
3bd0a4e558 Update flags in README. 2015-05-28 23:16:49 -06:00
bemasher
efea18916c Implement decimation in AlphaMaxBetaMinLUT. 2015-05-28 21:55:18 -06:00
bemasher
3a29e9b7ff Implement decimation in R900 decoder. 2015-05-28 21:44:59 -06:00
bemasher
586359c74a Move decimation to demodulator, only MagLUT for now. 2015-05-25 02:21:33 -06:00
bemasher
1373ceb5d7 Fix block read, reads might span writes to the pipe. 2015-05-25 01:43:20 -06:00
bemasher
588ef5220c Read whole blocks from server concurrently with io.Pipe(). 2015-05-24 02:24:13 -06:00
bemasher
f7828796bf Remove config debugging statements. 2015-05-24 02:18:06 -06:00
bemasher
c2855e7ded Implement decimation, except in R900 decoder. 2015-05-24 02:05:44 -06:00
bemasher
f3d51f2331 Update compatible meter table location. 2015-03-18 22:29:55 -06:00
bemasher
3249c73543 Move compatible meter table to csv format. 2015-03-18 22:27:12 -06:00
Douglas Hall
e1d9b04354 Merge pull request #27 from sandeen/master
r900: decode leak, fraud, and backflow status flags
2015-03-15 16:28:09 -06:00
Eric Sandeen
233608205f r900: decode leak, fraud, and backflow status flags
TL;DR - this patch decodes the unknown2 and unknown4 fields into leak,
fraud, and backflow status information.

Pretty output now shows:

NoUse:$BIN BackFlow:$FLAG ... Leak:$BIN LeakNow:$FLAG

Where BackFlow is 0, 1, or 2 for no, low, or high backflow in past 35d;
LeakNow is 0, 1, or 2 for no, low, or high leak in the past 24h;
NoUse and Leak is a number from 0 through 6 representing the number of
days in the past 35 days for which either no use or a leak has occurred,
according to this table:

bucket  days
------  ----
0       0
1       1-2
2       3-7
3       8-14
4       15-21
5       22-34
6       35+

I sorted this out using public documentation such as
https://www.neptunetg.com/About/Case-Studies/Water/Westbank-Irrigation-District,-BC
which indicates that the utility can gather stats on days of no use and
days of leak in the past 35 days, and that it is divided into buckets as
shown in the table above.

Using this public information, intentionally triggering the leak flag in
my home, and listening to many meters over time led me to this decoding
of the unknown2 and unknown4 fields.
 
The 8 unknown2 bits seem to show days of no use and backflow information,
like this (? is still unknown, but this patch lumps it in with the bucket
number):
 
???BBBHL

BBB: bucket # for days of no use in past 35 days
H: high backflow in past 35 days
L: low backflow in past 35 days

Backflow seems to require some minimum frequency of occurrence before it
will set the flag.

Similarly, the 6 unknown4 bits look like this:

?BBBHL

BBB: bucket # for days of leak in past 35 days
H: continuous leak in past 24 hours
L: intermittent leak in past 24 hours

Intermittent leaks are described by Neptune in other public docs as:

"Intermittent leak indicates that water has been used for at
least 50 of the 96 15-minute intervals during a 24-hour period"

and a continuous leak indicates use in every 15-minute period over
the past 24h.
2015-03-15 10:19:52 -05:00
bemasher
9be534c2d0 Add use cases. 2015-03-11 22:19:35 -06:00
bemasher
d4afba8974 Add 100GDLAN, update commodity type list. 2015-03-11 21:40:06 -06:00
bemasher
15e1d1dc69 Optimize execute loop for magnitude demodulators. 2015-02-22 04:12:02 -07:00
bemasher
169c5300c6 Switch to bytes.Equal in preamble search. Simplify calculations for slice allocation. 2015-02-21 06:47:22 -07:00
bemasher
0f08d69aa1 Add user-submitted meters. 2015-02-01 04:10:38 -07:00
bemasher
eb2b055444 Fix galois field package path. 2015-01-23 02:10:15 -07:00
bemasher
a88bbf1bc8 Simplify symbol decoding process. 2015-01-23 01:54:25 -07:00
bemasher
57e1be9ab6 Add gf package license. 2015-01-22 02:35:48 -07:00
bemasher
6f3d973f18 Integrate R900 decoder.
Update license information.
Add R900 decoder.
Update usage and information.
Remove CRC logging.
2015-01-22 02:28:06 -07:00
bemasher
8a570305da Add r900 decoder. 2015-01-22 02:12:06 -07:00
bemasher
19dadbb681 Move CenterFreq to PacketConfig for per protocol defaults. 2015-01-22 01:49:22 -07:00
bemasher
8865d2e83f Update travis-ci configuration. 2015-01-22 01:41:33 -07:00
bemasher
979aa93184 Fix rounding problem in parser. 2015-01-21 23:56:57 -07:00
bemasher
0c47867cb0 Move s to correct scope. 2015-01-13 00:53:35 -07:00
bemasher
c9c1d9932b Simplify duplicate packet check. 2015-01-13 00:50:06 -07:00
bemasher
3c661614b6 Separate demodulated signal from filtered signal. 2015-01-08 02:01:37 -07:00
bemasher
dbb422783b Set actual field lengths. 2015-01-08 02:00:53 -07:00
bemasher
86a2abf632 Refactor for signal access from parser. 2015-01-07 00:20:56 -07:00
bemasher
11d5802d4b Rename for later protocols. 2015-01-06 20:59:19 -07:00
bemasher
03369d1499 Simplify packed byte array length calculation. 2014-12-08 21:40:29 -07:00
bemasher
3bcb52b572 Fix packed byte array length calculation. 2014-12-08 21:33:03 -07:00
bemasher
83005a5c92 Revert "Add donate section and button."
This reverts commit fadf9aa8d9.
2014-12-08 18:46:48 -07:00
Douglas Hall
fadf9aa8d9 Add donate section and button. 2014-12-01 22:52:01 -07:00
bemasher
b326ea960f Limit checksum to valid length. 2014-11-13 01:51:47 -07:00
bemasher
2d12445897 Update usage. 2014-11-10 04:17:17 -07:00
15 changed files with 1163 additions and 490 deletions

7
.gitignore vendored
View file

@ -1,2 +1,7 @@
data
*.exe
*.exe
scm/Makefile
release.7z
*.diff
desktop.ini
*~

View file

@ -1,4 +1,7 @@
sudo: false
language: go
go:
- 1.2
- 1.3
- 1.4

View file

@ -3,8 +3,8 @@ Utilities often use "smart meters" to optimize their residential meter reading i
This project is a software defined radio receiver for these messages. We make use of an inexpensive rtl-sdr dongle to allow users to non-invasively record and analyze the commodity consumption of their household.
[![Build Status](http://img.shields.io/travis/bemasher/rtlamr.svg?style=flat)](https://travis-ci.org/bemasher/rtlamr)
[![AGPL License](http://img.shields.io/badge/license-AGPL-blue.svg?style=flat)](http://www.gnu.org/licenses/agpl-3.0.html)
[![Build Status](https://travis-ci.org/bemasher/rtlamr.svg?branch=master&style=flat)](https://travis-ci.org/bemasher/rtlamr)
[![AGPLv3 License](http://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](http://choosealicense.com/licenses/agpl-3.0/)
### Requirements
* GoLang >=1.2 (Go build environment setup guide: http://golang.org/doc/code.html)
@ -25,28 +25,29 @@ Available command-line flags are as follows:
```
Usage of rtlamr:
-cpuprofile=: write cpu profile to this file
-decimation=1: integer decimation factor, keep every nth sample
-duration=0: time to run for, 0 for infinite, ex. 1h5m10s
-fastmag=false: use faster alpha max + beta min magnitude approximation
-filterid=0: display only messages matching given id
-filtertype=0: display only messages matching given type
-filterid=: display only messages matching an id in a comma-separated list of ids.
-filtertype=: display only messages matching a type in a comma-separated list of types.
-format=plain: format to write log messages in: plain, csv, json, xml or gob
-gobunsafe=false: allow gob output to stdout
-logfile=/dev/stdout: log statement dump file
-msgtype=scm: message type to receive: scm or idm
-msgtype=scm: message type to receive: scm, idm or r900
-quiet=false: suppress printing state information at startup
-samplefile=/dev/null: raw signal dump file
-single=false: one shot execution
-symbollength=73: symbol length in samples, see -help for valid lengths
-single=false: one shot execution, if used with -filterid, will wait for exactly one packet from each meter id
-symbollength=72: symbol length in samples, see -help for valid lengths
-unique=false: suppress duplicate messages from each meter
rtltcp specific:
-agcmode=false: enable/disable rtl agc
-centerfreq=920299072: center frequency to receive on
-centerfreq=100M: center frequency to receive on
-directsampling=false: enable/disable direct sampling
-freqcorrection=0: frequency correction in ppm
-gainbyindex=0: set gain by index
-offsettuning=false: enable/disable offset tuning
-rtlxtalfreq=0: set rtl xtal frequency
-samplerate=2400000: sample rate
-samplerate=2.4M: sample rate
-server=127.0.0.1:1234: address or hostname of rtl_tcp instance
-testmode=false: enable/disable test mode
-tunergain=0: set tuner gain in dB
@ -67,7 +68,9 @@ $ rtlamr
If you want to run the spectrum server on a different machine than the receiver you'll want to specify an address to listen on that is accessible from the machine `rtlamr` will run on with the `-a` option for `rtl_tcp` with an address accessible by the system running the receiver.
### Messages
Currently both SCM (Standard Consumption Message) and IDM (Interval Data Message) packets can be decoded but are mutually exclusive, you cannot receive both simultaneously. See [Wikipedia: Encoder Receiver Transmitter](http://en.wikipedia.org/wiki/Encoder_receiver_transmitter) for more details on packet structure.
Currently both SCM (Standard Consumption Message) and IDM (Interval Data Message) packets can be decoded but are mutually exclusive, you cannot receive both simultaneously. See [RTLAMR: Protocol](http://bemasher.github.io/rtlamr/protocol.html) for more details on packet structure.
There's now experimental support for meters with R900 transmitters!
### Sensitivity
Using a NooElec NESDR Nano R820T with the provided antenna, I can reliably receive standard consumption messages from ~300 different meters and intermittently from another ~600 meters. These figures are calculated from the number of messages received during a 25 minute window. Reliably in this case means receiving at least 10 of the expected 12 messages and intermittently means 3-9 messages.
@ -82,26 +85,41 @@ If you've got a meter not on the list that you've successfully received messages
### Ethics
_Do not use this for nefarious purposes._ If you do, I don't want to know about it, I am not and will not be responsible for your lack of common decency and/or foresight. However, if you find a clever non-evil use for this, by all means, share.
### Use Cases
These are a few examples of ways this tool could be used:
**Ethical**
* Track down stray appliances.
* Track power generated vs. power consumed.
* Find a water leak with rtlamr rather than from your bill.
* Optimize your thermostat to reduce energy consumption.
* Mass collection for research purposes. (_Please_ anonymize your data.)
**Unethical**
* Using data collected to determine living patterns of specific persons with the intent to act on this data, particularly without express permission to do so.
### License
The source of this project is licensed under Affero GPL. According to [http://choosealicense.com/licenses/agpl/](http://choosealicense.com/licenses/agpl/) you may:
The source of this project is licensed under Affero GPL v3.0. According to [http://choosealicense.com/licenses/agpl-3.0/](http://choosealicense.com/licenses/agpl-3.0/) you may:
#### Required:
* Source code must be made available when distributing the software. In the case of LGPL, the source for the library (and not the entire program) must be made available.
* Include a copy of the license and copyright notice with the code.
* Indicate significant changes made to the code.
* **Disclose Source:** Source code must be made available when distributing the software. In the case of LGPL, the source for the library (and not the entire program) must be made available.
* **License and copyright notice:** Include a copy of the license and copyright notice with the code.
* **Network Use is Distribution:** Users who interact with the software via network are given the right to receive a copy of the corresponding source code.
* **State Changes:** Indicate significant changes made to the code.
#### Permitted:
* This software and derivatives may be used for commercial purposes.
* You may distribute this software.
* This software may be modified.
* You may use and modify the software without distributing it.
* **Commercial Use:** This software and derivatives may be used for commercial purposes.
* **Distribution:** You may distribute this software.
* **Modification:** This software may be modified.
* **Patent Grant:** This license provides an express grant of patent rights from the contributor to the recipient.
* **Private Use:** You may use and modify the software without distributing it.
#### Forbidden:
* Software is provided without warranty and the software author/license owner cannot be held liable for damages.
* You may not grant a sublicense to modify and distribute this software to third parties not included in the license.
* **Hold Liable:** Software is provided without warranty and the software author/license owner cannot be held liable for damages.
* **Sublicensing:** You may not grant a sublicense to modify and distribute this software to third parties not included in the license.
### Feedback
If you have any general questions or feedback leave a comment below. For bugs, feature suggestions and anything directly relating to the program itself, submit an issue in github.

View file

@ -1,5 +1,5 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
// Copyright (C) 2015 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
@ -17,6 +17,7 @@
package decode
import (
"bytes"
"fmt"
"log"
"math"
@ -24,39 +25,86 @@ import (
// PacketConfig specifies packet-specific radio configuration.
type PacketConfig struct {
DataRate int
DataRate int
BlockSize, BlockSize2 int
SymbolLength, SymbolLength2 int
SampleRate int
PreambleSymbols, PacketSymbols int
PreambleLength, PacketLength int
BufferLength int
Preamble string
BufferLength int
CenterFreq uint32
}
func (cfg PacketConfig) Log() {
log.Println("BlockSize:", cfg.BlockSize)
log.Println("SampleRate:", cfg.SampleRate)
log.Println("DataRate:", cfg.DataRate)
log.Println("SymbolLength:", cfg.SymbolLength)
log.Println("PreambleSymbols:", cfg.PreambleSymbols)
log.Println("PreambleLength:", cfg.PreambleLength)
log.Println("PacketSymbols:", cfg.PacketSymbols)
log.Println("PacketLength:", cfg.PacketLength)
log.Println("Preamble:", cfg.Preamble)
func (cfg PacketConfig) Decimate(decimation int) PacketConfig {
cfg.BlockSize /= decimation
cfg.BlockSize2 /= decimation
cfg.SymbolLength /= decimation
cfg.SymbolLength2 /= decimation
cfg.SampleRate /= decimation
cfg.DataRate /= decimation
cfg.PreambleLength /= decimation
cfg.PacketLength /= decimation
cfg.BufferLength /= decimation
return cfg
}
func (d Decoder) Log() {
if d.Decimation != 1 {
log.Printf("BlockSize: %d|%d\n", d.Cfg.BlockSize, d.DecCfg.BlockSize)
log.Println("CenterFreq:", d.Cfg.CenterFreq)
log.Printf("SampleRate: %d|%d\n", d.Cfg.SampleRate, d.DecCfg.SampleRate)
log.Printf("DataRate: %d|%d\n", d.Cfg.DataRate, d.DecCfg.DataRate)
log.Printf("SymbolLength: %d|%d\n", d.Cfg.SymbolLength, d.DecCfg.SymbolLength)
log.Println("PreambleSymbols:", d.Cfg.PreambleSymbols)
log.Printf("PreambleLength: %d|%d\n", d.Cfg.PreambleLength, d.DecCfg.PreambleLength)
log.Println("PacketSymbols:", d.Cfg.PacketSymbols)
log.Printf("PacketLength: %d|%d\n", d.Cfg.PacketLength, d.DecCfg.PacketLength)
log.Println("Preamble:", d.Cfg.Preamble)
if d.Cfg.SymbolLength%d.Decimation != 0 {
log.Println("Warning: decimated symbol length is non-integral, sensitivity may be poor")
}
if d.DecCfg.SymbolLength < 3 {
log.Fatal("Error: illegal decimation factor, choose a smaller factor")
}
return
}
log.Println("CenterFreq:", d.Cfg.CenterFreq)
log.Println("SampleRate:", d.Cfg.SampleRate)
log.Println("DataRate:", d.Cfg.DataRate)
log.Println("SymbolLength:", d.Cfg.SymbolLength)
log.Println("PreambleSymbols:", d.Cfg.PreambleSymbols)
log.Println("PreambleLength:", d.Cfg.PreambleLength)
log.Println("PacketSymbols:", d.Cfg.PacketSymbols)
log.Println("PacketLength:", d.Cfg.PacketLength)
log.Println("Preamble:", d.Cfg.Preamble)
}
// Decoder contains buffers and radio configuration.
type Decoder struct {
Cfg PacketConfig
Decimation int
DecCfg PacketConfig
IQ []byte
Signal []float64
Filtered []float64
Quantized []byte
csum []float64
lut MagnitudeLUT
csum []float64
demod Demodulator
preamble []byte
slices [][]byte
@ -65,22 +113,33 @@ type Decoder struct {
}
// Create a new decoder with the given packet configuration.
func NewDecoder(cfg PacketConfig, fastMag bool) (d Decoder) {
func NewDecoder(cfg PacketConfig, decimation int) (d Decoder) {
d.Cfg = cfg
d.Cfg.SymbolLength2 = d.Cfg.SymbolLength << 1
d.Cfg.SampleRate = d.Cfg.DataRate * d.Cfg.SymbolLength
d.Cfg.PreambleLength = d.Cfg.PreambleSymbols * d.Cfg.SymbolLength2
d.Cfg.PacketLength = d.Cfg.PacketSymbols * d.Cfg.SymbolLength2
d.Cfg.BlockSize = NextPowerOf2(d.Cfg.PreambleLength)
d.Cfg.BlockSize2 = d.Cfg.BlockSize << 1
d.Cfg.BufferLength = d.Cfg.PacketLength + d.Cfg.BlockSize
d.Decimation = decimation
d.DecCfg = d.Cfg.Decimate(d.Decimation)
// Allocate necessary buffers.
d.IQ = make([]byte, d.Cfg.BufferLength<<1)
d.Signal = make([]float64, d.Cfg.BufferLength)
d.Quantized = make([]byte, d.Cfg.BufferLength)
d.Signal = make([]float64, d.DecCfg.BufferLength)
d.Filtered = make([]float64, d.DecCfg.BufferLength)
d.Quantized = make([]byte, d.DecCfg.BufferLength)
d.csum = make([]float64, d.Cfg.BlockSize+d.Cfg.SymbolLength2+1)
d.csum = make([]float64, (d.DecCfg.PacketLength - d.DecCfg.SymbolLength2 + 1))
// Calculate magnitude lookup table specified by -fastmag flag.
if fastMag {
d.lut = NewAlphaMaxBetaMinLUT()
} else {
d.lut = NewSqrtMagLUT()
}
d.demod = NewSqrtMagLUT()
// Pre-calculate a byte-slice version of the preamble for searching.
d.preamble = make([]byte, len(d.Cfg.Preamble))
@ -93,82 +152,57 @@ func NewDecoder(cfg PacketConfig, fastMag bool) (d Decoder) {
// Slice quantized sample buffer to make searching for the preamble more
// memory local. Pre-allocate a flat buffer so memory is contiguous and
// assign slices to the buffer.
d.slices = make([][]byte, d.Cfg.SymbolLength2)
flat := make([]byte, d.Cfg.BlockSize2-(d.Cfg.BlockSize2%d.Cfg.SymbolLength2))
d.slices = make([][]byte, d.DecCfg.SymbolLength2)
flat := make([]byte, d.DecCfg.BlockSize2-(d.DecCfg.BlockSize2%d.DecCfg.SymbolLength2))
symbolsPerBlock := d.DecCfg.BlockSize2 / d.DecCfg.SymbolLength2
for symbolOffset := range d.slices {
lower := symbolOffset * (d.Cfg.BlockSize2 / d.Cfg.SymbolLength2)
upper := (symbolOffset + 1) * (d.Cfg.BlockSize2 / d.Cfg.SymbolLength2)
lower := symbolOffset * symbolsPerBlock
upper := (symbolOffset + 1) * symbolsPerBlock
d.slices[symbolOffset] = flat[lower:upper]
}
// Signal up to the final stage is 1-bit per byte. Allocate a buffer to
// store packed version 8-bits per byte.
d.pkt = make([]byte, d.Cfg.PacketSymbols>>3)
d.pkt = make([]byte, (d.DecCfg.PacketSymbols+7)>>3)
return
}
// Decode accepts a sample block and performs various DSP techniques to extract a packet.
func (d Decoder) Decode(input []byte) (pkts [][]byte) {
func (d Decoder) Decode(input []byte) []int {
// Shift buffers to append new block.
copy(d.IQ, d.IQ[d.Cfg.BlockSize<<1:])
copy(d.Signal, d.Signal[d.Cfg.BlockSize:])
copy(d.Quantized, d.Quantized[d.Cfg.BlockSize:])
copy(d.Signal, d.Signal[d.DecCfg.BlockSize:])
copy(d.Filtered, d.Filtered[d.DecCfg.BlockSize:])
copy(d.Quantized, d.Quantized[d.DecCfg.BlockSize:])
copy(d.IQ[d.Cfg.PacketLength<<1:], input[:])
iqBlock := d.IQ[d.Cfg.PacketLength<<1:]
signalBlock := d.Signal[d.Cfg.PacketLength:]
signalBlock := d.Signal[d.DecCfg.PacketLength:]
// Compute the magnitude of the new block.
d.lut.Execute(iqBlock, signalBlock)
d.demod.Execute(iqBlock, signalBlock)
signalBlock = d.Signal[d.Cfg.PacketLength-d.Cfg.SymbolLength2:]
signalBlock = d.Signal[d.DecCfg.PacketLength-d.DecCfg.SymbolLength2:]
filterBlock := d.Filtered[d.DecCfg.PacketLength-d.DecCfg.SymbolLength2:]
// Perform matched filter on new block.
d.Filter(signalBlock)
signalBlock = d.Signal[d.Cfg.PacketLength-d.Cfg.SymbolLength2:]
d.Filter(signalBlock, filterBlock)
// Perform bit-decision on new block.
Quantize(signalBlock, d.Quantized[d.Cfg.PacketLength-d.Cfg.SymbolLength2:])
Quantize(filterBlock, d.Quantized[d.DecCfg.PacketLength-d.DecCfg.SymbolLength2:])
// Pack the quantized signal into slices for searching.
d.Pack(d.Quantized[:d.Cfg.BlockSize2], d.slices)
d.Pack(d.Quantized[:d.DecCfg.BlockSize2], d.slices)
// Get a list of indexes the preamble exists at.
indexes := d.Search(d.slices, d.preamble)
// We will likely find multiple instances of the message so only keep
// track of unique instances.
seen := make(map[string]bool)
// For each of the indexes the preamble exists at.
for _, qIdx := range indexes {
// Check that we're still within the first sample block. We'll catch
// the message on the next sample block otherwise.
if qIdx > d.Cfg.BlockSize {
continue
}
// Packet is 1 bit per byte, pack to 8-bits per byte.
for pIdx := 0; pIdx < d.Cfg.PacketSymbols; pIdx++ {
d.pkt[pIdx>>3] <<= 1
d.pkt[pIdx>>3] |= d.Quantized[qIdx+(pIdx*d.Cfg.SymbolLength2)]
}
// Store the packet in the seen map and append to the packet list.
pktStr := fmt.Sprintf("%02X", d.pkt)
if !seen[pktStr] {
seen[pktStr] = true
pkts = append(pkts, make([]byte, len(d.pkt)))
copy(pkts[len(pkts)-1], d.pkt)
}
}
return
// Return a list of indexes the preamble exists at.
return d.Search(d.slices, d.preamble)
}
// A MagnitudeLUT knows how to perform complex magnitude on a slice of IQ samples.
type MagnitudeLUT interface {
// A Demodulator knows how to demodulate an array of uint8 IQ samples into an
// array of float64 samples.
type Demodulator interface {
Execute([]byte, []float64)
}
@ -187,46 +221,18 @@ func NewSqrtMagLUT() (lut MagLUT) {
// Calculates complex magnitude on given IQ stream writing result to output.
func (lut MagLUT) Execute(input []byte, output []float64) {
for idx := range output {
lutIdx := idx << 1
output[idx] = math.Sqrt(lut[input[lutIdx]] + lut[input[lutIdx+1]])
}
}
decIdx := 0
dec := (len(input) / len(output))
// Alpha*Max + Beta*Min Magnitude Approximation Lookup Table.
type AlphaMaxBetaMinLUT []float64
// Pre-computes absolute values with most common DC offset for rtl-sdr dongles.
func NewAlphaMaxBetaMinLUT() (lut AlphaMaxBetaMinLUT) {
lut = make([]float64, 0x100)
for idx := range lut {
lut[idx] = math.Abs(127.4 - float64(idx))
}
return
}
// Calculates complex magnitude on given IQ stream writing result to output.
func (lut AlphaMaxBetaMinLUT) Execute(input []byte, output []float64) {
const (
α = 0.948059448969
ß = 0.392699081699
)
for idx := range output {
lutIdx := idx << 1
i := lut[input[lutIdx]]
q := lut[input[lutIdx+1]]
if i > q {
output[idx] = α*i + ß*q
} else {
output[idx] = α*q + ß*i
}
for idx := 0; decIdx < len(output); idx += dec {
output[decIdx] = math.Sqrt(lut[input[idx]] + lut[input[idx+1]])
decIdx++
}
}
// Matched filter for Manchester coded signals. Output signal's sign at each
// sample determines the bit-value since Manchester symbols have odd symmetry.
func (d Decoder) Filter(input []float64) {
func (d Decoder) Filter(input, output []float64) {
// Computing the cumulative summation over the signal simplifies
// filtering to the difference of a pair of subtractions.
var sum float64
@ -236,10 +242,11 @@ func (d Decoder) Filter(input []float64) {
}
// Filter result is difference of summation of lower and upper symbols.
lower := d.csum[d.Cfg.SymbolLength:]
upper := d.csum[d.Cfg.SymbolLength2:]
for idx := range input[:len(input)-d.Cfg.SymbolLength2] {
input[idx] = (lower[idx] - d.csum[idx]) - (upper[idx] - lower[idx])
lower := d.csum[d.DecCfg.SymbolLength:]
upper := d.csum[d.DecCfg.SymbolLength2:]
n := len(input) - d.DecCfg.SymbolLength2
for idx := 0; idx < n; idx++ {
output[idx] = (lower[idx] - d.csum[idx]) - (upper[idx] - lower[idx])
}
return
@ -257,10 +264,16 @@ func Quantize(input []float64, output []byte) {
// Packs quantized signal into slices such that the first rank represents
// sample offsets and the second represents the value of each symbol from the
// given offset.
//
// Transforms:
// <--Sym1--><--Sym2--><--Sym3--><--Sym4--><--Sym5--><--Sym6--><--Sym7--><--Sym8-->
// <12345678><12345678><12345678><12345678><12345678><12345678><12345678><12345678>
// to:
// <11111111><22222222><33333333><44444444><55555555><66666666><77777777><88888888>
func (d Decoder) Pack(input []byte, slices [][]byte) {
for symbolOffset, slice := range slices {
for symbolIdx := range slice {
slice[symbolIdx] = input[symbolIdx*d.Cfg.SymbolLength2+symbolOffset]
slice[symbolIdx] = input[symbolIdx*d.DecCfg.SymbolLength2+symbolOffset]
}
}
@ -271,17 +284,11 @@ func (d Decoder) Pack(input []byte, slices [][]byte) {
// preamble is found at. Indexes are absolute in the unsliced quantized
// buffer.
func (d Decoder) Search(slices [][]byte, preamble []byte) (indexes []int) {
preambleLength := len(preamble)
for symbolOffset, slice := range slices {
for symbolIdx := range slice[:len(slice)-len(preamble)] {
var result uint8
for bitIdx, bit := range preamble {
result |= bit ^ slice[symbolIdx+bitIdx]
if result != 0 {
break
}
}
if result == 0 {
indexes = append(indexes, symbolIdx*d.Cfg.SymbolLength2+symbolOffset)
for symbolIdx := range slice[:len(slice)-preambleLength] {
if bytes.Equal(preamble, slice[symbolIdx:][:preambleLength]) {
indexes = append(indexes, symbolIdx*d.DecCfg.SymbolLength2+symbolOffset)
}
}
}
@ -289,6 +296,40 @@ func (d Decoder) Search(slices [][]byte, preamble []byte) (indexes []int) {
return
}
// Given a list of indeces the preamble exists at, sample the appropriate bits
// of the signal's bit-decision. Pack bits of each index into an array of byte
// arrays and return.
func (d Decoder) Slice(indices []int) (pkts [][]byte) {
// We will likely find multiple instances of the message so only keep
// track of unique instances.
seen := make(map[string]bool)
// For each of the indices the preamble exists at.
for _, qIdx := range indices {
// Check that we're still within the first sample block. We'll catch
// the message on the next sample block otherwise.
if qIdx > d.DecCfg.BlockSize {
continue
}
// Packet is 1 bit per byte, pack to 8-bits per byte.
for pIdx := 0; pIdx < d.DecCfg.PacketSymbols; pIdx++ {
d.pkt[pIdx>>3] <<= 1
d.pkt[pIdx>>3] |= d.Quantized[qIdx+(pIdx*d.DecCfg.SymbolLength2)]
}
// Store the packet in the seen map and append to the packet list.
pktStr := fmt.Sprintf("%02X", d.pkt)
if !seen[pktStr] {
seen[pktStr] = true
pkts = append(pkts, make([]byte, len(d.pkt)))
copy(pkts[len(pkts)-1], d.pkt)
}
}
return
}
func NextPowerOf2(v int) int {
return 1 << uint(math.Ceil(math.Log2(float64(v))))
}

View file

@ -1,5 +1,5 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
// Copyright (C) 2015 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
@ -17,6 +17,7 @@
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"encoding/xml"
@ -28,6 +29,7 @@ import (
"strings"
"github.com/bemasher/rtlamr/csv"
"github.com/bemasher/rtlamr/parse"
)
var logFilename = flag.String("logfile", "/dev/stdout", "log statement dump file")
@ -36,46 +38,45 @@ var logFile *os.File
var sampleFilename = flag.String("samplefile", os.DevNull, "raw signal dump file")
var sampleFile *os.File
var msgType = flag.String("msgtype", "scm", "message type to receive: scm or idm")
var fastMag = flag.Bool("fastmag", false, "use faster alpha max + beta min magnitude approximation")
var msgType = flag.String("msgtype", "scm", "message type to receive: scm, idm, scm+idm or r900")
var symbolLength = flag.Int("symbollength", 73, "symbol length in samples, see -help for valid lengths")
var symbolLength = flag.Int("symbollength", 72, "symbol length in samples")
var decimation = flag.Int("decimation", 1, "integer decimation factor, keep every nth sample")
var timeLimit = flag.Duration("duration", 0, "time to run for, 0 for infinite, ex. 1h5m10s")
var meterID UintMap
var meterType UintMap
var meterID MeterIDFilter
var meterType MeterTypeFilter
var unique = flag.Bool("unique", false, "suppress duplicate messages from each meter")
var encoder Encoder
var format = flag.String("format", "plain", "format to write log messages in: plain, csv, json, xml or gob")
var gobUnsafe = flag.Bool("gobunsafe", false, "allow gob output to stdout")
var quiet = flag.Bool("quiet", false, "suppress printing state information at startup")
var single = flag.Bool("single", false, "one shot execution")
var single = flag.Bool("single", false, "one shot execution, if used with -filterid, will wait for exactly one packet from each meter id")
func RegisterFlags() {
meterID = make(UintMap)
meterType = make(UintMap)
meterID = MeterIDFilter{make(UintMap)}
meterType = MeterTypeFilter{make(UintMap)}
flag.Var(meterID, "filterid", "display only messages matching an id in a comma-separated list of ids.")
flag.Var(meterType, "filtertype", "display only messages matching a type in a comma-separated list of types.")
// Override default center frequency.
centerFreqFlag := flag.CommandLine.Lookup("centerfreq")
centerFreqString := strconv.FormatUint(CenterFreq, 10)
centerFreqFlag.DefValue = centerFreqString
centerFreqFlag.Value.Set(centerFreqString)
rtlamrFlags := map[string]bool{
"logfile": true,
"samplefile": true,
"msgtype": true,
"symbollength": true,
"decimation": true,
"duration": true,
"filterid": true,
"filtertype": true,
"format": true,
"gobunsafe": true,
"quiet": true,
"unique": true,
"single": true,
"cpuprofile": true,
"fastmag": true,
@ -123,7 +124,7 @@ func HandleFlags() {
*format = strings.ToLower(*format)
switch *format {
case "plain":
break
encoder = PlainEncoder{*sampleFilename, logFile}
case "csv":
encoder = csv.NewEncoder(logFile)
case "json":
@ -169,3 +170,52 @@ func (m UintMap) Set(value string) error {
return nil
}
type MeterIDFilter struct {
UintMap
}
func (m MeterIDFilter) Filter(msg parse.Message) bool {
return m.UintMap[uint(msg.MeterID())]
}
type MeterTypeFilter struct {
UintMap
}
func (m MeterTypeFilter) Filter(msg parse.Message) bool {
return m.UintMap[uint(msg.MeterType())]
}
type UniqueFilter map[uint][]byte
func NewUniqueFilter() UniqueFilter {
return make(UniqueFilter)
}
func (uf UniqueFilter) Filter(msg parse.Message) bool {
checksum := msg.Checksum()
mid := uint(msg.MeterID())
if val, ok := uf[mid]; ok && bytes.Compare(val, checksum) == 0 {
return false
}
uf[mid] = make([]byte, len(checksum))
copy(uf[mid], checksum)
return true
}
type PlainEncoder struct {
sampleFilename string
logFile *os.File
}
func (pe PlainEncoder) Encode(msg interface{}) (err error) {
if pe.sampleFilename == os.DevNull {
_, err = fmt.Fprintln(pe.logFile, msg.(parse.LogMessage).StringNoOffset())
} else {
_, err = fmt.Fprintln(pe.logFile, msg.(parse.LogMessage))
}
return
}

View file

@ -1,5 +1,5 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
// Copyright (C) 2015 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
@ -18,7 +18,6 @@ package idm
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
@ -29,37 +28,70 @@ import (
)
func NewPacketConfig(symbolLength int) (cfg decode.PacketConfig) {
cfg.CenterFreq = 912600155
cfg.DataRate = 32768
cfg.SymbolLength = symbolLength
cfg.SymbolLength2 = cfg.SymbolLength << 1
cfg.SampleRate = cfg.DataRate * cfg.SymbolLength
cfg.PreambleSymbols = 32
cfg.PacketSymbols = 92 * 8
cfg.PreambleLength = cfg.PreambleSymbols * cfg.SymbolLength2
cfg.PacketLength = cfg.PacketSymbols * cfg.SymbolLength2
cfg.BlockSize = decode.NextPowerOf2(cfg.PreambleLength)
cfg.BlockSize2 = cfg.BlockSize << 1
cfg.BufferLength = cfg.PacketLength + cfg.BlockSize
cfg.Preamble = "01010101010101010001011010100011"
return
}
type Parser struct {
decode.Decoder
crc.CRC
}
func NewParser() (p Parser) {
func (p Parser) Dec() decode.Decoder {
return p.Decoder
}
func (p Parser) Cfg() decode.PacketConfig {
return p.Decoder.Cfg
}
func NewParser(symbolLength, decimation int) (p Parser) {
p.Decoder = decode.NewDecoder(NewPacketConfig(symbolLength), decimation)
p.CRC = crc.NewCRC("CCITT", 0xFFFF, 0x1021, 0x1D0F)
return
}
func (p Parser) Parse(indices []int) (msgs []parse.Message) {
seen := make(map[string]bool)
for _, pkt := range p.Decoder.Slice(indices) {
s := string(pkt)
if seen[s] {
continue
}
seen[s] = true
data := parse.NewDataFromBytes(pkt)
// If the packet is too short, bail.
if l := len(data.Bytes); l != 92 {
continue
}
// If the checksum fails, bail.
if residue := p.Checksum(data.Bytes[4:92]); residue != p.Residue {
continue
}
idm := NewIDM(data)
// If the meter id is 0, bail.
if idm.ERTSerialNumber == 0 {
continue
}
msgs = append(msgs, idm)
}
return
}
// Standard Consumption Message
type IDM struct {
Preamble uint32 // Training and Frame sync.
@ -81,11 +113,36 @@ type IDM struct {
PacketCRC uint16
}
type Interval [47]uint16
func NewIDM(data parse.Data) (idm IDM) {
idm.Preamble = binary.BigEndian.Uint32(data.Bytes[0:4])
idm.PacketTypeID = data.Bytes[4]
idm.PacketLength = data.Bytes[5]
idm.HammingCode = data.Bytes[6]
idm.ApplicationVersion = data.Bytes[7]
idm.ERTType = data.Bytes[8] & 0x0F
idm.ERTSerialNumber = binary.BigEndian.Uint32(data.Bytes[9:13])
idm.ConsumptionIntervalCount = data.Bytes[13]
idm.ModuleProgrammingState = data.Bytes[14]
idm.TamperCounters = data.Bytes[15:21]
idm.AsynchronousCounters = binary.BigEndian.Uint16(data.Bytes[21:23])
idm.PowerOutageFlags = data.Bytes[23:29]
idm.LastConsumptionCount = binary.BigEndian.Uint32(data.Bytes[29:33])
// func (interval Interval) MarshalText() (text []byte, err error) {
// return []byte(fmt.Sprintf("%+v", interval)), nil
// }
offset := 264
for idx := range idm.DifferentialConsumptionIntervals {
interval, _ := strconv.ParseUint(data.Bits[offset:offset+9], 2, 9)
idm.DifferentialConsumptionIntervals[idx] = uint16(interval)
offset += 9
}
idm.TransmitTimeOffset = binary.BigEndian.Uint16(data.Bytes[86:88])
idm.SerialNumberCRC = binary.BigEndian.Uint16(data.Bytes[88:90])
idm.PacketCRC = binary.BigEndian.Uint16(data.Bytes[90:92])
return
}
type Interval [47]uint16
func (interval Interval) Record() (r []string) {
for _, val := range interval {
@ -106,6 +163,12 @@ func (idm IDM) MeterType() uint8 {
return idm.ERTType
}
func (idm IDM) Checksum() []byte {
checksum := make([]byte, 2)
binary.BigEndian.PutUint16(checksum, idm.PacketCRC)
return checksum
}
func (idm IDM) String() string {
var fields []string
@ -151,43 +214,3 @@ func (idm IDM) Record() (r []string) {
return
}
func (p Parser) Parse(data parse.Data) (msg parse.Message, err error) {
var idm IDM
if residue := p.Checksum(data.Bytes[4:]); residue != p.Residue {
err = fmt.Errorf("checksum failed: 0x%04X", residue)
return
}
idm.Preamble = binary.BigEndian.Uint32(data.Bytes[0:4])
idm.PacketTypeID = data.Bytes[4]
idm.PacketLength = data.Bytes[5]
idm.HammingCode = data.Bytes[6]
idm.ApplicationVersion = data.Bytes[7]
idm.ERTType = data.Bytes[8] & 0x0F
idm.ERTSerialNumber = binary.BigEndian.Uint32(data.Bytes[9:13])
idm.ConsumptionIntervalCount = data.Bytes[13]
idm.ModuleProgrammingState = data.Bytes[14]
idm.TamperCounters = data.Bytes[15:21]
idm.AsynchronousCounters = binary.BigEndian.Uint16(data.Bytes[21:23])
idm.PowerOutageFlags = data.Bytes[23:29]
idm.LastConsumptionCount = binary.BigEndian.Uint32(data.Bytes[29:33])
offset := 264
for idx := range idm.DifferentialConsumptionIntervals {
interval, _ := strconv.ParseUint(data.Bits[offset:offset+9], 2, 9)
idm.DifferentialConsumptionIntervals[idx] = uint16(interval)
offset += 9
}
idm.TransmitTimeOffset = binary.BigEndian.Uint16(data.Bytes[86:88])
idm.SerialNumberCRC = binary.BigEndian.Uint16(data.Bytes[88:90])
idm.PacketCRC = binary.BigEndian.Uint16(data.Bytes[90:92])
if idm.ERTSerialNumber == 0 {
return idm, errors.New("invalid meter id")
}
return idm, nil
}

View file

@ -1,39 +0,0 @@
package main
import (
"crypto/rand"
"github.com/bemasher/rtlamr/decode"
"testing"
)
func BenchmarkSqrtMag(b *testing.B) {
lut := decode.NewSqrtMagLUT()
input := make([]byte, 8192)
output := make([]float64, 4096)
rand.Read(input)
b.SetBytes(4096)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
lut.Execute(input, output)
}
}
func BenchmarkAlphaMaxBetaMinMag(b *testing.B) {
lut := decode.NewAlphaMaxBetaMinLUT()
input := make([]byte, 8192)
output := make([]float64, 4096)
rand.Read(input)
b.SetBytes(4096)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
lut.Execute(input, output)
}
}

35
meters.csv Normal file
View file

@ -0,0 +1,35 @@
URL,Manufacturer,Model Name,Commodity,ERT Type,Lower (MHz),Upper (MHz)
http://fcc.io/EO945ES-1,Itron,45ES-1,Electric,7,910,920
http://fcc.io/EO950ESS,Itron,50ESS,Electric,8,910,920
http://fcc.io/EO951ESS,Itron,51ESS,Electric,7,910,920
http://fcc.io/EO952ESS,Itron,52ESS,Electric,5,910,920
http://fcc.io/EO953ESS,Itron,53ESS,Electric,8,902,928
,Itron,54ESS,Electric,4,,
,Itron,55ESS,Electric,5,,
,Itron,56ESS,Electric,7,,
http://fcc.io/SK9AMI-4,Itron,AMI4,Electric,4,902.2,927.8
http://fcc.io/SK9AMI6,Itron,AMI6,Electric,4,902.2,927.8
http://fcc.io/SK9AMI6,Itron,AMI6,Electric,4,909.6,921.8
http://fcc.io/SK9C1A-2,Itron,C1A-2,Electric,4,910,920
http://fcc.io/SK9C1A-2,Itron,C1A-2,Electric,4,917.6,917.6
http://fcc.io/SK9C1A-3,Itron,C1A-3,Electric,"4,7",909,922
http://fcc.io/SK9AMI-3,Itron,C2SOD,Electric,4,902.2,927.8
http://fcc.io/SK9C3A-1L,Itron,C3A1L,Electric,4,909,922
http://fcc.io/SK9C3A-1H,Itron,C3A-1H,Electric,"4,8",909,922
http://fcc.io/SK9AMI-5,Itron,CVSO-B,Electric,4,902.2,927.8
http://fcc.io/SK9R300S-2,Itron,R300S2,Electric,8,909.3,918.4
,Itron,40G,Gas,2,,
http://fcc.io/EO9100G,Itron,100G,Gas,12,903,928
http://fcc.io/100GDLAN,Itron,100GDLAN,Gas,12,908,926.8
http://fcc.io/EWQ100T,Itron,100T,Gas,9,903,926.9
http://fcc.io/EO960W,Itron,60W,Water,13,910,919.8
http://fcc.io/EO980WI,Itron,80W-i,Water,13,910,920
http://fcc.io/EWQ100W,Itron,100W,Water,11,903,927
http://fcc.io/F9CC1C-3,Schlumberger,CENTRON OOK RF,Electric,12,917.58,917.58
http://fcc.io/TEB-41ER-1,Landis+Gyr,AirPoint 41 Series,Electric,5,910,920
http://fcc.io/TEB-AIRPT622,Landis+Gyr,AirPoint Focus,Electric,5,913.672,916.138
http://fcc.io/TEB-AIRPT652,Landis+Gyr,AirPoint iCon,Electric,5,913.75,916.25
http://fcc.io/TEB-AIRPT654,Landis+Gyr,AirPoint I-210,Electric,5,913.672,916.138
http://fcc.io/TEB-AIRPT657,Landis+Gyr,HP AirPoint I-210,Electric,5,909.586,921.773
http://fcc.io/TEB-AIRPT677,Landis+Gyr,HP AirPoint,Electric,5,909.586,921.773
http://fcc.io/TEB-AIRPT725,Landis+Gyr,HP AirPoint,Electric,5,909.586,921.773
1 URL Manufacturer Model Name Commodity ERT Type Lower (MHz) Upper (MHz)
2 http://fcc.io/EO945ES-1 Itron 45ES-1 Electric 7 910 920
3 http://fcc.io/EO950ESS Itron 50ESS Electric 8 910 920
4 http://fcc.io/EO951ESS Itron 51ESS Electric 7 910 920
5 http://fcc.io/EO952ESS Itron 52ESS Electric 5 910 920
6 http://fcc.io/EO953ESS Itron 53ESS Electric 8 902 928
7 Itron 54ESS Electric 4
8 Itron 55ESS Electric 5
9 Itron 56ESS Electric 7
10 http://fcc.io/SK9AMI-4 Itron AMI4 Electric 4 902.2 927.8
11 http://fcc.io/SK9AMI6 Itron AMI6 Electric 4 902.2 927.8
12 http://fcc.io/SK9AMI6 Itron AMI6 Electric 4 909.6 921.8
13 http://fcc.io/SK9C1A-2 Itron C1A-2 Electric 4 910 920
14 http://fcc.io/SK9C1A-2 Itron C1A-2 Electric 4 917.6 917.6
15 http://fcc.io/SK9C1A-3 Itron C1A-3 Electric 4,7 909 922
16 http://fcc.io/SK9AMI-3 Itron C2SOD Electric 4 902.2 927.8
17 http://fcc.io/SK9C3A-1L Itron C3A1L Electric 4 909 922
18 http://fcc.io/SK9C3A-1H Itron C3A-1H Electric 4,8 909 922
19 http://fcc.io/SK9AMI-5 Itron CVSO-B Electric 4 902.2 927.8
20 http://fcc.io/SK9R300S-2 Itron R300S2 Electric 8 909.3 918.4
21 Itron 40G Gas 2
22 http://fcc.io/EO9100G Itron 100G Gas 12 903 928
23 http://fcc.io/100GDLAN Itron 100GDLAN Gas 12 908 926.8
24 http://fcc.io/EWQ100T Itron 100T Gas 9 903 926.9
25 http://fcc.io/EO960W Itron 60W Water 13 910 919.8
26 http://fcc.io/EO980WI Itron 80W-i Water 13 910 920
27 http://fcc.io/EWQ100W Itron 100W Water 11 903 927
28 http://fcc.io/F9CC1C-3 Schlumberger CENTRON OOK RF Electric 12 917.58 917.58
29 http://fcc.io/TEB-41ER-1 Landis+Gyr AirPoint 41 Series Electric 5 910 920
30 http://fcc.io/TEB-AIRPT622 Landis+Gyr AirPoint Focus Electric 5 913.672 916.138
31 http://fcc.io/TEB-AIRPT652 Landis+Gyr AirPoint iCon Electric 5 913.75 916.25
32 http://fcc.io/TEB-AIRPT654 Landis+Gyr AirPoint I-210 Electric 5 913.672 916.138
33 http://fcc.io/TEB-AIRPT657 Landis+Gyr HP AirPoint I-210 Electric 5 909.586 921.773
34 http://fcc.io/TEB-AIRPT677 Landis+Gyr HP AirPoint Electric 5 909.586 921.773
35 http://fcc.io/TEB-AIRPT725 Landis+Gyr HP AirPoint Electric 5 909.586 921.773

View file

@ -2,36 +2,8 @@ Want to help make this information more accurate? If you have a meter not listed
Submit your meter: https://docs.google.com/forms/d/1WPhliiE7tnbTKa-WDQ9Bf7UIf27qPMqX0IarxFGWGKY/viewform?usp=send_form
According to a [marketing document](http://marketing.itron.com/campaign/ItronSCMPlus_FAQMay2012.pdf) by Itron the following commodities have these ert types:
* Electric: 04, 05, 07, 08
* Gas: 02, 09, 12
* Water: 11, 13
* Electric: 04, 07, 08
* Gas: 12
* Water: 11
| URL | Model Name | Commodity | ERT Type | Lower (MHz) | Upper (MHz) |
|:------------------------ | ----------:| --------- | --------:| -----------:| -----------:|
| http://fcc.io/EO945ES-1 | 45ES-1 | Electric | 7 | 910.0 | 920.0 |
| http://fcc.io/EO950ESS | 50ESS | Electric | 8 | 910.0 | 920.0 |
| http://fcc.io/EO951ESS | 51ESS | Electric | 7 | 910.0 | 920.0 |
| http://fcc.io/EO952ESS | 52ESS | Electric | 5 | 910.0 | 920.0 |
| http://fcc.io/EO953ESS | 53ESS | Electric | 8 | 902.0 | 928.0 |
| | 54ESS | Electric | 4 | | |
| | 55ESS | Electric | 5 | | |
| | 56ESS | Electric | 7 | | |
| http://fcc.io/SK9AMI-4 | AMI4 | Electric | 4 | 902.2 | 927.8 |
| http://fcc.io/SK9AMI6 | AMI6 | Electric | 4 | 902.2 | 927.8 |
| http://fcc.io/SK9AMI6 | AMI6 | Electric | 4 | 909.6 | 921.8 |
| http://fcc.io/SK9C1A-2 | C1A-2 | Electric | 4 | 910.0 | 920.0 |
| http://fcc.io/SK9C1A-2 | C1A-2 | Electric | 4 | 917.6 | 917.6 |
| http://fcc.io/SK9C1A-3 | C1A-3 | Electric | 4,7 | 909.0 | 922.0 |
| http://fcc.io/SK9AMI-3 | C2SOD | Electric | 4 | 902.2 | 927.8 |
| http://fcc.io/SK9C3A-1L | C3A1L | Electric | 4, | 909.0 | 922.0 |
| http://fcc.io/SK9C3A-1H | C3A-1H | Electric | 4,8 | 909.0 | 922.0 |
| http://fcc.io/SK9AMI-5 | CVSO-B | Electric | 4 | 902.2 | 927.8 |
| http://fcc.io/SK9R300S-2 | R300S2 | Electric | 8 | 909.3 | 918.4 |
| | 40G | Gas | 2 | | |
| http://fcc.io/EO9100G | 100G | Gas | 12 | 903.0 | 928.0 |
| http://fcc.io/EWQ100T | 100T | Gas | 9 | 903.0 | 926.9 |
| http://fcc.io/EO960W | 60W | Water | 13 | 910.0 | 919.8 |
| http://fcc.io/EO980WI | 80W-i | Water | 13 | 910.0 | 920.0 |
| http://fcc.io/EWQ100W | 100W | Water | 11 | 903.0 | 927.0 |
The compatible meter table has moved to a csv file: [ERT Compatible Meters](https://github.com/bemasher/rtlamr/blob/master/meters.csv)

View file

@ -1,73 +1,103 @@
package parse
import (
"fmt"
"strconv"
"time"
"github.com/bemasher/rtlamr/csv"
)
const (
TimeFormat = "2006-01-02T15:04:05.000"
)
type Data struct {
Bits string
Bytes []byte
}
func NewDataFromBytes(data []byte) (d Data) {
d.Bytes = data
for _, b := range data {
d.Bits += fmt.Sprintf("%08b", b)
}
return
}
func NewDataFromBits(data string) (d Data) {
d.Bits = data
d.Bytes = make([]byte, len(data)>>3+1)
for idx := 0; idx < len(data); idx += 8 {
b, _ := strconv.ParseUint(d.Bits[idx:idx+8], 2, 8)
d.Bytes[idx>>3] = uint8(b)
}
return
}
type Parser interface {
Parse(Data) (Message, error)
}
type Message interface {
MsgType() string
MeterID() uint32
MeterType() uint8
csv.Recorder
}
type LogMessage struct {
Time time.Time
Offset int64
Length int
Message
}
func (msg LogMessage) String() string {
return fmt.Sprintf("{Time:%s Offset:%d Length:%d %s:%s}",
msg.Time.Format(TimeFormat), msg.Offset, msg.Length, msg.MsgType(), msg.Message,
)
}
func (msg LogMessage) StringNoOffset() string {
return fmt.Sprintf("{Time:%s %s:%s}", msg.Time.Format(TimeFormat), msg.MsgType(), msg.Message)
}
func (msg LogMessage) Record() (r []string) {
r = append(r, msg.Time.Format(time.RFC3339Nano))
r = append(r, strconv.FormatInt(msg.Offset, 10))
r = append(r, strconv.FormatInt(int64(msg.Length), 10))
r = append(r, msg.Message.Record()...)
return r
}
package parse
import (
"fmt"
"strconv"
"time"
"github.com/bemasher/rtlamr/decode"
"github.com/bemasher/rtlamr/csv"
)
const (
TimeFormat = "2006-01-02T15:04:05.000"
)
type Data struct {
Bits string
Bytes []byte
}
func NewDataFromBytes(data []byte) (d Data) {
d.Bytes = data
for _, b := range data {
d.Bits += fmt.Sprintf("%08b", b)
}
return
}
func NewDataFromBits(data string) (d Data) {
d.Bits = data
d.Bytes = make([]byte, (len(data)+7)>>3)
for idx := 0; idx < len(data); idx += 8 {
b, _ := strconv.ParseUint(d.Bits[idx:idx+8], 2, 8)
d.Bytes[idx>>3] = uint8(b)
}
return
}
type Parser interface {
Parse([]int) []Message
Dec() decode.Decoder
Cfg() decode.PacketConfig
Log()
}
type Message interface {
csv.Recorder
MsgType() string
MeterID() uint32
MeterType() uint8
Checksum() []byte
}
type LogMessage struct {
Time time.Time
Offset int64
Length int
Message
}
func (msg LogMessage) String() string {
return fmt.Sprintf("{Time:%s Offset:%d Length:%d %s:%s}",
msg.Time.Format(TimeFormat), msg.Offset, msg.Length, msg.MsgType(), msg.Message,
)
}
func (msg LogMessage) StringNoOffset() string {
return fmt.Sprintf("{Time:%s %s:%s}", msg.Time.Format(TimeFormat), msg.MsgType(), msg.Message)
}
func (msg LogMessage) Record() (r []string) {
r = append(r, msg.Time.Format(time.RFC3339Nano))
r = append(r, strconv.FormatInt(msg.Offset, 10))
r = append(r, strconv.FormatInt(int64(msg.Length), 10))
r = append(r, msg.Message.Record()...)
return r
}
type FilterChain []MessageFilter
func (fc *FilterChain) Add(filter MessageFilter) {
*fc = append(*fc, filter)
}
func (fc FilterChain) Match(msg Message) bool {
if len(fc) == 0 {
return true
}
for _, filter := range fc {
if !filter.Filter(msg) {
return false
}
}
return true
}
type MessageFilter interface {
Filter(Message) bool
}

27
r900/gf/LICENSE Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

172
r900/gf/gf.go Normal file
View file

@ -0,0 +1,172 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gf implements arithmetic over Galois Fields. Generalized for any valid order.
package gf
import "strconv"
// A Field represents an instance of GF(order) defined by a specific polynomial.
type Field struct {
order int
log []byte // log[0] is unused
exp []byte
}
// NewField returns a new field corresponding to the polynomial poly and
// generator α. The choice of generator α only affects the Exp and Log
// operations.
func NewField(order, poly, α int) *Field {
if order < 0 || order > 256 {
panic("gf: invalid order: " + strconv.Itoa(order))
}
if poly < order || poly >= order<<1 || reducible(poly) {
panic("gf: invalid polynomial: " + strconv.Itoa(poly))
}
f := Field{order - 1, make([]byte, order), make([]byte, (order-1)<<1)}
x := 1
for i := 0; i < f.order; i++ {
if x == 1 && i != 0 {
panic("gf: invalid generator " + strconv.Itoa(α) +
" for polynomial " + strconv.Itoa(poly))
}
f.exp[i] = byte(x)
f.exp[i+f.order] = byte(x)
f.log[x] = byte(i)
x = mul(x, α, order, poly)
}
f.log[0] = byte(f.order)
for i := 0; i < f.order; i++ {
if f.log[f.exp[i]] != byte(i) {
panic("bad log")
}
if f.log[f.exp[i+f.order]] != byte(i) {
panic("bad log")
}
}
for i := 1; i < order; i++ {
if f.exp[f.log[i]] != byte(i) {
panic("bad log")
}
}
return &f
}
// nbit returns the number of significant in p.
func nbit(p int) uint {
n := uint(0)
for ; p > 0; p >>= 1 {
n++
}
return n
}
// polyDiv divides the polynomial p by q and returns the remainder.
func polyDiv(p, q int) int {
np := nbit(p)
nq := nbit(q)
for ; np >= nq; np-- {
if p&(1<<(np-1)) != 0 {
p ^= q << (np - nq)
}
}
return p
}
// mul returns the product x*y mod poly, a GF(order) multiplication.
func mul(x, y, order, poly int) int {
z := 0
for x > 0 {
if x&1 != 0 {
z ^= y
}
x >>= 1
y <<= 1
if y&order != 0 {
y ^= poly
}
}
return z
}
// reducible reports whether p is reducible.
func reducible(p int) bool {
// Multiplying n-bit * n-bit produces (2n-1)-bit,
// so if p is reducible, one of its factors must be
// of np/2+1 bits or fewer.
np := nbit(p)
for q := 2; q < 1<<(np/2+1); q++ {
if polyDiv(p, q) == 0 {
return true
}
}
return false
}
// Add returns the sum of x and y in the field.
func (f *Field) Add(x, y byte) byte {
return x ^ y
}
// Exp returns the base-α exponential of e in the field.
// If e < 0, Exp returns 0.
func (f *Field) Exp(e int) byte {
if e < 0 {
return 0
}
return f.exp[e%f.order]
}
// Log returns the base-α logarithm of x in the field.
// If x == 0, Log returns -1.
func (f *Field) Log(x byte) int {
if x == 0 {
return -1
}
return int(f.log[x])
}
// Inv returns the multiplicative inverse of x in the field.
// If x == 0, Inv returns 0.
func (f *Field) Inv(x byte) byte {
if x == 0 {
return 0
}
return f.exp[f.order-int(f.log[x])]
}
// Mul returns the product of x and y in the field.
func (f *Field) Mul(x, y byte) byte {
if x == 0 || y == 0 {
return 0
}
return f.exp[int(f.log[x])+int(f.log[y])]
}
// Calculate syndrome for a message encoded using the field generated for a
// particular Reed-Solomon polynomial. Offset defines the coefficient offset.
func (f *Field) Syndrome(message []byte, paritySymbolCount, offset int) (syndrome []byte) {
if offset < 0 || offset > f.order {
panic("gf: invalid offset: " + strconv.Itoa(offset))
}
if paritySymbolCount < 0 || paritySymbolCount > len(message) {
panic("gf: invalid paritySymbolCount: " + strconv.Itoa(paritySymbolCount))
}
syndrome = make([]byte, paritySymbolCount)
for idx, syn := range syndrome {
syn = message[0]
for _, v := range message[1:] {
syn = f.Mul(syn, f.Exp(offset+idx)) ^ v
}
syndrome[idx] = syn
}
return syndrome
}

285
r900/r900.go Normal file
View file

@ -0,0 +1,285 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package r900
import (
"bytes"
"fmt"
"math"
"strconv"
"github.com/bemasher/rtlamr/decode"
"github.com/bemasher/rtlamr/parse"
"github.com/bemasher/rtlamr/r900/gf"
)
const (
PayloadSymbols = 42
)
func NewPacketConfig(symbolLength int) (cfg decode.PacketConfig) {
cfg.CenterFreq = 912380000
cfg.DataRate = 32768
cfg.SymbolLength = symbolLength
cfg.PreambleSymbols = 32
cfg.PacketSymbols = 116
cfg.Preamble = "00000000000000001110010101100100"
return
}
type Parser struct {
decode.Decoder
field *gf.Field
rsBuf [31]byte
csum []float64
filtered [][3]float64
quantized []byte
}
func NewParser(symbolLength, decimation int) (p Parser) {
p.Decoder = decode.NewDecoder(NewPacketConfig(symbolLength), decimation)
// GF of order 32, polynomial 37, generator 2.
p.field = gf.NewField(32, 37, 2)
p.csum = make([]float64, p.Decoder.DecCfg.BufferLength+1)
p.filtered = make([][3]float64, p.Decoder.DecCfg.BufferLength)
p.quantized = make([]byte, p.Decoder.DecCfg.BufferLength)
return
}
func (p Parser) Dec() decode.Decoder {
return p.Decoder
}
func (p Parser) Cfg() decode.PacketConfig {
return p.Decoder.Cfg
}
// Perform matched filtering.
func (p Parser) Filter() {
// This function computes the convolution of each symbol kernel with the
// signal. The naive approach requires for each symbol to calculate the
// summation of samples between a pair of indices.
// 0 |--------|
// 1 |--------|
// 2 |--------|
// 3 |--------|
// To avoid redundant calculations we compute the cumulative sum of the
// signal. This reduces each summation to the difference between the two
// indices of the cumulative sum.
var sum float64
for idx, v := range p.Decoder.Signal {
sum += v
p.csum[idx+1] = sum
}
// There are six symbols, composed of three base symbols and their bitwise
// inversions. Compute the convolution of each base symbol with the
// signal.
// 1100 -> 0011
// 1010 -> 0101
// 1001 -> 0110
// This is basically unreadable because of a lot of algebraic
// simplification but is necessary for efficiency.
for idx := 0; idx < p.Decoder.DecCfg.BufferLength-p.Decoder.DecCfg.SymbolLength*4; idx++ {
c0 := p.csum[idx]
c1 := p.csum[idx+p.Decoder.DecCfg.SymbolLength] * 2
c2 := p.csum[idx+p.Decoder.DecCfg.SymbolLength*2] * 2
c3 := p.csum[idx+p.Decoder.DecCfg.SymbolLength*3] * 2
c4 := p.csum[idx+p.Decoder.DecCfg.SymbolLength*4]
p.filtered[idx][0] = c2 - c4 - c0 // 1100
p.filtered[idx][1] = c1 - c2 + c3 - c4 - c0 // 1010
p.filtered[idx][2] = c1 - c3 + c4 - c0 // 1001
}
}
// Determine the symbol that exists at each sample of the signal.
func (p Parser) Quantize() {
// 0 0011, 3 1100
// 1 0101, 4 1010
// 2 0110, 5 1001
for idx, vec := range p.filtered {
argmax := byte(0)
max := math.Abs(vec[0])
// If v1 is larger than v0, update max and argmax.
if v1 := math.Abs(vec[1]); v1 > max {
max = v1
argmax = 1
}
// If v2 is larger than the greater of v1 or v0, update max and argmax.
if v2 := math.Abs(vec[2]); v2 > max {
max = v2
argmax = 2
}
// Set the output symbol index.
p.quantized[idx] = argmax
// If the sign is negative, jump to the index of the inverted symbol.
if vec[argmax] > 0 {
p.quantized[idx] += 3
}
}
}
// Given a list of indices the preamble exists at, decode and parse a message.
func (p Parser) Parse(indices []int) (msgs []parse.Message) {
p.Filter()
p.Quantize()
preambleLength := p.Decoder.DecCfg.PreambleLength
symbolLength := p.Decoder.DecCfg.SymbolLength
symbols := make([]byte, 21)
zeros := make([]byte, 5)
seen := make(map[string]bool)
for _, preambleIdx := range indices {
if preambleIdx > p.Decoder.DecCfg.BlockSize {
break
}
payloadIdx := preambleIdx + preambleLength
var digits string
for idx := 0; idx < PayloadSymbols*4*p.Decoder.DecCfg.SymbolLength; idx += symbolLength * 4 {
qIdx := payloadIdx + idx
digits += strconv.Itoa(int(p.quantized[qIdx]))
}
var (
bits string
badSymbol bool
)
for idx := 0; idx < len(digits); idx += 2 {
symbol, _ := strconv.ParseInt(digits[idx:idx+2], 6, 32)
if symbol > 31 {
badSymbol = true
break
}
symbols[idx>>1] = byte(symbol)
bits += fmt.Sprintf("%05b", symbol)
}
if badSymbol || seen[bits] {
continue
}
seen[bits] = true
copy(p.rsBuf[:], symbols[:16])
copy(p.rsBuf[26:], symbols[16:])
syndromes := p.field.Syndrome(p.rsBuf[:], 5, 29)
if !bytes.Equal(zeros, syndromes) {
continue
}
id, _ := strconv.ParseUint(bits[:32], 2, 32)
unkn1, _ := strconv.ParseUint(bits[32:40], 2, 8)
nouse, _ := strconv.ParseUint(bits[40:46], 2, 6)
backflow, _ := strconv.ParseUint(bits[46:48], 2, 2)
consumption, _ := strconv.ParseUint(bits[48:72], 2, 24)
unkn3, _ := strconv.ParseUint(bits[72:74], 2, 2)
leak, _ := strconv.ParseUint(bits[74:78], 2, 4)
leaknow, _ := strconv.ParseUint(bits[78:80], 2, 2)
var r900 R900
r900.ID = uint32(id)
r900.Unkn1 = uint8(unkn1)
r900.NoUse = uint8(nouse)
r900.BackFlow = uint8(backflow)
r900.Consumption = uint32(consumption)
r900.Unkn3 = uint8(unkn3)
r900.Leak = uint8(leak)
r900.LeakNow = uint8(leaknow)
copy(r900.checksum[:], symbols[16:])
msgs = append(msgs, r900)
}
return
}
type R900 struct {
ID uint32 `xml:",attr"` // 32 bits
Unkn1 uint8 `xml:",attr"` // 8 bits
NoUse uint8 `xml:",attr"` // 6 bits, day bins of no use
BackFlow uint8 `xml:",attr"` // 2 bits, backflow past 35d hi/lo
Consumption uint32 `xml:",attr"` // 24 bits
Unkn3 uint8 `xml:",attr"` // 2 bits
Leak uint8 `xml:",attr"` // 4 bits, day bins of leak
LeakNow uint8 `xml:",attr"` // 2 bits, leak past 24h hi/lo
checksum [5]byte
}
func (r900 R900) MsgType() string {
return "R900"
}
func (r900 R900) MeterID() uint32 {
return r900.ID
}
func (r900 R900) MeterType() uint8 {
return r900.Unkn1
}
func (r900 R900) Checksum() []byte {
return r900.checksum[:]
}
func (r900 R900) String() string {
return fmt.Sprintf("{ID:%10d Unkn1:0x%02X NoUse:%2d BackFlow:%1d Consumption:%8d Unkn3:0x%02X Leak:%2d LeakNow:%1d}",
r900.ID,
r900.Unkn1,
r900.NoUse,
r900.BackFlow,
r900.Consumption,
r900.Unkn3,
r900.Leak,
r900.LeakNow,
)
}
func (r900 R900) Record() (r []string) {
r = append(r, strconv.FormatUint(uint64(r900.ID), 10))
r = append(r, strconv.FormatUint(uint64(r900.Unkn1), 10))
r = append(r, strconv.FormatUint(uint64(r900.NoUse), 10))
r = append(r, strconv.FormatUint(uint64(r900.BackFlow), 10))
r = append(r, strconv.FormatUint(uint64(r900.Consumption), 10))
r = append(r, strconv.FormatUint(uint64(r900.Unkn3), 10))
r = append(r, strconv.FormatUint(uint64(r900.Leak), 10))
r = append(r, strconv.FormatUint(uint64(r900.LeakNow), 10))
return
}

186
recv.go
View file

@ -1,5 +1,5 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
// Copyright (C) 2015 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
@ -20,47 +20,51 @@ import (
"encoding/xml"
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"runtime/pprof"
"strings"
"sync"
"time"
"github.com/bemasher/rtlamr/decode"
"github.com/bemasher/rtlamr/idm"
"github.com/bemasher/rtlamr/parse"
"github.com/bemasher/rtlamr/r900"
"github.com/bemasher/rtlamr/scm"
"github.com/bemasher/rtltcp"
)
const (
CenterFreq = 920299072
)
var rcvr Receiver
type Receiver struct {
rtltcp.SDR
d decode.Decoder
p parse.Parser
p parse.Parser
q parse.Parser
fc parse.FilterChain
}
func (rcvr *Receiver) NewReceiver() {
switch strings.ToLower(*msgType) {
case "scm":
rcvr.d = decode.NewDecoder(scm.NewPacketConfig(*symbolLength), *fastMag)
rcvr.p = scm.NewParser()
rcvr.p = scm.NewParser(*symbolLength, *decimation)
case "idm":
rcvr.d = decode.NewDecoder(idm.NewPacketConfig(*symbolLength), *fastMag)
rcvr.p = idm.NewParser()
rcvr.p = idm.NewParser(*symbolLength, *decimation)
case "scm+idm":
rcvr.p = idm.NewParser(*symbolLength, *decimation)
rcvr.q = scm.NewParser(*symbolLength, *decimation)
case "r900":
rcvr.p = r900.NewParser(*symbolLength, *decimation)
default:
log.Fatalf("Invalid message type: %q\n", *msgType)
}
if !*quiet {
rcvr.d.Cfg.Log()
log.Println("CRC:", rcvr.p)
rcvr.p.Log()
if(rcvr.q != nil) {
rcvr.q.Log()
}
}
// Connect to rtl_tcp server.
@ -86,16 +90,24 @@ func (rcvr *Receiver) NewReceiver() {
sampleRateFlagSet = true
case "gainbyindex", "tunergainmode", "tunergain", "agcmode":
gainFlagSet = true
case "unique":
rcvr.fc.Add(NewUniqueFilter())
case "filterid":
rcvr.fc.Add(meterID)
case "filtertype":
rcvr.fc.Add(meterType)
}
})
// Set some parameters for listening.
if !centerfreqFlagSet {
if centerfreqFlagSet {
rcvr.SetCenterFreq(uint32(rcvr.Flags.CenterFreq))
} else {
rcvr.SetCenterFreq(rcvr.p.Cfg().CenterFreq)
}
if !sampleRateFlagSet {
rcvr.SetSampleRate(uint32(rcvr.d.Cfg.SampleRate))
rcvr.SetSampleRate(uint32(rcvr.p.Cfg().SampleRate))
}
if !gainFlagSet {
rcvr.SetGainMode(true)
@ -105,64 +117,66 @@ func (rcvr *Receiver) NewReceiver() {
}
func (rcvr *Receiver) Run() {
// Setup signal channel for interruption.
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Kill, os.Interrupt)
in, out := io.Pipe()
in2, out2 := io.Pipe()
// Setup time limit channel
tLimit := make(<-chan time.Time, 1)
if *timeLimit != 0 {
tLimit = time.After(*timeLimit)
}
block := make([]byte, rcvr.d.Cfg.BlockSize2)
start := time.Now()
for {
// Exit on interrupt or time limit, otherwise receive.
select {
case <-sigint:
return
case <-tLimit:
fmt.Println("Time Limit Reached:", time.Since(start))
return
default:
// Read new sample block.
_, err := rcvr.Read(block)
go func() {
tcpBlock := make([]byte, 16384)
for {
n, err := rcvr.Read(tcpBlock)
if err != nil {
log.Fatal("Error reading samples: ", err)
return
}
out.Write(tcpBlock[:n])
if(rcvr.q != nil) {
out2.Write(tcpBlock[:n])
}
}
}()
pktFound := false
for _, pkt := range rcvr.d.Decode(block) {
scm, err := rcvr.p.Parse(parse.NewDataFromBytes(pkt))
var wg sync.WaitGroup
start := time.Now()
looper := func(p parse.Parser, fc parse.FilterChain, in io.Reader) {
// Setup signal channel for interruption.
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Kill, os.Interrupt)
// Setup time limit channel
tLimit := make(<-chan time.Time, 1)
if *timeLimit != 0 {
tLimit = time.After(*timeLimit)
}
block := make([]byte, p.Cfg().BlockSize2)
for {
// Exit on interrupt or time limit, otherwise receive.
select {
case <-sigint:
return
case <-tLimit:
fmt.Println("Time Limit Reached:", time.Since(start))
return
default:
// Read new sample block.
_, err := io.ReadFull(in, block)
if err != nil {
// log.Println(err)
continue
log.Fatal("Error reading samples: ", err)
}
if len(meterID) > 0 && !meterID[uint(scm.MeterID())] {
continue
}
pktFound := false
indices := p.Dec().Decode(block)
if len(meterType) > 0 && !meterType[uint(scm.MeterType())] {
continue
}
var msg parse.LogMessage
msg.Time = time.Now()
msg.Offset, _ = sampleFile.Seek(0, os.SEEK_CUR)
msg.Length = rcvr.d.Cfg.BufferLength << 1
msg.Message = scm
if encoder == nil {
// A nil encoder is just plain-text output.
if *sampleFilename == os.DevNull {
fmt.Fprintln(logFile, msg.StringNoOffset())
} else {
fmt.Fprintln(logFile, msg)
for _, pkt := range p.Parse(indices) {
if !fc.Match(pkt) {
continue
}
} else {
var msg parse.LogMessage
msg.Time = time.Now()
msg.Offset, _ = sampleFile.Seek(0, os.SEEK_CUR)
msg.Length = p.Cfg().BufferLength << 1
msg.Message = pkt
err = encoder.Encode(msg)
if err != nil {
log.Fatal("Error encoding message: ", err)
@ -173,27 +187,41 @@ func (rcvr *Receiver) Run() {
if _, ok := encoder.(*xml.Encoder); ok {
fmt.Fprintln(logFile)
}
}
pktFound = true
if *single {
break
}
}
if pktFound {
if *sampleFilename != os.DevNull {
_, err = sampleFile.Write(rcvr.d.IQ)
if err != nil {
log.Fatal("Error writing raw samples to file:", err)
pktFound = true
if *single {
if len(meterID.UintMap) == 0 {
break
} else {
delete(meterID.UintMap, uint(pkt.MeterID()))
}
}
}
if *single {
return
if pktFound {
if *sampleFilename != os.DevNull {
_, err = sampleFile.Write(p.Dec().IQ)
if err != nil {
log.Fatal("Error writing raw samples to file:", err)
}
}
if *single && len(meterID.UintMap) == 0 {
return
}
}
}
}
}
wg.Add(1);
go looper(rcvr.p, rcvr.fc, in);
if(rcvr.q != nil) {
wg.Add(1);
go looper(rcvr.q, rcvr.fc, in2);
}
wg.Wait();
}
func init() {

View file

@ -1,5 +1,5 @@
// RTLAMR - An rtl-sdr receiver for smart meters operating in the 900MHz ISM band.
// Copyright (C) 2014 Douglas Hall
// Copyright (C) 2015 Douglas Hall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
@ -17,7 +17,7 @@
package scm
import (
"errors"
"encoding/binary"
"fmt"
"strconv"
@ -27,69 +27,68 @@ import (
)
func NewPacketConfig(symbolLength int) (cfg decode.PacketConfig) {
cfg.CenterFreq = 912600155
cfg.DataRate = 32768
cfg.SymbolLength = symbolLength
cfg.SymbolLength2 = cfg.SymbolLength << 1
cfg.SampleRate = cfg.DataRate * cfg.SymbolLength
cfg.PreambleSymbols = 21
cfg.PacketSymbols = 96
cfg.PreambleLength = cfg.PreambleSymbols * cfg.SymbolLength2
cfg.PacketLength = cfg.PacketSymbols * cfg.SymbolLength2
cfg.BlockSize = decode.NextPowerOf2(cfg.PreambleLength)
cfg.BlockSize2 = cfg.BlockSize << 1
cfg.BufferLength = cfg.PacketLength + cfg.BlockSize
cfg.Preamble = "111110010101001100000"
return
}
type Parser struct {
decode.Decoder
crc.CRC
}
func NewParser() (p Parser) {
func NewParser(symbolLength, decimation int) (p Parser) {
p.Decoder = decode.NewDecoder(NewPacketConfig(symbolLength), decimation)
p.CRC = crc.NewCRC("BCH", 0, 0x6F63, 0)
return
}
func (p Parser) Parse(data parse.Data) (msg parse.Message, err error) {
var scm SCM
func (p Parser) Dec() decode.Decoder {
return p.Decoder
}
if l := len(data.Bytes); l < 12 {
err = fmt.Errorf("packet too short: %d", l)
return
}
if p.Checksum(data.Bytes[2:12]) != 0 {
err = errors.New("checksum failed")
return
func (p Parser) Cfg() decode.PacketConfig {
return p.Decoder.Cfg
}
func (p Parser) Parse(indices []int) (msgs []parse.Message) {
seen := make(map[string]bool)
for _, pkt := range p.Decoder.Slice(indices) {
s := string(pkt)
if seen[s] {
continue
}
seen[s] = true
data := parse.NewDataFromBytes(pkt)
// If the packet is too short, bail.
if l := len(data.Bytes); l != 12 {
continue
}
// If the checksum fails, bail.
if p.Checksum(data.Bytes[2:12]) != 0 {
continue
}
scm := NewSCM(data)
// If the meter id is 0, bail.
if scm.ID == 0 {
continue
}
msgs = append(msgs, scm)
}
ertid, _ := strconv.ParseUint(data.Bits[21:23]+data.Bits[56:80], 2, 32)
erttype, _ := strconv.ParseUint(data.Bits[26:30], 2, 8)
tamperphy, _ := strconv.ParseUint(data.Bits[24:26], 2, 8)
tamperenc, _ := strconv.ParseUint(data.Bits[30:32], 2, 8)
consumption, _ := strconv.ParseUint(data.Bits[32:56], 2, 32)
checksum, _ := strconv.ParseUint(data.Bits[80:96], 2, 16)
scm.ID = uint32(ertid)
scm.Type = uint8(erttype)
scm.TamperPhy = uint8(tamperphy)
scm.TamperEnc = uint8(tamperenc)
scm.Consumption = uint32(consumption)
scm.Checksum = uint16(checksum)
if scm.ID == 0 {
err = errors.New("invalid ert id")
}
return scm, err
return
}
// Standard Consumption Message
@ -99,7 +98,25 @@ type SCM struct {
TamperPhy uint8 `xml:",attr"`
TamperEnc uint8 `xml:",attr"`
Consumption uint32 `xml:",attr"`
Checksum uint16 `xml:",attr"`
ChecksumVal uint16 `xml:"Checksum,attr"`
}
func NewSCM(data parse.Data) (scm SCM) {
ertid, _ := strconv.ParseUint(data.Bits[21:23]+data.Bits[56:80], 2, 26)
erttype, _ := strconv.ParseUint(data.Bits[26:30], 2, 4)
tamperphy, _ := strconv.ParseUint(data.Bits[24:26], 2, 2)
tamperenc, _ := strconv.ParseUint(data.Bits[30:32], 2, 2)
consumption, _ := strconv.ParseUint(data.Bits[32:56], 2, 24)
checksum, _ := strconv.ParseUint(data.Bits[80:96], 2, 16)
scm.ID = uint32(ertid)
scm.Type = uint8(erttype)
scm.TamperPhy = uint8(tamperphy)
scm.TamperEnc = uint8(tamperenc)
scm.Consumption = uint32(consumption)
scm.ChecksumVal = uint16(checksum)
return
}
func (scm SCM) MsgType() string {
@ -114,9 +131,15 @@ func (scm SCM) MeterType() uint8 {
return scm.Type
}
func (scm SCM) Checksum() []byte {
checksum := make([]byte, 2)
binary.BigEndian.PutUint16(checksum, scm.ChecksumVal)
return checksum
}
func (scm SCM) String() string {
return fmt.Sprintf("{ID:%8d Type:%2d Tamper:{Phy:%02X Enc:%02X} Consumption:%8d CRC:0x%04X}",
scm.ID, scm.Type, scm.TamperPhy, scm.TamperEnc, scm.Consumption, scm.Checksum,
scm.ID, scm.Type, scm.TamperPhy, scm.TamperEnc, scm.Consumption, scm.ChecksumVal,
)
}
@ -126,7 +149,7 @@ func (scm SCM) Record() (r []string) {
r = append(r, "0x"+strconv.FormatUint(uint64(scm.TamperPhy), 16))
r = append(r, "0x"+strconv.FormatUint(uint64(scm.TamperEnc), 16))
r = append(r, strconv.FormatUint(uint64(scm.Consumption), 10))
r = append(r, "0x"+strconv.FormatUint(uint64(scm.Checksum), 16))
r = append(r, "0x"+strconv.FormatUint(uint64(scm.ChecksumVal), 16))
return
}