Strip configuration. Decouple packet configuration and decoding.

This commit is contained in:
bemasher 2014-08-17 09:50:06 -06:00
parent 33e0ed66bb
commit fb778569be
10 changed files with 367 additions and 768 deletions

View file

@ -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
}

View file

@ -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
View file

@ -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
View 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
View 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())
}

View file

@ -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
View 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
View 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
View file

@ -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
View 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
}