Skip to content

Instantly share code, notes, and snippets.

@sbinet
Created February 8, 2018 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sbinet/a7a27eceec539cbd3871c8639802e90f to your computer and use it in GitHub Desktop.
Save sbinet/a7a27eceec539cbd3871c8639802e90f to your computer and use it in GitHub Desktop.
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>
`
@sbinet
Copy link
Author

sbinet commented Feb 14, 2018

screenshot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment