From d333230481ec18828d6d2b5fbc2246501473f92c Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Tue, 19 Dec 2023 02:02:44 -0700 Subject: [PATCH 1/8] add ability to decode packets directly to graphite tsdb --- main.go | 52 ++++++++++++++++++-- protocol/decode.go | 113 +++++++++++++++++++++++++++++++++++++++++++ protocol/protocol.go | 4 +- 3 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 protocol/decode.go diff --git a/main.go b/main.go index a864d0c..fb8a4e1 100644 --- a/main.go +++ b/main.go @@ -33,10 +33,12 @@ package main import ( + "bytes" "flag" "io" "log" "math/rand" + "net" "os" "os/signal" "time" @@ -60,7 +62,8 @@ var ( verbose *bool // -v = emit verbose debug messages disableAfc *bool // -noafc = disable any automatic corrections deviceString *string // -d = device serial number or device index - + graphiteSrv *string // -gs = decode the packets and send to graphite + graphitePrefix *string // -gp = prefix for graphite metrics // general actChan [maxTr]int // list with actual channels (0-7); // next values are zero (non-meaning) @@ -120,7 +123,7 @@ var ( func init() { - VERSION := "0.15" + VERSION := "0.15.1md" var ( tr int mask int @@ -148,6 +151,8 @@ var ( verbose = flag.Bool("v", false, "emit verbose debug messages") disableAfc = flag.Bool("noafc", false, "disable any AFC") deviceString = flag.String("d","0","device serial number or device index") + graphiteSrv = flag.String("gs", "", "decode packets and send to graphite server") + graphitePrefix = flag.String("gp", "wx.davis.", "prefix for graphite metrics") flag.Parse() protocol.Verbose = *verbose @@ -295,6 +300,15 @@ func main() { } }() + var graphiteChan chan []string + if *graphiteSrv != "" { + graphiteChan = make(chan []string) + go sendToGraphite(*graphiteSrv, *graphitePrefix, graphiteChan) + defer close(graphiteChan) + } else { + graphiteSrv = nil + } + defer func() { in.Close() out.Close() @@ -430,7 +444,9 @@ func main() { if *undefined { log.Printf("%02X %d %d %d %d %d msg.ID=%d undefined:%d", msg.Data, chTotMsgs[0], chTotMsgs[1], chTotMsgs[2], chTotMsgs[3], totInit, msg.ID, idUndefs) - } else { + } else if graphiteSrv != nil { + graphiteChan <- protocol.DecodeMsg(msg) + } else { log.Printf("%02X %d %d %d %d %d msg.ID=%d", msg.Data, chTotMsgs[0], chTotMsgs[1], chTotMsgs[2], chTotMsgs[3], totInit, msg.ID) } @@ -489,3 +505,33 @@ func Min(values []int64) (ptr int) { } return ptr } + +func sendToGraphite(server string, prefix string, graphiteChan chan[]string) { + var conn io.Writer + var err error + if server == "-" { + conn = os.Stdout + } else { + conn, err = net.Dial("udp", server) + if err != nil { + log.Printf("failed to open %s: %s", server, err) + os.Exit(1) + return + } + } + log.Printf("connected to %s for graphite output", server) + for msgs := range graphiteChan { + /* note that every call to conn.write generates a udp packet; the + least we can do is bundle the messages that all arrive + together */ + var b bytes.Buffer + for _, line := range msgs { + fmt.Fprintf(&b, "%s%s %d\n", prefix, line, time.Now().Unix()) + } + _, err = b.WriteTo(conn) + if err != nil { + log.Printf("failed to write to %s: %s", server, err) + os.Exit(1) + } + } +} diff --git a/protocol/decode.go b/protocol/decode.go new file mode 100644 index 0000000..7764577 --- /dev/null +++ b/protocol/decode.go @@ -0,0 +1,113 @@ +package protocol + +import ( + "fmt" + "log" +) + +func DecodeMsg(m Message) (obs []string) { + /* sensor messages arrive with 'channel' numbers, which has no + relation to a go chan or an RF frequency. we only understand the + ISS ('integrated sensor suite') channel 0, used by Vantage Vue. */ + if m.ID != 0 { + log.Printf("received message for unsupported channel %d", m.ID) + return + } + + windspeed_raw := m.Data[1] + /* TODO: weewx rtldavis.py has a giant complicated error correction + matrix for windspeed. But from comments it's not clear it + applies to Vantage Vue? */ + obs = append(obs, fmt.Sprintf("windspeed_raw %d", windspeed_raw)) + + winddir_vue := float64(m.Data[2])*1.40625 + 0.3 + obs = append(obs, fmt.Sprintf("winddir %.2f", winddir_vue)) + + msg_type := (m.Data[0] >> 4) & 0x0F + /* most of the time we will use the 10-bit number in this weird place */ + raw := ((int16(m.Data[3]) << 2) + int16(m.Data[4])>>6) & 0x03FF + switch msg_type { + case 0x02: + /* supercap voltage */ + obs = append(obs, fmt.Sprintf("supercap_v_raw %d", raw)) + if raw != 0x03FF { + obs = append(obs, fmt.Sprintf("supercap_v %0.2f", float32(raw)/300.0)) + } + case 0x04: + /* UV radiation */ + obs = append(obs, fmt.Sprintf("uv_raw %d", raw)) + if raw != 0x03FF { + obs = append(obs, fmt.Sprintf("uv %0.2f", raw/50.0)) + } + case 0x05: + /* rain rate */ + time_between_tips_raw := ((int16(m.Data[4]) & 0x30) << 4) + int16(m.Data[3]) + rain_rate := 0.0 + if time_between_tips_raw == 0x03FF { + rain_rate = 0.0 /* time between tips is infinity => no rain */ + } else if m.Data[4]&0x40 == 0 { + /* "heavy rain", time-between-tips is scaled up by 4 bits */ + /* TODO: we are assuming the 0.1 inch (=2.54mm) size bucket */ + rain_rate = 3600.0 / (float64(time_between_tips_raw) / 16.0) * 2.54 + } else { + /* "light rain" formula */ + rain_rate = 3600.0 / float64(time_between_tips_raw) * 2.54 + } + obs = append(obs, fmt.Sprintf("rain_rate_mmh %.2f", rain_rate)) + case 0x06: + /* solar radiation */ + if raw < 0x03fE { + sr := float64(raw) * 1.757936 + obs = append(obs, fmt.Sprintf("solar_radiation %.2f", sr)) + } + case 0x07: + /* solar panel output */ + if raw != 0x03FF { + obs = append(obs, fmt.Sprintf("solar_panel_v %.2f", float32(raw)/300.0)) + } + case 0x08: + /* temperature */ + raw = (int16(m.Data[3]) << 4) + (int16(m.Data[4]) >> 4) + if raw != 0x0FFC { + if m.Data[4]&0x08 != 0 { + /* digital sensor */ + obs = append(obs, fmt.Sprintf("temp %.2f", float32(raw)/10.0)) + } else { + /* TODO: thermistor */ + log.Printf("can't interpret analog temperature sensor reading %d", raw) + obs = append(obs, fmt.Sprintf("temp_raw %d", raw)) + } + } + case 0x09: + /* 10-min average wind gust */ + gust_raw := m.Data[3] + gust_index := m.Data[5] >> 4 /* ??? */ + obs = append(obs, fmt.Sprintf("gust_mph %d", gust_raw)) + obs = append(obs, fmt.Sprintf("gust_index %d", gust_index)) + case 0x0a: + /* humidity */ + raw = (int16(m.Data[4]>>4) << 8) + int16(m.Data[3]) + if raw != 0 { + if m.Data[4]&0x08 != 0 { + /* digital sensor */ + obs = append(obs, fmt.Sprintf("humidity %.2f", float32(raw)/10.0)) + } else { + /* TODO: two other types of digital sensor and one analog */ + log.Printf("can't interpret humidity sensor reading %d", raw) + obs = append(obs, fmt.Sprintf("humidity_raw %d", raw)) + } + } + case 0x0e: + /* rain */ + raw = int16(m.Data[3]) /* "rain count raw"?? */ + /* ignore the high bit because apparently some counters wrap at + 128 and others at 256, and it doesn't matter */ + if raw != 0x80 { + raw &= 0x7f + obs = append(obs, fmt.Sprintf("rain_count %d", raw)) + } + default: + log.Printf("Unknown data packet type 0x%02x: %02x", m.ID, m.Data) + } + return +} diff --git a/protocol/protocol.go b/protocol/protocol.go index ff29896..9d03e91 100644 --- a/protocol/protocol.go +++ b/protocol/protocol.go @@ -257,7 +257,8 @@ func (p *Parser) Parse(pkts []dsp.Packet) (msgs []Message) { type Message struct { dsp.Packet - ID byte + ID byte + BatteryLow bool } func NewMessage(pkt dsp.Packet) (m Message) { @@ -265,6 +266,7 @@ func NewMessage(pkt dsp.Packet) (m Message) { m.Data = make([]byte, len(pkt.Data)-2) copy(m.Data, pkt.Data[2:]) m.ID = m.Data[0] & 0x7 + m.BatteryLow = ((m.Data[0] >> 3) & 0x01 == 1) return m } From 714265e2e3d9e8b530ab832db58b6ad1b430d99c Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 16:54:16 -0700 Subject: [PATCH 2/8] add error correction, there are probably bugs in this code --- main.go | 2 +- protocol/decode.go | 114 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index fb8a4e1..0099dbc 100644 --- a/main.go +++ b/main.go @@ -123,7 +123,7 @@ var ( func init() { - VERSION := "0.15.1md" + VERSION := "0.15.2md" var ( tr int mask int diff --git a/protocol/decode.go b/protocol/decode.go index 7764577..036670b 100644 --- a/protocol/decode.go +++ b/protocol/decode.go @@ -23,6 +23,11 @@ func DecodeMsg(m Message) (obs []string) { winddir_vue := float64(m.Data[2])*1.40625 + 0.3 obs = append(obs, fmt.Sprintf("winddir %.2f", winddir_vue)) + /* apply the error correction table that might not even be for the + Vantage Vue; it's unclear */ + winddir := CorrectWindspeed(m.Data[1], m.Data[2]) + obs = append(obs, fmt.Sprintf("windspeed %.2f", winddir)) + msg_type := (m.Data[0] >> 4) & 0x0F /* most of the time we will use the 10-bit number in this weird place */ raw := ((int16(m.Data[3]) << 2) + int16(m.Data[4])>>6) & 0x03FF @@ -111,3 +116,112 @@ func DecodeMsg(m Message) (obs []string) { } return } + +/* this table is from github.com/lheijst/weewx-rtldavis. It says the + offsets were determined by feeding constructed packets to a Davis + Envoy and reading the serial output packets. + + Columns are wind direction, rows are windspeed. +*/ + +var wind_offsets = [55][35]int16{ + {0, 1, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 127, 128}, + {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0}, + {4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0}, + {5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0}, + {6, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0}, + {7, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0}, + {8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0}, + {9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0}, + {10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0}, + {11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0}, + {12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0}, + {13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {15, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {17, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {18, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0}, + {19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 1, 0, 0}, + {20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0}, + {21, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0}, + {22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0}, + {23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0}, + {24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 2, 0, 0}, + {25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 4, 4, 2, 0, 0}, + {26, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 4, 2, 0, 0}, + {27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0}, + {28, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0}, + {29, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0}, + {30, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0}, + {35, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 6, 5, 2, 0, -1}, + {40, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 6, 6, 2, 0, -1}, + {45, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 7, 6, 2, -1, -1}, + {50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 7, 7, 2, -1, -2}, + {55, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 8, 7, 2, -1, -2}, + {60, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 8, 8, 2, -1, -2}, + {65, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 9, 8, 2, -2, -3}, + {70, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 5, 9, 9, 2, -2, -3}, + {75, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 6, 10, 9, 2, -2, -3}, + {80, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 6, 10, 10, 2, -2, -3}, + {85, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, 2, 7, 11, 11, 2, -3, -4}, + {90, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 7, 12, 11, 2, -3, -4}, + {95, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 2, 2, 2, 1, 1, 1, 1, 2, 7, 12, 12, 3, -3, -4}, + {100, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 2, 1, 1, 1, 1, 2, 8, 13, 12, 3, -3, -4}, + {105, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 8, 13, 13, 3, -3, -4}, + {110, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 8, 14, 14, 3, -3, -5}, + {115, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 9, 15, 14, 3, -3, -5}, + {120, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 3, 9, 15, 15, 3, -4, -5}, + {125, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 3, 10, 16, 16, 3, -4, -5}, + {130, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 3, 10, 17, 16, 3, -4, -6}, + {135, 1, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 3, 3, 2, 2, 2, 1, 1, 3, 10, 17, 17, 4, -4, -6}, + {140, 1, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 4, 3, 3, 2, 2, 2, 1, 1, 3, 11, 18, 17, 4, -4, -6}, + {145, 2, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 3, 2, 2, 1, 1, 3, 11, 19, 18, 4, -4, -6}, + {150, 2, 2, 2, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4, 4, 4, 4, 3, 3, 2, 2, 1, 1, 3, 12, 19, 19, 4, -4, -6}, +} + +func CorrectWindspeed(s byte, d byte) float32 { + /* speed is mph, direction is degrees where 0 = north */ + + /* table is treated as having east-west symmetry */ + if d > 127 { + d = byte(256 - int16(d)) + } + + /* on each axis, find the first term greater than x */ + row, col := 1, 1 + for wind_offsets[row][0] <= int16(s) && row < 54 { + row++ + } + for wind_offsets[0][col] <= int16(d) && col < 34 { + col++ + } + + corr := float32(wind_offsets[row][col]) + var delta_s, delta_d float32 + + /* we are talking about fractions of a mph at this point, but + nevertheless we soldier on and do a bilinear interpretation to + approximate the corrections where the table does not have an + exact match */ + + if col > 1 && wind_offsets[0][col] != int16(d) { + d_selected := float32(wind_offsets[0][col]) + d_prev := float32(wind_offsets[0][col-1]) + corr_prev := float32(wind_offsets[row][col-1]) + delta_d = (corr_prev - corr) + + (float32(d)-d_prev)/(d_selected-d_prev)*(corr-corr_prev) + } + + if row > 1 && wind_offsets[row][0] != int16(s) { + s_selected := float32(wind_offsets[row][0]) + s_prev := float32(wind_offsets[row-1][0]) + corr_prev := float32(wind_offsets[row-1][col]) + delta_s = (corr_prev - corr) + + (float32(s)-s_prev)/(s_selected-s_prev)*(corr-corr_prev) + } + + return float32(s) + corr + delta_d + delta_s +} From 446dec99ebf0929aa49a8a672000244281d2d161 Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 21:43:57 -0700 Subject: [PATCH 3/8] explain what happened here --- README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83187fa..1397ed9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,41 @@ # rtldavis -## rtldavis +## About this Repository + +This fork (mdickers47/rtldavis) is a fork of +[http://github.com/lheijst/rtldavis]. The way the lheijst and bemasher +versions work is that they implement the Davis frequency hopping, +demodulate the packets, and dump them to the log file as hex bytes. +Something else has to decode the messages to create weather data. +Luc wrote [https://github.com/lheijst/weewx-rtldavis][weewx-rtldavis] +which is in Python and operates as a weewx driver to update a weewx +database. + +My change is that I have adapted the packet parsing code from +weewx-rtldavis into the go binary. You can now write the decoded weather +data to stdout with the option `-g -`. It will be written as one data +point per line, where each data point is three columns, which are metric +name, value, and UNIX timestamp: -### About this Repository +``` +wx.davis.windspeed_raw 0 1703133526 +wx.davis.winddir 143.74 1703133526 +wx.davis.windspeed 0.00 1703133526 +wx.davis.temp 50.20 1703133526 +``` + +This is the Graphite/Carbon "line receiver" format, so, by supplying +`-w server:udpport`, this rtldavis binary will send the data directly +to Graphite via UDP packets. + +*Beware* that I have only implemented the parsing for the Vantage Vue +ISS sensor package with the 0.2 inch rain bucket, because that is what +I have. If the `-w` option is used with a different weather station, +it will be wrong. + +- 2023-12-19 M. Dickerson + +From here on is the README from lheijst/rtldavis. This repository is a fork of [https://github.com/bemasher/rtldavis](https://github.com/bemasher/rtldavis) for use with custom receiver modules. It has been modified in numerous ways. 1) Added EU frequencies From 77c6c4e0f6e545a5b4fdca2a89a7581881f5127f Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 21:47:59 -0700 Subject: [PATCH 4/8] fix readme typos --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1397ed9..63e71f8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This fork (mdickers47/rtldavis) is a fork of versions work is that they implement the Davis frequency hopping, demodulate the packets, and dump them to the log file as hex bytes. Something else has to decode the messages to create weather data. -Luc wrote [https://github.com/lheijst/weewx-rtldavis][weewx-rtldavis] +Luc wrote [https://github.com/lheijst/weewx-rtldavis](weewx-rtldavis) which is in Python and operates as a weewx driver to update a weewx database. @@ -24,13 +24,13 @@ wx.davis.windspeed 0.00 1703133526 wx.davis.temp 50.20 1703133526 ``` -This is the Graphite/Carbon "line receiver" format, so, by supplying -`-w server:udpport`, this rtldavis binary will send the data directly +This is the Graphite/Carbon "line receiver" format. If you specify +`-g server:udpport`, this rtldavis binary will send the data directly to Graphite via UDP packets. *Beware* that I have only implemented the parsing for the Vantage Vue ISS sensor package with the 0.2 inch rain bucket, because that is what -I have. If the `-w` option is used with a different weather station, +I have. If the `-g` option is used with a different weather station, it will be wrong. - 2023-12-19 M. Dickerson From a6c107e50dac0f1e507887d12b2fe6146c43a4c2 Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 21:55:52 -0700 Subject: [PATCH 5/8] add comment explaining what happened here --- protocol/decode.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/protocol/decode.go b/protocol/decode.go index 036670b..a6154a9 100644 --- a/protocol/decode.go +++ b/protocol/decode.go @@ -1,3 +1,22 @@ +/* + This module decodes the binary data packets into weather messages. + Every packet carries one byte for wind speed, one byte for wind + direction, and one other sensor reading of 10 or more bits, labeled by + a message identifier. + + The code is adapted from the weewx driver named weewx-rtldavis, + written by Luc Heijst (also the author of the upstream fork of + rtldavis): + + https://github.com/lheijst/weewx-rtldavis + + If a bug here is also present in the weewx-rtldavis reference code, + it should be fixed in both places. If it is only in this code, + then it was introduced by me when translating from Python to Go. + + 2023-12-19 M. Dickerson +*/ + package protocol import ( From a90a57087afb624c1ad8aacb837405e329ce3a62 Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 22:05:01 -0700 Subject: [PATCH 6/8] this TODO is todone --- protocol/decode.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/protocol/decode.go b/protocol/decode.go index a6154a9..d5c53d4 100644 --- a/protocol/decode.go +++ b/protocol/decode.go @@ -34,9 +34,6 @@ func DecodeMsg(m Message) (obs []string) { } windspeed_raw := m.Data[1] - /* TODO: weewx rtldavis.py has a giant complicated error correction - matrix for windspeed. But from comments it's not clear it - applies to Vantage Vue? */ obs = append(obs, fmt.Sprintf("windspeed_raw %d", windspeed_raw)) winddir_vue := float64(m.Data[2])*1.40625 + 0.3 From e8282f21b9ef9b1f554e551ed6f30740752c4f9f Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Wed, 20 Dec 2023 22:14:29 -0700 Subject: [PATCH 7/8] trying to make this build in "go install" context --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 0099dbc..6cc73ae 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,7 @@ import ( "strconv" "fmt" - "github.com/lheijst/rtldavis/protocol" + "github.com/mdickers47/rtldavis/protocol" "github.com/jpoirier/gortlsdr" ) const maxTr = 8 From 1184e9c93b3061e70cdc00558d8e2325dfb2ba2d Mon Sep 17 00:00:00 2001 From: Mikey Dickerson Date: Fri, 22 Dec 2023 14:12:49 -0700 Subject: [PATCH 8/8] bug: rain spoon is 0.01 inch, not 0.1 inch! --- protocol/decode.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/protocol/decode.go b/protocol/decode.go index d5c53d4..6e7fe4a 100644 --- a/protocol/decode.go +++ b/protocol/decode.go @@ -68,11 +68,11 @@ func DecodeMsg(m Message) (obs []string) { rain_rate = 0.0 /* time between tips is infinity => no rain */ } else if m.Data[4]&0x40 == 0 { /* "heavy rain", time-between-tips is scaled up by 4 bits */ - /* TODO: we are assuming the 0.1 inch (=2.54mm) size bucket */ - rain_rate = 3600.0 / (float64(time_between_tips_raw) / 16.0) * 2.54 + /* TODO: we are assuming the 0.01 inch (=0.254mm) size bucket */ + rain_rate = 3600.0 / (float64(time_between_tips_raw) / 16.0) * 0.254 } else { /* "light rain" formula */ - rain_rate = 3600.0 / float64(time_between_tips_raw) * 2.54 + rain_rate = 3600.0 / float64(time_between_tips_raw) * 0.254 } obs = append(obs, fmt.Sprintf("rain_rate_mmh %.2f", rain_rate)) case 0x06: