commit 6a63bf0e0d1effa9270015f0c8a3330f62d158a9 Author: David Högborg Date: Wed Jul 15 01:09:43 2015 +0200 initial source diff --git a/gopow/gopow.go b/gopow/gopow.go new file mode 100644 index 0000000..2a1a24c --- /dev/null +++ b/gopow/gopow.go @@ -0,0 +1,100 @@ +package gopow + +import ( + "fmt" + "image" + "image/png" + "os" + + log "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" +) + +type RunConfig struct { + InputFile string + OutputFile string + Format string + Downsample int +} + +type GoPow struct { + config *RunConfig + image *image.RGBA +} + +func NewGoPow(c *cli.Context) (*GoPow, error) { + + config := &RunConfig{ + InputFile: c.String("input"), + OutputFile: c.String("output"), + Format: c.String("format"), + Downsample: c.Int("downsample"), + } + + if config.InputFile == "" { + return nil, fmt.Errorf("missing input file") + } + + if config.Format == "" { + config.Format = "png" + } + + if config.OutputFile == "" { + config.OutputFile = config.InputFile + "." + config.Format + } + + log.WithFields(log.Fields{ + "input": config.InputFile, + }).Info("GoPow init") + log.WithFields(log.Fields{ + "output": config.OutputFile, + }).Info("GoPow init") + log.WithFields(log.Fields{ + "format": config.Format, + }).Info("GoPow init") + + g := &GoPow{ + config: config, + } + + return g, nil +} + +func (g *GoPow) Render() error { + + log.Debug("staring render") + + table, err := NewTable(g.config.InputFile) + if err != nil { + return err + } + + g.image = table.Image() + + for y, row := range table.Rows { + for x, _ := range row.Samples { + g.image.Set(x, y, table.ColorAt(x, y)) + } + } + + return nil +} + +func (g *GoPow) Write() error { + + log.WithFields(log.Fields{ + "file": g.config.OutputFile, + }).Debug("staring output write") + + out, err := os.Create(g.config.OutputFile) + if err != nil { + return err + } + + err = png.Encode(out, g.image) + if err != nil { + return err + } + + return nil +} diff --git a/gopow/line.go b/gopow/line.go new file mode 100644 index 0000000..939bdbc --- /dev/null +++ b/gopow/line.go @@ -0,0 +1,154 @@ +package gopow + +import ( + "strconv" + "strings" + "time" + + log "github.com/Sirupsen/logrus" +) + +type LineComplex struct { + Time *time.Time + Hash string // a unique hash for this line in time + + HzLow float64 + HzHigh float64 + HzStep float64 + SampleCount int + + Samples []float64 +} + +type LineSort []*LineComplex + +func (a LineSort) Len() int { + return len(a) +} + +func (a LineSort) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a LineSort) Less(i, j int) bool { + if a[i].Time == nil { + return false + } + + if a[j].Time == nil { + return true + } + + return a[i].Time.Unix() < a[j].Time.Unix() +} + +func NewLineComplex(cells []string) *LineComplex { + + // bail early if there is something wrong with the line + if len(cells) < 7 { + return &LineComplex{} + } + + date := cells[0] + clock := cells[1] + + const format = "2006-01-02 15:04:05" + datetime, err := time.Parse(format, date+clock) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "string": date + clock, + }).Fatal("date parsing failure") + } + + hzLow, _ := strconv.ParseFloat(cells[2], 64) + hzHigh, _ := strconv.ParseFloat(cells[3], 64) + hzStep, _ := strconv.ParseFloat(cells[3], 64) + sc, _ := strconv.ParseInt(cells[4], 10, 64) + + samples := []float64{} + for _, s := range cells[6:] { + sf64, err := strconv.ParseFloat(strings.Trim(s, " "), 64) + if err != nil { + samples = append(samples, 0) + } else { + samples = append(samples, sf64) + } + } + + // downsampl := 1 + // // the mean of 10 hz makes up a pixel + // samplesFilter := []float64{} + // for i := 0; i < len(samples); i = i + downsampl { + + // mean := 0.0 + // samps := 0 + + // for ii := 0; ii < downsampl; ii++ { + // if len(samples)-1 > i+ii { + // samps++ + // mean += samples[i+ii] + // } + // } + + // mean = mean / float64(samps) + + // samplesFilter = append(samplesFilter, mean) + // } + + return &LineComplex{ + Time: &datetime, + Hash: cells[0] + cells[1], + HzLow: hzLow, + HzHigh: hzHigh, + HzStep: hzStep, + SampleCount: int(sc), + + Samples: samples, // the rest of the cells end up as samples + } +} + +func (l *LineComplex) AddSamples(line *LineComplex) { + + if line.HzHigh > l.HzHigh { + l.HzHigh = line.HzHigh + } + + if line.HzLow < l.HzLow { + l.HzLow = line.HzLow + } + + if l.Samples == nil { + l.Samples = []float64{} + } + + l.Samples = append(l.Samples, line.Samples...) + +} + +func (l *LineComplex) HighSample() float64 { + + high := float64(-99999) + for _, sample := range l.Samples { + if sample > high { + high = sample + } + } + + return high +} + +func (l *LineComplex) LowSample() float64 { + low := float64(99999) + for _, sample := range l.Samples { + if sample < low { + low = sample + } + } + + return low +} + +func (l *LineComplex) Sample(x int) float64 { + return l.Samples[x] +} diff --git a/gopow/table.go b/gopow/table.go new file mode 100644 index 0000000..93ef038 --- /dev/null +++ b/gopow/table.go @@ -0,0 +1,177 @@ +package gopow + +import ( + "image" + "image/color" + "io/ioutil" + "sort" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/dustin/go-humanize" + "github.com/lucasb-eyer/go-colorful" +) + +type TableComplex struct { + File string // our input file + + Rows []*LineComplex + + Min float64 // minimum power value, used for color rendering + Max float64 // maximum dito + + FreqStart float64 + FreqEnd float64 + + Bins int // horizontal slots, columns, bandwidth + Integrations int // vertical slots, rows + + TimeStart time.Time // real time + TimeEnd time.Time +} + +func NewTable(file string) (*TableComplex, error) { + + log.Debug("creating table") + + t := &TableComplex{} + + err := t.Load(file) + if err != nil { + return nil, err + } + + return t, nil +} + +func (t *TableComplex) Load(file string) error { + + log.Debug("loading table") + + t.File = file + + buff, err := ioutil.ReadFile(t.File) + if err != nil { + return err + } + + log.WithFields(log.Fields{ + "bytes": len(buff), + "size": humanize.Bytes(uint64(len(buff))), + }).Debug("file loaded") + + t.Rows = t.parseBuffer(buff) + + return nil +} + +func (t *TableComplex) parseBuffer(filebuffer []byte) []*LineComplex { + + t.Max = float64(-99999999) + t.Min = float64(99999999) + + block := string(filebuffer) + lines := strings.Split(block, "\n") + + table := map[string][]*LineComplex{} + + for _, l := range lines { + + cells := strings.Split(l, ",") + line := NewLineComplex(cells) + + if table[line.Hash] == nil { + table[line.Hash] = []*LineComplex{} + } + + table[line.Hash] = append(table[line.Hash], line) + } + + rows := []*LineComplex{} + + // loop over hash keys with lines + for _, lines := range table { + + row := t.IntegrateLines(lines) + + if row != nil { + + rows = append(rows, row) + + if t.Min > row.LowSample() { + t.Min = row.LowSample() + } + + if t.Max < row.HighSample() { + t.Max = row.HighSample() + } + } + } + + sort.Sort(LineSort(rows)) + + log.WithFields(log.Fields{ + "pMax": t.Max, + "pMin": t.Min, + }).Debug("integrated lines") + + t.Integrations = len(rows) + + if t.Integrations > 0 { + t.Bins = len(rows[0].Samples) + } else { + log.Fatal("no samples found") + } + + log.WithFields(log.Fields{ + "bins": t.Bins, + "integrations": t.Integrations, + }).Debug("parsed table") + + return rows +} + +func (t *TableComplex) Image() *image.RGBA { + + log.WithFields(log.Fields{ + "width": t.Bins, + "height": t.Integrations, + }).Debug("create image") + + return image.NewRGBA(image.Rect(0, 0, int(t.Bins), int(t.Integrations))) +} + +func (t *TableComplex) IntegrateLines(lines []*LineComplex) *LineComplex { + + if len(lines) == 0 { + return nil + } + + masterline := lines[0] + for i, l := range lines { + if i > 0 { + masterline.AddSamples(l) + } + + } + + return masterline +} + +func (t *TableComplex) ColorAt(x, y int) color.Color { + + cell := t.Rows[y].Sample(x) + + hue_start := 236.0 + hue_end := 0.0 + + span := (t.Min - t.Max) * -1 + h_per_deg := (hue_start - hue_end) / span + pow_normalized := cell - t.Min + pow_degrees := pow_normalized * h_per_deg + hue := hue_start - pow_degrees + + return colorful.Hsv(hue, 1, 0.8) + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..16bdfb1 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "./gopow" + log "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "os" +) + +func main() { + + app := cli.NewApp() + app.Name = "RTL GoPow" + app.Usage = "Render a rtl_power CSV output as waterfall image" + app.Version = "0.0.1" + app.Author = "github.com/dhogborg" + app.Email = "d@hogborg.se" + + app.Action = func(c *cli.Context) { + + if c.Bool("verbose") == true { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.InfoLevel) + } + + pow, err := gopow.NewGoPow(c) + if err != nil { + log.WithFields(log.Fields{ + "error": err.Error(), + }).Fatal("load failed") + return + } + + err = pow.Render() + if err != nil { + log.WithFields(log.Fields{ + "error": err.Error(), + }).Fatal("render failed") + return + } + + err = pow.Write() + if err != nil { + log.WithFields(log.Fields{ + "error": err.Error(), + }).Fatal("write failed") + return + } + + } + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "input,i", + Value: "", + Usage: "CSV input file generated by rtl_power", + }, + cli.StringFlag{ + Name: "output,o", + Value: "", + Usage: "Output file, default same as input file with new extension", + }, + cli.StringFlag{ + Name: "format,f", + Value: "png", + Usage: "Output file format, default png", + }, + cli.IntFlag{ + Name: "downsample,d", + Value: 10, + Usage: "Downsample bandwidth by factor. Use if sampled bandwidth is unmanagable wide. 1 is 1:1, 10 is 1:10 and so on.", + }, + cli.BoolFlag{ + Name: "verbose", + Usage: "Enable more verbose output", + }, + } + + app.Run(os.Args) +}