Created
February 8, 2018 20:10
-
-
Save sbinet/a7a27eceec539cbd3871c8639802e90f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"bytes" | |
"encoding/base64" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"image/color" | |
"io" | |
"log" | |
"math" | |
"net/http" | |
"os" | |
"strings" | |
"time" | |
"go-hep.org/x/hep/hbook" | |
"go-hep.org/x/hep/hplot" | |
"golang.org/x/net/websocket" | |
"gonum.org/v1/plot" | |
"gonum.org/v1/plot/plotter" | |
"gonum.org/v1/plot/vg" | |
"gonum.org/v1/plot/vg/draw" | |
"gonum.org/v1/plot/vg/vgimg" | |
"gonum.org/v1/plot/vg/vgsvg" | |
) | |
const ( | |
nsensors = 8 | |
) | |
var ( | |
addrFlag = flag.String("addr", ":5555", "server address:port") | |
datac = make(chan plots) | |
// xticks defines how we convert and display time.Time values. | |
xticks = plot.TimeTicks{Format: "2006-01-02\n15:04"} | |
h1 = hbook.NewH1D(nsensors, 0.5, nsensors+0.5) | |
) | |
func main() { | |
flag.Parse() | |
done := make(chan bool) | |
go collect(datac, done) | |
http.HandleFunc("/", plotHandle) | |
http.Handle("/data", websocket.Handler(dataHandler)) | |
err := http.ListenAndServe(*addrFlag, nil) | |
if err != nil { | |
done <- true | |
log.Fatal(err) | |
} | |
} | |
func collect(datac chan plots, done chan bool) { | |
ticker := time.NewTicker(5 * time.Second) | |
defer ticker.Stop() | |
for { | |
select { | |
case <-ticker.C: | |
plots, err := parse() | |
if err != nil { | |
log.Printf("could not parse input data: %v", err) | |
continue | |
} | |
datac <- plots | |
case <-done: | |
return | |
} | |
} | |
} | |
type plots struct { | |
Sensors map[string]string `json:"sensors"` | |
} | |
func parse() (plots, error) { | |
var ps plots | |
f, err := os.Open("data.txt") | |
if err != nil { | |
return ps, err | |
} | |
defer f.Close() | |
sc := bufio.NewScanner(f) | |
if !sc.Scan() { | |
return ps, io.ErrUnexpectedEOF | |
} | |
line := sc.Text() | |
const ( | |
timeFormat = "2006-01-02 15:04:05.999999" | |
startHeader = "program start: " | |
endHeader = "program end: " | |
) | |
if !strings.HasPrefix(line, startHeader) { | |
return ps, fmt.Errorf("missing program start header") | |
} | |
start, err := time.Parse(timeFormat, line[len(startHeader):]) | |
if err != nil { | |
return ps, fmt.Errorf("could not parse start header: %v", err) | |
} | |
sensors := Sensors{ | |
sensors: []Sensor{ | |
Sensor{name: "sensor-1"}, | |
Sensor{name: "sensor-2"}, | |
Sensor{name: "sensor-3"}, | |
Sensor{name: "sensor-4"}, | |
Sensor{name: "sensor-5"}, | |
Sensor{name: "sensor-6"}, | |
Sensor{name: "sensor-7"}, | |
Sensor{name: "sensor-8"}, | |
}, | |
} | |
if !sc.Scan() { | |
return ps, fmt.Errorf("could not scan start-footer") | |
} | |
line = sc.Text() | |
if line != "" { | |
return ps, fmt.Errorf("invalid start-footer: %q", line) | |
} | |
for { | |
err := sensors.parseBlock(sc) | |
if err != nil { | |
if err == io.EOF { | |
break | |
} | |
return ps, fmt.Errorf("could not parse data block: %v", err) | |
} | |
} | |
line = sc.Text() | |
if !strings.HasPrefix(line, endHeader) { | |
return ps, fmt.Errorf("missing program end header") | |
} | |
log.Printf("parsed %v", start) | |
log.Printf("#data: %d", len(sensors.sensors[0].xs)) | |
const N = 1000 | |
if len(sensors.sensors[0].xs) > N { | |
for i, v := range sensors.sensors { | |
sensors.sensors[i].xs = v.xs[len(v.xs)-N:] | |
sensors.sensors[i].ys = v.ys[len(v.ys)-N:] | |
} | |
} | |
return sensors.plots() | |
} | |
type Sensor struct { | |
name string | |
xs []float64 | |
ys []float64 | |
} | |
type Sensors struct { | |
sensors []Sensor | |
} | |
func (s *Sensors) parseBlock(sc *bufio.Scanner) error { | |
var err error | |
const ( | |
startHeader = "read start: " | |
stopHeader = "read stop: " | |
endHeader = "program end: " | |
timeFormat = "2006-01-02 15:04:05.999999" | |
) | |
if !sc.Scan() { | |
return fmt.Errorf("could not scan start-block header") | |
} | |
line := sc.Text() | |
log.Printf("start> %q", line) | |
switch { | |
case strings.HasPrefix(line, startHeader): | |
// ok. | |
case strings.HasPrefix(line, endHeader): | |
return io.EOF | |
default: | |
return fmt.Errorf("invalid start-block header: %q", line) | |
} | |
start, err := time.Parse(timeFormat, line[len(startHeader):]) | |
if err != nil { | |
return fmt.Errorf("could not parse start-block header: %v", err) | |
} | |
if !sc.Scan() { | |
return fmt.Errorf("could not scan start-block header") | |
} | |
line = sc.Text() | |
log.Printf("stop> %q", line) | |
if !strings.HasPrefix(line, stopHeader) { | |
return fmt.Errorf("invalid stop-block header") | |
} | |
stop, err := time.Parse(timeFormat, line[len(stopHeader):]) | |
if err != nil { | |
return fmt.Errorf("could not parse stop-block header: %v", err) | |
} | |
if !sc.Scan() { | |
return fmt.Errorf("could not scan data block") | |
} | |
line = sc.Text() | |
log.Printf("data> %q", line) | |
// hack, re-use JSON parser because we are lazy... | |
raw := struct { | |
Data [4][8]float64 | |
}{} | |
err = json.NewDecoder(bytes.NewReader([]byte(line))).Decode(&raw.Data[0]) | |
if err != nil { | |
return err | |
} | |
xstart := float64(start.UTC().Unix()) | |
// FIXME: we only use the first block of data | |
for i, v := range raw.Data[0] { | |
switch v { | |
case 0: | |
v = 1 | |
case 1: | |
v = 0 | |
} | |
s.sensors[i].xs = append(s.sensors[i].xs, xstart) | |
s.sensors[i].ys = append(s.sensors[i].ys, v) | |
h1.Fill(float64(i), v) | |
} | |
if !sc.Scan() { | |
return fmt.Errorf("could not scan block footer") | |
} | |
line = sc.Text() | |
if line != "" { | |
return fmt.Errorf("invalid block footer: %q", line) | |
} | |
log.Printf("parse block %v -> %v", start, stop) | |
return err | |
} | |
func (s *Sensors) plots() (plots, error) { | |
var ( | |
ps = plots{Sensors: make(map[string]string)} | |
err error | |
) | |
for _, v := range s.sensors { | |
ps.Sensors[v.name] = v.plot() | |
} | |
ps.Sensors["sensors"] = makePlot(h1) | |
return ps, err | |
} | |
func makePlot(h *hbook.H1D) string { | |
p := hplot.New() | |
p.X.Label.Text = "Sensors" | |
hh := hplot.NewH1D(h) | |
hh.Color = color.RGBA{B: 255, A: 255} | |
p.Add(hh) | |
p.Add(hplot.NewGrid()) | |
return renderPNG(p) | |
} | |
func (s *Sensor) plot() string { | |
p := hplot.New() | |
p.X.Label.Text = "Time" | |
p.X.Tick.Marker = xticks | |
p.Add(hplot.NewGrid()) | |
data := hplot.ZipXY(s.xs, s.ys) | |
line, points, err := plotter.NewLinePoints(data) | |
if err != nil { | |
panic(err) | |
} | |
line.Color = color.RGBA{R: 255, A: 255} | |
points.Color = line.Color | |
points.Shape = draw.CircleGlyph{} | |
p.Add(line, points) | |
return renderPNG(p) | |
} | |
func renderPNG(p *hplot.Plot) string { | |
size := 10 * vg.Centimeter | |
canvas := vgimg.New(size, size/vg.Length(math.Phi)) | |
p.Draw(draw.New(canvas)) | |
out := new(bytes.Buffer) | |
_, err := vgimg.PngCanvas{canvas}.WriteTo(out) | |
if err != nil { | |
panic(err) | |
} | |
return base64.StdEncoding.EncodeToString(out.Bytes()) | |
} | |
func renderSVG(p *hplot.Plot) string { | |
size := 10 * vg.Centimeter | |
canvas := vgsvg.New(size, size/vg.Length(math.Phi)) | |
p.Draw(draw.New(canvas)) | |
out := new(bytes.Buffer) | |
_, err := canvas.WriteTo(out) | |
if err != nil { | |
panic(err) | |
} | |
return string(out.Bytes()) | |
} | |
func plotHandle(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, page) | |
} | |
func dataHandler(ws *websocket.Conn) { | |
for data := range datac { | |
err := websocket.JSON.Send(ws, data) | |
if err != nil { | |
log.Printf("error sending data: %v\n", err) | |
return | |
} | |
} | |
} | |
const page = ` | |
<html> | |
<head> | |
<title>Plotting stuff with gonum/plot</title> | |
<script type="text/javascript"> | |
var sock = null; | |
var plots = {}; | |
function update() { | |
for (i=0; i<8; i++) { | |
var id = "sensor-" + (i+1); | |
console.log("id: "+id); | |
var p = document.getElementById(id); | |
p.src = "data:image/png;base64,"+plots[id]; | |
} | |
var p = document.getElementById("sensors"); | |
p.src = "data:image/png;base64,"+plots["sensors"]; | |
}; | |
window.onload = function() { | |
sock = new WebSocket("ws://"+location.host+"/data"); | |
sock.onmessage = function(event) { | |
var data = JSON.parse(event.data); | |
// console.log("data: "+JSON.stringify(data)); | |
plots = data.sensors; | |
update(); | |
}; | |
}; | |
</script> | |
<style> | |
.my-plot-style { | |
width: 400px; | |
height: 200px; | |
font-size: 14px; | |
line-height: 1.2em; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="header"> | |
<h2>Plots</h2> | |
</div> | |
<div id="content"> | |
<img id="sensors" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-1" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-2" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-3" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-4" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-5" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-6" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-7" class="my-plot-style" src="" alt="N/A"/> | |
<br> | |
<img id="sensor-8" class="my-plot-style" src="" alt="N/A"/> | |
</div> | |
</body> | |
</html> | |
` |
Author
sbinet
commented
Feb 14, 2018
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment