~jshsj/go_quiz

7f7816a2099f76fc98c4ffe1f167b7464521ca4f — JSH 1 year, 8 months ago master
initial commit
11 files changed, 615 insertions(+), 0 deletions(-)

A .gitignore
A Gopkg.lock
A Gopkg.toml
A README.md
A breadboard.dts
A client.go
A compile_dts.sh
A go_buzzer
A home.html
A hub.go
A main.go
A  => .gitignore +2 -0
@@ 1,2 @@
static/
vendor/
\ No newline at end of file

A  => Gopkg.lock +17 -0
@@ 1,17 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.


[[projects]]
  digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
  name = "github.com/gorilla/websocket"
  packages = ["."]
  pruneopts = "UT"
  revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
  version = "v1.4.0"

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  input-imports = ["github.com/gorilla/websocket"]
  solver-name = "gps-cdcl"
  solver-version = 1

A  => Gopkg.toml +30 -0
@@ 1,30 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
#   name = "github.com/user/project"
#   version = "1.0.0"
#
# [[constraint]]
#   name = "github.com/user/project2"
#   branch = "dev"
#   source = "github.com/myfork/project2"
#
# [[override]]
#   name = "github.com/x/y"
#   version = "2.4.0"
#
# [prune]
#   non-go = false
#   go-tests = true
#   unused-packages = true


[prune]
  go-tests = true
  unused-packages = true

A  => README.md +11 -0
@@ 1,11 @@
### Raspberry Pi Websocket Proof of Concept

A proof of concept game, supposed to be controlled by three buttons on a raspberry pi.

It uses a websocket the broadcast the screen to multiple monitors.

`breadboard.dts` is meant to be compiled on the raspberry pi (using the script `compile_dts.sh`) to make the breadboard buttons emulate normal keystrokes.

##### Requirements

`github.com/gorilla/websocket`
\ No newline at end of file

A  => breadboard.dts +32 -0
@@ 1,32 @@
    /dts-v1/;
    /plugin/;
    / {
       compatible = "brcm,bcm2835", "brcm,bcm2708", "brcm,bcm2709";
       
       fragment@0 {
          target-path = "/";
          __overlay__ {
             keypad: breadboard_keys {
                compatible = "gpio-keys";
                #address-cells = <1>;
                #size-cells = <0>;
		autorepeat;
                button@22 {
                   label = "breadboard 3";
                   linux,code = <4>;
                   gpios = <&gpio 22 1>;
                };
                button@17 {
                   label = "breadboard 1";
                   linux,code = <2>;
                   gpios = <&gpio 17 1>;
                };
                button@27 {
                   label = "breadboard 2";
                   linux,code = <3>;
                   gpios = <&gpio 27 1>;
                };
             };
          };
       };
    };
\ No newline at end of file

A  => client.go +137 -0
@@ 1,137 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"bytes"
	"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 (
	newline = []byte{'\n'}
	space   = []byte{' '}
)

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
}

// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
	for {
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break
		}
		message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
		c.hub.broadcast <- message
	}
}

// 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)

			// Add queued chat messages to the current websocket message.
			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-c.send)
			}

			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 serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{hub: hub, 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()
	go client.readPump()
}

A  => compile_dts.sh +2 -0
@@ 1,2 @@
#! /bin/sh
dtc -I dts -O dtb -o /boot/overlays/breadboard.dtbo breadboard.dts
\ No newline at end of file

A  => go_buzzer +0 -0

A  => home.html +139 -0
@@ 1,139 @@
<!DOCTYPE html>
<html lang="en">

    <head>
        <title>Chat Example</title>
        <script type="text/javascript">
            window.onload = function () {
                const START = 0;
                const QUESTIONS = 1;
                const RESULT = 2;

                var conn;
                let images = document.querySelectorAll(".initial-image");
                let defaultImage = document.querySelector(".default-image");
                let state = START;
                // let timer = null;
                // let countdown = 30;
                // let conn = null;
                // let joined = false;

                // document.getElementById("form").onsubmit = function () {
                //     if (!conn) {
                //         return false;
                //     }
                //     if (!msg.value) {
                //         return false;
                //     }
                //     conn.send(msg.value);
                //     msg.value = "";
                //     return false;
                // };

                document.addEventListener("keydown", (e) => {
                    switch (e.key) {
                        case "1": send_event("gut"); break;
                        case "2": send_event("unsicher"); break;
                        case "3": send_event("schlecht"); break;
                        default: break;
                    }
                });

                function send_event(answer) {
                    console.log("Sending" + answer);
                    conn.send(answer);
                }

                if (window["WebSocket"]) {
                    conn = new WebSocket("ws://" + document.location.host + "/ws");
                    conn.onclose = function (evt) {
                        //CLOSE
                    };
                    conn.onmessage = function (evt) {
                        handleMessage(evt.data)
                    };
                } else {
                    var item = document.createElement("div");
                    item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
                    appendLog(item);
                }

                function handleMessage(message) {
                    console.log(message);
                    if (message.startsWith("Start")) {
                        showStart();
                    } else if (message.startsWith("Question")) {
                        let num = message.split(" ")[1];
                        showQuestion(num);
                    } else if (message.startsWith("Result")) {
                        let num = message.split(" ")[1];
                        showResult(num);
                    }
                }

                function showStart() {
                    clearActive();
                    defaultImage.classList.add("active");
                }

                function showQuestion(num) {
                    clearActive();
                    images[num].classList.add("active");
                }

                function showResult(num) {
                    clearActive();
                    //results[num].classList.add("active");

                }

                function clearActive() {
                    let active = document.querySelector(".active");
                    if (active != null) {
                        active.classList.remove("active");
                    }
                }
            };
        </script>
        <style type="text/css">
            html {
                overflow: hidden;
            }

            body {
                overflow: hidden;
                padding: 0;
                margin: 0;
                width: 100%;
                height: 100%;
                background: gray;
            }


            .initial-image {}

            .image {
                opacity: 0;
                position: absolute;
                top: 0;
                left: 0;
            }


            .active {
                opacity: 1;
            }
        </style>
    </head>

    <body>
        <img class="default-image image" src="/static/images/mountains.png">
        <img class="initial-image image" id="initial-1" src="/static/images/image_0.jpg">
        <img class="initial-image image" id="initial-2" src="/static/images/image_1.jpg">
        <img class="initial-image image" id="initial-3" src="/static/images/image_2.jpg">
        <img class="initial-image image" id="initial-4" src="/static/images/image_3.jpg">
        <img class="initial-image image" id="initial-5" src="/static/images/image_4.jpg">
        <img class="initial-image image" id="initial-6" src="/static/images/image_5.jpg">
    </body>

