Strip configuration. Decouple packet configuration and decoding.
This commit is contained in:
parent
33e0ed66bb
commit
fb778569be
10 changed files with 367 additions and 768 deletions
49
bch/bch.go
49
bch/bch.go
|
|
@ -1,49 +0,0 @@
|
|||
// Implements BCH error correction and detection.
|
||||
package bch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// BCH Error Correction
|
||||
type BCH struct {
|
||||
GenPoly uint
|
||||
PolyLen byte
|
||||
}
|
||||
|
||||
// Given a generator polynomial, calculate the polynomial length.
|
||||
func NewBCH(poly uint) (bch BCH) {
|
||||
bch.GenPoly = poly
|
||||
|
||||
p := bch.GenPoly
|
||||
for ; bch.PolyLen < 32 && p > 0; bch.PolyLen, p = bch.PolyLen+1, p>>1 {
|
||||
}
|
||||
bch.PolyLen--
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bch BCH) String() string {
|
||||
return fmt.Sprintf("{GenPoly:%X PolyLen:%d}", bch.GenPoly, bch.PolyLen)
|
||||
}
|
||||
|
||||
// Syndrome calculation implemented using LSFR (linear feedback shift
|
||||
// register). Parameter bits is a string of bits (0, 1).
|
||||
func (bch BCH) Encode(bits string) (checksum uint) {
|
||||
// For each byte of data.
|
||||
for idx := range bits {
|
||||
// Rotate register and shift in bit.
|
||||
checksum <<= 1
|
||||
if bits[idx] == '1' {
|
||||
checksum |= 1
|
||||
}
|
||||
// If MSB of register is non-zero XOR with generator polynomial.
|
||||
if checksum>>bch.PolyLen != 0 {
|
||||
checksum ^= bch.GenPoly
|
||||
}
|
||||
}
|
||||
|
||||
// Mask to valid length
|
||||
checksum &= (1 << bch.PolyLen) - 1
|
||||
return
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package bch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
const (
|
||||
GenPoly = 0x16F63
|
||||
)
|
||||
|
||||
var bch = NewBCH(GenPoly)
|
||||
|
||||
func TestNOP(t *testing.T) {
|
||||
checksum := bch.Encode(strings.Repeat("0", 80))
|
||||
if checksum != 0 {
|
||||
t.Fatalf("Expected: %d Got: %d\n", 0, checksum)
|
||||
}
|
||||
}
|
||||
|
||||
type BitString string
|
||||
|
||||
// Generate a random 64-bit bitstring and pad to 80 bits with zeros.
|
||||
func (bs BitString) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
var bits string
|
||||
for i := 0; i < 64; i++ {
|
||||
if rand.NormFloat64() > 0.5 {
|
||||
bits += "1"
|
||||
} else {
|
||||
bits += "0"
|
||||
}
|
||||
}
|
||||
|
||||
bits += strings.Repeat("0", 16)
|
||||
|
||||
return reflect.ValueOf(BitString(bits))
|
||||
}
|
||||
|
||||
// Encode a random bitstring with checksum of 0, replace checksum with
|
||||
// calculated value and recalculate, result should be zero.
|
||||
func TestIdentity(t *testing.T) {
|
||||
err := quick.Check(func(bs BitString) bool {
|
||||
bits := string(bs)
|
||||
|
||||
checksum := bch.Encode(string(bits))
|
||||
bits = bits[:64] + fmt.Sprintf("%016b", checksum)
|
||||
|
||||
checksum = bch.Encode(bits)
|
||||
|
||||
return checksum == 0
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Error testing identity:", err)
|
||||
}
|
||||
}
|
||||
342
config.go
342
config.go
|
|
@ -1,342 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bemasher/rtlamr/csv"
|
||||
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
logFilename string
|
||||
sampleFilename string
|
||||
format string
|
||||
|
||||
ServerAddr *net.TCPAddr
|
||||
TimeLimit time.Duration
|
||||
|
||||
MeterID uint
|
||||
MeterType uint
|
||||
|
||||
SymbolLength int
|
||||
|
||||
BlockSize uint
|
||||
SampleRate uint
|
||||
|
||||
PreambleLength uint
|
||||
PacketLength uint
|
||||
|
||||
Log *log.Logger
|
||||
LogFile *os.File
|
||||
|
||||
GobUnsafe bool
|
||||
Encoder Encoder
|
||||
|
||||
SampleFile *os.File
|
||||
|
||||
Quiet bool
|
||||
Single bool
|
||||
ShortHelp bool
|
||||
LongHelp bool
|
||||
}
|
||||
|
||||
func (c *Config) Parse() (err error) {
|
||||
longHelp := map[string]string{
|
||||
// short help
|
||||
"h": `Print short help.`,
|
||||
|
||||
// long help
|
||||
"help": `Print long help.`,
|
||||
|
||||
// duration
|
||||
"duration": `Sets time to receive for, 0 for infinite. Defaults to infinite.
|
||||
If the time limit expires during processing of a block (which is quite
|
||||
likely) it will exit on the next pass through the receive loop. Exiting
|
||||
after an expired duration will print the total runtime to the log file.`,
|
||||
|
||||
// filterid
|
||||
"filterid": `Sets a meter id to filter by, 0 for no filtering. Defaults to no filtering.
|
||||
Any received messages not matching the given id will be silently ignored.`,
|
||||
|
||||
// filtertype
|
||||
"filtertype": `Sets an ert type to filter by, 0 for no filtering. Defaults to no filtering.
|
||||
Any received messages not matching the given type will be silently ignored.`,
|
||||
|
||||
// format
|
||||
"format": `Sets the log output format. Defaults to plain.
|
||||
Plain text is formatted using the following format string:
|
||||
|
||||
{Time:%s Offset:%d Length:%d SCM:{ID:%8d Type:%2d Tamper:%+v Consumption:%8d Checksum:0x%04X}}
|
||||
|
||||
No fields are omitted for csv, json, xml or gob output. Plain text conditionally
|
||||
omits offset and length fields if not dumping samples to file via -samplefile.
|
||||
|
||||
For json and xml output each line is an element, there is no root node.`,
|
||||
"gobunsafe": `Must be true to allow writing gob encoded output to stdout. Defaults to false.
|
||||
Doing so would normally break a terminal, so we disable it unless
|
||||
explicitly enabled.`,
|
||||
|
||||
// logfile
|
||||
"logfile": `Sets file to dump log messages to. Defaults to os.DevNull and prints to stderr.
|
||||
Log messages have the following structure:
|
||||
|
||||
type Message struct {
|
||||
Time time.Time
|
||||
Offset int64
|
||||
Length int
|
||||
SCM SCM
|
||||
}
|
||||
|
||||
type SCM struct {
|
||||
ID uint32
|
||||
Type uint8
|
||||
Tamper Tamper
|
||||
Consumption uint32
|
||||
Checksum uint16
|
||||
}
|
||||
|
||||
type Tamper struct {
|
||||
Phy uint8
|
||||
Enc uint8
|
||||
}
|
||||
|
||||
Messages are encoded one per line for all encoding formats except gob.`,
|
||||
|
||||
// quiet
|
||||
"quiet": `Omits state information logged on startup. Defaults to false.
|
||||
Below is sample output:
|
||||
|
||||
2014/07/01 02:45:42.416406 Server: 127.0.0.1:1234
|
||||
2014/07/01 02:45:42.417406 BlockSize: 4096
|
||||
2014/07/01 02:45:42.417406 SampleRate: 2392064
|
||||
2014/07/01 02:45:42.417406 DataRate: 32768
|
||||
2014/07/01 02:45:42.417406 SymbolLength: 73
|
||||
2014/07/01 02:45:42.417406 PreambleSymbols: 42
|
||||
2014/07/01 02:45:42.417406 PreambleLength: 3066
|
||||
2014/07/01 02:45:42.417406 PacketSymbols: 192
|
||||
2014/07/01 02:45:42.417406 PacketLength: 14016
|
||||
2014/07/01 02:45:42.417406 CenterFreq: 920299072
|
||||
2014/07/01 02:45:42.417406 TimeLimit: 0
|
||||
2014/07/01 02:45:42.417406 Format: plain
|
||||
2014/07/01 02:45:42.417406 LogFile: /dev/stdout
|
||||
2014/07/01 02:45:42.417406 SampleFile: NUL
|
||||
2014/07/01 02:45:43.050442 BCH: {GenPoly:16F63 PolyLen:16}
|
||||
2014/07/01 02:45:43.050442 GainCount: 29
|
||||
2014/07/01 02:45:43.051442 Running...`,
|
||||
|
||||
// samplefile
|
||||
"samplefile": `Sets file to dump samples for decoded packets to. Defaults to os.DevNull.
|
||||
Output file format are interleaved in-phase and quadrature samples. Each
|
||||
are unsigned bytes. These are unmodified output from the dongle. This flag
|
||||
enables offset and length fields in plain text log messages. Only samples
|
||||
for correctly received messages are dumped.`,
|
||||
|
||||
// single
|
||||
"single": `Provides one shot execution. Defaults to false.
|
||||
Receiver listens until exactly one message is received before exiting.`,
|
||||
|
||||
// symbollength
|
||||
"symbollength": `Sets the desired symbol length. Defaults to 73.
|
||||
Sample rate is determined from this value as follows:
|
||||
|
||||
DataRate = 32768
|
||||
SampleRate = SymbolLength * DataRate
|
||||
|
||||
The symbol length also determines the size of the convolution used for the preamble search:
|
||||
|
||||
PreambleSymbols = 42
|
||||
BlockSize = 1 << uint(math.Ceil(math.Log2(float64(PreambleSymbols * SymbolLength))))
|
||||
|
||||
Valid symbol lengths are given below (symbol length: bandwidth):
|
||||
|
||||
BlockSize: 512 (fast)
|
||||
7: 229.376 kHz, 8: 262.144 kHz, 9: 294.912 kHz
|
||||
|
||||
BlockSize: 2048 (medium)
|
||||
28: 917.504 kHz, 29: 950.272 kHz, 30: 983.040 kHz
|
||||
31: 1.015808 MHz, 32: 1.048576 MHz, 33: 1.081344 MHz,
|
||||
34: 1.114112 MHz, 35: 1.146880 MHz, 36: 1.179648 MHz,
|
||||
37: 1.212416 MHz, 38: 1.245184 MHz, 39: 1.277952 MHz,
|
||||
40: 1.310720 MHz, 41: 1.343488 MHz, 42: 1.376256 MHz,
|
||||
43: 1.409024 MHz, 44: 1.441792 MHz, 45: 1.474560 MHz,
|
||||
46: 1.507328 MHz, 47: 1.540096 MHz, 48: 1.572864 MHz
|
||||
|
||||
BlockSize: 4096 (slow)
|
||||
49: 1.605632 MHz, 50: 1.638400 MHz, 51: 1.671168 MHz,
|
||||
52: 1.703936 MHz, 53: 1.736704 MHz, 54: 1.769472 MHz,
|
||||
55: 1.802240 MHz, 56: 1.835008 MHz, 57: 1.867776 MHz,
|
||||
58: 1.900544 MHz, 59: 1.933312 MHz, 60: 1.966080 MHz,
|
||||
61: 1.998848 MHz, 62: 2.031616 MHz, 63: 2.064384 MHz,
|
||||
64: 2.097152 MHz, 65: 2.129920 MHz, 66: 2.162688 MHz,
|
||||
67: 2.195456 MHz, 68: 2.228224 MHz, 69: 2.260992 MHz,
|
||||
70: 2.293760 MHz, 71: 2.326528 MHz, 72: 2.359296 MHz,
|
||||
73: 2.392064 MHz
|
||||
|
||||
BlockSize: 4096 (slow, untested)
|
||||
74: 2.424832 MHz, 75: 2.457600 MHz, 76: 2.490368 MHz,
|
||||
77: 2.523136 MHz, 78: 2.555904 MHz, 79: 2.588672 MHz,
|
||||
80: 2.621440 MHz, 81: 2.654208 MHz, 82: 2.686976 MHz,
|
||||
83: 2.719744 MHz, 84: 2.752512 MHz, 85: 2.785280 MHz,
|
||||
86: 2.818048 MHz, 87: 2.850816 MHz, 88: 2.883584 MHz,
|
||||
89: 2.916352 MHz, 90: 2.949120 MHz, 91: 2.981888 MHz,
|
||||
92: 3.014656 MHz, 93: 3.047424 MHz, 94: 3.080192 MHz,
|
||||
95: 3.112960 MHz, 96: 3.145728 MHz, 97: 3.178496 MHz`,
|
||||
}
|
||||
|
||||
mainFlags := flag.NewFlagSet("main", flag.ContinueOnError)
|
||||
|
||||
mainFlags.StringVar(&c.logFilename, "logfile", "/dev/stdout", "log statement dump file")
|
||||
mainFlags.StringVar(&c.sampleFilename, "samplefile", os.DevNull, "raw signal dump file")
|
||||
|
||||
mainFlags.IntVar(&c.SymbolLength, "symbollength", 73, `symbol length in samples, see -help for valid lengths`)
|
||||
|
||||
mainFlags.DurationVar(&c.TimeLimit, "duration", 0, "time to run for, 0 for infinite")
|
||||
mainFlags.UintVar(&c.MeterID, "filterid", 0, "display only messages matching given id")
|
||||
mainFlags.UintVar(&c.MeterType, "filtertype", 0, "display only messages matching given type")
|
||||
mainFlags.StringVar(&c.format, "format", "plain", "format to write log messages in: plain, csv, json, xml or gob")
|
||||
mainFlags.BoolVar(&c.GobUnsafe, "gobunsafe", false, "allow gob output to stdout")
|
||||
mainFlags.BoolVar(&c.Quiet, "quiet", false, "suppress printing state information at startup")
|
||||
mainFlags.BoolVar(&c.Single, "single", false, "one shot execution")
|
||||
mainFlags.BoolVar(&c.ShortHelp, "h", false, "print short help")
|
||||
mainFlags.BoolVar(&c.LongHelp, "help", false, "print long help")
|
||||
|
||||
rcvr.Flags.BoolVar(&c.ShortHelp, "h", false, "print short help")
|
||||
rcvr.Flags.BoolVar(&c.LongHelp, "help", false, "print long help")
|
||||
|
||||
// Override default center frequency.
|
||||
centerfreqFlag := rcvr.Flags.Lookup("centerfreq")
|
||||
centerfreqFlag.DefValue = fmt.Sprintf("%d", CenterFreq)
|
||||
centerfreqFlag.Value.Set(fmt.Sprintf("%d", CenterFreq))
|
||||
|
||||
mainFlags.Usage = func() {}
|
||||
mainFlags.SetOutput(ioutil.Discard)
|
||||
rcvr.Flags.SetOutput(ioutil.Discard)
|
||||
|
||||
mainFlags.Parse(os.Args[1:])
|
||||
rcvr.Flags.Parse(os.Args[1:])
|
||||
|
||||
mainFlags.SetOutput(os.Stderr)
|
||||
rcvr.Flags.SetOutput(os.Stderr)
|
||||
|
||||
mainFlags.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
mainFlags.PrintDefaults()
|
||||
fmt.Println()
|
||||
fmt.Println("rtltcp specific:")
|
||||
rcvr.Flags.PrintDefaults()
|
||||
}
|
||||
|
||||
if c.ShortHelp {
|
||||
mainFlags.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if c.LongHelp {
|
||||
mainFlags.VisitAll(func(f *flag.Flag) {
|
||||
if help, exists := longHelp[f.Name]; exists {
|
||||
f.Usage = help + "\n"
|
||||
}
|
||||
})
|
||||
|
||||
mainFlags.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Open or create the log file.
|
||||
if c.logFilename == "/dev/stdout" {
|
||||
c.LogFile = os.Stdout
|
||||
} else {
|
||||
c.LogFile, err = os.Create(c.logFilename)
|
||||
}
|
||||
|
||||
// Create a new logger with the log file as output.
|
||||
c.Log = log.New(c.LogFile, "", log.Ldate|log.Lmicroseconds)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create the sample file.
|
||||
c.SampleFile, err = os.Create(c.sampleFilename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
validSymbolLengths := map[int]bool{
|
||||
7: true, 8: true, 9: true, 28: true, 29: true, 30: true, 31: true,
|
||||
32: true, 33: true, 34: true, 35: true, 36: true, 37: true, 38: true,
|
||||
39: true, 40: true, 41: true, 42: true, 43: true, 44: true, 45: true,
|
||||
46: true, 47: true, 48: true, 49: true, 50: true, 51: true, 52: true,
|
||||
53: true, 54: true, 55: true, 56: true, 57: true, 58: true, 59: true,
|
||||
60: true, 61: true, 62: true, 63: true, 64: true, 65: true, 66: true,
|
||||
67: true, 68: true, 69: true, 70: true, 71: true, 72: true, 73: true,
|
||||
74: true, 75: true, 76: true, 77: true, 78: true, 79: true, 80: true,
|
||||
81: true, 82: true, 83: true, 84: true, 85: true, 86: true, 87: true,
|
||||
88: true, 89: true, 90: true, 91: true, 92: true, 93: true, 94: true,
|
||||
95: true, 96: true, 97: true,
|
||||
}
|
||||
|
||||
if !validSymbolLengths[c.SymbolLength] {
|
||||
log.Printf("warning: invalid symbol length, probably won't receive anything")
|
||||
}
|
||||
|
||||
c.SampleRate = DataRate * uint(c.SymbolLength)
|
||||
|
||||
c.PreambleLength = PreambleSymbols * uint(c.SymbolLength)
|
||||
c.PacketLength = PacketSymbols * uint(c.SymbolLength)
|
||||
|
||||
// Power of two sized DFT requires BlockSize to also be power of two.
|
||||
// BlockSize must be greater than the preamble length, so calculate next
|
||||
// largest power of two from preamble length.
|
||||
c.BlockSize = NextPowerOf2(c.PreambleLength)
|
||||
|
||||
// Create encoder for specified logging format.
|
||||
switch strings.ToLower(c.format) {
|
||||
case "plain":
|
||||
break
|
||||
case "csv":
|
||||
c.Encoder = csv.NewEncoder(c.LogFile)
|
||||
case "json":
|
||||
c.Encoder = json.NewEncoder(c.LogFile)
|
||||
case "xml":
|
||||
c.Encoder = xml.NewEncoder(c.LogFile)
|
||||
case "gob":
|
||||
c.Encoder = gob.NewEncoder(c.LogFile)
|
||||
|
||||
// Don't let the user output gob to stdout unless they really want to.
|
||||
if !c.GobUnsafe && c.logFilename == "/dev/stdout" {
|
||||
fmt.Println("Gob encoded messages are not stdout safe, specify logfile or use gobunsafe flag.")
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
// We didn't get a valid encoder, exit and say so.
|
||||
log.Fatal("Invalid log format:", c.format)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c Config) Close() {
|
||||
c.LogFile.Close()
|
||||
c.SampleFile.Close()
|
||||
}
|
||||
|
||||
// JSON, XML and GOB all implement this interface so we can simplify log
|
||||
// output formatting.
|
||||
type Encoder interface {
|
||||
Encode(interface{}) error
|
||||
}
|
||||
|
||||
func NextPowerOf2(v uint) uint {
|
||||
return 1 << uint(math.Ceil(math.Log2(float64(v))))
|
||||
}
|
||||
55
crc/crc.go
Normal file
55
crc/crc.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package crc
|
||||
|
||||
import "fmt"
|
||||
|
||||
type CRC struct {
|
||||
Name string
|
||||
Init uint16
|
||||
Poly uint16
|
||||
Residue uint16
|
||||
|
||||
tbl Table
|
||||
}
|
||||
|
||||
func NewCRC(name string, init, poly, residue uint16) (crc CRC) {
|
||||
crc.Name = name
|
||||
crc.Init = init
|
||||
crc.Poly = poly
|
||||
crc.Residue = residue
|
||||
crc.tbl = NewTable(crc.Poly)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (crc CRC) String() string {
|
||||
return fmt.Sprintf("{Name:%s Init:0x%04X Poly:0x%04X Residue:0x%04X}", crc.Name, crc.Init, crc.Poly, crc.Residue)
|
||||
}
|
||||
|
||||
func (crc CRC) Checksum(data []byte) uint16 {
|
||||
return Checksum(crc.Init, data, crc.tbl)
|
||||
}
|
||||
|
||||
type Table [256]uint16
|
||||
|
||||
func NewTable(poly uint16) (table Table) {
|
||||
for tIdx := range table {
|
||||
crc := uint16(tIdx) << 8
|
||||
for bIdx := 0; bIdx < 8; bIdx++ {
|
||||
if crc&0x8000 != 0 {
|
||||
crc = crc<<1 ^ poly
|
||||
} else {
|
||||
crc = crc << 1
|
||||
}
|
||||
}
|
||||
table[tIdx] = crc
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func Checksum(init uint16, data []byte, table Table) (crc uint16) {
|
||||
crc = init
|
||||
for _, v := range data {
|
||||
crc = crc<<8 ^ table[crc>>8^uint16(v)]
|
||||
}
|
||||
return
|
||||
}
|
||||
45
crc/crc_test.go
Normal file
45
crc/crc_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package crc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
crand "crypto/rand"
|
||||
mrand "math/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
Trials = 512
|
||||
)
|
||||
|
||||
var crcs = []CRC{
|
||||
{"IBM", 0, 0x8005, 0, Table{}},
|
||||
{"BCH", 0, 0x6F63, 0, Table{}},
|
||||
{"CCITT", 0xFFFF, 0x1021, 0x1D0F, Table{}},
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
for _, crc := range crcs {
|
||||
t.Logf("%+v\n", crc)
|
||||
crc.tbl = NewTable(crc.Poly)
|
||||
for trial := 0; trial < Trials; trial++ {
|
||||
length := mrand.Intn(32)&0xFE + 8
|
||||
|
||||
buf := make([]byte, length)
|
||||
crand.Read(buf[:length-2])
|
||||
|
||||
intermediate := crc.Checksum(buf[:length-2])
|
||||
binary.BigEndian.PutUint16(buf[length-2:], intermediate)
|
||||
|
||||
check := crc.Checksum(buf)
|
||||
if check != 0 {
|
||||
t.Fatalf("%s failed: %02X %04X %04X\n", crc.Name, buf, intermediate, check)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
mrand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
36
csv/csv.go
36
csv/csv.go
|
|
@ -1,36 +0,0 @@
|
|||
package csv
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Produces a list of fields making up a record.
|
||||
type Recorder interface {
|
||||
Record() []string
|
||||
}
|
||||
|
||||
// An Encoder writes CSV records to an output stream.
|
||||
type Encoder struct {
|
||||
w *csv.Writer
|
||||
}
|
||||
|
||||
// NewEncoder returns a new encoder that writes to w.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return &Encoder{w: csv.NewWriter(w)}
|
||||
}
|
||||
|
||||
// Encode writes a CSV record representing v to the stream followed by a
|
||||
// newline character. Value given must implement the Recorder interface.
|
||||
func (enc *Encoder) Encode(v interface{}) (err error) {
|
||||
record, ok := v.(Recorder)
|
||||
if !ok {
|
||||
return errors.New("value does not satisfy Recorder interface")
|
||||
}
|
||||
|
||||
err = enc.w.Write(record.Record())
|
||||
enc.w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
58
dsp.go
Normal file
58
dsp.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import "strconv"
|
||||
|
||||
// A lookup table for calculating magnitude of an interleaved unsigned byte
|
||||
// stream.
|
||||
type MagLUT []float64
|
||||
|
||||
// Shifts sample by 127.4 (most common DC offset value of rtl-sdr dongles) and
|
||||
// stores square.
|
||||
func NewMagLUT() (lut MagLUT) {
|
||||
lut = make([]float64, 0x100)
|
||||
for idx := range lut {
|
||||
lut[idx] = 127.4 - float64(idx)
|
||||
lut[idx] *= lut[idx]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Matched filter implemented as integrate and dump. Output array is equal to
|
||||
// the number of manchester coded symbols per packet.
|
||||
func MatchedFilter(input []float64, bits int) (output []float64) {
|
||||
output = make([]float64, bits)
|
||||
|
||||
fIdx := 0
|
||||
for idx := 0; fIdx < bits; idx += rcvr.pktConfig.SymbolLength * 2 {
|
||||
offset := idx + rcvr.pktConfig.SymbolLength
|
||||
|
||||
for i := 0; i < rcvr.pktConfig.SymbolLength; i++ {
|
||||
output[fIdx] += input[idx+i] - input[offset+i]
|
||||
}
|
||||
fIdx++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BitSlice(input []float64) (data Data) {
|
||||
for _, v := range input {
|
||||
if v > 0.0 {
|
||||
data.Bits += "1"
|
||||
} else {
|
||||
data.Bits += "0"
|
||||
}
|
||||
}
|
||||
|
||||
if len(data.Bits)%8 != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data.Bytes = make([]byte, len(data.Bits)>>3)
|
||||
for byteIdx := range data.Bytes {
|
||||
bitIdx := byteIdx << 3
|
||||
b, _ := strconv.ParseUint(data.Bits[bitIdx:bitIdx+8], 2, 8)
|
||||
data.Bytes[byteIdx] = byte(b)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
45
pkt.go
Normal file
45
pkt.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type PacketDecoder interface {
|
||||
PacketConfig() PacketConfig
|
||||
SearchPreamble([]float64) int
|
||||
Decode(Data) (fmt.Stringer, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type PacketConfig struct {
|
||||
SymbolLength int
|
||||
BlockSize uint
|
||||
SampleRate uint
|
||||
|
||||
PreambleSymbols uint
|
||||
PacketSymbols uint
|
||||
|
||||
PreambleLength uint
|
||||
PacketLength uint
|
||||
|
||||
PreambleBits string
|
||||
}
|
||||
|
||||
func (pc PacketConfig) String() string {
|
||||
return fmt.Sprintf("{SymbolLength:%d BlockSize:%d SampleRate:%d PreambleSymbols:%d "+
|
||||
"PacketSymbols:%d PreambleLength:%d PacketLength:%d PreambleBits:%q}",
|
||||
pc.SymbolLength,
|
||||
pc.BlockSize,
|
||||
pc.SampleRate,
|
||||
pc.PreambleSymbols,
|
||||
pc.PacketSymbols,
|
||||
pc.PreambleLength,
|
||||
pc.PacketLength,
|
||||
pc.PreambleBits,
|
||||
)
|
||||
}
|
||||
|
||||
func NextPowerOf2(v uint) uint {
|
||||
return 1 << uint(math.Ceil(math.Log2(float64(v))))
|
||||
}
|
||||
335
recv.go
335
recv.go
|
|
@ -17,77 +17,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bemasher/rtlamr/bch"
|
||||
"github.com/bemasher/rtlamr/preamble"
|
||||
"github.com/bemasher/rtltcp"
|
||||
)
|
||||
|
||||
const (
|
||||
CenterFreq = 920299072
|
||||
DataRate = 32768
|
||||
|
||||
PreambleSymbols = 42
|
||||
Preamble = 0x1F2A60
|
||||
PreambleBits = "111110010101001100000"
|
||||
|
||||
PacketSymbols = 192
|
||||
|
||||
GenPoly = 0x16F63
|
||||
|
||||
RestrictLocal = false
|
||||
TimeFormat = "2006-01-02T15:04:05.000"
|
||||
)
|
||||
|
||||
var (
|
||||
rcvr Receiver
|
||||
config Config
|
||||
rcvr Receiver
|
||||
)
|
||||
|
||||
type Receiver struct {
|
||||
rtltcp.SDR
|
||||
|
||||
pd preamble.PreambleDetector
|
||||
bch bch.BCH
|
||||
lut MagLUT
|
||||
|
||||
pktDecoder PacketDecoder
|
||||
pktConfig PacketConfig
|
||||
}
|
||||
|
||||
func (rcvr *Receiver) Init() {
|
||||
// Plan the preamble detector.
|
||||
rcvr.pd = preamble.NewPreambleDetector(uint(config.BlockSize<<1), config.SymbolLength, PreambleBits)
|
||||
func (rcvr *Receiver) NewReceiver(pktDecoder PacketDecoder) {
|
||||
rcvr.RegisterFlags()
|
||||
flag.Parse()
|
||||
rcvr.HandleFlags()
|
||||
|
||||
// Create a new BCH for error detection.
|
||||
rcvr.bch = bch.NewBCH(GenPoly)
|
||||
if !config.Quiet {
|
||||
config.Log.Printf("BCH: %+v\n", rcvr.bch)
|
||||
}
|
||||
rcvr.pktDecoder = pktDecoder
|
||||
rcvr.pktConfig = pktDecoder.PacketConfig()
|
||||
|
||||
rcvr.lut = NewMagLUT()
|
||||
|
||||
// Connect to rtl_tcp server.
|
||||
if err := rcvr.Connect(nil); err != nil {
|
||||
config.Log.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Tell the user how many gain settings were reported by rtl_tcp.
|
||||
if !config.Quiet {
|
||||
config.Log.Println("GainCount:", rcvr.SDR.Info.GainCount)
|
||||
}
|
||||
|
||||
rcvr.HandleFlags()
|
||||
log.Println("GainCount:", rcvr.SDR.Info.GainCount)
|
||||
|
||||
// Set some parameters for listening.
|
||||
rcvr.SetCenterFreq(uint32(rcvr.Flags.CenterFreq))
|
||||
rcvr.SetSampleRate(uint32(config.SampleRate))
|
||||
rcvr.SetCenterFreq(CenterFreq)
|
||||
rcvr.SetSampleRate(uint32(rcvr.pktConfig.SampleRate))
|
||||
rcvr.SetGainMode(true)
|
||||
|
||||
return
|
||||
|
|
@ -96,301 +74,96 @@ func (rcvr *Receiver) Init() {
|
|||
// Clean up rtl_tcp connection and destroy preamble detection plan.
|
||||
func (rcvr *Receiver) Close() {
|
||||
rcvr.SDR.Close()
|
||||
rcvr.pd.Close()
|
||||
rcvr.pktDecoder.Close()
|
||||
}
|
||||
|
||||
func (rcvr *Receiver) Run() {
|
||||
cfg := rcvr.pktConfig
|
||||
|
||||
// Setup signal channel for interruption.
|
||||
sigint := make(chan os.Signal, 1)
|
||||
signal.Notify(sigint, os.Kill, os.Interrupt)
|
||||
|
||||
// Allocate sample and demodulated signal buffers.
|
||||
raw := make([]byte, (config.PacketLength+config.BlockSize)<<1)
|
||||
amBuf := make([]float64, config.PacketLength+config.BlockSize)
|
||||
raw := make([]byte, (cfg.PacketLength+cfg.BlockSize)<<1)
|
||||
amBuf := make([]float64, cfg.PacketLength+cfg.BlockSize)
|
||||
|
||||
// Setup time limit channel
|
||||
tLimit := make(<-chan time.Time, 1)
|
||||
if config.TimeLimit != 0 {
|
||||
tLimit = time.After(config.TimeLimit)
|
||||
}
|
||||
|
||||
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:
|
||||
copy(raw, raw[config.BlockSize<<1:])
|
||||
copy(amBuf, amBuf[config.BlockSize:])
|
||||
copy(raw, raw[cfg.BlockSize<<1:])
|
||||
copy(amBuf, amBuf[cfg.BlockSize:])
|
||||
|
||||
// Read new sample block.
|
||||
_, err := rcvr.Read(raw[config.PacketLength<<1:])
|
||||
_, err := rcvr.Read(raw[cfg.PacketLength<<1:])
|
||||
if err != nil {
|
||||
config.Log.Fatal("Error reading samples: ", err)
|
||||
log.Fatal("Error reading samples: ", err)
|
||||
}
|
||||
|
||||
// AM Demodulate
|
||||
block := amBuf[config.PacketLength:]
|
||||
rawBlock := raw[config.PacketLength<<1:]
|
||||
block := amBuf[cfg.PacketLength:]
|
||||
rawBlock := raw[cfg.PacketLength<<1:]
|
||||
for idx := range block {
|
||||
block[idx] = math.Sqrt(rcvr.lut[rawBlock[idx<<1]] + rcvr.lut[rawBlock[(idx<<1)+1]])
|
||||
}
|
||||
|
||||
// Detect preamble in first half of demod buffer.
|
||||
rcvr.pd.Execute(amBuf)
|
||||
align := rcvr.pd.ArgMax()
|
||||
align := rcvr.pktDecoder.SearchPreamble(amBuf)
|
||||
|
||||
// Bad framing, catch message on next block.
|
||||
if uint(align) > config.BlockSize {
|
||||
if uint(align) > cfg.BlockSize {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter signal and bit slice enough data to catch the preamble.
|
||||
filtered := MatchedFilter(amBuf[align:], PreambleSymbols>>1)
|
||||
bits := BitSlice(filtered)
|
||||
filtered := MatchedFilter(amBuf[align:], int(cfg.PreambleSymbols>>1))
|
||||
data := BitSlice(filtered)
|
||||
|
||||
// If the preamble matches.
|
||||
if bits == PreambleBits {
|
||||
if data.Bits == cfg.PreambleBits {
|
||||
// Filter, slice and parse the rest of the buffered samples.
|
||||
filtered := MatchedFilter(amBuf[align:], PacketSymbols>>1)
|
||||
bits := BitSlice(filtered)
|
||||
|
||||
// If the checksum fails, bail.
|
||||
if rcvr.bch.Encode(bits[16:]) != 0 {
|
||||
continue
|
||||
}
|
||||
filtered := MatchedFilter(amBuf[align:], int(cfg.PacketSymbols>>1))
|
||||
data := BitSlice(filtered)
|
||||
|
||||
// Parse SCM
|
||||
scm, err := ParseSCM(bits)
|
||||
if err != nil {
|
||||
config.Log.Fatal("Error parsing SCM: ", err)
|
||||
}
|
||||
|
||||
// If filtering by ID and ID doesn't match, bail.
|
||||
if config.MeterID != 0 && uint(scm.ID) != config.MeterID {
|
||||
continue
|
||||
}
|
||||
|
||||
// If filtering by type and type doesn't match, bail.
|
||||
if config.MeterType != 0 && uint(scm.Type) != config.MeterType {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get current file offset.
|
||||
offset, err := config.SampleFile.Seek(0, os.SEEK_CUR)
|
||||
if err != nil {
|
||||
config.Log.Fatal("Error getting sample file offset: ", err)
|
||||
}
|
||||
|
||||
// Dump message to file.
|
||||
_, err = config.SampleFile.Write(raw)
|
||||
if err != nil {
|
||||
config.Log.Fatal("Error dumping samples: ", err)
|
||||
}
|
||||
|
||||
msg := Message{time.Now(), offset, len(raw), scm}
|
||||
|
||||
// Write or encode message to log file.
|
||||
if config.Encoder == nil {
|
||||
// A nil encoder is just plain-text output.
|
||||
fmt.Fprintf(config.LogFile, "%+v", msg)
|
||||
} else {
|
||||
err = config.Encoder.Encode(msg)
|
||||
if err != nil {
|
||||
log.Fatal("Error encoding message: ", err)
|
||||
}
|
||||
|
||||
// The XML encoder doesn't write new lines after each
|
||||
// element, add them.
|
||||
if strings.ToLower(config.format) == "xml" {
|
||||
fmt.Fprintln(config.LogFile)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Single {
|
||||
return
|
||||
scm, err := rcvr.pktDecoder.Decode(data)
|
||||
if err == nil {
|
||||
fmt.Printf("%+v\n", scm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A lookup table for calculating magnitude of an interleaved unsigned byte
|
||||
// stream.
|
||||
type MagLUT []float64
|
||||
|
||||
// Shifts sample by 127.4 (most common DC offset value of rtl-sdr dongles) and
|
||||
// stores square.
|
||||
func NewMagLUT() (lut MagLUT) {
|
||||
lut = make([]float64, 0x100)
|
||||
for idx := range lut {
|
||||
lut[idx] = 127.4 - float64(idx)
|
||||
lut[idx] *= lut[idx]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Matched filter implemented as integrate and dump. Output array is equal to
|
||||
// the number of manchester coded symbols per packet.
|
||||
func MatchedFilter(input []float64, bits int) (output []float64) {
|
||||
output = make([]float64, bits)
|
||||
|
||||
fIdx := 0
|
||||
for idx := 0; fIdx < bits; idx += config.SymbolLength * 2 {
|
||||
offset := idx + config.SymbolLength
|
||||
|
||||
for i := 0; i < config.SymbolLength; i++ {
|
||||
output[fIdx] += input[idx+i] - input[offset+i]
|
||||
}
|
||||
fIdx++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func BitSlice(input []float64) (output string) {
|
||||
for _, v := range input {
|
||||
if v > 0.0 {
|
||||
output += "1"
|
||||
} else {
|
||||
output += "0"
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseUint(raw string) uint64 {
|
||||
tmp, _ := strconv.ParseUint(raw, 2, 64)
|
||||
return tmp
|
||||
}
|
||||
|
||||
// Message for logging.
|
||||
type Message struct {
|
||||
Time time.Time
|
||||
Offset int64
|
||||
Length int
|
||||
SCM SCM
|
||||
}
|
||||
|
||||
func (msg Message) String() string {
|
||||
// If we aren't dumping samples, omit offset and packet length.
|
||||
if config.sampleFilename == os.DevNull {
|
||||
return fmt.Sprintf("{Time:%s SCM:%+v}\n",
|
||||
msg.Time.Format(TimeFormat), msg.SCM,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("{Time:%s Offset:%d Length:%d SCM:%+v}\n",
|
||||
msg.Time.Format(TimeFormat), msg.Offset, msg.Length, msg.SCM,
|
||||
)
|
||||
}
|
||||
|
||||
func (msg Message) Record() (record []string) {
|
||||
record = append(record, msg.Time.Format(time.RFC3339Nano))
|
||||
record = append(record, strconv.FormatInt(int64(msg.Offset), 10))
|
||||
record = append(record, strconv.FormatInt(int64(msg.Length), 10))
|
||||
record = append(record, msg.SCM.Record()...)
|
||||
return record
|
||||
}
|
||||
|
||||
// Standard Consumption Message
|
||||
type SCM struct {
|
||||
ID uint32
|
||||
Type uint8
|
||||
Tamper Tamper
|
||||
Consumption uint32
|
||||
Checksum uint16
|
||||
}
|
||||
|
||||
func (scm SCM) String() string {
|
||||
return fmt.Sprintf("{ID:%8d Type:%2d Tamper:%+v Consumption:%8d Checksum:0x%04X}",
|
||||
scm.ID, scm.Type, scm.Tamper, scm.Consumption, scm.Checksum,
|
||||
)
|
||||
}
|
||||
|
||||
func (scm SCM) Record() (record []string) {
|
||||
record = append(record, strconv.FormatInt(int64(scm.ID), 10))
|
||||
record = append(record, strconv.FormatInt(int64(scm.Type), 10))
|
||||
record = append(record, scm.Tamper.Record()...)
|
||||
record = append(record, strconv.FormatInt(int64(scm.Consumption), 10))
|
||||
record = append(record, fmt.Sprintf("0x%04X", scm.Checksum))
|
||||
return
|
||||
}
|
||||
|
||||
type Tamper struct {
|
||||
Phy uint8
|
||||
Enc uint8
|
||||
}
|
||||
|
||||
func (t Tamper) String() string {
|
||||
return fmt.Sprintf("{Phy:%d Enc:%d}", t.Phy, t.Enc)
|
||||
}
|
||||
|
||||
func (tamper Tamper) Record() (record []string) {
|
||||
record = append(record, strconv.FormatInt(int64(tamper.Phy), 10))
|
||||
record = append(record, strconv.FormatInt(int64(tamper.Enc), 10))
|
||||
return
|
||||
}
|
||||
|
||||
// Given a string of bits, parse the message.
|
||||
func ParseSCM(data string) (scm SCM, err error) {
|
||||
if len(data) != 96 {
|
||||
return scm, errors.New("invalid input length")
|
||||
}
|
||||
|
||||
scm.ID = uint32(ParseUint(data[21:23] + data[56:80]))
|
||||
scm.Type = uint8(ParseUint(data[26:30]))
|
||||
scm.Tamper.Phy = uint8(ParseUint(data[24:26]))
|
||||
scm.Tamper.Enc = uint8(ParseUint(data[30:32]))
|
||||
scm.Consumption = uint32(ParseUint(data[32:56]))
|
||||
scm.Checksum = uint16(ParseUint(data[80:96]))
|
||||
|
||||
return scm, nil
|
||||
type Data struct {
|
||||
Bits string
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
func init() {
|
||||
rcvr.RegisterFlags()
|
||||
|
||||
err := config.Parse()
|
||||
if err != nil {
|
||||
log.Fatal("Error parsing flags: ", err)
|
||||
}
|
||||
|
||||
rcvr.Init()
|
||||
log.SetFlags(log.Lshortfile | log.Lmicroseconds)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if !config.Quiet {
|
||||
config.Log.Println("Server:", rcvr.Flags.ServerAddr)
|
||||
config.Log.Println("BlockSize:", config.BlockSize)
|
||||
config.Log.Println("SampleRate:", config.SampleRate)
|
||||
config.Log.Println("DataRate:", DataRate)
|
||||
config.Log.Println("SymbolLength:", config.SymbolLength)
|
||||
config.Log.Println("PreambleSymbols:", PreambleSymbols)
|
||||
config.Log.Println("PreambleLength:", config.PreambleLength)
|
||||
config.Log.Println("PacketSymbols:", PacketSymbols)
|
||||
config.Log.Println("PacketLength:", config.PacketLength)
|
||||
config.Log.Println("CenterFreq:", rcvr.Flags.CenterFreq)
|
||||
config.Log.Println("TimeLimit:", config.TimeLimit)
|
||||
scmd := NewSCMDecoder(73)
|
||||
cfg := scmd.pktConfig
|
||||
|
||||
config.Log.Println("Format:", config.format)
|
||||
config.Log.Println("LogFile:", config.logFilename)
|
||||
config.Log.Println("SampleFile:", config.sampleFilename)
|
||||
|
||||
if config.MeterID != 0 {
|
||||
config.Log.Println("FilterID:", config.MeterID)
|
||||
}
|
||||
}
|
||||
log.Println("Server:", rcvr.Flags.ServerAddr)
|
||||
log.Println("BlockSize:", cfg.BlockSize)
|
||||
log.Println("SampleRate:", cfg.SampleRate)
|
||||
log.Println("DataRate:", 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("PreambleBits:", scmd.pktConfig.PreambleBits)
|
||||
log.Println("CRC:", scmd.crc)
|
||||
|
||||
rcvr.NewReceiver(scmd)
|
||||
defer rcvr.Close()
|
||||
defer config.Close()
|
||||
|
||||
if !config.Quiet {
|
||||
config.Log.Println("Running...")
|
||||
}
|
||||
|
||||
rcvr.Run()
|
||||
}
|
||||
|
|
|
|||
110
scm.go
Normal file
110
scm.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bemasher/rtlamr/crc"
|
||||
"github.com/bemasher/rtlamr/preamble"
|
||||
)
|
||||
|
||||
type SCMDecoder struct {
|
||||
pd preamble.PreambleDetector
|
||||
crc crc.CRC
|
||||
|
||||
pktConfig PacketConfig
|
||||
}
|
||||
|
||||
func (scmd SCMDecoder) String() string {
|
||||
return fmt.Sprintf("{Packetconfig:%s CRC:%s}", scmd.pktConfig, scmd.crc)
|
||||
}
|
||||
|
||||
func NewSCMDecoder(symbolLength int) (scmd SCMDecoder) {
|
||||
var pc PacketConfig
|
||||
|
||||
pc.SymbolLength = symbolLength
|
||||
|
||||
pc.PreambleSymbols = 42
|
||||
pc.PacketSymbols = 192
|
||||
|
||||
pc.PreambleLength = pc.PreambleSymbols * uint(pc.SymbolLength)
|
||||
pc.PacketLength = pc.PacketSymbols * uint(pc.SymbolLength)
|
||||
pc.BlockSize = NextPowerOf2(pc.PreambleLength)
|
||||
|
||||
pc.SampleRate = DataRate * uint(pc.SymbolLength)
|
||||
|
||||
pc.PreambleBits = strconv.FormatUint(0x1F2A60, 2)
|
||||
pc.PreambleBits += strings.Repeat("0", int(pc.PreambleSymbols>>1)-len(pc.PreambleBits))
|
||||
|
||||
scmd.pktConfig = pc
|
||||
|
||||
scmd.pd = preamble.NewPreambleDetector(uint(pc.BlockSize<<1), pc.SymbolLength, pc.PreambleBits)
|
||||
scmd.crc = crc.NewCRC("BCH", 0, 0x6F63, 0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (scmd SCMDecoder) Close() {
|
||||
scmd.pd.Close()
|
||||
}
|
||||
|
||||
func (scmd SCMDecoder) PacketConfig() PacketConfig {
|
||||
return scmd.pktConfig
|
||||
}
|
||||
|
||||
func (scmd SCMDecoder) SearchPreamble(buf []float64) int {
|
||||
scmd.pd.Execute(buf)
|
||||
return scmd.pd.ArgMax()
|
||||
}
|
||||
|
||||
// Standard Consumption Message
|
||||
type SCM struct {
|
||||
ID uint32
|
||||
Type uint8
|
||||
Tamper struct {
|
||||
Phy uint8
|
||||
Enc uint8
|
||||
}
|
||||
Consumption uint32
|
||||
Checksum uint16
|
||||
}
|
||||
|
||||
func (scm SCM) String() string {
|
||||
return fmt.Sprintf("{ID:%8d Type:%2d Tamper:{Phy:%d Enc:%d} Consumption:%8d Checksum:0x%04X}",
|
||||
scm.ID, scm.Type, scm.Tamper.Phy, scm.Tamper.Enc, scm.Consumption, scm.Checksum,
|
||||
)
|
||||
}
|
||||
|
||||
func (scmd SCMDecoder) Decode(data Data) (fmt.Stringer, error) {
|
||||
var scm SCM
|
||||
|
||||
if len(data.Bits) != int(scmd.pktConfig.PacketSymbols>>1) {
|
||||
return scm, errors.New("invalid input length")
|
||||
}
|
||||
|
||||
if scmd.crc.Checksum(data.Bytes[2:]) != 0 {
|
||||
return scm, errors.New("checksum failed")
|
||||
}
|
||||
|
||||
id, _ := 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(id)
|
||||
scm.Type = uint8(ertType)
|
||||
scm.Tamper.Phy = uint8(tamperPhy)
|
||||
scm.Tamper.Enc = uint8(tamperEnc)
|
||||
scm.Consumption = uint32(consumption)
|
||||
scm.Checksum = uint16(checksum)
|
||||
|
||||
if scm.ID == 0 {
|
||||
return scm, errors.New("invalid meter id")
|
||||
}
|
||||
|
||||
return scm, nil
|
||||
}
|
||||
Loading…
Reference in a new issue