diff --git a/README.md b/README.md index e7d60ff..772c94b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,25 @@ Make sure you place it in a folder of its own. It will create a subfolder to hol Default (no arguments) mode will launch into jukebox mode, playing random Bytejams from LCDZ. It can be provided with a JSON file playlist (from remote and local) or .zip file. +The JSON file format looks like: +```JSON +{ + "title": "TIC-80 selected works", + "description": "(with FFT)", // (optional) + "items": [ + { + "location": "https://livecode.demozoo.org/shader_file_sources/2023_05_15_byte_jam_monday_night_bytes/nusan.lua", + "author": "Nusan", // (optional) + "description": "FieldFX Byte Jam - 15/05/2023", // (optional) + "playtime": 30 // (optional) + } + ] +} +``` + +A JSON file can be included in a zip file with the name 'index.json' + + Applications: - To project onto a wall at parties to preach the good TIC and Bytejam words. @@ -142,6 +161,10 @@ For the previous work ByteJammer builds on, testing, good-will, and support: Aldroid, Gasman, Lex Bailey, Mantratronic, NesBox, NuSan, PS, Raccoon Violet, Superogue, Totetmatt. +Additional development: + +zeno4ever. + Thanks to those whose work features on the [walkthrough video](https://youtube.com/watch?v=erhyvrGxwZY): Alia, Aldroid, Dave84, Gasman, Lex Bailey, Gigabates, Mantratronic, NuSan, PS, Superogue, Suule, Synesthesia, TôBach. diff --git a/client-jukebox.go b/client-jukebox.go index 0d5c103..66b80db 100644 --- a/client-jukebox.go +++ b/client-jukebox.go @@ -1,10 +1,15 @@ package main -import "log" +import ( + "log" + "time" -func startClientJukebox(host string, port int, playlist *Playlist) error { - ch := make(chan Msg) - j, err := NewJukebox(playlist, &ch) + "github.com/creativenucleus/bytejammer/comms" +) + +func startClientJukebox(host string, port int, playtime time.Duration, playlist *Playlist) error { + ch := make(chan comms.Msg) + j, err := NewJukebox(playlist, playtime, &ch) if err != nil { return err } @@ -17,18 +22,16 @@ func startClientJukebox(host string, port int, playlist *Playlist) error { go func() { for { - select { - case msg, ok := <-ch: - if ok { - switch msg.Type { - case "tic-state": - // #TODO: line endings for data? UTF-8? - msg := Msg{Type: "tic-state", TicState: msg.TicState} - err = ws.sendData(msg) - if err != nil { - // #TODO: soften! - log.Fatal(err) - } + msg, ok := <-ch + if ok { + switch msg.Type { + case "tic-state": + // #TODO: line endings for data? UTF-8? + msg := comms.Msg{Type: "tic-state", TicState: msg.TicState} + err = ws.sendData(msg) + if err != nil { + // #TODO: soften! + log.Fatal(err) } } } @@ -36,6 +39,9 @@ func startClientJukebox(host string, port int, playlist *Playlist) error { }() j.start() + for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } } diff --git a/client-panel.go b/client-panel.go index 33d694f..d4185c7 100644 --- a/client-panel.go +++ b/client-panel.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/websocket" "github.com/tyler-sommer/stick" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/embed" ) @@ -27,9 +28,10 @@ const ( type ClientPanel struct { // #TODO: lock down to receiver only - chSendServerStatus chan ClientServerStatus + chSendClientStatus chan comms.DataClientStatus wsClient *websocket.Conn wsMutex sync.Mutex + chLog chan string } func startClientPanel(port int) error { @@ -50,13 +52,23 @@ func startClientPanel(port int) error { fmt.Printf("In a web browser, go to http://localhost:%d/%s\n", port, session) cp := ClientPanel{ - chSendServerStatus: make(chan ClientServerStatus), + chSendClientStatus: make(chan comms.DataClientStatus), + chLog: make(chan string), } + + go func() { + for { + logMsg := <-cp.chLog + cp.sendLog(logMsg) + } + }() + http.HandleFunc(fmt.Sprintf("/%s", session), cp.webClientIndex) http.HandleFunc(fmt.Sprintf("/%s/api/identity.json", session), cp.webClientApiIdentityJSON) http.HandleFunc(fmt.Sprintf("/%s/api/join-server.json", session), cp.webClientApiJoinServerJSON) http.HandleFunc(fmt.Sprintf("/%s/ws-client", session), cp.wsWebClient()) - if err := webServer.ListenAndServe(); err != nil { + err = webServer.ListenAndServe() + if err != nil { return err } @@ -105,14 +117,16 @@ func (cp *ClientPanel) webClientApiIdentityJSON(w http.ResponseWriter, r *http.R apiOutResponse(w, nil, http.StatusCreated) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, fmt.Errorf("method not allowed"), http.StatusMethodNotAllowed) } } func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": - cp.chSendServerStatus <- ClientServerStatus{isConnected: false} + cp.chLog <- "Request: Join Server" + + cp.chSendClientStatus <- comms.DataClientStatus{IsConnected: false} // #TODO: Cleaner way to do this? type reqType struct { @@ -141,7 +155,7 @@ func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http return } - err = startClientServerConn(req.Host, port, identity, cp.chSendServerStatus) + err = startClientServerConn(req.Host, port, identity, cp.chSendClientStatus) if err != nil { apiOutErr(w, err, http.StatusInternalServerError) return @@ -149,32 +163,37 @@ func (cp *ClientPanel) webClientApiJoinServerJSON(w http.ResponseWriter, r *http apiOutResponse(w, nil, http.StatusCreated) default: - apiOutErr(w, errors.New("Method not allowed"), http.StatusMethodNotAllowed) + apiOutErr(w, errors.New("method not allowed"), http.StatusMethodNotAllowed) } } func (cp *ClientPanel) wsWebClient() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error - cp.wsClient, err = wsUpgrader.Upgrade(w, r, nil) + + comms.WsUpgrade(w, r, func(conn *websocket.Conn) error { + cp.wsClient = conn + defer func() { cp.wsClient = nil }() + + go cp.wsRead() + go cp.wsWrite() + + // #TODO: handle exit + for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) + } + }) if err != nil { log.Print("upgrade:", err) return } - defer cp.wsClient.Close() - - go cp.wsRead() - go cp.wsWrite() - - // #TODO: handle exit - for { - } } } func (cp *ClientPanel) wsRead() { for { - var msg Msg + var msg comms.Msg err := cp.wsClient.ReadJSON(&msg) if err != nil { log.Println("read:", err) @@ -196,23 +215,25 @@ func (cp *ClientPanel) wsWrite() { }() */ for { - select { - // case <-done: - // return - // case <-statusTicker.C: - // fmt.Println("TICKER!") - - case status := <-cp.chSendServerStatus: - msg := Msg{Type: "server-status", ServerStatus: status} - err := cp.sendData(&msg) - if err != nil { - // #TODO: relax - log.Fatal(err) - } + status := <-cp.chSendClientStatus + msg := comms.Msg{Type: "client-status", ClientStatus: status} + err := cp.sendData(&msg) + if err != nil { + // #TODO: relax + log.Fatal(err) } } } +func (cp *ClientPanel) sendLog(message string) { + msg := comms.Msg{Type: "log", Log: comms.DataLog{Msg: message}} + + err := cp.sendData(&msg) + if err != nil { + log.Println("read:", err) + } +} + func (cp *ClientPanel) sendData(data interface{}) error { cp.wsMutex.Lock() defer cp.wsMutex.Unlock() diff --git a/client-ws.go b/client-ws.go index c0f3d17..97a069f 100644 --- a/client-ws.go +++ b/client-ws.go @@ -8,6 +8,7 @@ import ( "path/filepath" "time" + "github.com/creativenucleus/bytejammer/comms" "github.com/creativenucleus/bytejammer/config" "github.com/creativenucleus/bytejammer/machines" "github.com/creativenucleus/bytejammer/util" @@ -15,18 +16,14 @@ import ( type ClientWS struct { ws *WebSocketLink - chMsg chan Msg + chMsg chan comms.Msg basepath string } -type ClientServerStatus struct { - isConnected bool -} - -func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan ClientServerStatus) error { - chServerStatus <- ClientServerStatus{isConnected: false} +func startClientServerConn(host string, port int, identity *Identity, chServerStatus chan comms.DataClientStatus) error { + chServerStatus <- comms.DataClientStatus{IsConnected: false} cws := ClientWS{ - chMsg: make(chan Msg), + chMsg: make(chan comms.Msg), } cws.basepath = filepath.Clean(fmt.Sprintf("%sclient-data/%s", config.WORK_DIR, util.GetSlugFromTime(time.Now()))) @@ -51,7 +48,7 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt break } defer cws.ws.Close() - chServerStatus <- ClientServerStatus{isConnected: true} + chServerStatus <- comms.DataClientStatus{IsConnected: true} m, err := machines.LaunchMachine("TIC-80", true, true, false) if err != nil { @@ -65,6 +62,8 @@ func startClientServerConn(host string, port int, identity *Identity, chServerSt // Lock #TODO: use a channel to escape for { + // Removes 100% CPU warning - but this should really be restructured + time.Sleep(10 * time.Second) } } @@ -79,7 +78,7 @@ func clientOpenConnection(host string, port int) (*WebSocketLink, error) { func (cws *ClientWS) clientWsReader(tic *machines.Tic, identity *Identity) error { for { - var msg Msg + var msg comms.Msg err := cws.ws.conn.ReadJSON(&msg) if err != nil { log.Fatal(err) @@ -96,7 +95,7 @@ func (cws *ClientWS) clientWsReader(tic *machines.Tic, identity *Identity) error cws.handleChallengeRequest(msg.ChallengeRequest.Challenge, identity) case "tic-state": - tic.WriteImportCode(msg.TicState) + tic.WriteImportCode(msg.TicState.State, true) } } } @@ -107,13 +106,14 @@ func (cws *ClientWS) handleChallengeRequest(challenge string, identity *Identity return err } - fmt.Printf("%x", data) signed, err := identity.Crypto.Sign(data) if err != nil { return err } - cws.chMsg <- Msg{Type: "challenge-response", ChallengeResponse: DataChallengeResponse{Challenge: fmt.Sprintf("%x", signed)}} + cws.chMsg <- comms.Msg{Type: "challenge-response", ChallengeResponse: comms.DataChallengeResponse{ + Challenge: fmt.Sprintf("%x", signed), + }} return nil } @@ -126,9 +126,9 @@ func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { log.Fatal(err) } - msg := Msg{ + msg := comms.Msg{ Type: "identity", - Identity: DataIdentity{ + Identity: comms.DataIdentity{ Uuid: identity.Uuid.String(), DisplayName: identity.DisplayName, PublicKey: publicKeyRaw, @@ -174,7 +174,9 @@ func (cws *ClientWS) clientWsWriter(tic *machines.Tic, identity *Identity) { } // #TODO: line endings for data? UTF-8? - msg := Msg{Type: "tic-state", TicState: *ticState} + msg := comms.Msg{Type: "tic-state", TicState: comms.DataTicState{ + State: *ticState, + }} err = cws.ws.sendData(msg) if err != nil { log.Fatal(err) diff --git a/msg.go b/comms/msg.go similarity index 59% rename from msg.go rename to comms/msg.go index 3f00616..bc886cf 100644 --- a/msg.go +++ b/comms/msg.go @@ -1,16 +1,15 @@ -package main +package comms -import "github.com/creativenucleus/bytejammer/machines" +import ( + "github.com/creativenucleus/bytejammer/machines" +) type DataLog struct { Msg string } -type MsgTicState struct { - Code []byte - IsRunning bool - CursorX int - CursorY int +type DataClientStatus struct { + IsConnected bool } type DataIdentity struct { @@ -19,6 +18,10 @@ type DataIdentity struct { PublicKey []byte `json:"publicKey"` } +type DataTicState struct { + State machines.TicState +} + type DataCloseMachine struct { Uuid string `json:"uuid"` } @@ -41,35 +44,34 @@ type DataChallengeResponse struct { Challenge string `json:"challenge"` } -type MsgServerStatus struct { - Type string `json:"type"` - Data struct { - Clients []struct { - Uuid string - DisplayName string - ShortUuid string - Status string - MachineUuid string - LastPingTime string - } - Machines []struct { - Uuid string - MachineName string - ProcessID int - Platform string - Status string - ClientUuid string - JammerDisplayName string - LastSnapshotTime string - } - } `json:"data"` +type DataSessionStatus struct { + Port int + Clients []struct { + Uuid string + DisplayName string + ShortUuid string + Status string + MachineUuid string + LastPingTime string + } + Machines []struct { + Uuid string + MachineName string + ProcessID int + Platform string + Status string + ClientUuid string + JammerDisplayName string + LastSnapshotTime string + } } type Msg struct { Type string `json:"type"` Identity DataIdentity `json:"identity,omitempty"` - TicState machines.TicState `json:"tic-state,omitempty"` - ServerStatus ClientServerStatus `json:"server-status,omitempty"` + TicState DataTicState `json:"tic-state,omitempty"` + ClientStatus DataClientStatus `json:"client-status,omitempty"` + SessionStatus DataSessionStatus `json:"session-status,omitempty"` Log DataLog `json:"log,omitempty"` ConnectMachineClient DataConnectMachineClient `json:"connect-machine-client,omitempty"` DisconnectMachineClient DataDisconnectMachineClient `json:"disconnect-machine-client,omitempty"` diff --git a/comms/websocket.go b/comms/websocket.go new file mode 100644 index 0000000..af70b87 --- /dev/null +++ b/comms/websocket.go @@ -0,0 +1,32 @@ +package comms + +import ( + "fmt" + "net/http" + + "github.com/gorilla/websocket" +) + +var ( + WS_UPGRADER = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } +) + +type FnWsConn func(*websocket.Conn) error + +func WsUpgrade(w http.ResponseWriter, r *http.Request, fn FnWsConn) error { + conn, err := WS_UPGRADER.Upgrade(w, r, nil) + if err != nil { + return fmt.Errorf("client couldn't upgrade: %w", err) + } + defer conn.Close() + + err = fn(conn) + if err != nil { + return fmt.Errorf("client connection raised an error: %w", err) + } + + return nil +} diff --git a/embed/tic-code/jukebox.lua b/embed/tic-code/jukebox.lua index b21949f..42b7f28 100644 --- a/embed/tic-code/jukebox.lua +++ b/embed/tic-code/jukebox.lua @@ -46,6 +46,7 @@ function TIC() "LCDZ: Totetmatt, PSEnough", "Additional help: Mantratronic, Violet Procyon", "NuSan, and the Field-FX community", + "Additional dev: zeno4ever", } for t=1,#texts do diff --git a/embed/web/client/index.html b/embed/web/client/index.html index 50f4b41..b3c55ec 100644 --- a/embed/web/client/index.html +++ b/embed/web/client/index.html @@ -17,7 +17,7 @@ const ws = new BjmrWebSocket('{{session_key}}') const ajax = new BjmrAjax('{{session_key}}') - let IDENTITIES = {}; + let IDENTITIES = null; const refreshViewIdentities = () => { const elSelect = document.getElementById("identity-id"); @@ -40,10 +40,27 @@ if (res.ok) { IDENTITIES = res.data; refreshViewIdentities(); + refreshVisibility(); + } + } + + const refreshVisibility = () => { + if (IDENTITIES===null) { + // Not yet initialised + document.getElementById("section-create-identity").style.display = "none"; + document.getElementById("section-join-server").style.display = "none"; + } else if (Object.keys(IDENTITIES).length == 0) { + // Currently, only one identity is supported for clients + document.getElementById("section-create-identity").style.display = "block"; + document.getElementById("section-join-server").style.display = "none"; + } else { + document.getElementById("section-create-identity").style.display = "none"; + document.getElementById("section-join-server").style.display = "block"; } } window.onload = () => { + refreshVisibility(); fetchIdentites(); document.getElementById("create-identity").addEventListener("submit", (e) => { @@ -108,12 +125,12 @@ const msg = JSON.parse(evt.data); console.log(msg) switch(msg.type) { - case "server-status": + case "client-status": // handleMsgServerStatus(msg.data) break; case "log": - handleMsgLog(msg.data); + handleMsgLog(msg.log); break; default: @@ -128,7 +145,7 @@
Port: ${data.Port}
`; + document.getElementById("server-details").innerHTML = html; + + html = ''; if(!data.Clients || data.Clients.length == 0) { html = "