diff --git a/bch/bch.go b/bch/bch.go deleted file mode 100644 index 1986b67..0000000 --- a/bch/bch.go +++ /dev/null @@ -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 -} diff --git a/bch/bch_test.go b/bch/bch_test.go deleted file mode 100644 index 02f5e97..0000000 --- a/bch/bch_test.go +++ /dev/null @@ -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) - } -} diff --git a/config.go b/config.go deleted file mode 100644 index 21c7702..0000000 --- a/config.go +++ /dev/null @@ -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)))) -} diff --git a/crc/crc.go b/crc/crc.go new file mode 100644 index 0000000..46a527b --- /dev/null +++ b/crc/crc.go @@ -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 +} diff --git a/crc/crc_test.go b/crc/crc_test.go new file mode 100644 index 0000000..dbf1a27 --- /dev/null +++ b/crc/crc_test.go @@ -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()) +} diff --git a/csv/csv.go b/csv/csv.go deleted file mode 100644 index 4f2e942..0000000 --- a/csv/csv.go +++ /dev/null @@ -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 -} diff --git a/dsp.go b/dsp.go new file mode 100644 index 0000000..5520f24 --- /dev/null +++ b/dsp.go @@ -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 +} diff --git a/pkt.go b/pkt.go new file mode 100644 index 0000000..c365447 --- /dev/null +++ b/pkt.go @@ -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)))) +} diff --git a/recv.go b/recv.go index 63bab40..58725ef 100644 --- a/recv.go +++ b/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() } diff --git a/scm.go b/scm.go new file mode 100644 index 0000000..afb8008 --- /dev/null +++ b/scm.go @@ -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 +}