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)
#include "css.h"
#include "ext.h"
#include "vfo.h"
+#include "waterfall.h"
#include "log.h"
static GtkWidget *status;
+
void status_text(char *text) {
gtk_label_set_text(GTK_LABEL(status), text);
usleep(100000);
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);
}
audio_get_cards();
+ init_websocket_server();
// wait for get_cards to complete
// g_mutex_lock(&audio_mutex);
// g_mutex_unlock(&audio_mutex);
}
g_idle_add(ext_discovery, NULL);
+
return 0;
}
#include <unistd.h>
#include <stdbool.h>
+#include <libwebsockets.h>
+
#include "radio.h"
#include "vfo.h"
#include "waterfall.h"
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,
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;
}
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);
+ }
+ }
}
}
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
--- /dev/null
+<!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>