Last active
June 25, 2016 05:56
-
-
Save sielicki/8cc79f0cb6a4b4c229b9786dffcabdbe 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> | |
<title>Websocket Scale</title> | |
<style> | |
svg { | |
font: 10px sans-serif; | |
color: #d3d3d3; | |
} | |
.line { | |
fill: none; | |
stroke: #ffffff; | |
stroke-width: 4px; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #ffffff; | |
shape-rendering: crispEdges; | |
} | |
/* Space out content a bit */ | |
body { | |
padding-top: 20px; | |
padding-bottom: 20px; | |
} | |
/* Everything but the jumbotron gets side spacing for mobile first views */ | |
.header, | |
.marketing, | |
.footer { | |
padding-right: 15px; | |
padding-left: 15px; | |
} | |
/* Custom page header */ | |
.header { | |
padding-bottom: 20px; | |
border-bottom: 1px solid #e5e5e5; | |
} | |
/* Make the masthead heading the same height as the navigation */ | |
.header h3 { | |
margin-top: 0; | |
margin-bottom: 0; | |
line-height: 40px; | |
} | |
/* Custom page footer */ | |
.footer { | |
padding-top: 19px; | |
color: #777; | |
border-top: 1px solid #e5e5e5; | |
} | |
/* Customize container */ | |
@media (min-width: 768px) { | |
.container { | |
max-width: 930px; | |
} | |
} | |
.container-narrow > hr { | |
margin: 30px 0; | |
} | |
/* Main marketing message and sign up button */ | |
.jumbotron { | |
text-align: center; | |
border-bottom: 1px solid #e5e5e5; | |
} | |
.jumbotron .btn { | |
padding: 14px 24px; | |
font-size: 21px; | |
} | |
/* Supporting marketing content */ | |
.marketing { | |
margin: 40px 0; | |
} | |
.marketing p + h4 { | |
margin-top: 28px; | |
} | |
/* Responsive: Portrait tablets and up */ | |
@media screen and (min-width: 768px) { | |
/* Remove the padding we set earlier */ | |
.header, | |
.marketing, | |
.footer { | |
padding-right: 0; | |
padding-left: 0; | |
} | |
/* Space out the masthead */ | |
.header { | |
margin-bottom: 30px; | |
} | |
/* Remove the bottom border on the jumbotron for visual effect */ | |
.jumbotron { | |
border-bottom: 0; | |
} | |
} | |
</style> | |
<!-- Bootstrap --> | |
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/darkly/bootstrap.min.css" rel="stylesheet" integrity="sha384-kVo/Eh0sv7ZdiwZK32nRsp1FrDT3sLRLx3zVpSSTI9UdO5H02LJNLBg5F1gwvKg0" crossorigin="anonymous"> | |
</head> | |
<body> | |
<centering> | |
<div class="container"> | |
<div class="header clearfix"> | |
<h3 class="text-muted">AS-350D via websockets</h3> | |
</div> | |
<div class="jumbotron"> | |
<div class="row" style="width: 100%; height: auto;"> | |
<h2 id="Readout">Loading...</h2> | |
</div> | |
<div class="row" id="graph" style="width: 100%; height: auto;"> | |
</div> | |
</div> | |
</div> <!-- /container --> | |
</centering> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script type="text/javascript"> | |
var ws = new WebSocket("ws://" + location.host + "/ws"); | |
var mostRecent = ""; | |
ws.onmessage = function (evt) | |
{ | |
var rec = JSON.parse(evt.data); | |
document.getElementById("Readout").innerHTML = rec.Pounds.toString() + " lbs. " + rec.Ounces.toString() + " oz."; | |
mostRecent = rec.Pounds + rec.Ounces / 16; | |
}; | |
ws.onclose = function() | |
{ | |
document.getElementById("Readout").innerHTML = "ERROR! Disconnected." | |
}; | |
var n = 40, | |
random = d3.random.normal(0, .2), | |
data = d3.range(n).map(random); | |
var margin = {top: 20, right: 20, bottom: 20, left: 40}, | |
width = 600 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var x = d3.scale.linear() | |
.domain([0, n - 1]) | |
.range([0, width]); | |
var y = d3.scale.linear() | |
.domain([0, 40]) | |
.range([height, 0]); | |
var line = d3.svg.line() | |
.x(function(d, i) { return x(i); }) | |
.y(function(d, i) { return y(d); }); | |
var svg = d3.select("#graph").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
svg.append("defs").append("clipPath") | |
.attr("id", "clip") | |
.append("rect") | |
.attr("width", width) | |
.attr("height", height); | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + y(0) + ")") | |
.call(d3.svg.axis().scale(x).orient("bottom")); | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(d3.svg.axis().scale(y).orient("left")); | |
var path = svg.append("g") | |
.attr("clip-path", "url(#clip)") | |
.append("path") | |
.datum(data) | |
.attr("class", "line") | |
.attr("d", line); | |
tick(); | |
function tick() { | |
// push a new data point onto the back | |
data.push(mostRecent); | |
// redraw the line, and slide it to the left | |
path | |
.attr("d", line) | |
.attr("transform", null) | |
.transition() | |
.duration(500) | |
.ease("linear") | |
.attr("transform", "translate(" + x(-1) + ",0)") | |
.each("end", tick); | |
// pop the old data point off the front | |
data.shift(); | |
} | |
</script> | |
</body> | |
</html> |
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 ( | |
"golang.org/x/net/websocket" | |
"github.com/tarm/serial" | |
"log" | |
"net/http" | |
"bufio" | |
"strconv" | |
"strings" | |
) | |
type Reading struct { | |
Pounds float64 | |
Ounces float64 | |
} | |
func newReading(str string) (r Reading){ | |
pounds, err := strconv.ParseFloat(strings.TrimSpace(string(str[3:6])), 64) | |
if err != nil { | |
log.Fatal(err) | |
} | |
ounces, err := strconv.ParseFloat(strings.TrimSpace(string(str[11:14])), 64) | |
if err != nil { | |
log.Fatal(err) | |
} | |
return Reading{Pounds: pounds, Ounces: ounces} | |
} | |
var Clients chan *websocket.Conn | |
var Readings chan Reading | |
func Read(){ | |
c := &serial.Config{Name: "/dev/ttyUSB0", Baud: 9600} | |
s, err := serial.OpenPort(c) | |
if err != nil { | |
log.Fatal("Can't open: ", err) | |
} | |
_, err = s.Write([]byte{0x0E}) | |
if err != nil { | |
log.Fatal("Can't write: ", err) | |
} | |
scanner := bufio.NewScanner(s) | |
splitfunc := func(data []byte, atEOF bool) (advance int, token []byte, err error){ | |
// flush until 0x02 | |
for start := 0; start < len(data); start++ { | |
if data[start] == 0x02 && !atEOF { | |
for idx, b := range data[start:] { | |
if b == 0x03 { | |
return idx+1, data[:idx], nil | |
} | |
} | |
} | |
} | |
return 0, nil, nil | |
} | |
scanner.Split(splitfunc) | |
for { | |
if ret := scanner.Scan(); ret != false { | |
Readings <- func(str string) (r Reading){ | |
pounds, err := strconv.ParseFloat(strings.TrimSpace(string(str[3:6])), 64) | |
if err != nil { | |
log.Fatal(err) | |
} | |
ounces, err := strconv.ParseFloat(strings.TrimSpace(string(str[11:14])), 64) | |
if err != nil { | |
log.Fatal(err) | |
} | |
return Reading{Pounds: pounds, Ounces: ounces} | |
}(scanner.Text()) | |
} | |
} | |
} | |
func Push(){ | |
cliPool := make(map[*websocket.Conn]*interface{}) | |
var curReading Reading | |
for { | |
select { | |
case newClient := <-Clients: | |
cliPool[newClient] = nil | |
log.Printf("New Client: %s\n", newClient.Request().Host) | |
case newReading := <-Readings: | |
curReading = newReading | |
log.Printf("New Reading: %f lbs, %f oz\n", | |
curReading.Pounds, curReading.Ounces) | |
log.Printf("SizeOf clipool: %d\n", len(cliPool)) | |
for client, _ := range cliPool { | |
go func(ws *websocket.Conn){ | |
err := websocket.JSON.Send(ws, newReading) | |
if err != nil { | |
log.Println("sendgorout: ", err) | |
delete(cliPool, ws) | |
} | |
}(client) | |
} | |
default: | |
// Nothing | |
} | |
} | |
} | |
func main(){ | |
http.Handle("/", http.FileServer(http.Dir("."))) | |
http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn){ | |
Clients <- ws | |
for { | |
_, err := ws.Read(nil) | |
if err != nil { | |
log.Println("handler: ", err) | |
return | |
} | |
} | |
})) | |
Readings = make(chan Reading, 1) | |
Clients = make(chan *websocket.Conn, 24) | |
go Read() | |
go Push() | |
err := http.ListenAndServe(":80", nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment