From: Ramakrishnan Muthukrishnan <ram@rkrishnan.org> Date: Tue, 4 Feb 2025 17:21:31 +0000 (+0530) Subject: working waterfall on browser via websocket X-Git-Url: https://git.rkrishnan.org/vdrive/%22news.html/simplejson/flags/provisioning?a=commitdiff_plain;h=refs%2Fheads%2Fwebsocket2;p=pihpsdr.git working waterfall on browser via websocket --- 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>