Basic UI working state. Still needs clean and Assetfs to keep all dependencies in the application

This commit is contained in:
Nicholas Thompson
2019-02-26 23:25:37 +02:00
committed by ncthompson
parent f189ff0442
commit 8267e71f18
11 changed files with 773 additions and 58 deletions

24
cmd/guimock/main.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"flag"
"log"
"net/http"
"github.com/hpdvanwyk/invertergui/webgui"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
addr := flag.String("addr", ":8080", "TCP address to listen on.")
mk2 := NewMk2Mock()
gui := webgui.NewWebGui(mk2)
http.Handle("/ws", http.HandlerFunc(gui.ServeHub))
http.Handle("/", gui)
http.Handle("/js/controller.js", http.HandlerFunc(gui.ServeJS))
http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP))
http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP))
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(*addr, nil))
}

90
cmd/guimock/mock.go Normal file
View File

@@ -0,0 +1,90 @@
package main
import (
"fmt"
"time"
"github.com/hpdvanwyk/invertergui/mk2if"
)
type mock struct {
c chan *mk2if.Mk2Info
}
func NewMk2Mock() mk2if.Mk2If {
tmp := &mock{
c: make(chan *mk2if.Mk2Info, 1),
}
go tmp.genMockValues()
return tmp
}
func genBaseLeds(state mk2if.LEDstate) map[mk2if.Led]mk2if.LEDstate {
return map[mk2if.Led]mk2if.LEDstate{
mk2if.LedMain: state,
mk2if.LedAbsorption: state,
mk2if.LedBulk: state,
mk2if.LedFloat: state,
mk2if.LedInverter: state,
mk2if.LedOverload: state,
mk2if.LedLowBattery: state,
mk2if.LedTemperature: state,
}
}
func (m *mock) GetMk2Info() *mk2if.Mk2Info {
return &mk2if.Mk2Info{
OutCurrent: 2.0,
InCurrent: 2.3,
OutVoltage: 230.0,
InVoltage: 230.1,
BatVoltage: 25,
BatCurrent: -10,
InFrequency: 50,
OutFrequency: 50,
ChargeState: 1,
Errors: nil,
Timestamp: time.Now(),
LEDs: genBaseLeds(mk2if.LedOff),
}
}
func (m *mock) C() chan *mk2if.Mk2Info {
return m.c
}
func (m *mock) Close() {
}
func (m *mock) genMockValues() {
mult := 1.0
ledState := mk2if.LedOff
for {
input := &mk2if.Mk2Info{
OutCurrent: 2.0 * mult,
InCurrent: 2.3 * mult,
OutVoltage: 230.0 * mult,
InVoltage: 230.1 * mult,
BatVoltage: 25 * mult,
BatCurrent: -10 * mult,
InFrequency: 50 * mult,
OutFrequency: 50 * mult,
ChargeState: 1 * mult,
Errors: nil,
Timestamp: time.Now(),
Valid: true,
LEDs: genBaseLeds(ledState),
}
ledState = (ledState + 1) % 3
mult = mult - 0.1
if mult < 0 {
mult = 1.0
}
fmt.Printf("Sending\n")
m.c <- input
time.Sleep(1 * time.Second)
}
}

View File

@@ -32,14 +32,15 @@ package main
import (
"flag"
"github.com/hpdvanwyk/invertergui/mk2if"
"github.com/hpdvanwyk/invertergui/webgui"
"github.com/mikepb/go-serial"
"github.com/prometheus/client_golang/prometheus/promhttp"
"io"
"log"
"net"
"net/http"
"github.com/hpdvanwyk/invertergui/mk2if"
"github.com/hpdvanwyk/invertergui/webgui"
"github.com/mikepb/go-serial"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
@@ -81,6 +82,8 @@ func main() {
gui := webgui.NewWebGui(mk2)
http.Handle("/", gui)
http.Handle("/ws", http.HandlerFunc(gui.ServeHub))
http.Handle("/js/controller.js", http.HandlerFunc(gui.ServeJS))
http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP))
http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP))
http.Handle("/metrics", promhttp.Handler())

280
frontend/index.html Normal file
View File

