From 7888c5a7d1eb93b9d4da24caa6e1350eaf6a5048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedek=20L=C3=A1szl=C3=B3?= Date: Thu, 13 Jun 2024 19:25:45 +0200 Subject: [PATCH] continuous updates --- api/endpoints.go | 3 ++ api/read/read.go | 82 ++++++++++++++++++++++++++++++++++++++++-- api/structs/structs.go | 5 +++ api/write/write.go | 8 ++--- storage/storage.go | 9 +---- storage/tile.go | 6 ++-- web/index.html | 4 +-- web/place.js | 30 +++++++++------- 8 files changed, 117 insertions(+), 30 deletions(-) diff --git a/api/endpoints.go b/api/endpoints.go index 106f6ae..9b4d38a 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "path/filepath" "time" "git.tek.govt.hu/dowerx/place/api/read" @@ -14,6 +15,7 @@ func Listen(address string, staticPath string) { router.GET("/info", read.Info) router.GET("/tile", read.Tile) + router.GET("/updates", read.Continuous) router.POST("/set", write.Pixel) router.OPTIONS("/set", func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") @@ -24,6 +26,7 @@ func Listen(address string, staticPath string) { }) if staticPath != "" { + router.StaticFile("/", filepath.Join(staticPath, "index.html")) router.Static("/static", staticPath) } diff --git a/api/read/read.go b/api/read/read.go index fd55bb1..66cb52f 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -1,14 +1,37 @@ package read import ( - "errors" + "log" + "sync" + "time" "git.tek.govt.hu/dowerx/place/api/structs" "git.tek.govt.hu/dowerx/place/config" "git.tek.govt.hu/dowerx/place/storage" "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" ) +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +type ConnectionsMap struct { + sync.Mutex + Items map[*websocket.Conn]chan structs.Pixel +} + +func (c *ConnectionsMap) Send(pixel structs.Pixel) { + c.Lock() + for _, v := range c.Items { + v <- pixel + } + c.Unlock() +} + +var Connections ConnectionsMap = ConnectionsMap{Items: make(map[*websocket.Conn]chan structs.Pixel)} + func Info(c *gin.Context) { conf := config.GetConfig() c.JSON(200, gin.H{ @@ -37,5 +60,60 @@ func Tile(c *gin.Context) { } func Continuous(c *gin.Context) { - panic(errors.New("unimplomented")) + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + + Connections.Lock() + Connections.Items[conn] = make(chan structs.Pixel) + Connections.Unlock() + + conn.SetReadDeadline(time.Now().Add(time.Second * 60)) + conn.SetPongHandler(func(string) error { + conn.SetReadDeadline(time.Now().Add(time.Second * 60)) + return nil + }) + + // send pings + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for range ticker.C { + if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second)); err != nil { + return + } + } + }() + + // defer: close connection, remove channel + defer func() { + Connections.Lock() + for k, v := range Connections.Items { + if k == conn { + close(v) + delete(Connections.Items, k) + break + } + } + Connections.Unlock() + conn.Close() + log.Println("removed connection") + }() + + // send updates + go func() { + for pixel := range Connections.Items[conn] { + conn.WriteJSON(pixel) + } + }() + + // read loop to keep the connetion alive + for { + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } } diff --git a/api/structs/structs.go b/api/structs/structs.go index 8937ca8..9a6de48 100644 --- a/api/structs/structs.go +++ b/api/structs/structs.go @@ -11,6 +11,11 @@ type Color struct { B uint32 `form:"b" json:"b"` } +type Pixel struct { + Coordinates + Color +} + func (c *Color) RGBA() (r, g, b, a uint32) { return c.R, c.G, c.B, 65535 } diff --git a/api/write/write.go b/api/write/write.go index 46ea9ba..aaf12ea 100644 --- a/api/write/write.go +++ b/api/write/write.go @@ -3,6 +3,7 @@ package write import ( "errors" + "git.tek.govt.hu/dowerx/place/api/read" "git.tek.govt.hu/dowerx/place/api/structs" "git.tek.govt.hu/dowerx/place/auth" "git.tek.govt.hu/dowerx/place/config" @@ -13,10 +14,7 @@ import ( var timeout int = config.GetConfig().Timeout func Pixel(c *gin.Context) { - var info struct { - structs.Color - structs.Coordinates - } + var info structs.Pixel if c.ShouldBind(&info) != nil { c.AbortWithStatus(400) return @@ -38,6 +36,8 @@ func Pixel(c *gin.Context) { return } + go read.Connections.Send(info) + c.Status(200) } diff --git a/storage/storage.go b/storage/storage.go index 1323721..c4b3331 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -6,7 +6,6 @@ import ( "fmt" "image" "image/color" - "log" "path/filepath" "time" ) @@ -34,13 +33,7 @@ func Load(path string, canvasSize int, tileSize int) { for x := range tiles[y] { err := tiles[y][x].load(filepath.Join(path, fmt.Sprintf("%d-%d.png", x, y)), tileSize) if err != nil { - tiles[y][x].fill() - if err = tiles[y][x].save(); err != nil { - panic(err) - } - log.Printf("Created tile (%d-%d)\n", x, y) - } else { - log.Printf("Loaded tile (%d-%d)\n", x, y) + panic(err) } } } diff --git a/storage/tile.go b/storage/tile.go index 17eb3fc..3db71cb 100644 --- a/storage/tile.go +++ b/storage/tile.go @@ -19,13 +19,15 @@ func (t *tile) load(path string, tileSize int) error { t.path = path file, err := os.Open(path) if err != nil { - return err + t.fill() + return t.save() } defer file.Close() img, _, err := image.Decode(file) if err != nil { - return err + t.fill() + return t.save() } draw.Draw(t.image, img.Bounds(), img, img.Bounds().Min, draw.Src) diff --git a/web/index.html b/web/index.html index e131eb8..a6e5357 100644 --- a/web/index.html +++ b/web/index.html @@ -4,8 +4,8 @@ Place - - + + diff --git a/web/place.js b/web/place.js index d9cf314..56f7b39 100644 --- a/web/place.js +++ b/web/place.js @@ -1,4 +1,5 @@ -const apiBase = "http://localhost:8080"; +const apiBase = "localhost:8080"; +const ssl = false; var tiles = []; var color = { @@ -8,16 +9,16 @@ var color = { }; async function getInfo() { - let response = await fetch(`${apiBase}/info`); + let response = await fetch(`${ssl ? "https" : "http"}://${apiBase}/info`); return await response.json(); } -async function setPixel(canvas, ctx, x, y, c) { +async function sendPixel(canvas, x, y, c) { let bounds = canvas.getBoundingClientRect(); x = x - bounds.left; y = y - bounds.top; - let response = await fetch(`${apiBase}/set`, { + let response = await fetch(`${ssl ? "https" : "http"}://${apiBase}/set`, { method: "POST", credentials: 'include', headers: { @@ -32,13 +33,7 @@ async function setPixel(canvas, ctx, x, y, c) { }) }); - console.log(response); - switch (response.status) { - case 200: - ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`; - ctx.fillRect(x, y, 1, 1); - break; case 400: alert("missing parameters in request"); break; @@ -57,12 +52,21 @@ function init(info, ctx) { for (let x = 0; x < info.canvasSize; x++) { let tile = new Image(); tile.onload = () => ctx.drawImage(tile, x * info.tileSize, y * info.tileSize); - tile.src = `${apiBase}/tile?x=${x}&y=${y}`; + tile.src = `${ssl ? "https" : "http"}://${apiBase}/tile?x=${x}&y=${y}`; tiles[y].push(tile); } } } +async function updates(ctx) { + let socket = new WebSocket(`ws://${apiBase}/updates`); + socket.onmessage = async e => { + let pixel = await JSON.parse(e.data); + ctx.fillStyle = `rgb(${pixel.r}, ${pixel.g}, ${pixel.b})`; + ctx.fillRect(pixel.x, pixel.y, 1, 1); + } +} + async function main() { let info = await getInfo(); console.log(info); @@ -71,7 +75,9 @@ async function main() { let ctx = canvas.getContext("2d"); init(info, ctx); - canvas.onclick = e => setPixel(canvas, ctx, e.clientX, e.clientY, color); + updates(ctx); + + canvas.onclick = e => sendPixel(canvas, ctx, e.clientX, e.clientY, color); } main(); \ No newline at end of file