</html>
\ No newline at end of file

A  => hub.go +195 -0
@@ 1,195 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"fmt"
	"log"
	"os"
	"strings"
	"time"
)

// 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

	appstate ApplicationState

	timer *time.Timer

	started bool
}

const FILENAME = "./savefile"

const (
	START     = 0
	QUESTIONS = 1
	RESULT    = 2
)

const (
	GUT      = 0
	UNSICHER = 1
	SCHLECHT = 2
)

const TIMERDURATION = 5

// ApplicationState keeps the state of the app
type ApplicationState struct {
	counter int
	state   int
	results [5]int
}

func (s *ApplicationState) reset() {
	s.counter = 0
	s.state = START
	s.results = [5]int{0, 0, 0, 0, 0}
	log.Println("reset")
}

func (s *ApplicationState) addAnswer(answer int) {
	s.results[s.counter] = answer
	s.counter++

	if s.counter >= len(s.results) {
		s.state = RESULT
	}
}

func (s *ApplicationState) save() {
	log.Println("saving")
	f, err := os.OpenFile(FILENAME, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		panic(err)
	}

	defer f.Close()
	result := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(s.results)), " "), "[]") + "\n"
	if _, err = f.WriteString(result); err != nil {
		panic(err)
	}

}

func (s *ApplicationState) getResult() int {
	return 0
}

func (s *ApplicationState) asMessage() []byte {
	var message string
	switch s.state {
	case START:
		message = "Start"
	case QUESTIONS:
		message = fmt.Sprintf("Question %d", s.counter)
	case RESULT:
		message = fmt.Sprintf("Result %d", s.getResult())
		s.save()
		s.reset()
	}
	return []byte(message)
}

func newHub() *Hub {
	broadcast := make(chan []byte)
	appstate := ApplicationState{0, START, [5]int{0, 0, 0, 0, 0}}
	return &Hub{
		broadcast:  broadcast,
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
		appstate:   appstate,
		started:    false,
	}
}

func (h *Hub) handleMessage(message []byte) []byte {
	incoming := string(message)
	var msg []byte

	if incoming == "timeout" {
		h.appstate.reset()
		log.Print("timeout")
	} else if h.appstate.state == START {
		log.Print("start")
		h.appstate.state = QUESTIONS
	} else {
		switch incoming {
		case "gut":
			h.appstate.addAnswer(GUT)
		case "unsicher":
			h.appstate.addAnswer(UNSICHER)
		case "schlecht":
			h.appstate.addAnswer(SCHLECHT)
		default:
			break
		}
	}
	if h.appstate.state != START {
		if h.timer == nil {
			h.startTimer()
		}
		h.timer.Stop()
		h.startTimer()
	}

	msg = []byte(h.appstate.asMessage())
	return msg
}

func (h *Hub) startTimer() {
	h.timer = time.AfterFunc(TIMERDURATION*time.Second, func() {
		h.broadcast <- []byte("timeout")
	})
	//defer h.timer.Stop()
}

func (h *Hub) run() {
	for {
		select {
		case client := <-h.register:
			h.clients[client] = true
			// Send state to new clients
			client.send <- h.appstate.asMessage()
		case client := <-h.unregister:
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast:
			msg := h.handleMessage(message)
			// Send to all clients
			for client := range h.clients {
				select {
				case client.send <- msg:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
			//Timeout

		}
	}
}

func timeout(broadcast chan []byte) {
	broadcast <- []byte("timeout")
}

A  => main.go +50 -0
@@ 1,50 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"flag"
	"log"
	"net/http"
	"os/exec"
)

var addr = flag.String("addr", ":8080", "http service address")

func serveHome(w http.ResponseWriter, r *http.Request) {
	log.Println(r.URL)
	if r.URL.Path != "/" {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}
	if r.Method != "GET" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	http.ServeFile(w, r, "home.html")
}

func main() {
	flag.Parse()
	hub := newHub()
	go hub.run()
	http.HandleFunc("/", serveHome)
	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		serveWs(hub, w, r)
	})
	//Open browser
	cmd := exec.Command("google-chrome-stable", "--kiosk", "http://localhost:8080")
	errCmd := cmd.Start()
	if errCmd != nil {
		log.Fatal(errCmd)
	}
	//Start server
	fs := http.FileServer(http.Dir("static/"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))
	err := http.ListenAndServe(*addr, nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}