@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<link
rel="stylesheet"
href="https://bootswatch.com/4/darkly/bootstrap.min.css"
crossorigin="anonymous"
/>
<title>Victron Multiplus Monitor</title>
<style>
.dot-off {
height: 25px;
width: 25px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}
.dot-green {
height: 25px;
width: 25px;
background-color: #2aed4e;
border-radius: 50%;
display: inline-block;
}
.dot-red {
height: 25px;
width: 25px;
background-color: #ed3d34;
border-radius: 50%;
display: inline-block;
}
.blink-red {
height: 25px;
width: 25px;
background-color: #ed3d34;
border-radius: 50%;
display: inline-block;
animation: blinkingRedDot 1s infinite;
}
@keyframes blinkingRedDot {
0% {
background-color: #ed3d34;
}
49% {
background-color: transparent;
}
50% {
background-color: transparent;
}
99% {
background-color: #ed3d34;
}
100% {
background-color: #ed3d34;
}
}
.blink-green {
height: 25px;
width: 25px;
background-color: #2aed4e;
border-radius: 50%;
display: inline-block;
animation: blinkingGreenDot 1s infinite;
}
@keyframes blinkingGreenDot {
0% {
background-color: #2aed4e;
}
49% {
background-color: transparent;
}
50% {
background-color: transparent;
}
99% {
background-color: #2aed4e;
}
100% {
background-color: #2aed4e;
}
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="js/controller.js"></script>
<script type="text/javascript">
window.onload = function() {
loadContent();
};
</script>
<h1 class="display-4 text-center">Inverter GUI</h1>
<div class="container" id="app">
<div class="row">
<div class="col">
<hr />
</div>
</div>
<div class="row">
<div class="col-sm p-auto">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Output Current</h5>
<blockquote class="blockquote">{{ output_current }} A</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Output Voltage</h5>
<blockquote class="blockquote">{{ output_voltage }} V</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Output Frequency</h5>
<blockquote class="blockquote">
{{ output_frequency }} Hz
</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Output Power</h5>
<blockquote class="blockquote">{{ output_power }} W</blockquote>
</div>
</div>
</div>
<div class="col-sm p-auto">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Input Current</h5>
<blockquote class="blockquote">{{ input_current }} A</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Input Voltage</h5>
<blockquote class="blockquote">{{ input_voltage }} V</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Input Frequency</h5>
<blockquote class="blockquote">
{{ input_frequency }} Hz
</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Input Power</h5>
<blockquote class="blockquote">{{ input_power }} W</blockquote>
</div>
</div>
</div>
<div class="col-sm p-auto">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Battery Current</h5>
<blockquote class="blockquote">
{{ battery_current }} A
</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Battery Voltage</h5>
<blockquote class="blockquote">
{{ battery_voltage }} V
</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Battery Charge</h5>
<blockquote class="blockquote">{{ battery_charge }} %</blockquote>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Battery Power</h5>
<blockquote class="blockquote">{{ battery_power }} W</blockquote>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<hr />
</div>
</div>
<div class="row">
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Mains</h5>
<span v-bind:class="[led_mains]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Absorption</h5>
<span v-bind:class="[led_absorb]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Bulk</h5>
<span v-bind:class="[led_bulk]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center ">
<div class="card-body">
<h5 class="card-title">Float</h5>
<span v-bind:class="[led_float]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Inverter</h5>
<span v-bind:class="[led_inverter]"></span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Overload</h5>
<span v-bind:class="[led_overload]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Low Battery</h5>
<span v-bind:class="[led_bat_low]"></span>
</div>
</div>
</div>
<div class="col-sm p-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Temperature</h5>
<span v-bind:class="[led_over_temp]"></span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

69
frontend/js/controller.js Normal file
View File

@@ -0,0 +1,69 @@
function loadContent() {
var conn;
var app = new Vue({
el: "#app",
data: {
output_current: null,
output_voltage: 0,
output_frequency: 0,
output_power: 0,
input_current: 0,
input_voltage: 0,
input_frequency: 0,
input_power: 0,
battery_current: 0,
battery_voltage: 0,
battery_charge: 0,
battery_power: 0,
led_mains: "dot-off",
led_absorb: "dot-off",
led_bulk: "dot-off",
led_float: "dot-off",
led_inverter: "dot-off",
led_overload: "dot-off",
led_bat_low: "dot-off",
led_over_temp: "dot-off"
}
});
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/ws");
conn.onclose = function(evt) {
var item = document.createElement("a");
item.innerHTML = "<b>Connection closed.</b>";
};
conn.onmessage = function(evt) {
var update = JSON.parse(evt.data);
console.log(update);
app.output_current = update.output_current;
app.output_voltage = update.output_voltage;
app.output_frequency = update.output_frequency;
app.output_power = update.output_power;
app.input_current = update.input_current;
app.input_voltage = update.input_voltage;
app.input_frequency = update.input_frequency;
app.input_power = update.input_power;
app.battery_charge = update.battery_charge;
app.battery_voltage = update.battery_voltage;
app.battery_power = update.battery_power;
app.battery_current = update.battery_current;
leds = update.led_map;
app.led_mains = leds.led_mains;
app.led_absorb = leds.led_absorb;
app.led_bulk = leds.led_bulk;
app.led_float = leds.led_float;
app.led_inverter = leds.led_inverter;
app.led_overload = leds.led_overload;
app.led_bat_low = leds.led_bat_low;
app.led_over_temp = leds.led_over_temp;
};
} else {
var item = document.createElement("a");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
}
}

