You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

178 lines
5.0KB

  1. const spectrumCanvas = document.getElementById('spectrum');
  2. const waterfallCanvas = document.getElementById('waterfall');
  3. const statusEl = document.getElementById('status');
  4. const metaEl = document.getElementById('meta');
  5. let latest = null;
  6. let zoom = 1.0;
  7. let pan = 0.0;
  8. let isDragging = false;
  9. let dragStartX = 0;
  10. let dragStartPan = 0;
  11. function resize() {
  12. const dpr = window.devicePixelRatio || 1;
  13. const rect1 = spectrumCanvas.getBoundingClientRect();
  14. spectrumCanvas.width = rect1.width * dpr;
  15. spectrumCanvas.height = rect1.height * dpr;
  16. const rect2 = waterfallCanvas.getBoundingClientRect();
  17. waterfallCanvas.width = rect2.width * dpr;
  18. waterfallCanvas.height = rect2.height * dpr;
  19. }
  20. window.addEventListener('resize', resize);
  21. resize();
  22. function colorMap(v) {
  23. // v in [0..1]
  24. const r = Math.min(255, Math.max(0, Math.floor(255 * Math.pow(v, 0.6))));
  25. const g = Math.min(255, Math.max(0, Math.floor(255 * Math.pow(v, 1.1))));
  26. const b = Math.min(255, Math.max(0, Math.floor(180 * Math.pow(1 - v, 1.2))));
  27. return [r, g, b];
  28. }
  29. function renderSpectrum() {
  30. if (!latest) return;
  31. const ctx = spectrumCanvas.getContext('2d');
  32. const w = spectrumCanvas.width;
  33. const h = spectrumCanvas.height;
  34. ctx.clearRect(0, 0, w, h);
  35. // Grid
  36. ctx.strokeStyle = '#13263b';
  37. ctx.lineWidth = 1;
  38. for (let i = 1; i < 10; i++) {
  39. const y = (h / 10) * i;
  40. ctx.beginPath();
  41. ctx.moveTo(0, y);
  42. ctx.lineTo(w, y);
  43. ctx.stroke();
  44. }
  45. const { spectrum_db, sample_rate, center_hz } = latest;
  46. const n = spectrum_db.length;
  47. const span = sample_rate / zoom;
  48. const startHz = center_hz - span / 2 + pan * span;
  49. const endHz = center_hz + span / 2 + pan * span;
  50. const minDb = -120;
  51. const maxDb = 0;
  52. ctx.strokeStyle = '#48d1b8';
  53. ctx.lineWidth = 2;
  54. ctx.beginPath();
  55. for (let i = 0; i < n; i++) {
  56. const freq = center_hz + (i - n / 2) * (sample_rate / n);
  57. if (freq < startHz || freq > endHz) continue;
  58. const x = ((freq - startHz) / (endHz - startHz)) * w;
  59. const v = spectrum_db[i];
  60. const y = h - ((v - minDb) / (maxDb - minDb)) * h;
  61. if (i === 0) ctx.moveTo(x, y);
  62. else ctx.lineTo(x, y);
  63. }
  64. ctx.stroke();
  65. // Signals overlay
  66. ctx.strokeStyle = '#ffb454';
  67. ctx.lineWidth = 2;
  68. if (latest.signals) {
  69. for (const s of latest.signals) {
  70. const left = s.center_hz - s.bw_hz / 2;
  71. const right = s.center_hz + s.bw_hz / 2;
  72. if (right < startHz || left > endHz) continue;
  73. const x1 = ((left - startHz) / (endHz - startHz)) * w;
  74. const x2 = ((right - startHz) / (endHz - startHz)) * w;
  75. ctx.beginPath();
  76. ctx.moveTo(x1, h - 4);
  77. ctx.lineTo(x2, h - 4);
  78. ctx.stroke();
  79. }
  80. }
  81. metaEl.textContent = `Center ${(center_hz/1e6).toFixed(3)} MHz | Span ${(span/1e6).toFixed(3)} MHz`;
  82. }
  83. function renderWaterfall() {
  84. if (!latest) return;
  85. const ctx = waterfallCanvas.getContext('2d');
  86. const w = waterfallCanvas.width;
  87. const h = waterfallCanvas.height;
  88. // Scroll down
  89. const image = ctx.getImageData(0, 0, w, h);
  90. ctx.putImageData(image, 0, 1);
  91. const { spectrum_db, sample_rate, center_hz } = latest;
  92. const n = spectrum_db.length;
  93. const span = sample_rate / zoom;
  94. const startHz = center_hz - span / 2 + pan * span;
  95. const endHz = center_hz + span / 2 + pan * span;
  96. const minDb = -120;
  97. const maxDb = 0;
  98. const row = ctx.createImageData(w, 1);
  99. for (let x = 0; x < w; x++) {
  100. const freq = startHz + (x / (w - 1)) * (endHz - startHz);
  101. const bin = Math.floor((freq - (center_hz - sample_rate / 2)) / (sample_rate / n));
  102. if (bin >= 0 && bin < n) {
  103. const v = spectrum_db[bin];
  104. const norm = Math.max(0, Math.min(1, (v - minDb) / (maxDb - minDb)));
  105. const [r, g, b] = colorMap(norm);
  106. row.data[x * 4 + 0] = r;
  107. row.data[x * 4 + 1] = g;
  108. row.data[x * 4 + 2] = b;
  109. row.data[x * 4 + 3] = 255;
  110. } else {
  111. row.data[x * 4 + 3] = 255;
  112. }
  113. }
  114. ctx.putImageData(row, 0, 0);
  115. }
  116. function tick() {
  117. renderSpectrum();
  118. renderWaterfall();
  119. requestAnimationFrame(tick);
  120. }
  121. function connect() {
  122. const proto = location.protocol === 'https:' ? 'wss' : 'ws';
  123. const ws = new WebSocket(`${proto}://${location.host}/ws`);
  124. ws.onopen = () => {
  125. statusEl.textContent = 'Connected';
  126. };
  127. ws.onmessage = (ev) => {
  128. latest = JSON.parse(ev.data);
  129. };
  130. ws.onclose = () => {
  131. statusEl.textContent = 'Disconnected - retrying...';
  132. setTimeout(connect, 1000);
  133. };
  134. ws.onerror = () => {
  135. ws.close();
  136. };
  137. }
  138. spectrumCanvas.addEventListener('wheel', (ev) => {
  139. ev.preventDefault();
  140. const delta = Math.sign(ev.deltaY);
  141. zoom = Math.max(0.5, Math.min(10, zoom * (delta > 0 ? 1.1 : 0.9)));
  142. });
  143. spectrumCanvas.addEventListener('mousedown', (ev) => {
  144. isDragging = true;
  145. dragStartX = ev.clientX;
  146. dragStartPan = pan;
  147. });
  148. window.addEventListener('mouseup', () => { isDragging = false; });
  149. window.addEventListener('mousemove', (ev) => {
  150. if (!isDragging) return;
  151. const dx = ev.clientX - dragStartX;
  152. pan = dragStartPan - dx / spectrumCanvas.clientWidth;
  153. pan = Math.max(-0.5, Math.min(0.5, pan));
  154. });
  155. connect();
  156. requestAnimationFrame(tick);