aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Ashby <martin@martin-laptop.lan>2018-05-18 21:09:14 +0100
committerMartin Ashby <martin@martin-laptop.lan>2018-05-18 21:09:14 +0100
commitb3e76b0207bc4232b4b72626f8c4c802dcc63802 (patch)
tree91ae10d4a80b1f458d1711a947ca71651baad5ab
parentc2faad3ac834f622956787c87713ccf6ab9b61e0 (diff)
downloadunicornpaint-b3e76b0207bc4232b4b72626f8c4c802dcc63802.tar.gz
unicornpaint-b3e76b0207bc4232b4b72626f8c4c802dcc63802.tar.bz2
unicornpaint-b3e76b0207bc4232b4b72626f8c4c802dcc63802.tar.xz
unicornpaint-b3e76b0207bc4232b4b72626f8c4c802dcc63802.zip
Server ported to go, 50%, just need to do the actual unicorn implementation.
-rw-r--r--.gitignore1
-rw-r--r--.vscode/launch.json8
-rw-r--r--Server.go241
-rw-r--r--Server_test.go85
-rw-r--r--Unicorn.go198
-rw-r--r--Unicorn_test.go80
-rw-r--r--package.json2
-rw-r--r--src/App.js2
8 files changed, 614 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index 85bf288..730ce45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
/saves/
/permasaves/
/__pycache__
+debug
# misc
.DS_Store
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8795aee..a791e75 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,10 +5,16 @@
"version": "0.2.0",
"configurations": [
{
+ "name": "Go Server",
+ "type": "go",
+ "request": "launch",
+ "program": "${workspaceRoot}/Server.go"
+ },
+ {
"name": "Python: Current File",
"type": "python",
"request": "launch",
- "program": "${file}"z
+ "program": "${file}"
},
{
"name": "Python: Attach",
diff --git a/Server.go b/Server.go
new file mode 100644
index 0000000..f25376c
--- /dev/null
+++ b/Server.go
@@ -0,0 +1,241 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/gorilla/websocket"
+ "github.com/veandco/go-sdl2/sdl"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+)
+
+var (
+ unicorn Unicorn
+
+ upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(*http.Request) bool { return true },
+ }
+ delCh = make(chan *websocket.Conn)
+ addCh = make(chan *websocket.Conn)
+ broadcastCh = make(chan interface{})
+ clients = map[*websocket.Conn]bool{}
+)
+
+// Types of commands
+type commandType string
+
+const (
+ noop commandType = "NO_OP"
+ setPixel commandType = "SET_PIXEL"
+ clear commandType = "CLEAR"
+ save commandType = "SAVE"
+ load commandType = "LOAD"
+)
+
+type Command struct {
+ Type commandType `json:"type"`
+ X uint8 `json:"x"`
+ Y uint8 `json:"y"`
+ R uint8 `json:"r"`
+ G uint8 `json:"g"`
+ B uint8 `json:"b"`
+ SaveName string `json:"saveName"`
+}
+
+const (
+ savesDir = "saves"
+)
+
+type State struct {
+ Saves []string `json:"saves"`
+ Pixels [][]uint8arr `json:"pixels"`
+}
+
+// This is a trick to avoid the JSON serializer from
+// interpreting uint8's as bytes and encoding them in base64
+type uint8arr []uint8
+
+func (u uint8arr) MarshalJSON() ([]byte, error) {
+ var result string
+ if u == nil {
+ result = "null"
+ } else {
+ result = strings.Join(strings.Fields(fmt.Sprintf("%d", u)), ",")
+ }
+ return []byte(result), nil
+}
+
+func getState() *State {
+ infos, err := ioutil.ReadDir(savesDir)
+ if err != nil {
+ log.Printf("Error reading saves dir %v", err)
+ }
+ saveFileNames := make([]string, len(infos))
+ for ix, info := range infos {
+ saveFileNames[ix] = info.Name()
+ }
+
+ // Irritating conversion function
+ pixels := unicorn.GetPixels()
+ width := unicorn.GetWidth()
+ height := unicorn.GetHeight()
+
+ px2 := make([][]uint8arr, width)
+ for x := uint8(0); x < width; x++ {
+ px2[x] = make([]uint8arr, height)
+ for y := uint8(0); y < height; y++ {
+ px2[x][y] = uint8arr(pixels[x][y])
+ }
+ }
+
+ return &State{
+ Pixels: px2,
+ Saves: saveFileNames,
+ }
+}
+
+func savePic(saveFileName string) {
+ pixels := unicorn.GetPixels()
+ data, err := json.Marshal(pixels)
+ if err != nil {
+ log.Printf("Failed to save picture to JSON %v", err)
+ return
+ }
+ err = ioutil.WriteFile(path.Join(savesDir, saveFileName), data, 0644)
+ if err != nil {
+ log.Printf("Failed to write to save file %v", err)
+ return
+ }
+}
+
+func loadPic(saveFileName string) {
+ data, err := ioutil.ReadFile(path.Join(savesDir, saveFileName))
+ if err != nil {
+ log.Printf("Failed to read file %v", err)
+ return
+ }
+
+ newPixels := [][][]uint8{}
+ err = json.Unmarshal(data, &newPixels)
+ if err != nil {
+ log.Printf("Failed to parse file %v", err)
+ return
+ }
+
+ width := len(newPixels)
+ height := len(newPixels[0])
+ for x := 0; x < width; x++ {
+ for y := 0; y < height; y++ {
+ r, g, b := rgb(newPixels[x][y])
+ unicorn.SetPixel(uint8(x), uint8(y), r, g, b)
+ }
+ }
+ unicorn.Show()
+}
+
+func upgradeHandler(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("Failed to upgrade someone %v", err)
+ return
+ }
+
+ // Add you to my list, remember to remove later
+ addCh <- conn
+ defer func() { delCh <- conn }()
+
+ // Get you up to speed
+ err = conn.WriteJSON(getState())
+ if err != nil {
+ log.Printf("Failed to send initial state %v", err)
+ return
+ }
+
+ // Read & execute commands in a loop until error
+ for {
+ cmd := Command{}
+ err = conn.ReadJSON(&cmd)
+ if err != nil {
+ log.Printf("Error reading from client %v", err)
+ break
+ }
+
+ switch cmd.Type {
+ case noop:
+ // Don't do anything
+ case setPixel:
+ unicorn.SetPixel(cmd.X, cmd.Y, cmd.R, cmd.G, cmd.B)
+ unicorn.Show()
+ case clear:
+ unicorn.Clear()
+ unicorn.Show()
+ case save:
+ savePic(cmd.SaveName)
+ case load:
+ loadPic(cmd.SaveName)
+ }
+ // Pretty much all commands demand a change of state,
+ // do a broadcast each time
+ broadcastCh <- getState()
+ }
+}
+
+func handleSdlEvents() {
+ running := true
+ for running {
+ for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
+ switch event.(type) {
+ case *sdl.QuitEvent:
+ running = false
+ break
+ }
+ }
+ }
+}
+
+func handleClients() {
+ for {
+ select {
+ case c := <-addCh:
+ clients[c] = true
+ case c := <-delCh:
+ delete(clients, c)
+ case msg := <-broadcastCh:
+ doBroadcast(msg)
+ }
+ }
+}
+
+func main() {
+ uni, err := GetUnicorn()
+ if err != nil {
+ log.Fatalf("Couldn't get a unicorn :( %v", err)
+ }
+ unicorn = uni
+
+ log.Println("Starting server on port 3001")
+ http.Handle("/", http.FileServer(http.Dir("build")))
+ http.HandleFunc("/ws", upgradeHandler)
+ go http.ListenAndServe(":3001", nil)
+ go handleClients()
+ handleSdlEvents()
+}
+
+func doBroadcast(obj interface{}) {
+ for client := range clients {
+ err := client.WriteJSON(obj)
+
+ if err != nil {
+ log.Printf("Error writing to client, closing %v", err)
+ client.Close()
+ delete(clients, client)
+ }
+ }
+}
+
+
diff --git a/Server_test.go b/Server_test.go
new file mode 100644
index 0000000..66ce45b
--- /dev/null
+++ b/Server_test.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+ "testing"
+ "encoding/json"
+ "reflect"
+)
+
+func TestStateJson(t *testing.T) {
+ testPixels := make([][]uint8arr, 16)
+ for x:=0; x<16; x++ {
+ testPixels[x] = make([]uint8arr, 16)
+ for y:=0; y<16; y++ {
+ testPixels[x][y] = uint8arr{0, 2, 3}
+ }
+ }
+
+ testSaves := []string{"bob", "sally", "blah"}
+ s1 := &State{
+ Pixels: testPixels,
+ Saves: testSaves,
+ }
+
+ data, err := json.Marshal(s1)
+ if err != nil {
+ t.Errorf("Failed to write state to JSON %v", err)
+ }
+
+ s2 := &State{}
+ err = json.Unmarshal(data, s2)
+ if err != nil {
+ t.Errorf("Failed to read state from JSON %v", err)
+ }
+
+ if !reflect.DeepEqual(s1, s2) {
+ t.Errorf("Differences after serializing state %v %v", s1, s2)
+ }
+}
+
+func TestCommandJson(t *testing.T) {
+ cmd := Command{
+ Type: noop,
+ X: uint8(1),
+ Y: uint8(1),
+ R: uint8(1),
+ G: uint8(1),
+ B: uint8(1),
+ SaveName: "testing",
+ }
+
+ data, err := json.Marshal(cmd)
+ if err != nil {
+ t.Errorf("Error encoding command to JSON")
+ }
+
+ cmd2 := Command{}
+ err = json.Unmarshal(data, &cmd2)
+ if err != nil {
+ t.Errorf("Error decoding command from JSON")
+ }
+
+ if !reflect.DeepEqual(cmd, cmd2) {
+ t.Errorf("Differences after encoding JSON %v %v", cmd, cmd2)
+ }
+
+ cmd3 := Command{}
+ testData := []byte(`{ "type": "SET_PIXEL", "x": 1, "y": 2, "r": 255, "g": 255, "b": 255 }`)
+ err = json.Unmarshal(testData, &cmd3)
+ if err != nil {
+ t.Errorf("Error unmarshalling test JSON %v %s", err, testData)
+ }
+
+ cmd4 := Command{
+ Type: setPixel,
+ X: uint8(1),
+ Y: uint8(2),
+ R: uint8(255),
+ G: uint8(255),
+ B: uint8(255),
+ }
+
+ if !reflect.DeepEqual(cmd3, cmd4) {
+ t.Errorf("Json unmarshalled incorrectly to %v", cmd4)
+ }
+} \ No newline at end of file
diff --git a/Unicorn.go b/Unicorn.go
new file mode 100644
index 0000000..2e0e73f
--- /dev/null
+++ b/Unicorn.go
@@ -0,0 +1,198 @@
+package main
+
+import (
+ "github.com/veandco/go-sdl2/sdl"
+ // "golang.org/x/exp/io/spi"
+ // "github.com/veandco/go-sdl2/sdl"
+)
+
+// Unicorn ...
+// Object representing the Unicorn HAT to be controlled
+type Unicorn interface {
+ // Not all unicorns are the same size
+ GetWidth() uint8
+ GetHeight() uint8
+
+ // Array of pixels, indexed x, then y, then color (rgb)
+ GetPixels() [][][]uint8
+
+ // Set an individual pixel
+ SetPixel(x, y, r, g, b uint8)
+
+ // Flip the display buffer
+ Show()
+
+ // Set all pixels back to black
+ Clear()
+
+ // Turns off the LEDs
+ Off()
+}
+
+// GetUnicorn ...
+// Get a unicorn. Tries to get you a real one,
+// if it can't find one then gives you a fake one.
+func GetUnicorn() (unicorn Unicorn, err error) {
+ // unicorn, err = NewReal()
+ // if err != nil {
+ // log.Println("Couldn't get a real unicorn, trying a fake one")
+ // unicorn, err = NewFake(int8(16), int8(16))
+ // }
+ unicorn, err = NewFake(uint8(16), uint8(16))
+ return
+}
+
+// FakeUnicorn ...
+// Shows an SDL window pretending to be a unicorn.
+type FakeUnicorn struct {
+ pixels [][][]uint8
+ displayWidth int32
+ displayHeight int32
+
+ window *sdl.Window
+ renderer *sdl.Renderer
+}
+
+// NewFake ...
+// Constructs a new fake unicorn out of paint and glue
+func NewFake(width, height uint8) (*FakeUnicorn, error) {
+ if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil {
+ return nil, err
+ }
+
+ unicorn := &FakeUnicorn{
+ pixels: makePixels(width, height),
+ window: nil,
+ renderer: nil,
+ displayWidth: 300,
+ displayHeight: 300,
+ }
+ if err := unicorn.createWindow(); err != nil {
+ unicorn.Close()
+ return nil, err
+ }
+ if err := unicorn.createRenderer(); err != nil {
+ unicorn.Close()
+ return nil, err
+ }
+ return unicorn, nil
+}
+
+func (f *FakeUnicorn) createWindow() error {
+ window, err := sdl.CreateWindow("Fake Unicorn",
+ sdl.WINDOWPOS_UNDEFINED,
+ sdl.WINDOWPOS_UNDEFINED,
+ f.displayWidth,
+ f.displayHeight,
+ sdl.WINDOW_SHOWN)
+ f.window = window
+ return err
+}
+
+func (f *FakeUnicorn) createRenderer() error {
+ renderer, err := sdl.CreateRenderer(f.window, -1, sdl.RENDERER_ACCELERATED)
+ f.renderer = renderer
+ return err
+}
+
+func (f *FakeUnicorn) Close() error {
+ if f.window != nil {
+ f.window.Destroy()
+ }
+ if f.renderer != nil {
+ f.renderer.Destroy()
+ }
+ return nil
+}
+
+func (f *FakeUnicorn) GetWidth() uint8 {
+ return uint8(len(f.pixels))
+}
+func (f *FakeUnicorn) GetHeight() uint8 {
+ if len(f.pixels) > 0 {
+ return uint8(len(f.pixels[0]))
+ }
+ return 0
+}
+func (f *FakeUnicorn) GetPixels() [][][]uint8 {
+ return f.pixels
+}
+func (f *FakeUnicorn) SetPixel(x, y, r, g, b uint8) {
+ f.pixels[x][y] = []uint8{r, g, b}
+}
+func (f *FakeUnicorn) Show() {
+ width, height := f.GetWidth(), f.GetHeight()
+ for x := uint8(0); x < width; x++ {
+ for y := uint8(0); y < height; y++ {
+ r, g, b := rgb(f.pixels[x][y])
+ if err := f.renderer.SetDrawColor(r, g, b, uint8(255)); err != nil {
+ panic(err)
+ }
+ cellWidth := f.displayWidth / int32(width)
+ cellHeight := f.displayHeight / int32(height)
+ if err := f.renderer.FillRect(&sdl.Rect{
+ X: cellWidth * int32(x),
+ Y: f.displayHeight - (cellHeight * int32(y)) - cellHeight, // SDL Y coordinate is from the top
+ W: cellWidth,
+ H: cellHeight,
+ }); err != nil {
+ panic(err)
+ }
+ }
+ }
+ f.renderer.Present()
+}
+
+func rgb(pixel []uint8) (uint8, uint8, uint8) {
+ return pixel[0], pixel[1], pixel[2]
+}
+
+func (f *FakeUnicorn) Clear() {
+ f.pixels = makePixels(f.GetWidth(), f.GetHeight())
+}
+func (f *FakeUnicorn) Off() {
+ f.Close()
+}
+
+func makePixels(width, height uint8) [][][]uint8 {
+ pixels := make([][][]uint8, width)
+ for x := uint8(0); x < width; x++ {
+ pixels[x] = make([][]uint8, height)
+ for y := uint8(0); y < height; y++ {
+ pixels[x][y] = []uint8{0, 0, 0}
+ }
+ }
+ return pixels
+}
+
+// RealUnicorn ...
+// A real one! *gasps*
+// type RealUnicorn struct {}
+
+// // NewReal ...
+// // Constructs a new real unicorn from fairy dust and sprinkles
+// func NewReal() (*RealUnicorn, error) {
+// return nil, errors.New("Couldn't make a real unicorn sorry")
+// }
+
+// func (u *RealUnicorn) GetWidth() int8 {
+// return 0
+// }
+// func (u *RealUnicorn) GetHeight() int8 {
+// return 0
+// }
+// func (u *RealUnicorn) GetPixels() [][][]int8 {
+// return nil
+// }
+// func (u *RealUnicorn) SetPixel(x, y, r, g, b int8) {
+
+// }
+// func (u *RealUnicorn) Show() {
+
+// }
+// func (u *RealUnicorn) Clear() {
+
+// }
+// func (u *RealUnicorn) Off() {
+
+// }
diff --git a/Unicorn_test.go b/Unicorn_test.go
new file mode 100644
index 0000000..4b06a73
--- /dev/null
+++ b/Unicorn_test.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestGetUnicorn(t *testing.T) {
+
+}
+
+func TestFakeUnicorn(t *testing.T) {
+ unicorn, err := NewFake(uint8(16), uint8(16))
+ if err != nil {
+ t.Errorf("Got an error making a fake unicorn, shouldn't happen")
+ }
+ defer unicorn.Close()
+
+ // Check simple functions
+ if unicorn.GetHeight() != 16 {
+ t.Errorf("Height was wrong, expecting 16")
+ }
+ if unicorn.GetWidth() != 16 {
+ t.Errorf("Width was wrong, expecting 16")
+ }
+ // Pixels should be black to start with
+ pixels := unicorn.GetPixels()
+ for x := uint8(0); x < 16; x++ {
+ for y := uint8(0); y < 16; y++ {
+ if !reflect.DeepEqual(pixels[x][y], []uint8{0, 0, 0}) {
+ t.Errorf("Expecting black pixels to start with")
+ }
+ }
+ }
+
+ // Should be able to set a pixel, no others should change
+ unicorn.SetPixel(0, 0, uint8(255), uint8(255), uint8(255))
+ pixels = unicorn.GetPixels()
+ if !reflect.DeepEqual(pixels[0][0], []uint8{255, 255, 255}) {
+ t.Errorf("Pixel wasn't set when it should be")
+ }
+ for x := uint8(0); x < 16; x++ {
+ for y := uint8(0); y < 16; y++ {
+ if x == 0 && y == 0 {
+ continue
+ }
+ if !reflect.DeepEqual(pixels[x][y], []uint8{0, 0, 0}) {
+ t.Errorf("Expecting black pixels to start with")
+ }
+ }
+ }
+
+ // Should be able to set a second pixel
+ unicorn.SetPixel(3, 4, uint8(4), uint8(5), uint8(6))
+ pixels = unicorn.GetPixels()
+ for x := uint8(0); x < 16; x++ {
+ for y := uint8(0); y < 16; y++ {
+ checkcolor := []uint8{0, 0, 0}
+ if x == 0 && y == 0 {
+ checkcolor = []uint8{255, 255, 255}
+ } else if x == 3 && y == 4 {
+ checkcolor = []uint8{4, 5, 6}
+ }
+ if !reflect.DeepEqual(pixels[x][y], checkcolor) {
+ t.Errorf("Got incorrect pixel color at %d %d", x, y)
+ }
+ }
+ }
+
+ unicorn.Show()
+ time.Sleep(time.Duration(500) * time.Millisecond)
+ unicorn.SetPixel(10, 10, uint8(255), uint8(255), uint8(0))
+ unicorn.Show()
+ time.Sleep(time.Duration(500) * time.Millisecond)
+
+ unicorn.SetPixel(0, 15, uint8(255), uint8(0), uint8(0))
+ unicorn.Show()
+ time.Sleep(time.Duration(500) * time.Millisecond)
+}
diff --git a/package.json b/package.json
index 745911b..6086efb 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"react-twitter-widgets": "^1.7.1"
},
"scripts": {
- "start": "export PORT=3001 && react-scripts start",
+ "start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
diff --git a/src/App.js b/src/App.js
index 2d4f40f..7083ba6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -145,7 +145,7 @@ class App extends Component {
_connectWebsocket() {
let webSocketProto = window.location.protocol === "https:" ? "wss:" : "ws:"
let host = window.location.host
- // let host = "shinypi:3001"
+ // let host = window.location.hostname + ":3001"
this._websocket = new WebSocket(`${webSocketProto}//${host}/ws`)
this._websocket.onmessage = this._onMessage
this._websocket.onopen = this._onOpen