1
go.mod
View File

@@ -1,6 +1,7 @@
module github.com/hpdvanwyk/invertergui
require (
github.com/gorilla/websocket v1.4.0
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a
github.com/prometheus/client_golang v0.9.2
)

2
go.sum
View File

@@ -2,6 +2,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a h1:dCv1LIqgkmFyW9NTRX16FANqPkGcyCZAPV54TMGUV+I=

View File

@@ -30,16 +30,70 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package webgui
var htmlTemplate string = `<html>
var htmlTemplate string = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="5">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Victron Multiplus Monitor</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<div class="container">
<div class="row">
<div class="col-sm">
<div class="container">
<div class="card">
<div class="card-header">
Quote
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
</blockquote>
</div>
</div>
</div>
<div class="container">
<div class="card">
<div class="card-header">
Quote
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
</blockquote>
</div>
</div>
</div>
<div class="container">
<div class="card">
<div class="card-header">
Quote
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
</blockquote>
</div>
</div>
</div>
</div>
<div class="col-sm">
One of three columns
</div>
<div class="col-sm">
One of three columns
</div>
</div>
</div>
{{if .Error}} <p>Errors encountered: </p>
{{range .Error}}
<dt> {{.}}</dt>

View File

@@ -32,37 +32,41 @@ package webgui
import (
"fmt"
"github.com/hpdvanwyk/invertergui/mk2if"
"html/template"
"net/http"
"sync"
"time"
"github.com/hpdvanwyk/invertergui/mk2if"
"github.com/hpdvanwyk/invertergui/websocket"
)
const (
LedOff = "dot-off"
LedRed = "dot-red"
BlinkRed = "blink-red"
LedGreen = "dot-green"
BlinkGreen = "blink-green"
)
type WebGui struct {
respChan chan *mk2if.Mk2Info
stopChan chan struct{}
template *template.Template
muninRespChan chan muninData
poller mk2if.Mk2If
wg sync.WaitGroup
hub *websocket.Hub
pu *prometheusUpdater
}
func NewWebGui(source mk2if.Mk2If) *WebGui {
w := new(WebGui)
w.respChan = make(chan *mk2if.Mk2Info)
w.muninRespChan = make(chan muninData)
w.stopChan = make(chan struct{})
var err error
w.template, err = template.New("thegui").Parse(htmlTemplate)
if err != nil {
panic(err)
}
w.poller = source
w.pu = newPrometheusUpdater()
w.hub = websocket.NewHub()
w.wg.Add(1)
go w.dataPoll()
@@ -70,44 +74,55 @@ func NewWebGui(source mk2if.Mk2If) *WebGui {
}
type templateInput struct {
Error []error
Error []error `json:"errors"`
Date string
Date string `json:"date"`
OutCurrent string
OutVoltage string
OutPower string
OutCurrent string `json:"output_current"`
OutVoltage string `json:"output_voltage"`
OutPower string `json:"output_power"`
InCurrent string
InVoltage string
InPower string
InCurrent string `json:"input_current"`
InVoltage string `json:"input_voltage"`
InPower string `json:"input_power"`
InMinOut string
BatVoltage string
BatCurrent string
BatPower string
BatCharge string
BatVoltage string `json:"battery_voltage"`
BatCurrent string `json:"battery_current"`
BatPower string `json:"battery_power"`
BatCharge string `json:"battery_charge"`
InFreq string
OutFreq string
InFreq string `json:"input_frequency"`
OutFreq string `json:"output_frequency"`
/*
MainsLed string `json:"main_led"`
AbsorbLed string `json:"absorb_led"`
BulkLed string `json:"bulk_led"`
FloatLed string `json:"float_led"`
InverterLed string `json:"inverter_led"`
Leds []string
OverloadLed string `json:"overload_led"`
LowBatLed string `json:"low_bat_led"`
OverTempLed string `json:"over_temp_led"`
*/
LedMap map[string]string `json:"led_map"`
}
func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
statusErr := <-w.respChan
tmpInput := buildTemplateInput(statusErr)
err := w.template.Execute(rw, tmpInput)
if err != nil {
panic(err)
}
http.ServeFile(rw, r, "./frontend/index.html")
}
func ledName(nameInt int) string {
name, ok := mk2if.LedNames[nameInt]
func (w *WebGui) ServeJS(rw http.ResponseWriter, r *http.Request) {
http.ServeFile(rw, r, "./frontend/js/controller.js")
}
func (w *WebGui) ServeHub(rw http.ResponseWriter, r *http.Request) {
w.hub.ServeHTTP(rw, r)
}
func ledName(led mk2if.Led) string {
name, ok := mk2if.LedNames[led]
if !ok {
return "Unknown led"
}
@@ -121,24 +136,44 @@ func buildTemplateInput(status *mk2if.Mk2Info) *templateInput {
tmpInput := &templateInput{
Error: status.Errors,
Date: status.Timestamp.Format(time.RFC1123Z),
OutCurrent: fmt.Sprintf("%.3f", status.OutCurrent),
OutVoltage: fmt.Sprintf("%.3f", status.OutVoltage),
OutPower: fmt.Sprintf("%.3f", outPower),
InCurrent: fmt.Sprintf("%.3f", status.InCurrent),
InVoltage: fmt.Sprintf("%.3f", status.InVoltage),
InFreq: fmt.Sprintf("%.3f", status.InFrequency),
OutFreq: fmt.Sprintf("%.3f", status.OutFrequency),
InPower: fmt.Sprintf("%.3f", inPower),
OutCurrent: fmt.Sprintf("%.2f", status.OutCurrent),
OutVoltage: fmt.Sprintf("%.2f", status.OutVoltage),
OutPower: fmt.Sprintf("%.2f", outPower),
InCurrent: fmt.Sprintf("%.2f", status.InCurrent),
InVoltage: fmt.Sprintf("%.2f", status.InVoltage),
InFreq: fmt.Sprintf("%.2f", status.InFrequency),
OutFreq: fmt.Sprintf("%.2f", status.OutFrequency),
InPower: fmt.Sprintf("%.2f", inPower),
InMinOut: fmt.Sprintf("%.3f", inPower-outPower),
InMinOut: fmt.Sprintf("%.2f", inPower-outPower),
BatCurrent: fmt.Sprintf("%.3f", status.BatCurrent),
BatVoltage: fmt.Sprintf("%.3f", status.BatVoltage),
BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent),
BatCharge: fmt.Sprintf("%.3f", status.ChargeState*100),
BatCurrent: fmt.Sprintf("%.2f", status.BatCurrent),
BatVoltage: fmt.Sprintf("%.2f", status.BatVoltage),
BatPower: fmt.Sprintf("%.2f", status.BatVoltage*status.BatCurrent),
BatCharge: fmt.Sprintf("%.2f", status.ChargeState*100),
LedMap: map[string]string{},
}
for i := range status.LedListOn {
tmpInput.Leds = append(tmpInput.Leds, ledName(status.LedListOn[i]))
for k, v := range status.LEDs {
if k == mk2if.LedOverload || k == mk2if.LedTemperature || k == mk2if.LedLowBattery {
switch v {
case mk2if.LedOn:
tmpInput.LedMap[ledName(k)] = LedRed
case mk2if.LedBlink:
tmpInput.LedMap[ledName(k)] = BlinkRed
default:
tmpInput.LedMap[ledName(k)] = LedOff
}
} else {
switch v {
case mk2if.LedOn:
tmpInput.LedMap[ledName(k)] = LedGreen
case mk2if.LedBlink:
tmpInput.LedMap[ledName(k)] = BlinkGreen
default:
tmpInput.LedMap[ledName(k)] = LedOff
}
}
}
return tmpInput
}
@@ -160,8 +195,8 @@ func (w *WebGui) dataPoll() {
if s.Valid {
calcMuninValues(&muninValues, s)
w.pu.updatePrometheus(s)
w.hub.Broadcast(buildTemplateInput(s))
}
case w.respChan <- s:
case w.muninRespChan <- muninValues:
zeroMuninValues(&muninValues)
case <-w.stopChan:

93
websocket/client.go Normal file
View File

@@ -0,0 +1,93 @@
package websocket
import (
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// ServeWs handles websocket requests from the peer.
func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: h, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump()
}

64
websocket/hub.go Normal file
View File

@@ -0,0 +1,64 @@
package websocket
import (
"encoding/json"
)
// Hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
broadcast chan []byte
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
}
func NewHub() *Hub {
tmp := &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
go tmp.run()
return tmp
}
func (h *Hub) Broadcast(message interface{}) error {
payload, err := json.Marshal(message)
if err != nil {
return err
}
h.broadcast <- []byte(payload)
return nil
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}