From 745ffdd05c777fcc83c956c557ca8086883a5735 Mon Sep 17 00:00:00 2001
From: Ramakrishnan Muthukrishnan <ram@rkrishnan.org>
Date: Tue, 4 Feb 2025 22:51:31 +0530
Subject: [PATCH] working waterfall on browser via websocket

---
 Makefile       |   7 +-
 main.c         |  29 ++++---
 waterfall.c    | 173 ++++++++++++++++++++++++++++++++++++++
 waterfall.h    |   3 +-
 www/index.html | 219 +++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 416 insertions(+), 15 deletions(-)
 create mode 100644 www/index.html

diff --git a/Makefile b/Makefile
index 9c74803..8b9b546 100644
--- a/Makefile
+++ b/Makefile
@@ -173,8 +173,11 @@ ifeq ($(UNAME_S), Linux)
 RT_OPTION=-lrt
 endif
 
-LIBS=$(RT_OPTION) -lfftw3 -lm -lwdsp -lpthread $(AUDIO_LIBS) $(USBOZY_LIBS) $(GTKLIBS) $(GPIO_LIBS) $(MIDI_LIBS)
-INCLUDES=$(GTKINCLUDES)
+WEBSOCKETINCLUDES=`pkg-config --cflags libwebsockets`
+WEBSOCKETLIB=`pkg-config --libs libwebsockets`
+
+LIBS=$(RT_OPTION) -lfftw3 -lm -lwdsp -lpthread $(AUDIO_LIBS) $(USBOZY_LIBS) $(GTKLIBS) $(GPIO_LIBS) $(MIDI_LIBS) $(WEBSOCKETLIB)
+INCLUDES=$(GTKINCLUDES) $(WEBSOCKETINCLUDES)
 
 COMPILE=$(CC) $(CFLAGS) $(OPTIONS) $(INCLUDES)
 
diff --git a/main.c b/main.c
index 68ebbbd..2f865c9 100644
--- a/main.c
+++ b/main.c
@@ -60,6 +60,7 @@
 #include "css.h"
 #include "ext.h"
 #include "vfo.h"
+#include "waterfall.h"
 
 #include "log.h"
 
@@ -77,6 +78,7 @@ GtkWidget *grid;
 
 static GtkWidget *status;
 
