Compare commits
66 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a2c1361b | ||
|
|
40470d4488 | ||
|
|
1e03a514f8 | ||
|
|
c45fdef076 | ||
|
|
1364a85f48 | ||
|
|
5555d22393 | ||
|
|
d453c86711 | ||
|
|
42b1b3d07f | ||
|
|
6b376a3c26 | ||
|
|
f8797da412 | ||
|
|
938607e052 | ||
|
|
7314e7a38b | ||
|
|
548dac1a11 | ||
|
|
767fea762f | ||
|
|
42ceb2cce7 | ||
|
|
30ee13eec9 | ||
|
|
de00b1d77c | ||
|
|
c4340c3263 | ||
|
|
34b9eb6bb3 | ||
|
|
dbdbcae476 | ||
|
|
b9240f925b | ||
|
|
ae0364d9a0 | ||
|
|
07607af197 | ||
|
|
7f05214d2b | ||
|
|
8f3cb5205e | ||
|
|
04d73e42a7 | ||
|
|
de4bffe446 | ||
|
|
953f371bb6 | ||
|
|
22906a5949 | ||
|
|
3bd0a4e558 | ||
|
|
efea18916c | ||
|
|
3a29e9b7ff | ||
|
|
586359c74a | ||
|
|
1373ceb5d7 | ||
|
|
588ef5220c | ||
|
|
f7828796bf | ||
|
|
c2855e7ded | ||
|
|
f3d51f2331 | ||
|
|
3249c73543 | ||
|
|
e1d9b04354 | ||
|
|
233608205f | ||
|
|
9be534c2d0 | ||
|
|
d4afba8974 | ||
|
|
15e1d1dc69 | ||
|
|
169c5300c6 | ||
|
|
0f08d69aa1 | ||
|
|
eb2b055444 | ||
|
|
a88bbf1bc8 | ||
|
|
57e1be9ab6 | ||
|
|
6f3d973f18 | ||
|
|
8a570305da | ||
|
|
19dadbb681 | ||
|
|
8865d2e83f | ||
|
|
979aa93184 | ||
|
|
0c47867cb0 | ||
|
|
c9c1d9932b | ||
|
|
3c661614b6 | ||
|
|
dbb422783b | ||
|
|
86a2abf632 | ||
|
|
11d5802d4b | ||
|
|
03369d1499 | ||
|
|
3bcb52b572 | ||
|
|
83005a5c92 | ||
|
|
fadf9aa8d9 | ||
|
|
b326ea960f | ||
|
|
2d12445897 |
15 changed files with 1163 additions and 490 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,2 +1,7 @@
|
|||
data
|
||||
*.exe
|
||||
*.exe
|
||||
scm/Makefile
|
||||
release.7z
|
||||
*.diff
|
||||
desktop.ini
|
||||
*~
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
sudo: false
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
60
README.md
60
README.md
|
|
@ -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.
|
||||
|
||||
[](https://travis-ci.org/bemasher/rtlamr)
|
||||
[](http://www.gnu.org/licenses/agpl-3.0.html)
|
||||
[](https://travis-ci.org/bemasher/rtlamr)
|
||||
[](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.
|
||||
|
|
|
|||
281
decode/decode.go
281
decode/decode.go
|
|
@ -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))))
|
||||
}
|
||||
|
|
|
|||
82
flags.go
82
flags.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
145
idm/idm.go
145
idm/idm.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
39
mag_test.go
39
mag_test.go
|
|
@ -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
35
meters.csv
Normal 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
|
||||
|
36
meters.md
36
meters.md
|
|
@ -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)
|
||||
176
parse/parse.go
176
parse/parse.go
|
|
@ -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
27
r900/gf/LICENSE
Normal 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
172
r900/gf/gf.go
Normal 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
285
r900/r900.go
Normal 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
186
recv.go
|
|
@ -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() {
|
||||
|
|
|
|||
119
scm/scm.go
119
scm/scm.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue