From b3e76b0207bc4232b4b72626f8c4c802dcc63802 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Fri, 18 May 2018 21:09:14 +0100 Subject: Server ported to go, 50%, just need to do the actual unicorn implementation. --- .gitignore | 1 + .vscode/launch.json | 8 +- Server.go | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Server_test.go | 85 ++++++++++++++++++ Unicorn.go | 198 ++++++++++++++++++++++++++++++++++++++++++ Unicorn_test.go | 80 +++++++++++++++++ package.json | 2 +- src/App.js | 2 +- 8 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 Server.go create mode 100644 Server_test.go create mode 100644 Unicorn.go create mode 100644 Unicorn_test.go 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 @@ -4,11 +4,17 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "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 -- cgit v1.2.3-ZIG