+
 void status_text(char *text) {
   gtk_label_set_text(GTK_LABEL(status), text);
   usleep(100000);
@@ -132,24 +134,25 @@ bool keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data) {
 
 gboolean main_delete(GtkWidget *widget) {
   if (radio != NULL) {
+      cleanup_websocket_server();  // Add this line
 #ifdef GPIO
-    gpio_close();
+      gpio_close();
 #endif
 #ifdef CLIENT_SERVER
-    if (!radio_is_remote) {
+      if (!radio_is_remote) {
 #endif
-      switch (protocol) {
-      case ORIGINAL_PROTOCOL:
-        old_protocol_stop();
-        break;
-      case NEW_PROTOCOL:
-        new_protocol_stop();
-        break;
-      }
+	  switch (protocol) {
+	  case ORIGINAL_PROTOCOL:
+	      old_protocol_stop();
+	      break;
+	  case NEW_PROTOCOL:
+	      new_protocol_stop();
+	      break;
+	  }
 #ifdef CLIENT_SERVER
-    }
+      }
 #endif
-    radioSaveState();
+      radioSaveState();
   }
   _exit(0);
 }
@@ -159,6 +162,7 @@ static int init(void *data) {
 
   audio_get_cards();
 
+  init_websocket_server();
   // wait for get_cards to complete
   // g_mutex_lock(&audio_mutex);
   // g_mutex_unlock(&audio_mutex);
@@ -194,6 +198,7 @@ static int init(void *data) {
   }
 
   g_idle_add(ext_discovery, NULL);
+
   return 0;
 }
 
diff --git a/waterfall.c b/waterfall.c
index a117f4b..b835098 100644
--- a/waterfall.c
+++ b/waterfall.c
@@ -25,6 +25,8 @@
 #include <unistd.h>
 #include <stdbool.h>
 
+#include <libwebsockets.h>
+
 #include "radio.h"
 #include "vfo.h"
 #include "waterfall.h"
@@ -55,6 +57,12 @@ static gfloat hz_per_pixel;
 static int display_width;
 static int display_height;
 
+// Add these global variables near the top
+struct lws_context *ws_context = NULL;
+static pthread_t websocket_thread;
+static int websocket_running = 0;
+static struct lws *ws_client = NULL;  // Store most recent client
+
 /* Create a new surface of the appropriate size to store our scribbles */
 static gboolean waterfall_configure_event_cb(GtkWidget *widget,
                                              GdkEventConfigure *event,
@@ -110,6 +118,126 @@ static gboolean waterfall_scroll_event_cb(GtkWidget *widget,
     return receiver_scroll_event(widget, event, data);
 }
 
+static struct timespec last_write_time = {0, 0};
+
+// WebSocket protocol handler
+static int callback_waterfall(struct lws *wsi, enum lws_callback_reasons reason,
+                            void *user, void *in, size_t len) {
+    switch (reason) {
+	case LWS_CALLBACK_ESTABLISHED:
+            printf("WebSocket connection established\n");
+            ws_client = wsi;
+            clock_gettime(CLOCK_MONOTONIC, &last_write_time);
+            break;
+
+        case LWS_CALLBACK_SERVER_WRITEABLE:
+            {
+                struct timespec now;
+                clock_gettime(CLOCK_MONOTONIC, &now);
+                double elapsed = (now.tv_sec - last_write_time.tv_sec) * 1000.0 +
+                               (now.tv_nsec - last_write_time.tv_nsec) / 1000000.0;
+                if (elapsed < 50.0) {  // Limit to 20 updates per second
+                    return 0;
+                }
+                last_write_time = now;
+            }
+            break;
+
+        case LWS_CALLBACK_PROTOCOL_INIT:
+            printf("WebSocket protocol initialized\n");
+            break;
+
+        case LWS_CALLBACK_RECEIVE:
+            printf("Received data from browser, len: %zu\n", len);
+            break;
+
+
+        case LWS_CALLBACK_CLOSED:
+            printf("WebSocket connection closed\n");
+            if (ws_client == wsi) {
+                ws_client = NULL;
+            }
+            break;
+
+        default:
+            // printf("Callback reason: %d\n", reason);
+            break;
+    }
+
+    return 0;
+}
+
+
+static void* websocket_service_thread(void* arg) {
+    printf("WebSocket service thread started\n");
+    while (websocket_running) {
+        if (ws_context) {
+            lws_service(ws_context, 10);  // 10ms timeout
+        }
+        usleep(1000);  // Sleep for 1ms between service calls
+    }
+    printf("WebSocket service thread ending\n");
+    return NULL;
+}
+
+// Define protocols
+static struct lws_protocols protocols[] = {
+    {
+        .name = "waterfall",
+        .callback = callback_waterfall,
+        .per_session_data_size = 0,
+        .rx_buffer_size = 1024,
+    },
+    { NULL, NULL, 0, 0 } /* terminator */
+};
+
+void init_websocket_server() {
+    struct lws_context_creation_info info;
+    memset(&info, 0, sizeof info);
+
+    info.port = 8080;
+    info.protocols = protocols;
+    info.gid = -1;
+    info.uid = -1;
+    info.options = LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;
+
+    printf("Creating WebSocket context on port 8080\n");
+    
+    ws_context = lws_create_context(&info);
+    if (!ws_context) {
+        fprintf(stderr, "Failed to create WebSocket context\n");
+        return;
+    }
+
+    printf("WebSocket context created, starting service thread\n");
+    
+    // Start the service thread
+    websocket_running = 1;
+    if (pthread_create(&websocket_thread, NULL, websocket_service_thread, NULL) != 0) {
+        fprintf(stderr, "Failed to create WebSocket service thread\n");
+        lws_context_destroy(ws_context);
+        ws_context = NULL;
+        return;
+    }
+
+    printf("WebSocket server started successfully on port %d\n", info.port);
+}
+
+
+void cleanup_websocket_server() {
+    if (websocket_running) {
+        printf("Stopping WebSocket service thread\n");
+        websocket_running = 0;
+        pthread_join(websocket_thread, NULL);
+    }
+    
+    if (ws_context) {
+        printf("Destroying WebSocket context\n");
+        lws_context_destroy(ws_context);
+        ws_context = NULL;
+    }
+}
+
 void waterfall_update(RECEIVER *rx) {
     int i;
 
@@ -267,6 +395,51 @@ void waterfall_update(RECEIVER *rx) {
         }
 
         gtk_widget_queue_draw(rx->waterfall);
+
+
+	if (ws_client) {
+	    // Calculate min and max for normalization
+	    float min_val = rx->pixel_samples[0];
+	    float max_val = rx->pixel_samples[0];
+	    for (int i = 1; i < display_width; i++) {
+		if (rx->pixel_samples[i] < min_val) min_val = rx->pixel_samples[i];
+		if (rx->pixel_samples[i] > max_val) max_val = rx->pixel_samples[i];
+	    }
+    
+	    float range = max_val - min_val;
+    
+	    // Allocate 8-bit buffer (1/4 the size of float buffer)
+	    size_t payload_len = display_width;
+	    unsigned char *buf = malloc(LWS_PRE + payload_len + 8); // Extra 8 bytes for min/max
+	    if (buf) {
+		// Store min and max as floats at start of buffer for reconstruction
+		memcpy(&buf[LWS_PRE], &min_val, sizeof(float));
+		memcpy(&buf[LWS_PRE + sizeof(float)], &max_val, sizeof(float));
+        
+		// Convert floats to normalized 8-bit integers
+		for (int i = 0; i < display_width; i++) {
+		    float normalized = (rx->pixel_samples[i] - min_val) / range;
+		    buf[LWS_PRE + 8 + i] = (unsigned char)(normalized * 255);
+		}
+        
+		if (lws_callback_on_writable(ws_client) < 0) {
+		    printf("Failed to request writable callback\n");
+		    free(buf);
+		    return;
+		}
+
+		int written = lws_write(ws_client, &buf[LWS_PRE], payload_len + 8, LWS_WRITE_BINARY);
+		if (written < 0) {
+		    printf("WebSocket write failed\n");
+		} else if (written < payload_len + 8) {
+		    printf("Partial write: %d of %zu bytes\n", written, payload_len + 8);
+		} else {
+		    // printf("Successfully wrote %d bytes\n", written);
+		}
+        
+		free(buf);
+	    }
+	}
     }
 }
 
diff --git a/waterfall.h b/waterfall.h
index 3d41f52..85cf941 100644
--- a/waterfall.h
+++ b/waterfall.h
@@ -22,5 +22,6 @@
 
 extern void waterfall_update(RECEIVER *rx);
 extern void waterfall_init(RECEIVER *rx,int width,int height);
-
+extern void init_websocket_server(void);
+extern void cleanup_websocket_server(void);
 #endif
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 0000000..38b1648
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,219 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>SDR Waterfall Debug</title>
+    <style>
+        body {
+            margin: 0;
+            padding: 20px;
+            font-family: monospace;
+        }
+        #status {
+            padding: 10px;
+            border-bottom: 1px solid #ccc;
+            margin-bottom: 10px;
+        }
+        #waterfallCanvas {
+            padding: 10px;
+            background: #f0f0f0;
+            height: 300px;
+            overflow-y: auto;
+            white-space: pre;
+            font-size: 12px;
+        }
+    </style>
+</head>
+<body>
+    <div id="status">Disconnected</div>
+    <canvas id="waterfallCanvas"></canvas>
+
+    <script>
+
+        class Waterfall {
+	    constructor(canvasId, width, height) {
+		this.canvas = document.getElementById(canvasId);
+		this.canvas.width = width;
+		this.canvas.height = height;
+		this.ctx = this.canvas.getContext('2d');
+        
+		// Initialize image data
+		this.imageData = this.ctx.createImageData(width, height);
+        
+		// Configuration
+		this.waterfallLow = -113;
+		this.waterfallHigh = -77;
+        
+		// Colors (matching C code)
+		this.colorLow = {r: 0, g: 0, b: 0};      // black
+		this.colorHigh = {r: 255, g: 255, b: 0}; // yellow
+	    }
+
+	    update(samples) {
+		// Move existing data down
+		const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
+		this.ctx.putImageData(imageData, 0, 1);
+
+		// Create new line data
+		const lineData = this.ctx.createImageData(this.canvas.width, 1);
+        
+		// Fill new line with color-mapped sample data
+		for (let i = 0; i < samples.length; i++) {
+		    const sample = samples[i];
+		    const pixelIndex = i * 4;  // 4 components per pixel (RGBA)
+            
+		    if (sample < this.waterfallLow) {
+			lineData.data[pixelIndex] = this.colorLow.r;
+			lineData.data[pixelIndex + 1] = this.colorLow.g;
+			lineData.data[pixelIndex + 2] = this.colorLow.b;
+			lineData.data[pixelIndex + 3] = 255;  // Alpha
+		    } else if (sample > this.waterfallHigh) {
+			lineData.data[pixelIndex] = this.colorHigh.r;
+			lineData.data[pixelIndex + 1] = this.colorHigh.g;
+			lineData.data[pixelIndex + 2] = this.colorHigh.b;
+			lineData.data[pixelIndex + 3] = 255;
+		    } else {
+			const range = this.waterfallHigh - this.waterfallLow;
+			const offset = sample - this.waterfallLow;
+			const percent = offset / range;
+                
+			// Replicate the color gradient from C code
+			let r, g, b;
+			if (percent < 2/9) {
+			    const localPercent = percent / (2/9);
+			    r = (1 - localPercent) * this.colorLow.r;
+			    g = (1 - localPercent) * this.colorLow.g;
+			    b = this.colorLow.b + localPercent * (255 - this.colorLow.b);
+			} else if (percent < 3/9) {
+			    const localPercent = (percent - 2/9) / (1/9);
+			    r = 0;
+			    g = localPercent * 255;
+			    b = 255;
+			} else {
+			    const range = this.waterfallHigh - this.waterfallLow;
+			    const offset = sample - this.waterfallLow;
+			    const percent = offset / range;
+            
+			    let r, g, b;
+			    if (percent < 2/9) {
+				const localPercent = percent / (2/9);
+				r = (1 - localPercent) * this.colorLow.r;
+				g = (1 - localPercent) * this.colorLow.g;
+				b = this.colorLow.b + localPercent * (255 - this.colorLow.b);
+			    } else if (percent < 3/9) {
+				const localPercent = (percent - 2/9) / (1/9);
+				r = 0;
+				g = localPercent * 255;
+				b = 255;
+			    } else if (percent < 4/9) {
+				const localPercent = (percent - 3/9) / (1/9);
+				r = 0;
+				g = 255;
+				b = (1 - localPercent) * 255;
+			    } else if (percent < 5/9) {
+				const localPercent = (percent - 4/9) / (1/9);
+				r = localPercent * 255;
+				g = 255;
+				b = 0;
+			    } else if (percent < 7/9) {
+				const localPercent = (percent - 5/9) / (2/9);
+				r = 255;
+				g = (1 - localPercent) * 255;
+				b = 0;
+			    } else if (percent < 8/9) {
+				const localPercent = (percent - 7/9) / (1/9);
+				r = 255;
+				g = 0;
+				b = localPercent * 255;
+			    } else {
+				const localPercent = (percent - 8/9) / (1/9);
+				r = (0.75 + 0.25 * (1.0 - localPercent)) * 255.0;
+				g = localPercent * 255.0 * 0.5;
+				b = 255;
+			    }
+
+			    lineData.data[pixelIndex] = Math.floor(r);
+			    lineData.data[pixelIndex + 1] = Math.floor(g);
+			    lineData.data[pixelIndex + 2] = Math.floor(b);
+			    lineData.data[pixelIndex + 3] = 255;
+			}
+		    }
+		}
+        
+		// Draw new line at top
+		this.ctx.putImageData(lineData, 0, 0);
+	    }
+	}
+
+        // Wait for DOM to be ready
+        document.addEventListener('DOMContentLoaded', function() {
+            const statusDiv = document.getElementById('status');
+            let ws = null;
+            let reconnectAttempts = 0;
+            const MAX_RECONNECT_ATTEMPTS = 5;
+
+	    const waterfall = new Waterfall('waterfallCanvas', 1024, 400);
+
+            function log(message) {
+                const timestamp = new Date().toISOString();
+                console.log(message);
+            }
+
+            function connectWebSocket() {
+                if (ws === null || ws.readyState === WebSocket.CLOSED) {
+                    try {
+                        log('Attempting to connect...');
+                        ws = new WebSocket('ws://localhost:8080', 'waterfall');
+                        
+                        ws.onopen = () => {
+                            log('Connected successfully');
+                            statusDiv.textContent = 'Connected';
+                            statusDiv.style.color = '#00ff00';
+                            reconnectAttempts = 0;
+                        };
+
+                        ws.onclose = (event) => {
+                            log(`Connection closed (code: ${event.code})`);
+                            statusDiv.textContent = 'Disconnected';
+                            statusDiv.style.color = '#ff0000';
+                            ws = null;
+
+                            if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+                                reconnectAttempts++;
+                                log(`Reconnecting (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
+                                setTimeout(connectWebSocket, 2000);
+                            }
+                        };
+
+                        ws.onerror = (error) => {
+                            log('WebSocket error occurred');
+                        };
+
+			ws.onmessage = async (event) => {
+			    const buffer = await event.data.arrayBuffer();
+			    const data = new Uint8Array(buffer);
+    
+			    const minVal = new Float32Array(buffer.slice(0, 4))[0];
+			    const maxVal = new Float32Array(buffer.slice(4, 8))[0];
+			    const range = maxVal - minVal;
+
+			    const reconstructed = new Float32Array(data.length - 8);
+			    for (let i = 0; i < data.length - 8; i++) {
+				reconstructed[i] = (data[i + 8] / 255.0) * range + minVal;
+			    }
+
+			    waterfall.update(reconstructed);
+			};
+
+
+                    } catch (error) {
+                        log('Error creating WebSocket: ' + error);
+                    }
+                }
+            }
+
+            // Start the connection
+            connectWebSocket();
+        });
+    </script>
+</body>
+</html>
-- 
2.45.2