]> git.rkrishnan.org Git - pihpsdr.git/commitdiff
working waterfall on browser via websocket websocket2
authorRamakrishnan Muthukrishnan <ram@rkrishnan.org>
Tue, 4 Feb 2025 17:21:31 +0000 (22:51 +0530)
committerRamakrishnan Muthukrishnan <ram@rkrishnan.org>
Tue, 4 Feb 2025 17:21:31 +0000 (22:51 +0530)
Makefile
main.c
waterfall.c
waterfall.h
www/index.html [new file with mode: 0644]

index 9c74803fae39ba7055d7cfe61632b7ea763995e4..8b9b54661eeb32ffbeddd35772152e305d384911 100644 (file)
--- 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 68ebbbddb31e6521a9acf35b75052411bd34ac3c..2f865c9dec06913dc9b0a3b1410076b493d864bf 100644 (file)
--- 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;
 }
 
index a117f4b53d53edb8d0ab0bcd77db78b37ded47e3..b835098e1c01a1a5731d844359a85cecbf6c5c30 100644 (file)
@@ -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);
+           }
+       }
     }
 }
 
index 3d41f52bd9585c0eba9e19bfd4af5e1d50f3532e..85cf941269f44b5705c71c419d326f3ad8866a65 100644 (file)
@@ -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 (file)
index 0000000..38b1648
--- /dev/null
@@ -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>